一、Java内存模型
1、前言
Monitor 主要关注的是访问共享变量时,保证临界区代码的原子性。
2、定义
JMM 即 Java Memory Model,它定义了主存(所有线程共享的数据存放位置)、工作内存(每个线程私有的数据存放位置)抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。
JMM 体现在以下几个方面:
- 原子性 - 保证指令不会受到线程上下文切换的影响
- 可见性 - 保证指令不会受 CPU 缓存的影响
- 有序性 - 保证指令不会受 CPU 指令并行优化的影响
3、可见性(volatile)
public class Test05 {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while (run) {
// ....
}
});
t.start();
sleep(1000);
run = false;//线程t1不会如预想的停下来
}
}
分析:
- 初始状态,t 线程 刚开始从主内存读取了 run 的值到工作内存。
- 因为 t线程 需要频繁从主内存中读取 run 的值,JIT编译器 会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率。
- 1秒之后,main 线程修改了 run 的值,并同步至主内存,而 t线程 是从自己工作内存中的高速缓存中读取 run这个变量的值,结果永远是之前的值(true)。
解决办法:
volatile
关键字
- 可以用来修饰成员变量和静态成员变量,保证线程间变量的可见性,但不能保证原子性。
- 线程对变量进行修改后,要立刻写回主内存中。线程操作 volatile 变量都是直接操作主存。
- 线程对变量读取的时候,必须到主内存中读取变量的值,而不是缓存中。
public class Test05 {
volatile static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while (run) {
// ....
}
});
t1.start();
sleep(1000);
run = false;//线程t1会停止
}
}
使用 synchronized
也能达到预期效果
public class Test05 {
final static Object obj = new Object();
static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while (flag) {
synchronized (obj) {
if (!flag) {
break;
}
}
}
});
t1.start();
sleep(1000);
synchronized (obj){
flag = false;
}
}
}
4、原子性
原子是世界上的最小单位,具有不可分割性。比如 a = 0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作是原子操作。再比如 a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。
5、可见性 vs 原子性
上面的例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 的变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况。上面例子从字节码理解:
getstatic run //线程 t 获取 run(true)
getstatic run //线程 t 获取 run(true)
getstatic run //线程 t 获取 run(true)
getstatic run //线程 t 获取 run(true)
getstatic run //线程 main 修改 run 为 false,仅此一次
getstatic run //线程 t 获取 run(false)
注意:
synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低 。
如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到 对 run 变量的修改了,查看 println 源码即可知道原因。
6、利用volatile改进两阶段终止模式
两阶段终止模式详见 并发编程(二)
public class Test06 {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination tpt = new TwoPhaseTermination();
tpt.startMonitor();
Thread.sleep(3500);
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - stop");
tpt.stopMonitor();
}
}
//监控类
class TwoPhaseTermination{
//监控线程
private Thread monitor;
//停止标记
private volatile boolean stop = false;
//启动监控线程
public void startMonitor(){
monitor = new Thread(()->{
//循环监控
while (true) {
//获取当前线程
Thread current = Thread.currentThread();
//为true终止循环
if (stop) {
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 料理后事...");
break;
}
try {
Thread.sleep(1000); //情况1
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 执行监控记录"); //情况2
} catch (InterruptedException e) {
}
}
},"监控线程");
//启动线程
monitor.start();
}
//停止监控线程
public void stopMonitor(){
stop = true;
monitor.interrupt();//打断sleep
}
}
7、同步模式之Balking
Balking(犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做同样的事了,直接返回结束。
当多次调用 startMonitor() 时,保证只有一个监控线程启动
public class Test06 {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination tpt = new TwoPhaseTermination();
tpt.startMonitor();
tpt.startMonitor();
}
}
//监控类
class TwoPhaseTermination{
//监控线程
private Thread monitor;
//停止标记
private volatile boolean stop = false;
//判断是否执行过startMonitor方法
private boolean starting = false;
//启动监控线程
public void startMonitor(){
//保证每次只有一个监控线程启动
synchronized (this) {
if (starting) {
return;
}
starting = true;
}
monitor = new Thread(() -> {
//循环监控
while (true) {
//获取当前线程
Thread current = Thread.currentThread();
//为true终止循环
if (stop) {
System.out.println(new Date() + "【" + Thread.currentThread().getName() + "】 - 料理后事...");
break;
}
try {
Thread.sleep(1000); //情况1
System.out.println(new Date() + "【" + Thread.currentThread().getName() + "】 - 执行监控记录"); //情况2
} catch (InterruptedException e) {
}
}
}, "监控线程");
//启动线程
monitor.start();
}
//停止监控线程
public void stopMonitor(){
stop = true;
monitor.interrupt();//打断sleep
}
}
实现线程安全的单例模式(懒汉式)
public class Singleton {
//构造私有
private Singleton(){ }
//私有的静态属性
private static Singleton Instance = null;
//公共的静态方法
public static synchronized Singleton getInstance(){
if (Instance != null){
return Instance;
}
Instance = new Singleton();
return Instance;
}
}
8、有序性(指令重排)
JVM 会在不影响正确性的前提下,可以调整语句的执行顺序。
static int i;
static int j;
//在某个线程中进行赋值操作
i = ...;
j = ...;
//无论先执行 i 或 j,对最终的结果不会产生影响
//既可以
i = ...;
j = ...;
//也可以是
j = ...;
i = ...;
这种特征称之为【指令重排】,多线程下指令重排会影响正确性。
诡异的结果
int num = 0;
boolean ready = false;
//线程1执行此方法
public void actor1(User u){
if(ready){
u.age = num + num;
}else{
u.age = 1;
}
}
//线程2执行此方法
public void actor2(User u){
num = 2;
ready = true; //此处可能会发生指令重排序,可能先执行num=2也可能先执行ready=true
}
User 是一个对象,有一个属性 age 用来保存结果,可能会出现的几种情况:
- 情况1:线程1先执行,这时 ready = false,所以进 else 分支结果为1。
- 情况2:线程2先执行 num = 2,但没有来得及执行 ready = true,线程1执行,还是进else分支,结果为1。
- 情况3:线程2先执行 num = 2,执行 ready = true,线程1执行,进 if 分支结果为4。
- 情况4:线程2先执行 ready = true,切换到线程1,进入 if 分支,相加结果为0,再回线程2执行 num = 2。
这种现象叫做指令重排,是 JIT 编辑器在运行时的一些优化,这个现象需要通过大量测试才能复现。
借助 java 并发压测工具 jcstress 复现:
mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jcstress DarchetypeArtifactId=jcstress-java-test-archetype -DarchetypeVersion=0.5 -DgroupId=cn.itcast DartifactId=ordering -Dversion=1.0
创建 maven 项目,测试类:
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State public class ConcurrencyTest {
int num = 0;
boolean ready = false;
@Actor
public void actor1(user u) {
if(ready) {
u.age = num + num;
} else {
u.age = 1;
}
}
@Actor
public void actor2(User u) {
num = 2;
ready = true;
}
}
执行:
mvn clean install
java -jar target/jcstress.jar
结果:
*** INTERESTING tests
Some interesting behaviors observed. This is for the plain curiosity.
2 matching test results.
[OK] test.ConcurrencyTest
(JVM args: [-XX:-TieredCompilation])
Observed state Occurrences Expectation Interpretation
0 1,729 ACCEPTABLE_INTERESTING !!!!
1 42,617,915 ACCEPTABLE ok
4 5,146,627 ACCEPTABLE ok
[OK] test.ConcurrencyTest
(JVM args: [])
Observed state Occurrences Expectation Interpretation
0 1,652 ACCEPTABLE_INTERESTING !!!!
1 46,460,657 ACCEPTABLE ok
4 4,571,072 ACCEPTABLE ok
解决办法:
volatile 修饰变量,可以禁用指令重排
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State public class ConcurrencyTest {
int num = 0;
volatile boolean ready = false;
@Actor
public void actor1(User u) {
if(ready) {
u.age = num + num;
} else {
u.age = 1;
}
}
@Actor
public void actor2(User u) {
num = 2;
ready = true;
}
}
结果为:
*** INTERESTING tests
Some interesting behaviors observed. This is for the plain curiosity.
0 matching test results.
9、volatile原理
1、说明
volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
- 对 volatile 变量的写指令后会加入写屏障
- 对 volatile 变量的读指令后会加入读屏障
2、如何保证可见性
1、写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中。
public void actor2(User u){
num = 2;
ready = true;//ready 是 volatile 赋值带写屏障,会同步到主存中
//写屏障之后
...
}
2、读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新的数据。
public void actor1(user u){
//读屏障
//ready 是 volatile读取值带读屏障
if(ready){
u.age = num + num;
} else {
u.age = 1;
}
}
3、如何保证有序性
1、写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后。
public void actor2(User u){
num = 2;
ready = true;//ready 是 volatile 赋值带写屏障,会同步到主存中
//写屏障之后
...
}
2、读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前。
public void actor1(user u){
//读屏障
//ready 是 volatile读取值带读屏障
if(ready){
u.age = num + num;
} else {
u.age = 1;
}
}
不能解决指令交错:
- 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面的结果。
- 有序性的保证也只能保证了本线程内相关代码不被重排序。
说明: t1 线程做 i+1 操作,写入1,如果对 i 加了 volatile,这里就是写屏障,只能保证写屏障之前的更改会同步到主内存中,但是它控制不了 t2 线程可能会在它之前去读取(提前读)值,最后结果是 -1。
10、double-checked-locking模式
public final class Singleton{
private Singleton(){}
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance(){
//实例没创建,才会进入内部的 synchronized代码块
if(INSTANCE == null){
synchronized(Singleton.class){
//此处再判断一次,可能会出现其他线程创建了实例
if(INSTANCE == null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}