前一篇文章《Synchronized用法原理和锁优化升级过程》从面试角度详细分析了synchronized关键字原理,本篇文章主要围绕volatile关键字用代码分析下可见性,原子性,有序性,synchronized也辅助证实一下,来加深对锁的理解。程序员
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线程使用的值和主线程使用值不一致,致使程序混乱,这就是线程之间的不可见性,这么说你应该能明白了。线程间的不可见性是该程序死循环的根本缘由。spring
上述案例中,咱们用代码证实了线程间的共享变量是不可见的,其实你能够从上图得出结论:只要线程A的工做内存可以感知主内存中共享变量flag的值发生变化就行了,这样就能把最新的值更新到A线程的工做内存了,你只要能想到这里,问题就已经结束了,没错,volatile关键字就实现了这个功能,线程A能感知到主内存共享变量flag发生了变化,因而强制从主内存读取到flag最新值设置到本身工做内存,因此想要VolatileTest代码程序正常结束,用volatile关键字修饰共享变量flag,private volatile static boolean flag = true;就大功告成。volatile底层实现的硬件基础是基于硬件架构和缓存一致性协议。若是想深刻下,能够翻看上一篇文章《可见性是什么?(通俗易懂)》。必定要试试才会有收获哦!缓存
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有相同的可见性语义。springboot
原子性是指一个操做要么成功,要么失败,是一个不可分割的总体。多线程
/** * @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++时,就出现了线程安全问题。框架
咱们用synchronized关键字来改造上面的代码。ide
/** * @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,控制台输出结果以下图所示。
什么是有序性?咱们写的Java程序代码不老是按顺序执行的,都有可能出现程序重排序(指令重排)的状况,这么作的好处就是为了让执行块的程序代码先执行,执行慢的程序放到后面去,提升总体运行效率。画个简单图后举个实际运用案例代码,你们就学到了。
如上图所示,任务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编译器优化代码重排序后的结果。
被volatile修饰的共享变量至关于屏障,屏障的做用是不容许指令随意重排的,有序性主要表如今下面三个方面。
/** * @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;两行代码也作了重排序。证实了屏障上面的指令是能够重排序的。
如上图所示将c,d屏障放到普通变量上面,再次执行代码,依然会有x,y同时等于0的状况,证实了屏障下面的指令是能够重排的。
/** * @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,该程序会一直陷入死循环,结束不了,证实了屏障上下的代码不能够重排。
/** * @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的状况。
你们能够看到我最近几篇文章分析多线程花了很多精力都在谈论可见性,原子性等问题,由于这些特性是理解多线程的基础,在我看来基础又特别重要,因此怎么反复写我认为都不过度,在这以前有不少新手或者有2到3年工做经验的童鞋常常会问我关于Java的学习方法,我给他们的建议就是要扎实基础,别上来就学高级的知识点或者框架,好比ReentrantLock源码,springboot框架,就像你玩游戏,一开始你就玩难度级别比较高的,一旦坡度比较高你就会比较难受吃力更别说对着书本了,这就是真正的从入门到放弃的过程。同时在学习的时候别光思考,以为这个知识点本身会了就过了,这是不够的须要多写代码,多实践,你在这个过程当中再去加深本身对知识的理解与记忆,其实有不少知识你看起来是理解了,可是你没有动手去实践,你也没有真正理解,这样只看不作的方法我是不推荐的,本人本科毕业后工做7年,一直从事Java一线的研发工做,中间也带过团队,由于本身曾经也走过不少弯路踏着坑走过来的,对学习程序仍是有必定的心得体会,我会在从此的日子里持续整理把一些经验和知识方面的经历分享给你们,但愿你们喜欢关注我。我是叫练,叫个口号就开始练!
总结下来就是两句话:多动手,扎实基础。
今天给和你们聊了多线程的3个重要的特性,用代码实现的方式详细阐述了这些名词的含义,若是认真执行了一遍代码应该能看明白,喜欢的请点赞加关注哦。我是叫练【公众号】,边叫边练。