Java并发编程—内存模型
什么是并发?
并发是指多个执行单元同时并行执行,并发通常能够加大系统的利用率和吞吐量。不过并发的执行单位经常会造成共享资源出现竞争状态。
Java内存模型
在Java并发中,线程间的通信是通过Java内存模型(Java Memory Model)控制的。JMM能屏蔽掉各种硬件和操作系统的内存访问差异,使Java在各种平台让运行的效果一致。要实现Java并发编程我们必须要了解Java内存模型(JMM)。
下图是Java内存模型的示意图:
我们都知道Java中堆中的数据都是线程共享的,栈中的数据则是一个线程对应一个栈内存。所以在并发编程是我们并不需要考虑栈的并发问题。
由上面的内存模型可以知道,每一个线程都存在本地内存(本地内存中存放着共享变量);而所以的共享变量都是存在的在主内存中的,本地内存其实是一个虚拟内存,设计的目的是为了减少线程共享对象和主内存交互的次数,提高执行的效率。所以,这里就会涉及到线程间的通信和同步。
如果线程A与线程B需要进行的通信的话,比如线程A更新了内存值,然后当线程之间需要通信时会将线程A中本地内存值更新到主内存中,然后线程B就会去读取主内存中的值来更新本地内存中的值。JMM通过控制主内存与每个线程的本地内存之间的交互来为Java程序员提供内存可见性保证。
Java内存模型的三大特性
在JMM中有三个重要的特性必须要知道,这三个特点可以说是JMM的基础。
原子性:原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。
可见性:可见性就是指当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改(上面已经讲解)。
有序性:有序性即程序执行的顺序按照代码的先后顺序执行。
1.原子性
原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意 为“不可被中断的一个或一系列操作”。在Java主要是通过锁和CAS的方式来实现原子操作。
1.1.通过循环CAS的方式是实现原子操作
CAS其实就是Compare And Swap,意思就是比较然后再更新值。大致的流程如下所示:
for(;;) { // 循环
// 获取要更新的对象最新的值
// ......
// 然后进行原子的比较和更新新值
// ......
}
当然CAS有可能会出现以下三个问题:
1)ABA问题:就是当我们需要更新值时,我们获取的值为A,在这个时候其他线程对该值进行了操作改为B,很快当我们还没有比较的时候又有线程对B进行了修改,改回了A。对我们的更新来说我们无法区别该值是否已经改变。对于ABA问题的解决方案就是对数据增加版本号或者时间戳。
2)循环时间长开销比较大:如果循环CAS长时间更新不成功,会给CPU带来比较大的开销。
3)只能保证一个共享变量的原子操作:解决这个问题也很简单,只要将所有的对象封装到一个类中就可以了,如果必须要这样做。
1.2.通过加锁的方式实现
加锁可以保证在执行共享变量时,必须要获取得到锁对象才能执行锁定的代码块。在Java中包括有偏向锁、轻量级锁和互斥锁。这里简单介绍一下这三种锁:
偏向锁:顾名思义该锁是有偏向的锁,当线程去获取锁时会将自己的Thread ID放置到锁信息中(Mark Word)中。如果操作执行完毕,需要再次获取锁,就会先去锁中获取Thread ID进行对比。如果一致,则直接获取得到该锁,如果不是则说明该锁有竞争,然后就会将改锁升级为轻量级锁。说一个最直接的应用就是Vector,Vector是线程安全的List,如果进行增删查改时,取消偏向锁,每次都需要去获取锁那效率将会大大的降低。
轻量级锁:这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。当存在锁竞争的情况下,执行CAS指令失败的线程将调用操作系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒。
重量级锁:所谓的重量级锁就是synchronized和lock实现,通过获取锁对象来获取执行权。
下面是三种锁的优缺点对比:
2.有序性
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。但是重排序需要满足一定的条件,不是所有的指令都能重排序。JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁 止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。使用重排序,需要满足以下两个条件:
1. 在单线程环境下不能改变程序运行的结果
2. 存在数据依赖关系的不允许重排序
2.1.数据依赖性
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间 就存在数据依赖性。数据依赖分为下列3种类型:
上面3种情况,只要重排序两个操作的执行顺序,程序的执行结果就会被改变。
面提到过,编译器和处理器可能会对操作做重排序。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。
这里说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。
2.2.as-if-serial语义
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程) 程序的执行结果不能被改变。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因 为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被 编译器和处理器重排序。为了具体说明,请看下面计算圆面积的代码示例。
double pi = 3.14; // A
double r = 1.0; // B
double area = pi * r * r; // C
上面3个操作的数据依赖关系,如下图所示:
A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。因此在 最终执行的指令序列中,C不能被重排序到A和B的前面。但A和B之间没有数据依赖关系,编译器和处理器可以重排序A和B之间的执行顺序。
as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器、runtime和处理器 共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。asif-serial语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。这里《Java并发编程艺术》中已经说的很详细,所以就直接摘录下来。
3.对多线程环境下有序性的处理
基于上面有序性的特点,所以在单线程的情况下并不会发生任何的问题,重排序也不会影响最后的结果。但是在多线程环境下就不同了,因为控制依赖的重排序只对单个线程有效。比如:线程A有顺序操作:A1->A2->A3;线程B有操作B1->B2->B3。那么他们执行顺序就会有很多种,比如A1->B1->B2->B3->A2->A3等。如果线程A和B之间有操作共享变量,那就有可能出现问题,不过我们也可以看到,所有的操作对单个线程内是依赖有序的。这里先说一下JMM处理的结果就是基本不处理,根据书《Java并发编程的艺术》的原文理解的。
对于未同步或未正确同步的多线程程序,JMM只提供最小安全性:线程执行时读取到的 值,要么是之前某个线程写入的值,要么是默认值(0,Null,False),JMM保证线程读操作读取 到的值不会无中生有(Out Of Thin Air)的冒出来。为了实现最小安全性,JVM在堆上分配对象 时,首先会对内存空间进行清零,然后才会在上面分配对象(JVM内部会同步这两个操作)。因 此,在已清零的内存空间(Pre-zeroed Memory)分配对象时,域的默认初始化已经完成了。
理想化的顺序一致性模型
顺序一致性模型是一个理想状态的模型,它为程序员提供了极强的内存可见性保证。顺序一致性内存模型有两大特性。
1)一个线程中的所有操作必须按照程序的顺序来执行。
2)(不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。
顺序执行性的结构模型如下所示:
在概念上,顺序一致性模型有一个单一的全局内存,这个内存通过一个左右摆动的开关 可以连接到任意一个线程,同时每一个线程必须按照程序的顺序来执行内存读/写操作。其实就是将多个并发的线程进行排队执行。比如,线程A有A1->A2操作;线程B有B1->B2操作;线程C有C1->C2操作。而最后执行的顺序可能为A1->A2->B1->B2->C1->C2,线程见使用监视器锁来正确同步。我个人的理解也就是说单个线程的操作我们可以理解为原子操作,单个线程操作完毕后才能执行下一步操作。
不过在JMM中并没有采用这种理想的顺序一致性模型。主要还是效率问题,使用这种强一致性模型,JMM需要禁止大量的处理器和编译器的优化,这对程序的执行 性能会产生很大的影响。
内存屏障
内存屏障(Memory Barrier)是一种CPU指令。为了保证内存可见性,java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。内存屏障分为读屏障(Load Barrier)和写屏障(Store Barrier)。
Load Barrier:在指令前插入Load Barrier会强制的从主内存中读取新的数据
Store Barrier:在指令后插入Store Barrier会强制的将缓存中的数据写入到主内存中,让其他线程可见。
这样下来内存屏障就会有四种类型,如下所示:
总结
最后用一小段话总结:Java内存模型其实就是将多个线程理解成多个线程通道,每个线程通道有自己的本地内存,线程间的通信通过主内存实现。同时为了提高Java的执行速度,JMM会对代码进行有规则的重排序。这种排序只对单线程没有影响,对还是多线程还是会执行顺序混乱。JMM不会使用内存一致性模型来帮助我们进行代码同步,所以我们需要自己来设计。
上一篇: Java的内存模型(JMM)
下一篇: Java虚拟机内存模型