并发编程须要处理两个关键问题:线程之间如何通讯以及线程之间如何同步。java
通讯是指线程之间以何种机制来交换信息。线程之间的通讯机制有两种:共享内存和消息传递。git
共享内存模型中,线程之间共享程序的公共状态,经过读-写内存中的公共状态进行隐式通讯。多条线程共享一片内存,发送者将消息写入内存,接收者从内存中读取消息,从而实现了消息的传递。程序员
消息传递模型中,线程之间经过发送消息来进行显式通讯。github
同步是指程序中用于控制不一样线程间操做发生相对顺序的机制。在共享内存模型中,须要进行显式的同步,程序员必须显式指定某段代码须要在线程之间互斥执行;在消息传递模型中,消息发送必须在消息接收以前,所以同步是隐式进行的。编程
Java采用的是共享内存模型。数组
在 Java 中,全部实例域、静态域和数组元素存放在堆内存,堆内存在线程之间共享。局部变量、方法定义参数和异常处理器参数不会在线程之间共享。缓存
Java 内存模型和硬件的内存架构不一致,是交叉关系。不管是堆仍是栈,大部分数据都会存储到内存中,一部分栈和堆的数据也有可能存到CPU寄存器中。Java内存模型试图屏蔽各类硬件和操做系统的内存访问差别,以实现让Java程序在各类平台下都能达到一致的内存访问效果。安全
Java 内存模型的三大特性:原子性、可见性和顺序性多线程
原子性就是指一个操做中要么所有执行成功,不然失败。Java内存模型容许虚拟机将没有被volatile修饰的64位数据(long,double)的读写操做划分为两次32位操做进行。架构
i++这样的操做,实际上是分为获取i,i自增以及赋值给i三步的,若是要实现这样的原子操做就须要使用原子类实现,或者也可使用synchronized互斥锁来保证操做的原子性。
CAS 也就是 CompareAndSet, 在Java中能够经过循环CAS来实现原子操做。在JVM内部,除了偏向锁,JVM实现锁的方式都是用了CAS,也就是当一个线程想进入同步块的时候使用CAS获取锁,退出时使用CAS释放锁。
可见性指的是当一个线程修改了共享变量的值,其余线程可以当即得知这个修改。Java内存模型是经过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。
执行程序时,为了提升性能,编译器和处理器经常会对指令进行重排序。
重排序可能致使多线程程序出现内存可见性问题。JMM 经过插入特定类型的内存屏障指令来禁止特定类型的处理器重排序,确保了不一样的编译器和处理器平台上,能提供一致的内存可见性保证。
若是两个操做访问同一个变量,且这两个操做中有一个是写操做,这两个操做之间就存在数据依赖性。在重排序时,会遵照数据依赖性,不会改变存在数据依赖关系的两个操做的执行顺序,也就是不会重排序。可是,这是针对单个处理器或单个线程而言的,多线程或多处理器之间的数据依赖性不被考虑在内。
无论怎么重排序,单线程程序的执行结果不能被改变。as-if-serial 语义使得单线程程序员无需担忧重排序的干扰。
重排序可能会改变多线程程序的执行结果,以下图所示
JMM 一方面要为程序员提供足够强的内存可见性保证;另外一方面,对编译器和处理器的限制要尽量放松。
JMM 对不一样性质的重排序,采起了不一样的策略:
JSR-133 中对 happens-before 关系定义以下:
happens-before 与 as-if-serial 相比,后者保证了单线程内程序的执行结果不被改变;前者保证正确同步的多线程程序的执行结果不被改变。
JSR-133中定义了以下的 happens-before 规则:
可见性有三种实现方式:
在一个线程中写一个变量,在另外一个线程中读一个变量,并且写和读没有经过同步来排序。
在理想化的顺序一致性内存模型中,有两大特性:
JMM对正确同步的多线程程序的内存一致性作了以下保证:若是程序是正确同步的,程序的执行将具备顺序一致性,也即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。
JMM 的实现方针为:在不改变正确同步的程序执行结果的前提下,尽量为优化提供方便。所以,JMM 与上述理想化的顺序一致性内存模型有以下差别:
Java中可使用volatile关键字来保证顺序性,还能够用synchronized和lock来保证。
volatile 关键字解决的是内存可见性的问题,会使得全部对 volatile 变量的读写都会直接刷新到主存,保证了变量的可见性。
要注意的是,使用 volatile 关键字仅能实现对原始变量操做的原子性(boolean,int,long等),不能保证符合操做的原子性(如i++)。
一个 volatile 变量的单个读/写操做,和使用同一个锁对普通变量的读/写操做进行同步,执行的效果是相同的。锁的 happens-before 规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着对一个 volatile 变量的读,总能看到对这个变量最后的写入,从而实现了可见性。须要注意的是,对任意单个 volatile 变量的读/写具备原子性,可是相似于i++这种复合操做不具备原子性。
当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到内存。 当读一个 volatile 变量时,JMM 会把线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
具体来讲,线程A写一个 volatile 变量,实质上是线程A向接下来将要读这个 volatile 变量的线程发出了它修改的信息;线程B读一个 volatile 变量,实质上是线程B接收了以前某个线程发出的修改信息。
JVM 是经过进入和退出对象监视器来实现同步的。Java 中的每个对象均可以做为锁。
JDK 1.6 中对 synchronized 进行了优化,为了减小获取和释放锁带来的消耗引入了偏向所和轻量锁。也就是说锁一共有四种状态,级别从低到高分别是:无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态。锁能够升级可是不能降级。
synchronized 使用的锁是存放在 Java 对象头中的。若是对象是数组类型,则虚拟机用3个Word(字宽)存储对象头,若是对象是非数组类型,则用2字宽存储对象头。
Java 头中包含了Mark Word,用来存储对象的 hashCode 或者锁信息,在运行期间其中存储的数据会随着锁的标志位的变化而变化。
大多数状况下,锁不只不存在多线程竞争,并且老是由统一线程屡次得到,为了让线程获取锁的代价更低而引入了偏向锁。
它的核心思想是:若是一个线程得到了锁,那么锁就进入偏向模式。当这个线程再次请求锁时,无须再作任何同步操做。这样就节省了大量有关锁申请的操做,从而提升了程序性能。所以,对于几乎没有锁竞争的场合,偏向锁有比较好的优化效果,由于连续屡次极有多是同一个线程请求相同的锁。而对于锁竞争比较激烈的场合,其效果不佳。
释放锁:当有另一个线程获取这个锁时,持有偏向锁的线程就会释放锁,释放时会等待全局安全点(这一时刻没有字节码运行),接着会暂停拥有偏向锁的线程,根据锁对象目前是否被锁来断定将对象头中的 Mark Word 设置为无锁或者是轻量锁状态。
加锁: 当代码进入同步块时,若是同步对象为无锁状态时,当前线程会在栈帧中建立一个锁记录(Lock Record)区域,同时将锁对象的对象头中 Mark Word 拷贝到锁记录中,再尝试使用 CAS 将 Mark Word 更新为指向锁记录的指针。若是更新成功,当前线程就得到了锁。若是更新失败 JVM 会先检查锁对象的 Mark Word 是否指向当前线程的锁记录。若是是则说明当前线程拥有锁对象的锁,能够直接进入同步块。不是则说明有其余线程抢占了锁,尝试使用自旋锁来获取锁。
**解锁:**轻量锁的解锁过程也是利用 CAS 来实现的,会尝试锁记录替换回锁对象的 Mark Word 。若是替换成功则说明整个同步操做完成,失败则说明有其余线程尝试获取锁,这时就会唤醒被挂起的线程(此时已经膨胀为重量锁)
锁类型 | 优势 | 缺点 | 使用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不须要额外的消耗,和执行非同步方法比仅存在纳秒级的差距 | 若是线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提升了程序的响应速度 | 若是始终得不到锁竞争的线程使用自旋会消耗CPU | 追求响应时间,锁占用时间很短 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,锁占用时间较长 |
释放锁与volatile写有相同的内存语义,线程A释放锁,是A向要获取锁的线程发出A对共享变量修改的消息。 获取锁与volatile读有相同的内存语义,是线程B接收了以前线程发出的堆共享变量作的修改的消息。
从 ReentrantLock 中能够看到:
在juc包中源代码实现,能够发现Java线程之间通讯的通用化实现模式:
对于 final 域,遵循两个重排序规则:
public class FinalExample{
int i;
final int j;
static FinalExample obj;
public FinalExample(){
i=1;
j=2;
}
public static void writer(){
obj=new FinalExample();
}
public static void reader(){
FinalExample object=obj;
int a=object.i;
int b=object.j;
}
}
复制代码
假设线程A执行 writer() 方法,线程B执行 reader() 方法。
写final域的重排序规则 写 final 域的重排序规则禁止把 final 域的写重排序到构造函数以外。从而确保了在对象引用被任意线程可见以前,对象的final域已经被正确的初始化过了。在上述的代码中,线程B得到的对象,final域必定被正确初始化,普通域i却不必定。
读final域的重排序规则 在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序该操做。从而确保在读一个对象的final域以前,必定会先读包含这个final域的对象的引用
final域为引用类型 在构造函数内对一个final引用的对象的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,不能重排序。
可是,要获得上述的效果,须要保证在构造函数内部,不能让这个被构造对象的引用被其余线程所见,也就是不能有this逸出。
https://juejin.im/post/5c122d00e51d4541284cc592