线程安全之可见性

简介: 被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见final字段的值

1、并发中变量的可见性问题


在讲解线程安全的可见性问题前,先来解决几个简单的问题:


问题1:


变量分为哪几类?


全局变量有:


属性(静态的、非静态的)


局部变量有:


本地变量


参数


问题2:


如何在多线程下共享数据?


当然在问题1的答案下,我们知道多线程的数据共享可以使用全局变量(静态变量、共享对象)来解决。


问题3:


一个全局变量在线程1中被改变了,在线程2中能否看到该变量的最新值?


可能大多数人都会给出肯定的答案,既然是全局变量,便是所以线程共享的,线程1改了该变量的值,那么线程2肯定可以读到线程1修改后的值。为了颠覆这一认知,我们可以使用一个示例代码来看看:


代码逻辑:通过共享变量,在一个线程中控制另一个线程的执行流程


publicclassVolatileDemo{
//全局共享变量,标识状态privatestaticbooleanis=true;
publicstaticvoidmain(String[] args) {
newThread(newRunnable() {
@Overridepublicvoidrun() {
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() {
@Overridepublicvoidrun() {
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() {
@Overridepublicvoidrun() {
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实例,而不用去竞争锁,所有它某种意义下是提高了后来线程的效率。

相关文章
|
3月前
|
设计模式 安全 Java
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
64 1
|
3月前
线程可见性和关键字volatile
线程可见性和关键字volatile
|
5月前
|
缓存 安全 Java
多线程的三大特性:原子性、可见性和有序性
多线程的三大特性:原子性、可见性和有序性
102 0
|
5月前
|
缓存 算法 Java
多线程04 死锁,线程可见性
多线程04 死锁,线程可见性
36 0
|
5月前
|
缓存 安全 Java
3.线程安全之可见性、有序性、原子性是什么?
3.线程安全之可见性、有序性、原子性是什么?
64 0
3.线程安全之可见性、有序性、原子性是什么?
|
缓存 Java 编译器
【Java|多线程与高并发】volatile关键字和内存可见性问题
synchronized和volatile都是Java多线程中很重要的关键字,但它们的作用和使用场景有所不同。
|
缓存 安全 Java
【Java基础】线程的原子性、可见性、有序性及线程安全知识整理
一个操作或者多个操作,要么全部执行,并且执行的过程不会被打断, 要么就全部不执行(一个操作是不可被分割的)。
|
缓存 Java 编译器
并发编程-06线程安全性之可见性 (synchronized + volatile)
并发编程-06线程安全性之可见性 (synchronized + volatile)
83 0
|
安全 Java 编译器
【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序
【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序
【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序
|
Java 编译器
【多线程:volatile】可见性
【多线程:volatile】可见性
136 0