一、可见性
1、实例讲解
先看这样一段代码:
public class Test { static boolean a = true; public static void main(String[] args) { a = false; //对a执行写操作 System.out.println(a); //对a执行读操作 } }
我们在单线程中,对a执行了写操作,并且读取到了最新写的值,也就是说,单线程中对a的写操作时可见的。
那么我们再开启一个线程 :
public class Test { static boolean a = true; public static void main(String[] args) throws InterruptedException { new Thread(()-> { while(a) {} //死循环 }).start(); Thread.sleep(1000); //为了保证不会影响,停一秒再写 a = false; //对a执行写操作 System.out.println(a); //对a执行读操作 } }
可以看到,虽然a仍然打印出为false,但是程序没有结束,就说明在我们新开启的线程中a的值始终为true,他才可以一直执行while循环。换句话说,我们的主线程对a的写操作对于新开的线程的读操作来说是不可见的。
为什么这么长时间了,新开线程中的a还是true呢?那是因为新线程中一直在执行循环,使得线程没有机会去拿到主存中a的最新值,而是一直读取缓存中a的值。
那么,我们让循环沉睡一会儿,给他去读最新值的机会:
public class Test { static boolean a = true; public static void main(String[] args) throws InterruptedException { new Thread(()-> { while(a) { try { Thread.sleep(1); //睡1ms,给线程去读新值的机会 } catch (InterruptedException e) { e.printStackTrace(); } } //死循环 }).start(); Thread.sleep(1000); //为了保证不会影响,停一秒再写 a = false; //对a执行写操作 System.out.println(a); //对a执行读操作 } }
可以看到,程序很快就结束了,说明新线程已经读到了a的新值为false,结束了循环。
2、如何理解Java线程中的不可见性?
简单来说:线程1读,线程2写,而线程1读不到线程2写的值,这就是不可见性。
3、那么如何实现可见性呢?
就需要用到volatile关键字了:
public class Test { static volatile boolean a = true; public static void main(String[] args) throws InterruptedException { new Thread(()-> { while(a) {} //死循环 }).start(); Thread.sleep(1000); //为了保证不会影响,停一秒再写 a = false; //对a执行写操作 System.out.println(a); //对a执行读操作 } }
二、原子性
保证可见性可以保证一个线程写之后,另一个线程可以读到。
那假如一个进程既读取变量,又依赖读到的变量进行写操作呢?我们来看下面的例子 :
1、实例讲解
用两个线程分别执行10000次a++的操作,按道理来说,a的结果应该会增加20000
public class Test { static int a = 0; public static void main(String[] args) throws InterruptedException { for(int i=0; i<10000; i++) { new Thread(()-> { a++; }).start(); new Thread(()-> { a++; }).start(); } Thread.sleep(1000); //为了保证不会影响,停一秒再写 System.out.println(a); //对a执行读操作 } }
可以看到,结果和我们的预期对不上,那我们加上volatile关键字试一下:
结果还是对不上,这是为什么呢?
这就要探究a++的本质了
2、a++的本质
a++可以拆分为三个操作:1、读取a; 2、a+1; 3、将加之后的值赋给a
有可能会出现这种情况:
1、当a=0时,线程1读取a值,线程2也读取a值;
2、线程1将它读到的a值+1,此时为1,线程2也将它读到的a值+1,此时为1;
3、线程1将1这个值刷入主存,此时主存中的a=1;线程2也将1这个值刷入主存,此时为1
这显然是不对的,两个线程各执行一次a++,a的值应该+2才对。刚刚我们得到的值为19998,可能就是有两个线程在其他线程+之前读取到了a值。
那加上volatile关键字之后呢?
volatile关键字只能保证可见性,即一个线程写过之后,另一个线程能够立马读到。而假如线程2操作在线程1操作写之前就已经读了,那还是没办法改变这个情况。这两个线程都需要读取主存的值,并且每个线程都依赖自己读取的值进行写操作。这就需要保证原子性了。
3、使用Atomicxxxx保证原子性:
public class Test { static AtomicInteger a = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { for(int i=0; i<10000; i++) { new Thread(()-> { a.getAndAdd(1); }).start(); new Thread(()-> { a.getAndAdd(1); }).start(); } Thread.sleep(1000); //为了保证不会影响,停一秒再写 System.out.println(a); //对a执行读操作 } }
可以看到,a++的操作改成了getAndAdd(),读和写是一起执行的,这就不会在读值之后写值之前被其他线程插一杠子。
需要注意的一点是,原子性和可见性并不是相互独立的,保证原子性的前提是保证可见性,那为什么我们没有再用volatile修饰a来保证可见性呢?这就需要去看看AtomicInteger的源码了:
其实它的内部也使用了volatile关键字。
4、使用synchronized同步代码段强制实现原子性和可见性
除了Atomic,也可以使用synchronized同步代码段强制实现原子性。
public class Test { static AtomicInteger a = new AtomicInteger(0); static int b = 0; public static void main(String[] args) throws InterruptedException { for(int i=0; i<10000; i++) { new Thread(()-> { // a.getAndAdd(1); synchronized(Test.class) { b++; } }).start(); new Thread(()-> { // a.getAndAdd(1); synchronized(Test.class) { b++; } }).start(); } Thread.sleep(1000); //为了保证不会影响,停一秒再写 // System.out.println(a); //对a执行读操作 System.out.println(b); //对b执行读操作 } }
相比较Atomic,synchronized就更加重量级了。
另外:volatile不具有传染性,用volatile修饰的对象的内部属性不具有可见性,反之用volatile修饰的内部属性也不能保证所在对象的可见性。
参考:【java】并发之可见性与原子性_哔哩哔哩_bilibili