-
JMM
-
JMM是JAVA内存对象模型
-
Java线程有两块存储区域
- 线程工作内存
- 主内存
-
由于线程都有自己内部的独立的工作内存,当多个线程操作一个共享变量时,会存在不一致的问
-
看看下面的问题代码
/** * 该实例程序的原意是想在主线程中修改了共享变量num的值后 让子线程停止循环 * 但是子线程会一直循环,不知道什么时候停止 */ public class Demo1 { private static int num = 0; public static void main(String[] args) { new Thread(()->{ while(num == 0){ //在该线程中判断共享变量的值 //满足条件时 线程在这里循环 } }).start(); try { //主线程等待一秒 TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } //在主线程中修改共享变量 num = 1; System.out.println(num); } }
结果
-
原因:
子线程在启动时读取到了num的值为0,
然后主线程把num的值改为了1,
这时子线程中的num值还是为0,对主内存中的变化是不可见的。
解决方法:
用volatile修饰共享变量
private static volatile int num = 0;
-
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
-
JMM对这八种指令的使用,制定了如下规则
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存
-
volatile关键字
作用
-
保证可见性
当线程更改了共享变量后,这个改变在其他线程中是可见的
public class Demo1 { private static volatile int num = 0; public static void main(String[] args) { new Thread(()->{ while(num == 0){ //在该线程中判断共享变量的值 //满足条件时 线程在这里循环 } }).start(); try { //主线程等待一秒 TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } //在主线程中修改共享变量 num = 1; System.out.println(num); } }
共享变量num在主线程中被改为1后,子线程立马结束。
-
不保证原子性
public class Demo2 { private static volatile int num = 0; public static void main(String[] args) { //开启20条线程 for(int i = 1;i <= 20;i++){ new Thread(()->{ //每条线程循环1000次 for (int j = 1; j <= 1000 ; j++) { //下面这个++的操作 虽然num是volatile变量 也会出现多线程并发问题 num++; } }).start(); } //判断当前还有多少线程在执行 //除开当前main线程和gc线程 while (Thread.activeCount() > 2){ Thread.yield(); } System.out.println(num); } }
执行结果,(多次执行结果不相同)
解决方法,使用原子类替换
public class Demo2 { private static AtomicInteger num = new AtomicInteger(); public static void main(String[] args) { //开启20条线程 for(int i = 1;i <= 20;i++){ new Thread(()->{ //每条线程循环1000次 for (int j = 1; j <= 1000 ; j++) { num.getAndIncrement(); //这个是一个CAS操作 } }).start(); } //判断当前还有多少线程在执行 //除开当前main线程和gc线程 while (Thread.activeCount() > 2){ Thread.yield(); } System.out.println(num); } }
每次的执行结果都是20000
-
禁止指令重排
内存屏障原理
被volatile关键字修饰的变量,在被操作的前后会添加内存屏障。
-