1、并发中变量的可见性问题
在讲解线程安全的可见性问题前,先来解决几个简单的问题:
问题1:
变量分为哪几类?
全局变量有:
属性(静态的、非静态的)
局部变量有:
本地变量
参数
问题2:
如何在多线程下共享数据?
当然在问题1的答案下,我们知道多线程的数据共享可以使用全局变量(静态变量、共享对象)来解决。
问题3:
一个全局变量在线程1中被改变了,在线程2中能否看到该变量的最新值?
可能大多数人都会给出肯定的答案,既然是全局变量,便是所以线程共享的,线程1改了该变量的值,那么线程2肯定可以读到线程1修改后的值。为了颠覆这一认知,我们可以使用一个示例代码来看看:
代码逻辑:通过共享变量,在一个线程中控制另一个线程的执行流程
publicclassVolatileDemo{ //全局共享变量,标识状态privatestaticbooleanis=true; publicstaticvoidmain(String[] args) { newThread(newRunnable() { publicvoidrun() { inti=0; while (VolatileDemo.is){ i++; } System.out.println(i); } }).start(); try { //停止2秒种TimeUnit.SECONDS.sleep(2); }catch (InterruptedExceptione) { e.printStackTrace(); } //设置is为false,使得上面的while线程结束循环VolatileDemo.is=false; System.out.println("被置为了false了。"); } }
按我们的设计思路,当is设置为false了后,while循环应该会结束,并打印i的值,并且打印出最终的i的值。但事实并非我们想想的那样,如果大家执行上面这个main方法后,,会发现程序一直没有结束while循环,并不会打印出i的值。
总结:
并发的线程能不能看见到变量的最新值,这就是并发中变量的可见性问题。
思考:
①、上述代码中主线程main对is变量的改变,为什么对子线程是不可见的?
②、怎样才能让主线程main对is的改变是对子线程是可见的?
2、怎样才能可见
要让并发中共享变量可见,可以使用synchronized或者volatile。
2.1、使用synchronized
我们使用synchronized同步关键字对第1节的代码做一个适当的调整:
publicclassVolatileDemo{ //全局共享变量,标识状态privatestaticbooleanis=true; publicstaticvoidmain(String[] args) { newThread(newRunnable() { publicvoidrun() { inti=0; while (VolatileDemo.is){ synchronized (this){ i++; } } System.out.println(i); } }).start(); try { //停止2秒种TimeUnit.SECONDS.sleep(2); }catch (InterruptedExceptione) { e.printStackTrace(); } //设置is为false,使得上面的while线程结束循环VolatileDemo.is=false; System.out.println("被置为了false了。"); } }
执行结果:
被置为了false了。 81147243
2.2、使用volatile
这里省略其他代码,除了is加上volatile关键字外,其他部分代码同第1节:
publicclassVolatileDemo { //全局共享变量,标识状态privatestaticvolatilebooleanis=true; ... }
执行结果:
被置为了false了。 -512391385
i的结果为负的原因是因为int值溢出了。
思考:使用synchronized或者volatile为什么就可见了呢?
3、变量可见性、线程安全问题原因
3.1、Java内存模型
Java内存模型以及操作规范:
①、共享变量必须存放在主内存中; ②、线程有自己的工作内存,线程只可以操作自己的工作内存; ③、线程要操作共享变量,需要从主内存中读取到工作内存,改变值后需要从工作内存同步到主内存中。
3.2、Java内存模型带来的问题
问题1:
有变量A,多线程并发对其累加会有什么问题?如果三个线程并发操作A,大家读取A时都读到A=0,都对A+1,再将值同步回主内存。结果时多少?
答案肯定是1,因为大家都读到0,最后都将A+1=1的结果同步到主内存中,所以结果肯定是1,这就是带来了线程安全以及可见性问题,它的本质也就是:
Java的内存模型是导致线程安全问题、可见性问题的根本原因
问题2:
那么如何让线程2使用A时看到最新值?
实现步骤:
①、线程1修改A后必须立马同步回主内存 ②、线程2使用A时必须重新从主内存中读取到工作内存中
问题3:
那么实现了问题2的两个步骤,就一定能保证可见性?
3.3、同步协议
java内存模型-同步交互协议,规定了8种原子操作:
①、lock(锁定):将主内存中的变量锁定,为一个线程独占 ②、unclick(解锁):将lock加的锁定解除,此时其他线程可以有机会访问此变量 ③、read(读取):作用于主内存变量,将主内存中的变量值读取到工作内存中 ④、load(载入):作用于工作内存变量,将read读取的值保存到工作内存中的变量副本中 ⑤、use(使用):作用于工作内存变量,将值传递给线程的代码执行引擎 ⑥、assign(赋值):作用于工作内存变量,将执行引擎处理返回的值重新赋值给变量的副本 ⑦、store(存储):作用于工作内存变量,将变量副本的值传送到主内存中 ⑧、write(写入):作用于主内存变量,将store传送过来的值写入到主内存的共享变量中
将一个变量从主内存复制到工作内存中要顺序执行read、load操作;要将变量从工作内存同步回主内存要顺序执行store、write操作。只要求是顺序,没有要求一定是连续执行。
做了assign操作,必须同步回主内存,不能没有做assign,同步回主内存。
3.4、read/load操作示例
4、保证变量可见性的方式
4.1、final变量
个人认为final修饰的变量是不可变的,一旦它被初始化,它的值不在可变,所以在任何时候,任何子线程中读取到它的值都是一致的,所以它在多线程操作下是可见的。
以下是《深入理解Java虚拟机》第二版的原话(可能不太好理解):
被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见final字段的值
4.2、synchronized
4.2.1、synchronized语义规范
①、进入同步快前,先清空工作内存中的共享变量,从主内存中重新加载 ②、解锁前必须把修改的共享变量同步回到主内存中
4.2.2、synchronized是如何做到线程安全的
①、锁机制保护共享资源,只有获得锁的线程才可以操作共享资源 ②、synchronized语义规范保证了修改共享资源后,会同步到主内存,就做到了线程安全
虽然synchronized做到了以上两点,但是要实现共享变量的线程安全以及可见性的话,必须保证所有线程都竞争同一把锁,不能各自拿自己家的锁,然后各回各家。
4.3、volatile
4.3.1、volatile语义规范
①、使用volatile变量时,必须从主内存中加载,并且read、load是连续的 ②、修改volatile变量后,必须立马同步回主内存,并且store、write是连续的
4.3.2、volatile能做到线程安全吗
不能,因为它没有锁机制,线程可以并发操作共享资源
我们可以举个例子:
publicclassAtomicityDemo { staticvolatileintcount=0; publicstaticvoidincrease(){ count++; } publicstaticvoidmain(String[] args) { intthreads=20; CountDownLatchcdl=newCountDownLatch(threads); for (inti=0;i<threads;i++){ newThread(newRunnable() { publicvoidrun() { for (inti=0;i<10000;i++){ AtomicityDemo.increase(); } cdl.countDown(); } }).start(); } try { cdl.await(); } catch (InterruptedExceptione) { e.printStackTrace(); } System.out.println(count); } }
上述代码可以看到,总共开了20个线程,每个线程对count变量加10000次,如果volatile能保证线程安全的话,结果应该是200000。下面是4次执行结果:
86669 104288 88572 85813
每次结果都不等于200000,可以看出volatile并不是线程安全的。
4.3.3、为何使用volatile
既然同步关键字synchronized能保证线程安全以及可见性,为何还需要使用volatile,原因如下:
①、主要原因:volatile比synchronized简单 ②、volatile比synchronized性能要好,因为volatile没有加锁 ③、synchronized并不是在所以情况下都能保证可见性,因为必须所以线程同时使用一把锁 ④、volatile和synchronized同时使用的时候,可以适当的提高效率,比如懒汉式的单例模式(volatile的使用场景分析)
4.3.4、volatile的用途
volatile可用于限制局部代码指令的重排序:
4.3.5、volatile的使用场景
volatile的使用范围:
①、volatile只可以修饰成员变量(静态的、非静态的),因为只有成员变量才是所以线程共享的变量,而局部变量是线程独有,不存在可见性问题 ②、多线程并发下,才需要使用它
volatile典型的应用场景:
①、只有一个修改者,多个使用者,要求保证可见性的场景 状态标识 数据定期发布,多个获取者 ②、单例模式 懒汉式的单例模式
正确的懒汉式单例模式写法:
publicclassSingleton { //使用volatileprivatestaticvolatileSingletonsingleton; //私有化构造器privateSingleton() { } publicstaticSingletongetInstance(){ //第一次检查if (singleton==null){ synchronized(Singleton.class){ //第二次检查if (singleton==null){ singleton=newSingleton(); } } } returnsingleton; } }
两次检测的原因:
第一次检查很好理解,即先判断当前实例singleton 是否为null,如果不为null,则可以直接返回,避免了synchronized同步块竞争锁,影响效率;
进入synchronized同步块后第二次检查的原因是:假如有多个线程同时来获取singleton 实例,它们开始都得到的是null,然后都去竞争锁,但是一次只有一个线程能够获取到锁,当第一个线程创建好实例后出同步块,并更新了主内存的 singleton实例,那么第二个线程在抢到锁进入同步块时,按synchronized的语法规则,它应该清理下工作区的共享变量,并重新获取共享变 量,此时共享变量singleton不再为null,所以此时再次检查避免第二个线程又去创建实例,那样的话就不再是单例了。
那么既然两次检查同时能保证了可见性和线程安全问题,那为什么还需要volatile?
我们知道synchronized的可见性并不是很及时的,也就意味着它的store和write操作并非连续,中间可能会有其它原子操作,上面的 例子中,我们假设在第一个线程刚刚好创建实例后,但还没有出同步块,这是主内存中的变量singleton还是null,这是突然又来个100个,甚至更 多的线程来获取实例singleton,如果不使用volatile的话,它们得到singleton为null后,也会去synchronized并等 待锁,当然问题也不大,等锁就等锁吧,就是效率相对有点低。那么有什么办法可以让这后来的100个线程不用等锁而直接return呢,那肯定想到的是 volatile关键字,因为它的可见性是及时的,它的store和write操作是连续的,也就意味着第一个线程在创建完实例后,对其他线程是立即可见 的,所有在后来100个线程进来后,可以直接拿到singleton实例,而不用去竞争锁,所有它某种意义下是提高了后来线程的效率。