多线程之内存可见性

一、什么是可见性?

一个线程对共享变量值的修改,能够及时地被其他线程所看到。

共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量。

工作内存:每个线程拥有自己的工作内存,只能对自己工作内存中的变量副本进行修改,而不能直接修改主内存中的变量。

变量副本:主内存中变量的一份拷贝

 

二、主内存与工作内存之间的关系

【JAVA】多线程之内存可见性_共享变量

注意:

(1)线程对共享变量的所有操作都必须在自己的工作内存中进行,不能绕过工作内存直接从主内存中读写变量

(2)不同线程之间无法直接访问其他线程工作内存中的变量,线程之间变量值的传递需要通过主内存来完成

 

三、共享变量可见性实现的原理

线程1对共享变量的修改要想被线程2及时看到,必须要经过如下的2个步骤

(1)把工作内存1中更新过的共享变量刷新到主内存中

(2)将主内存中最新的共享变量的值更新到工作内存2中

变量传递顺序

【JAVA】多线程之内存可见性_synchronized_02

 

四、可见性实现方式

(1)synchronized,能够实现原子性(同步)与可见性

(2)volatile,能够实现变量可见性,但不能保证变量的原子性


【1】synchronized实现可见性

JMM(java内存模型)中关于synchronized的两条规定

  • 线程解锁前,必须把共享变量的最新值刷新到主内存中
  • 线程加锁时,清空工作内存中的共享变量的值,从而使用共享变量时需要从主内存中读取最新的值

那么,线程执行互斥代码的流程就是:

(1)获得互斥锁

(2)清空工作内存

(3)从主内存中拷贝变量的最新副本到工作内存中

(4)执行互斥代码

(5)将更改后的共享变量的值刷新到主内存中

(6)释放互斥锁


【2】volatile实现可见性

通过加入内存屏障和禁止指令重排序优化来实现的。每次读取用volatile修饰的变量的值,都会从主内存中读取该变量。

通俗地讲:volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。这样在任何时刻,不同的线程总能看到该变量的最新值。

那么,线程写volatile变量的过程:

(1)改变线程工作内存中volatile变量副本的值

(2)将改变后的副本的值从工作内存刷新到主内存

线程读volatile变量的值的过程:

(1)从主内存中读取volatile变量的最新值到线程的工作内存中

(2)从工作内存中读取volatile变量的副本


【3】volatile不能保证变量复合操作的原子性

例如:以下操作不是原子操作

private int number=0;
number++;

可以分解成如下操作:

(1)读取number的值

(2)将number的值加1

(3)写入最新的number的值


假如我们使得volatile int  i=0;并且大量线程调用i的自增操作,那么volatile可以保证变量的安全吗?

不可以保证,volatile不能保证变量操作的原子性,自增操作包括三个步骤,分别是读取,加一,写入,由于这三个子操作的原子性不能被保证,那么n个线程总共调用n次i++的操作后,最后的i的值并不是大家想的n,而是一个比n小的数。

解释:比如A线程执行自增操作,刚读取到i的初始值0,然后就被阻塞了。B线程现在开始执行,还是读取到i的初始值0,执行自增操作,此时i的值为1。然后A线程阻塞结束,对刚才拿到的0执行加一与写入操作,执行成功后,i的值被写成1了,我们预期输出2,可是输出的是1,输出比预期小。

代码验证:

package day0829;

import java.util.ArrayList;
import java.util.List;

public class VolatileTest {
public volatile int i = 0;

public void increase() {
i++;
}

public static void main(String args[]) throws InterruptedException {
List<Thread> threadList = new ArrayList<>();
VolatileTest test = new VolatileTest();
for (int j = 0; j < 10000; j++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
test.increase();
}
});
thread.start();
threadList.add(thread);
}

//等待所有线程执行完毕
for (Thread thread : threadList) {
thread.join();
}
System.out.print(test.i);
}
}

此时输出为:

【JAVA】多线程之内存可见性_共享变量_03


那么如何保证i自增操作的原子性呢?

(1)使用synchronized关键字

(2)使用ReentranLock

(3)使用AtomicInteger


相关问题:

(1)即使没有保证可见性的措施,很多时候共享变量依然能够在主内存和工作内存中得到及时的更新?

一般只有在短时间内高并发的情况下才会出现变量得不到及时更新的情况,因为cpu在执行时会很快地刷新缓存,所以一般情况下很难看到这种不可见的问题。

 

五、synchronized和volatile的区别


六、多线程中其他知识点

指令重排序

代码书写的顺序与程序实际执行的顺序不同,指令重排序是编译器或处理器为了提高性能而做的优化。


as-if-serial语义

无论如何进行重排序,程序执行的结果与代码原本顺序执行的结果一致(java会保证在单线程下遵循此语义)

重排序不会给单线程带来内存可见性问题,但在多线程中,程序交错执行,重排序可能会造成内存可见性问题