Java并发知识分享 - 三斗君

java

Java并发知识分享

volatile的内存语义

从JSR-133(即从JDK1.5开始),volatile变量的写-读可以实现线程之间的通信

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

当读一个volatile变量时,JMM会把该线程对象的本地内存置为无效。线程接下来将从主内存中读取共享变量。导致本地内存与主内存值一致。

volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以确保整个临界区代码的执行具有原子性。

 

concurrent包中,会发现一个通用的实现模式:

首先,声明共享变量为volatile。

然后,使用CAS的原子条件更新来实现线程之间的同步。

同时,配置以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信

 

final域的内存语义

对于final域,编译器和处理器要遵守两个重排序规则:

1.在构造函数内对一个final域的写入,与随后把这个构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

2.初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

注意:

1)JMM禁止编译器把final域的写重排序到构造函数之外

2)编译器会在final域的写之后,构造函数的return之前,插入一个Store屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。

 

在构造函数内部,不能让这个被构造对象的引用为其他线程所见,也就是对象引用不能在构造函数中“逸出”。

 

happens-before

happens-before是JMM最核心的概念。所以应该充分理解,不然你还学什么java。

1. JMM的设计

1) 程序员对内存模型的使用。程序员希望内存模型易于理解、易于编程。程序员希望基于一个强内存模型来编写代码

2)编译器和处理器对内存模型的实现。束缚越少越好,这样可以尽可能优化来提高性能。编译器和处理器希望实现一个弱内存模型。

JSR-133 找到了这个平衡点

JMM把happens-before要求禁止的重排序分为下面两类:

1)会改变程序执行结果的重排序

2)不会改变程序执行结果的重排序

JMM对这两种性质的重排序,采取了不同的策略:

1)对于第一种,JMM要求编译器和处理器必须禁止这种重排序。

2)对于第二种,JMM要求编译器和处理器不做要求(JMM允许这种重排序)

JMM遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。

 

2.happens-before定义

JSR-133 使用happens-before的概念来指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内,也可以是在不同线程之间。因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证。

JSR-133 中的定义:

1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一直,那么这种重排序并不非法。

上面1)是对程序员的承诺

上面2)是对编译器和处理器重排序的约束原则

 

as-if-serial语义保证单线程内程序的执行结果不被改变

happens-before保证正确同步的多线程的执行结果不被改变

其实都是幻觉,只不过JMM帮了你

3.happens-before规则

JSR-133 定义了happens-before规则:

1)程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。

2)监视器规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。

3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。

4)传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

5)start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。

6)join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before与线程A从ThreadB.join()操作成功返回。

 

线程

简介

操作系统调度的最小单元,多个线程能够同时执行。线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。由于处理器的高速切换,感觉是在同时执行。

 

可以查看当前正在运行的线程的一些信息

 

线程的优先级

在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1-10,在线程构件的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多余优先级低的线程。
设置优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算的线程则设置较低的优先级,确保处理器不会被独占。在不同的JVM以及操作系统上,线程规划会存在差异。
只是一种对处理器的建议,并不能保证优先级高的一定优先执行。

线程的状态

Java线程在运行的生命周期中可能处于6种不同的状态,在给定的一个时刻,线程只能处于其中一个状态。

NEW:初始状态,线程被构建,但是还没有调用start()方法

RUNNABLE:运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中”

BLOCKED:阻塞状态,表示线程阻塞于锁

WAITING:等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)

TIME_WAITING:超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的

TERMINATED:终止状态,表示当前线程已经执行完毕


Daemon线程

Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。
注意:Daemon属性需要在启动线程之前设置,不能在启动线程之后设置。并不能保证finally代码一块一定会执行

启动和终止线程

1.构造线程

在运行线程之前首先要构造一个线程对象,线程对象在构造的时候需要提供线程所需要的属性,如线程组、线程优先级、是否是Daemon线程等信息

2.启动线程

调用start()方法就可以启动这个线程。最好给线程设置个名字,排查问题好排查。

3.中断

中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断。其他线程通过interrupt()方法对其进行中断操作。
线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识进行复位。如果该线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的interrupted()时依旧会返回false。

4.安全地终止线程

中断操作时一种简便的线程间交互方式,而这种交互方式最适合用来你取消或停止任务。

线程间通信

1.volatile和synchronized关键字

关键字volatile可以用来修饰字段(成员变量),就是告知程序对该变量的访问用从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。过多地使用volatile是不必要的,因为它会降低程序执行的效率。

关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一时刻,只能有一个线程处于方法或同步代码块中,它保证了线程对变量访问的可见性和排他性。

 

任意一个对象都有自己的监视器,当这个对象由同步代码块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态。

如果获取监视器失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object前驱释放了锁,则 该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

 

2.等待/通知机制

一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()方法或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。

两个线程通过对象来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

 

细节:

1.使用wiat、notify、notifyAll时需要先对调用对象加锁

2.调用wait方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列。

3.notify或notifyAll方法调用后,等待线程依旧不会从wait返回,需要调用notify或notifyAll的线程释放锁之后,等待线程才有机会从wait返回

4.notify方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll方法则是将等待队列中的所有线程全部移到同步队列,被移动的线程状态由WAITING变为BLOCKED

5.wait方法返回的前提是获得了调用对象的锁

 

从上述细节中可以看到,等待/通知机制依托于同步机制,其目的就是确保等待线程从wait方法返回时能够感知到通知线程对变量做出的修改。

 

等待/通知的经典范式

等待方遵循如下原则

1)获取对象的锁

2)如果条件不满足,那么调用对象的wait方法,被通知后仍要检查条件

3)条件满足则执行对应的逻辑

对应的伪代码:

synchronized(对象){

    while(条件不满足){

        对象.wait()

    }

    对应的处理逻辑

}

 

通知方遵循如下原则

1)获得对象的锁

2)改变条件

3)通知所有等待在对象上的线程

对应的伪代码:

synchronized(对象){

    改变条件

    对象.notifyAll()

}

3.Thread.join()的使用

如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。

变相的等待/通知模型

4.ThreadLocal

ThreadLocal,即线程变量,是一个以ThreadLocal对象为键;任意对象为值的存储结构。

一个线程上可以绑上多个ThreadLocal

非常实用的一个功能。

 

关键词:

JMM: Java内存模型

重排序:在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。

等待队列:如果线程处于WAITING状态,则会被移至这个队列中

同步队列:如果线程处于BLOCKED状态,则会被移至这个队列中

发表于

2018-12-11 17:27 

三斗君 

阅读(139) 

评论(0) 

编辑 

收藏 

举报

 

以上是 Java并发知识分享 - 三斗君 的全部内容, 来源链接: utcz.com/z/391151.html

回到顶部