背景
上篇文章介绍了java的53个关键字,其中个人感觉volatile和synchronized两个java关键字能够重点具体介绍下.这两个关键字都是作用在多线程并发环境下,其中volatile能保障操作对象的可见性和有序性,synchronized能保障操作对象的原子性和可见性.
JMM
多线程并发环境有必要先理解JMM(java memory model),在理解JMM前咱们须要晓得PC物理机的内存模型,如图:
CPU解决指令的性能很高,而CPU间接从内存中读取数据的性能相对来说就很慢了,所以如果间接都从内存中读取数据,会重大拖慢PC的处理速度.所以才会有CPU缓存,个别都是3级缓存,级别越高CPU读取性能越高,CPU内存也有寄存器,它的读取性能是最高的.
JMM内存模型如图:
留神JMM不是实在物理的内存构造,它是java虚拟机栈工作内存的标准.每个线程都有本人的工作内存,线程对所有变量的操作都必须在工作内存中进行,而不能间接对主存进行操作,并且每个线程不能拜访其余线程的工作内存.
可见性
当一个共享变量被批改时,它的值会立刻更新到主内存,当有其余线程读取时,都会去主存中读取最新值.阐明该变量是对所有线程是具备可见性.
有序性
java虚拟机的编译器和处理器会对指令进行重排序优化,来晋升代码的执行效率.重排序会根据happens-before准则,保障指令代码的有序性.重排序不会影响单线程状况下的执行后果,但多线程并发的状况下可能会影响到它的正确性.所以并发状况下须要避免虚拟机对肯定代码的重排序.
原子性
多个代码执行,要么同时都执行,要么都不执行,像原子一样不能被宰割,即这些操作不可被中断,咱们就说这些操作是具备原子性.
volatile
可见性代码例子
public class VisibilityDemo { static boolean flag = true; public static void main(String[] args) throws Exception { new Thread(()->{ System.out.println("开始循环啦~~~"); while(flag){ } System.out.println("循环退出了~~~"); }, "t1").start(); Thread.sleep(2000); new Thread(()->{ System.out.println("flag的值批改为false"); flag=false; }, "t2").start(); }}
t1线程中的flag始终获取不到t2线程批改flag变量后的值所以始终在循环中,运行后果如下图:
对变量应用volatile润饰就能够退出循环了.
public class VisibilityVolatileDemo { static volatile boolean flag = true; public static void main(String[] args) throws Exception { new Thread(()->{ System.out.println("开始循环啦~~~"); while(flag){ } System.out.println("循环退出了~~~"); }, "t1").start(); Thread.sleep(2000); new Thread(()->{ System.out.println("flag的值批改为false"); flag=false; }, "t2").start(); }}
t1线程中的flag能获取到t2线程批改flag变量后的值所以就退出循环了,运行后果如下图:
有序性代码例子
public class OrderlinessDemo { public static int a=0,b=0,i=0,j=0; public static void main(String[] args) throws Exception { int count = 0; while(true){ a=0; b=0; i=0; j=0; Thread t1 = new Thread(() -> { a = 1; i = b; }, "t1"); Thread t2 = new Thread(() -> { b = 1; j = a; }, "t2"); t1.start(); t2.start(); t1.join(); t2.join(); count++; System.out.println("第"+ count +"次输入后果, i = "+i+", j ="+j); if(i==0 && j==0){ break; } } }}
实践上来说,并发状况下只可能输入i=0,j=1;i=1,j=0;i=1,j=1的状况.只有当产生指令重排序,能力输入i=0,j=0的状况,运行后果如下图:
public class OrderlinessVolatileDemo { public static volatile int a=0,b=0,i=0,j=0; public static void main(String[] args) throws Exception { int count = 0; while(true){ a=0; b=0; i=0; j=0; Thread t1 = new Thread(() -> { a = 1; i = b; }, "t1"); Thread t2 = new Thread(() -> { b = 1; j = a; }, "t2"); t1.start(); t2.start(); t1.join(); t2.join(); count++; System.out.println("第"+ count +"次输入后果, i = "+i+", j ="+j); if(i==0 && j==0){ break; } } }}
增加volatile润饰变量后,始终没有呈现i=0,j=0的输入状况,运行后果如下图:
原子性代码例子
public class AtomicityDemo { static int count = 0; public static void main(String[] args) { for(int i=0; i<100;i++){ new Thread(()->{ for(int j=0;j<10000;j++){ count++; System.out.println("输入后果:"+count); } }).start(); } }}
100个线程进行累加1W次,输入的后果始终小于100W且每次运行的后果大概率会都不一样,运行后果如下图:
第一次:
第二次:
第三次:
public class AtomicityVolatileDemo { volatile static int count = 0; public static void main(String[] args) { for(int i=0; i<100;i++){ new Thread(()->{ for(int j=0;j<10000;j++){ count++; System.out.println("输入后果:"+count); } }).start(); } }}
应用volatile润饰变量后,后果也是小于100W,阐明volatile是无奈保障原子性的,但最初输入的数值会比下面未应用volatile润饰的运行后果更靠近100W的后果,运行后果如下图:
第一次:
第二次:
第三次:
synchronized
原子性和可见性代码例子
public class AtomicitySynDemo { static int count = 0; public static void main(String[] args) { for(int i=0; i<100;i++){ new Thread(()->{ for (int j = 0; j < 10000; j++) { synchronized (AtomicitySynDemo.class) { count++; System.out.println("输入后果:" + count); } } }).start(); } }}
应用synchronized的代码块,能保障代码块外面的操作具备的原子性和变量的可见性,所以每次运行后果都是100W.
总结
特点
volatile关键字解决的是内存可见性和有序性的问题.可见性是因为变量的写操作都会间接刷新到主存,读操作都会去主存中同步.有序性是变量通过内存屏障(是一种屏障指令,它使得CPU或编译器对屏障指令的前和后所收回的内存操作执行一个排序的束缚)来禁止指令重排序.
synchronized关键字通过锁机制来保障代码块的同步程序执行,从而解决操作原子性的问题.同时synchronized代码块的每次执行开始和完结,都会别离将变量读取主存和写入主存中,从而解决变量可见性的问题.synchronized不能避免同步代码块外面的代码进行重排序,所以不能解决代码块的有序性.
[下一篇 介绍synchronized对象锁]