0.简介
前一篇文章《Synchronized 用法原理和锁优化升级过程》从面试角度详细分析了 synchronized 关键字原理,本篇文章主要围绕 volatile 关键字用代码分析下可见性,原子性,有序性,synchronized 也辅助证明一下,来加深对锁的理解。<br />
<br />**
<a name="XQJYM"></a>
1.可见性
<a name="GUU7n"></a>
1.1 不可见性
A 线程操作共享变量后,该共享变量对线程 B 是不可见的。我们来看下面的代码。
package com.duyang.thread.basic.volatiletest;
/**
* @author :jiaolian
* @date :Created in 2020-12-22 10:10
* @description:不可见性测试
* @modified By:
* 公众号:叫练
*/
public class VolatileTest {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
while (flag){
//注意在这里不能有输出
};
System.out.println("threadA over");
});
threadA.start();
//休眠100毫秒,让线程A先执行
Thread.sleep(100);
//主线程设置共享变量flag等于false
flag = false;
}
}
复制代码
上述代码中,在主线程中启动了线程 A,主线程休眠 100 毫秒,目的是让线程 A 先执行,主线程最后设置共享变量 flag 等于 false,控制台没有输出结果,程序死循环没有结束不了。如下图所示主线程执行完后 flag = false 后 Java 内存模型(JMM),主线程把自己工作内存的 flag 值设置成 false 后同步到主内存,此时主内存 flag=false,线程 A 并没有读取到主内存最新的 flag 值(false),主线程执行完毕,线程 A 工作内存一直占着 cpu 时间片不会从主内存更新最新的 flag 值,线程 A 看不到主内存最新值,A 线程使用的值和主线程使用值不一致,导致程序混乱,这就是线程之间的不可见性,这么说你应该能明白了。线程间的不可见性是该程序死循环的根本原因。<br />
<a name="tomSB"></a>
1.2 volatile 可见性
上述案例中,我们用代码证明了线程间的共享变量是不可见的,其实你可以从上图得出结论:只要*线程 A 的工作内存能够感知*主内存中共享变量 flag 的值发生变化就好了,这样就能把最新的值更新到 A 线程的工作内存了,你只要能想到这里,问题就已经结束了,没错,volatile 关键字就实现了这个功能,线程 A 能感知到主内存共享变量 flag 发生了变化,于是强制从主内存读取到 flag 最新值设置到自己工作内存,所以想要 VolatileTest 代码程序正常结束,用 volatile 关键字修饰共享变量 flag,private volatile static boolean flag = true;就大功告成。volatile 底层实现的硬件基础是基于硬件架构和缓存一致性协议。如果想深入下,可以翻看上一篇文章《*可见性是什么?(通俗易懂)*》。一定要试试才会有收获哦!<br />
<a name="axYMm"></a>
1.3 synchronized 可见性
synchronized 是能保证共享变量可见的。每次获取锁都重新从主内存读取最新的共享变量。
package com.duyang.thread.basic.volatiletest;
/**
* @author :jiaolian
* @date :Created in 2020-12-22 10:10
* @description:不可见性测试
* @modified By:
* 公众号:叫练
*/
public class VolatileTest {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
while (flag){
synchronized (VolatileTest.class){
}
};
System.out.println("threadA over");
});
threadA.start();
//休眠100毫秒,让线程A先执行
Thread.sleep(100);
//主线程设置共享变量flag等于false
flag = false;
}
}
复制代码
上述代码中,我在线程 A 的 while 循环中加了一个同步代码块,synchronized (VolatileTest.class)锁的是 VolatileTest 类的 class。最终程序输出"threadA over",程序结束。可以得出结论:线程 A 每次加锁前会去读取主内存共享变量 flag=false 这条最新的数据。由此证明 synchronized 关键字和 volatile 有相同的可见性语义。<br />
<a name="DH5Ie"></a>
2.原子性
<a name="Rr7iw"></a>
2.1 原子性
原子性是指一个操作要么成功,要么失败,是一个不可分割的整体。
<a name="jEtIq"></a>
2.2 volatile 非原子性
/**
* @author :jiaolian
* @date :Created in 2020-12-22 11:22
* @description:Volatile关键字原子性测试
* @modified By:
* 公众号:叫练
*/
public class VolatileAtomicTest {
private volatile static int count = 0;
public static void main(String[] args) throws InterruptedException {
Task task = new Task();
Thread threadA = new Thread(task);
Thread threadB = new Thread(task);
threadA.start();
threadB.start();
//主线程等待AB执行完毕!
threadA.join();
threadB.join();
System.out.println("累加count="+count);
}
private static class Task implements Runnable {
@Override
public void run() {
for(int i=0; i<10000; i++) {
count++;
}
}
}
}
复制代码
上述代码中,在主线程中启动了线程 A,B,每个线程将共享变量 count 值加 10000 次,线程 AB 运行完成之后输出 count 累加值;下图是控制台输出结果,答案不等于 20000,证明了 volatile 修饰的共享变量并不保证原子性。出现这个问题的根本原因的 count++,这个操作不是原子操作,在 JVM 中将 count++分成 3 步操作执行。
读取 count 值。
将 count 加 1。
写入 count 值到主内存。
当多线程操作 count++时,就出现了线程安全问题。<br />
<a name="bDREX"></a>
2.3 synchronized 原子性
我们用 synchronized 关键字来改造上面的代码。
/**
* @author :jiaolian
* @date :Created in 2020-12-22 11:22
* @description:Volatile关键字原子性测试
* @modified By:
* 公众号:叫练
*/
public class VolatileAtomicTest {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Task task = new Task();
Thread threadA = new Thread(task);
Thread threadB = new Thread(task);
threadA.start();
threadB.start();
//主线程等待AB执行完毕!
threadA.join();
threadB.join();
System.out.println("累加count="+count);
}
private static class Task implements Runnable {
@Override
public void run() {
//this锁住的是Task对象实例,也就是task
synchronized (this) {
for(int i=0; i<10000; i++) {
count++;
}
}
}
}
}
复制代码
上述代码中,在线程自增的方法中加了 synchronized(this)同步代码块,this 锁住的是 Task 对象实例,也就是 task 对象;线程 A,B 执行顺序是同步的,所以最终 AB 线程运行的结果是 20000,控制台输出结果如下图所示。<br />
<a name="N1KUn"></a>
3.有序性
<a name="0bKcN"></a>
3.1 有序性
什么是有序性?我们写的 Java 程序代码不总是按顺序执行的,都有可能出现程序重排序(指令重排)的情况,这么做的好处就是为了让执行块的程序代码先执行,执行慢的程序放到后面去,提高整体运行效率。画个简单图后举个实际运用案例代码,大家就学到了。<br />
<br />如上图所示,任务 1 耗时长,任务 2 耗时短,JIT 编译程序后,任务 2 先执行,再执行任务 1,对程序最终运行结果没有影响,但是提高了效率啊(任务 2 先运行完对结果没有影响,但提高了响应速度)!
/**
* @author :jiaolian
* @date :Created in 2020-12-22 15:09
* @description:指令重排测试
* @modified By:
* 公众号:叫练
*/
public class CodeOrderTest {
private static int x,y,a,b=0;
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
while (true) {
//初始化4个变量
x = 0;
y = 0;
a = 0;
b = 0;
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
a = 3;
x = b;
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
b = 3;
y = a;
}
});
threadA.start();
threadB.start();
threadA.join();
threadB.join();
count++;
if (x == 0 && y==0) {
System.out.println("执行次数:"+count);
break;
} else {
System.out.println("执行次数:"+count+","+"x:"+x +" y:"+y);
}
}
}
}
复制代码
上述代码中,循环启动线程 A,B,如果说 x,y 都等于 0 时,程序退出。count 是程序次数计数器。下图是控制台程序打印部分结果。从图上可以分析出 x,y 都等于 0 时,线程 A 的 a = 3; x = b;两行代码做了重排序,线程 B 中 b = 3;y = a;两行代码也做了重排序。这就是 JIT 编译器优化代码重排序后的结果。<br />
<a name="2MVmi"></a>
3.2 volatile 有序性
被 volatile 修饰的共享变量相当于屏障,屏障的作用是不允许指令随意重排的,有序性主要表现在下面三个方面。
<a name="H00cy"></a>
3.2.1 屏障上面的指令可以重排序。
/**
* @author :jiaolian
* @date :Created in 2020-12-22 15:09
* @description:指令重排测试
* @modified By:
* 公众号:叫练
*/
public class VolatileCodeOrderTest {
private static int x,y,a,b=0;
private static volatile int c = 0;
private static volatile int d = 0;
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
while (true) {
//初始化4个变量
x = 0;
y = 0;
a = 0;
b = 0;
c = 0;
d = 0;
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
a = 3;
x = b;
c = 4;
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
b = 3;
y = a;
d = 4;
}
});
threadA.start();
threadB.start();
threadA.join();
threadB.join();
count++;
if (x == 0 && y==0) {
System.out.println("执行次数:"+count);
break;
} else {
System.out.println("执行次数:"+count+","+"x:"+x +" y:"+y);
}
}
}
}
复制代码
上述代码中,循环启动线程 A,B,如果说 x,y 都等于 0 时,程序退出。共享变量 c,d 是 volatile 修饰,相当于内存屏障,count 是程序次数计数器。下图是控制台程序打印部分结果。从图上可以分析出 x,y 都等于 0 时,线程 A 的 a = 3; x = b;两行代码做了重排序,线程 B 中 b = 3;y = a;两行代码也做了重排序。证明了屏障上面的指令是可以重排序的。<br />
<a name="QWsjR"></a>
3.2.2 屏障下面的指令可以重排序。
<br />如上图所示将 c,d 屏障放到普通变量上面,再次执行代码,依然会有 x,y 同时等于 0 的情况,证明了屏障下面的指令是可以重排的。
<a name="LyhSF"></a>
3.2.3 屏障上下的指令不可以重排序。
/**
* @author :jiaolian
* @date :Created in 2020-12-22 15:09
* @description:指令重排测试
* @modified By:
* 公众号:叫练
*/
public class VolatileCodeOrderTest {
private static int x,y,a,b=0;
private static volatile int c = 0;
private static volatile int d = 0;
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
while (true) {
//初始化4个变量
x = 0;
y = 0;
a = 0;
b = 0;
c = 0;
d = 0;
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
a = 3;
//禁止上下重排
c = 4;
x = b;
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
b = 3;
//禁止上下重排
d = 4;
y = a;
}
});
threadA.start();
threadB.start();
threadA.join();
threadB.join();
count++;
if (x == 0 && y==0) {
System.out.println("执行次数:"+count);
break;
} else {
System.out.println("执行次数:"+count+","+"x:"+x +" y:"+y);
}
}
}
}
复制代码
如上述代码,将屏障放在中间,会禁止上下指令重排,x,y 变量不可能同时为 0,该程序会一直陷入死循环,结束不了,证明了屏障上下的代码不可以重排。
<a name="JhKfO"></a>
3.3 synchronized 有序性
/**
* @author :jiaolian
* @date :Created in 2020-12-22 15:09
* @description:指令重排测试
* @modified By:
* 公众号:叫练
*/
public class VolatileCodeOrderTest {
private static int x,y,a,b=0;
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
while (true) {
//初始化4个变量
x = 0;
y = 0;
a = 0;
b = 0;
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
synchronized (VolatileCodeOrderTest.class) {
a = 3;
x = b;
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
synchronized (VolatileCodeOrderTest.class) {
b = 3;
y = a;
}
}
});
threadA.start();
threadB.start();
threadA.join();
threadB.join();
count++;
if (x == 0 && y==0) {
System.out.println("执行次数:"+count);
break;
} else {
System.out.println("执行次数:"+count+","+"x:"+x +" y:"+y);
}
}
}
}
复制代码
上述代码中,x,y 也不可能同时等于 0,synchronized 锁的 VolatileCodeOrderTest 的 class 对象,线程 A,B 是同一把锁,代码是同步执行的,是有先后顺序的,所以 synchronized 也能保证有序性。值得注意的一点是上述代码 synchronized 不能用 synchronized(this),this 表示当前线程也就是 threadA 或 threadB,就不是同一把锁了,如果用 this 测试会出现 x,y 同时等于 0 的情况。
<a name="Xs0SH"></a>
4.程序员学习方法心得
大家可以看到我最近几篇文章分析多线程花了不少精力都在谈论可见性,原子性等问题,因为这些特性是理解多线程的基础,在我看来基础又特别重要,所以怎么反复写我认为都不过分,在这之前有很多新手或者有 2 到 3 年工作经验的童鞋经常会问我关于 Java 的学习方法,我给他们的建议就是要扎实基础,别上来就学高级的知识点或者框架,比如 ReentrantLock 源码,springboot 框架,就像你玩游戏,一开始你就玩难度级别比较高的,一旦坡度比较高你就会比较难受吃力更别说对着书本了,这就是真正的从入门到放弃的过程。同时在学习的时候别光思考,觉得这个知识点自己会了就过了,这是不够的需要多写代码,多实践,你在这个过程中再去加深自己对知识的理解与记忆,其实有很多知识你看起来是理解了,但是你没有动手去实践,你*也没有真正理解*,这样只看不做的方法我是不推荐的,本人本科毕业后工作 7 年,一直从事 Java 一线的研发工作,中间也带过团队,因为自己曾经也走过很多弯路踏着坑走过来的,对学习程序还是有一定的心得体会,我会在今后的日子里持续整理把一些经验和知识方面的经历分享给大家,希望大家喜欢关注我。我是叫练,叫个口号就开始练!<br />****总结下来就是两句话:多动手,扎实基础********。****<br />
<a name="j2WeG"></a>
5.总结
今天给和大家聊了多线程的 3 个重要的特性,用代码实现的方式详细阐述了这些名词的含义,如果认真执行了一遍代码应该能看明白,喜欢的请点赞加关注哦。我是叫练【公众号】,边叫边练。<br />
评论