目录
- Java内存模型
- 可见性例子和volatile
- volatile如何保证可见性
- 原子性与单例模式
- i++非原子性
- 线程安全
Java内存模型
参考学习: Java Memory Model外文文档
-
CPU与内存,可参考:https://blog.csdn.net/qq_26437925/article/details/145303267
-
Java线程与内存
-
主内存:java虚拟机规定所有的变量(不是程序中的变量)都必须在主内存中产生;为了方便理解,可以认为是堆区。可以与前面说的物理机的主内存相比,只不过物理机的主内存是整个机器的内存,而虚拟机的主内存是虚拟机内存中的一部分。
-
工作内存:java虚拟机中每个线程都有自己的工作内存,该内存是线程私有的;为了方便理解,可以认为是虚拟机栈。线程的工作内存保存了线程需要的变量在主内存中的副本。虚拟机规定,线程对主内存变量的修改必须在线程的工作内存中进行,不能直接读写主内存中的变量。不同的线程之间也不能相互访问对方的工作内存。如果线程之间需要传递变量的值,则必须通过主内存来作为中介进行传递。
主内存与Java工作内存之间的具体交互协议,虚拟机保证如下的每一种操作都是原子的,不可再分的(对于double,long类型的变量有例外,商用JVM基本优化了这个问题)
-
lock: 作用于主内存的变量,它把一个变量标识为一个线程独占的状态
-
unlock:作用于主内存的变量, 解锁
-
load:作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
-
use:作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作
-
assign: 作用于工作内存的变量,它把一个从执行引擎接收到的赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
-
store: 作用于工作内存的变量,它把工作内存中的一个变量的值传送到主内存中,以便随后的write操作使用
-
write:作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
可见性例子和volatile
public class Main {
private static volatile Boolean flag = true;
public static void main(String[] args) throws Exception{
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("A start");
while (flag) {
}
System.out.println("A end");
}
});
thread.start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
}
flag = false;
System.out.println("main end");
}
}
输出如下:
主线程后执行,设置了flag=false
,由于volatile的作用,导致线程可见flag
,所以线程A可以结束。
volatile如何保证可见性
硬件层两个内存屏障:load barrier、store barrier;其有两个功能:
- 禁止屏障前后的指令重排序
- 强制把写缓冲区的的数据写入主内存
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad Barriers | Load1;LoadLoad;Load2 | 该屏障确保Load1数据的装载先于Load2及其后所有装载指令的的操作 |
StoreStore Barriers | Store1;StoreStore;Store2 | 该屏障确保Store1立刻刷新数据到内存(使其对其他处理器可见)的操作先于Store2及其后所有存储指令的操作 |
LoadStore Barriers | Load1;LoadStore;Store2 | 确保Load1的数据装载先于Store2及其后所有的存储指令刷新数据到内存的操作 |
StoreLoad Barriers | Store1;StoreLoad;Load2 | 该屏障确保Store1立刻刷新数据到内存的操作先于Load2及其后所有装载装载指令的操作。它会使该屏障之前的所有内存访问指令(存储指令和访问指令)完成之后,才执行该屏障之后的内存访问指令 |
其中StoreLoad Barriers同时具备其他三个屏障的效果,因此也称之为全能屏障(mfence),是目前大多数处理器所支持的;但是相对其它屏障,该屏障的开销相对昂贵。
volatile
正是通过加入内存屏障,禁止指令重排优化来实现可见性和有序性,即
- 每个volatile写操作的前面插入一个
StoreStore
屏障; - 在每个volatile写操作的后面插入一个
StoreLoad
屏障(全能屏障); - 在每个volatile读操作的前面插入一个
LoadLoad
屏障; - 在每个volatile读操作的后面插入一个
LoadStore
屏障。
所以线程写volatile变量的过程:
- 改变线程工作内存的中volatile变量副本的值。
- 将改变后的副本的值从工作内存刷新到主内存。
线程读volatile变量的过程:
- 从主内存中读取volatile变量的最新值到线程的工作内存中。
- 从工作内存中读取volatile变量的副本。
如上例子中,当主线程写flag时,会将数据刷新到主内存中;而线程thread读取的时候,也是确保读取到的是主内存数据,所有能够实现例子代码中的可见性验证。
原子性与单例模式
class Singleton{
private byte[] data = new byte[1024];
private static Singleton instance = null;
public static Singleton getInstance(){
if (null == instance) {
synchronized (Singleton.class) {
System.out.println("new Singleton");
instance = new Singleton();
}
}
return instance;
}
}
public class Main {
public static void main(String[] args) throws Exception{
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
Singleton singleton = Singleton.getInstance();
}).start();
}
}
}
多次运行,可以看到有输出如下的例子:
不是double check的单例模式,实际上会new出多个实例,无法实现单例模式。
因为Object o = new Object();
的汇编指令如下,不是一个原子操作
0 new #2 <java/lang/Object>
3 dup
4 invokespecial #1 <java/lang/Object.<init>>
7 astore_1
8 return
一个对象创建的过程:(记住3步)
- 堆内存中申请了一块内存(new指令)【半初始化状态,成员变量初始化为默认值】
- 这块内存的构造方法执行(invokespecial指令)
- 栈中变量建立连接到这块内存(astore_1指令)
i++非原子性
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class Main {
public volatile static int num = 0;
public static void add() {
num++;
}
public synchronized static void addSync() {
num++;
}
private final static int N = 30;
public static void main(String[] args) throws Exception {
Thread[] threads = new Thread[N];
for(int i=0;i<N;i++){
threads[i] = new Thread(()->{
try{
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(10));
int addCnt = 100;
for(int j=0;j<addCnt;j++){
add();
}
}catch (Exception e){
e.printStackTrace();
}
});
threads[i].start();
}
for(int i=0;i<N;i++) {
threads[i].join();
}
System.out.println("num:" + num);
}
}
/* output
小于3000的值
*/
线程安全
- 什么是线程安全问题?
当多个线程共享同一个全局变量,做写的时候,可能会受到其它线程的干扰,导致数据有问题,这中现象叫做线程安全问题
关键词:共享数据,多线程,并发写操作
结合本文和上一篇:https://blog.csdn.net/qq_26437925/article/details/145303267 看到了原子性,可见性,顺序性三个重要性质,这构成了多线程和线程安全编程的基础。