JUC - 多线程之JMM;volatile
Posted MinggeQingchun
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC - 多线程之JMM;volatile相关的知识,希望对你有一定的参考价值。
一、JMM
Java Memory Model(JMM)Java内存模型,区别与java内存结构。JMM定义了一套在多线程读写共享数据(变量、数组)时,对数据的可见性、有序性和原子性的规则和保障
(一)JMM结构规范
JMM规定了所有的变量都存储在主内存(Main Memory)中
每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)
不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成
在java中,所有实例域、静态域和数组元素存储在堆内存中,堆内存在线程之间共享(本文使用“共享变量”这个术语代指实例域,静态域和数组元素)。局部变量(Local variables),方法定义参数(java语言规范称之为formal method parameters)和异常处理器参数(exception handler parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响
(二)主内存和本地内存结构
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。本地内存它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化之后的一个数据存放位置
如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类 型的变量来说,load、store、read和write操作在某些平台上允许例外)
1、lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
2、unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量 才可以被其他线程锁定
3、read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便 随后的load动作使用
4、load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
5、use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机 遇到一个需要使用到变量的值,就会使用到这个指令
6、assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变 量副本中
7、store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中, 以便后续的write使用
8、write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内 存的变量中
JMM对这八种指令的使用,制定了如下规则
1、不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须 write
2、不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
3、不允许一个线程将没有assign的数据从工作内存同步回主内存
4、一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量 实施use、store操作之前,必须经过assign和load操作
5、一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解 锁
6、如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前, 必须重新load或assign操作初始化变量的值
7、如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
8、对一个变量进行unlock操作之前,必须把此变量同步回主内存
(三)JMM三个特征
Java内存模型保证了并发编程中数据的原子性、可见性、有序性
1、原子性
原子性是指一个操作是不可中断的,要么全部执行成功,要么全部执行失败。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰
多线程情况下,对同一个对象进行操作时,会导致字节码指令交错执行,从而产生原子性问题,可以通过synchronize关键字解决
原子性操作指相应的操作是单一不可分割的操作。在我们学化学这门课程的时候,对于里面讲到的原子性相信大家都非常明白,原子是微观世界中最小的不可再进行分割的单元,原子是最小的粒子。java里面的原子性操作也是如此,它代表着一个操作不能再进行分割是最小的执行单元。
原子性类似于事务操作,要么全部执行成功,要么回退到执行该操作之前的状态
i = 0; //1
j = i ; //2
i++; //3
i = j + 1; //4
上面四个操作,有哪个几个是原子操作,那几个不是?如果不是很理解,可能会认为都是原子性操作,其实只有1才是原子操作,其余均不是
1 在Java中,对基本数据类型的变量和赋值操作都是原子性操作;
2 中包含了两个操作:读取i,将i值赋值给j
3 中包含了三个操作:读取i值、i + 1 、将+1结果赋值给i;
4 中同三一样
在Java中,对基本数据类型的变量和赋值操作都是原子性操作
i = 0;
2、可见性
可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改
3、有序性(指令重排)
如果在本线程内观察,所有的操作都是有序的;(线程内表现为串行的语义)如果在一个线程中观察另外一个线程,所有的操作都是无序的
多线程情况下,JVM会进行指令重排,会影响有序性
有序性最终表述的现象是CPU是否按照既定代码顺序执行依次执行指令。编译器和CPU为了提高指令的执行效率可能会进行指令重排序,这使得代码的实际执行方式可能不是按照我们所认为的方式进行,在单线程的情况下只要保证最终执行结果正确即可
int i = 0; //语句1
boolean flag = false; //语句2
i = 1; //语句3
flag = true; //语句4
上面代码最终执行结果是i=1、flag=true,在不影响这个结果的情况下语句2可能比语句1先执行,语句4可能比语句3先执行
JMM提供了内置解决方案(happen-before 原则)及其外部可使用的同步手段(synchronized/volatile 等),确保了程序执行在并发编程中的 原子性,可视性及其有序性
(四)happen-before原则
happen-before是在JMM中用来实现并发编程中的有序性的。主要包括了以下八个规则:
1、程序顺序性原则:应该线程按照代码的顺序执行
2、锁原则:如果一个对象已经加锁,那么后续的再对其加锁,一定发生在解锁之后
3、对象终结原则:对象的构造函数一定发生在对象终结之前
4、volatile变量规则:被volatile修改的变量写操作,Happens-Before于任意后续对这个变量操作的读
跟线程相关的4个原则
1、线程启动原则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作
2、线程中断原则:线程中断发生在线程中断检查之前
3、线程的结束原则:如果线程A中执行了 ThreadB.join(),那么线程B的所有操作都发生在线程A的ThreadB.join()之后的操作
4、线程传递性原则:A happen-before B, B happen-before C ,那么A 一定 happen-before C
二、volatile
volatile关键字是Java虚拟机提供的的最轻量级的同步机制,它作为一个修饰符,用来修饰变量
volatile关键字保证变量对所有线程可见性,禁止指令重排,但是不保证原子性
(一)保证可见性
如下,我们有2条线程 t1 和 main主线程,num在主线程中改为1,但是分支线程t1 并不知道num已经变为1 ,还在根据 num == 0进行循环,程序一直在运行
此时,我们将全局变量 num 加上volatile修饰,t1线程立马结束循环
/**
* volatile关键字保证变量对所有线程可见性,禁止指令重排,但是不保证原子性
* 1、volatile保证可见性
*/
public class VolatileTest
// 不加 volatile 程序就会死循环!
// 加 volatile 可以保证可见性
private volatile static int num = 0;
public static void main(String[] args)
new Thread(() ->
while (num == 0) // 线程 t1 对主内存主线程 num = 1 的变化不知道
,"t1").start();
try
TimeUnit.SECONDS.sleep(1);
catch (InterruptedException e)
e.printStackTrace();
// 主线程中修改 num = 1
num = 1;
System.out.println(num);
(二)不保证原子性
public class VolatileTest1
private volatile static int num = 0;
public static void main(String[] args)
// 开启10条线程,每条线程执行1000次循环+1,理论结果执行完成 num=10000
for (int i = 0; i < 10; i++)
new Thread(() ->
for (int i1 = 0; i1 < 1000; i1++)
add();
).start();
while (Thread.activeCount()>2) // Java中默认开启了2条线程 main gc
Thread.yield();
System.out.println(Thread.currentThread().getName() + " num = " + num);
public static void add()
num++;
不管执行多少次,会发现 num 都不是 10000
如果不使用 Lock 和 Synchronized ,解决 volatile不保证原子性问题;使用 java.util.concurrent.atomic 包下的原子类操作
此时妥妥的稳稳的输出 num = 10000
/**
* volatile关键字保证变量对所有线程可见性,禁止指令重排,但是不保证原子性
* 2、volatile 不保证原子性
*
* 解决 volatile不保证原子性问题;使用java.util.concurrent.atomic 包下的原子类操作(不使用Lock和Synchronized)
*/
public class VolatileTest2Atomic
// volatile 不保证原子性
//private volatile static int num = 0;
private volatile static AtomicInteger num = new AtomicInteger();
public static void main(String[] args)
// 开启10条线程,每条线程执行1000次循环+1,理论结果执行完成 num=10000
for (int i = 0; i < 10; i++)
new Thread(() ->
for (int i1 = 0; i1 < 1000; i1++)
add();
).start();
while (Thread.activeCount()>2) // Java中默认开启了2条线程 main gc
Thread.yield();
System.out.println(Thread.currentThread().getName() + " num = " + num);
public static void add()
// 不是一个原子性操作
//num++;
// AtomicInteger + 1 方法, CAS
num.getAndIncrement();
(三)禁止指令重排
有序性最终表述的现象是CPU是否按照既定代码顺序执行依次执行指令。编译器和CPU为了提高指令的执行效率可能会进行指令重排序,这使得代码的实际执行方式可能不是按照我们所认为的方式进行,在单线程的情况下只要保证最终执行结果正确即可
int i = 0; //语句1
boolean flag = false; //语句2
i = 1; //语句3
flag = true; //语句4
上面代码最终执行结果是i=1、flag=true,在不影响这个结果的情况下语句2可能比语句1先执行,语句4可能比语句3先执行
/**
* volatile关键字保证变量对所有线程可见性,禁止指令重排,但是不保证原子性
* 3、volatile 禁止指令重排
*/
public class VolatileTest3
private static VolatileTest3 volatileTest3;
private static boolean isInit = false;
public static void main(String[] args)
for (int i = 0; i < 1000; i++)
volatileTest3 = null;
isInit = false;
new Thread(() ->
volatileTest3 = new VolatileTest3(); //语句1
isInit = true; //语句2
).start();
new Thread(() ->
if(isInit)
volatileTest3.doSomething();
).start();
public void doSomething()
System.out.println("doSomething");
我们所期望的结果应该是每次都会打印doSOmething,可是这里会报空指针异常,出现这种情况的原因就是因为指令重排导致,上面语句1和语句2最终执行顺序可能会变为语句2先执行,语句1还未执行,此时刚有有一个线程独到了isInit的值为true,此时通过对象取调用方法就报空指针,因为此时SerialTest对象还未被实例化
指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确
volatile关键字修饰时编译后会多出一个lock前缀指令
lock指令相当于一个内存屏障:重排序时不能把后面的指令重排序到内存屏障之前的位置
1、在每个 volatile 写操作的前面插入一个 StoreStore 屏障:禁止上面的普通写和下面的 volatile 写重排序
2、在每个 volatile 写操作的后面插入一个 StoreLoad 屏障:防止上面的 volatile 写与下面可能有的 volatile读/写重排序
3、在每个 volatile 读操作的后面插入一个 LoadLoad 屏障:禁止下面所有的普通读操作和上面的 volatile 读重排序
4、在每个 volatile 读操作的后面插入一个 LoadStore 屏障:禁止下面所有的普通写操作和上面的 volatile 读重排序
(四)volatile和synchronized区别
1、volatile是线程同步的轻量级实现,性能比synchronize好
2、volatile只能修饰变量,而synchronize可以修饰方法、代码块和变量
3、volatile多线程时不会发生阻塞,而synchronize会阻塞线程
4、volatile可以保证可见性和有序性(禁止指令重排),无法保证原子性,而synchronize都可以保证
volatile就是保证变量对其他线程的可见性和防止指令重排序
而synchronize解决多个线程访问资源的同步性
以上是关于JUC - 多线程之JMM;volatile的主要内容,如果未能解决你的问题,请参考以下文章
JUC高级多线程_11:JMM与Volatile的具体介绍与使用
Juc13_JVM-JMM-CPU底层执行全过程缓存一致性协议MESI
JUC并发编程 共享模型之内存 -- Java 内存模型 & 原子性 & 可见性(可见性问题及解决 & 可见性 VS 原子性 & volatile(易变关键字))(代码