volatile关键字概述
概述
多个线程修改共享的成员变量,会出现一个线程修改了成员变量,另一个线程不能立即看到该线程修改变量后的最新值
问题
所有共享变量存在于主内存,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题。
解决
第一种是加锁,第二种是volatile关键字
volatile的特性
概述
1.volatile不能保证原子性问题
2.volatile可以防止指令重排序操作
volatile不能保证原子性问题
首先需要了解的是java对基本类型变量的读取和赋值是原子操作,如i = 1的赋值操作,但是像j = i或者i++这样的操作就不是原子类操作,他可以拆分为多次原子操作,比如先读取i,再将i的值赋值为j。所以,一个变量被volatile修饰了,那么可以保证这个变量在读取的时候值是最新的,但是一旦对变量进行自增这样的非原子操作,就不能保证。
比如一个变量i被volatile修饰,两个线程都对这个变量进行修改,都对其进行i++操作,i++可以分为三步:先获取到变量i的值,其次对i的值+1,最后将新值写到缓存中。线程A首先得到变量i的值100,还没来得及修改就阻塞了,这时线程B也得到了i,并且做完了i++,由于volatile主内存这时候的值是101。但是线程A已经读到了i的值100,也就是说这个原子操作已经结束了,所以这个可见性来的有点晚,所以线程A做完操作后刷新到主内存,i的值在主内存还是101
禁止指令重排序
重排序的好处:
提高程序运行的速度

重排序带来的安全性问题:
每个线程内部的代码不一定按代码顺序执行,由于指令重排序可能顺序颠倒造成安全性问题,也就是多个线程之间操作的不可见性。
volatile禁止重排序
变量被volatile修饰后,不能发生重排序
volatile的内存语义
概述
happends-before表示前一个操作的结果可以被后续的操作获取,比如前面一个操作将变量a赋值成1,后面一个操作一定可以知道a变成1了。
happends-before规则
程序顺序规则(单线程规则)
一个线程的每个操作,happends-before于该线程中的任意操作。
同一个线程中前面所有的写操作对后面的操作可见。
锁规则
对一个锁的解锁,happends-before于后续对这个锁的加锁。
如果线程1解锁了monitor a,接着线程2锁定了a,那么线程1解锁a之前的操作对线程2可见(线程1和线程2可以是同一个线程)
volatile变量规则
对一个volatile域的写,happends-before于后续对这个volatile域的读
如果线程1写入了volatile变量v(临界资源),线程2读取了v,那么线程1写入v之前的写操作都对线程2可见(线程1和线程2是同一个操作)
传递性
如果A happends-before B, B happends-before C, 那么A happends-before C
start()规则
主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。
注意:线程B启动之后,线程A对变量修改线程B未必可见
join()规则
线程A执行ThreadB.join() 成功返回,那么线程B中的任意操作happends-before于线程A从ThreadB.join()操作成功返回
详细解释:线程t1写入的所有变量,在任意其他线程t2调用t1.join(),或者t1.Alive()返回后,都对t2可见
volatile写读建立的happends-before规则的演示


小结

volatile面试与总结
long和double的原子性
在java中,long和double都是64位,分为高32位和低32位,如果是一个32位的系统,读和写long和double涉及到原子性问题,因为32位的系统要读完一个64位的变量,要分为两步,每次读取32位。
那么如果有两个线程写一个变量内存,一个进程写低32位,一个进程写高32位,就会造成该64位数据是失效的数据
不过是64位的系统上对long和double的操作都是一次性读写的
32位解决方法:
变量用volatile修饰
volatile在双重检查加锁的单例中的作用
单例概述
单例是需要在内存中永远只有一个类的实例
单例的作用:节约内存和保证共享计算的结果正确,以及方便管理
单例模式有8种
存在饿汉单例模式和懒汉单例模式
饿汉单例:在获取单例对象之前已经创建了
懒汉单例:在真正需要单例的时候才创建出对象
饿汉单例模式的两种写法
饿汉式(静态常量)
public class Singleton1 {
//2.定义一个静态常量保存一个唯一的实例对象【单例】
private static final Singleton1 INSTANCE = new Singleton1();
//1.构造器私有
private Singleton1(){
}
//3.提供一个方法返回单例对象
public static Singleton1 getInstance(){
return INSTANCE;
}
}
饿汉式(静态代码块)
public class Singleton2{
//2.定义一个静态常量保存一个唯一的实例对象【单例】
private static final Singleton2 INSTANCE;
//通过静态代码块创建的对象
static {
INSTANCE = new Singleton2();
}
//1.构造器私有
private Singleton2(){
}
//3.提供一个方法返回单例对象
public static Singleton2 getInstance(){
return INSTANCE;
}
}
懒汉单例的4种写法
特点:在真正需要单例对象的时候才创建出对象。在java程序中,有时候可能需要推迟一些高开销对象的初始化操作,并且只有在用到这些对象的时候才初始化,也就是延迟初始化。
懒汉式(线程不安全)
public class Singleton3 {
//2.声明一个静态变量,用于存放对象
private static Singleton3 INSTANCE;
//1.私有构造器
private Singleton3(){
}
//3.提供一个方法用于获取对象:先询问对象是否存在,若已经存在对象,则返回。如果对象为null,则创建对象
public static Singleton3 getINSTANCE(){
if(INSTANCE == null){
//进入此:说明这是第一次创建对象
INSTANCE = new Singleton3();
}
return INSTANCE;
}
}
线程不安全:如果存在同时存在两个或多个线程同时进入INSTANCE == null这一步,就违背了单例原则。
懒汉式(线程安全)
public class Singleton4 {
//2.声明一个静态变量,用于存放对象
private static Singleton4 INSTANCE;
//1.私有构造器
private Singleton4(){
}
//3.提供一个方法用于获取对象:先询问对象是否存在,若已经存在对象,则返回。如果对象为null,则创建对象
//4.对对象的获取方法使用synchronized修饰
public synchronized static Singleton4 getINSTANCE(){
if(INSTANCE == null){
INSTANCE = new Singleton4();
}
return INSTANCE;
}
}
性能较差,并发下只能有一个线程获取单例对象
懒汉式(线程不安全,优化后的似乎线程安全)
public class Singleton5 {
//2.声明一个静态变量,用于存放对象
private static Singleton5 INSTANCE;
//1.私有构造器
private Singleton5(){
}
//3.提供一个方法用于获取对象:先询问对象是否存在,若已经存在对象,则返回。如果对象为null,则创建对象
public static Singleton5 getINSTANCE(){
if(INSTANCE == null){
//存在问题:当第一个线程创建对象后,第二个线程会再次创建对象
synchronized (Singleton5.class){
INSTANCE = new Singleton5();
}
}
return INSTANCE;
}
}
懒汉式(双重检查 线程安全)
最好,最安全的方式,推荐写
public class Singleton6 {
//2.提供静态变量保存实例对象
private volatile static Singleton6 INSTANCE;
//1.私有化构造器
private Singleton6(){}
//3.提供获取对象的方法
public static Singleton6 getInstance(){
//第一重检查:针对很多个线程同时想要创建对象的情况
if(INSTANCE == null){
//同步代码块锁定
synchronized (Singleton6.class){
//第二重锁检查(针对比如A,B两个线程都为null,第一个线程创建完对象,第二个等待锁的线程拿到锁的情况)
if(INSTANCE == null){
INSTANCE = new Singleton6();
}
}
}
return INSTANCE;
}
}
双重检查的懒汉模式为什么静态成员变量还要加上volatile修饰?
1.禁止指令重排序
new Singleton6()不是一个原子操作,编译器可能会指令重排序,使得分配内存后先返回引用再构造器初始化对象。而线程B在线程A返回引用后判断INSTANCE就不为null,拿到了将是一个未初始化对象的半成品,容易触发“NPE异常”,也就是空指针异常。

2.保证可见性
由于可见性问题,线程A在自己的工作线程创建了实例,但还未同步到主内存中,此时线程B在工作内存中判断INSTANCE还是null,那么线程B又创建了一个实例。
加了volatile以后保证及时同步到主内存,线程B可以立即发现INSTANCE不为null
静态类部类单例方法
public class Singleton7 {
//1.私有化构造器
private Singleton7(){
}
//2.提供一个静态内部类创建实例对象
private static class Inner{
private static final Singleton7 INSTANCE = new Singleton7();
}
//3.提供一个方法用于获取实例对象
public static Singleton7 getInstance(){
return Inner.INSTANCE;
}
}
优点:
1.静态类部类保证在调用时才被加载,符合懒汉设计模式。加上在获取类的期间,jvm会获取一个锁,保证了线程安全
2.对比双重检查,代码更简洁。
但双重检查除了对静态字段实现延迟加载初始化外,还可以对实例字段实现延迟初始化。
枚举实现单例
public enum Singleton8 {
//实际上枚举是一种多例的模式,但可以直接定义一个实例就相当于单例了
INSTANCE;
}
volatile应用场景
适合做纯赋值操作,不适合做a++操作
图中,flag被volatile修饰,初始值是false

触发器
volatile作为刷新之前变量的触发器,一旦某个线程读取到volatile修饰的变量,那么触发获取到该之前的变量都是最新且可见的。因为volatile保证可见性和禁止指令重排序。
volatile与synchronized的区别
1.volatile只能修饰实例变量和类变量,synchronized可以修饰方法以及代码块
2.volatile可以保证数据的可见性,但是不能保证原子性,而synchronized是一种互斥的机制,可以保证可见性和原子性
3.volatile用于禁止指令重排序,可以解决单双例检查对象初始化代码执行乱序的情况
4.volatile是线程同步的轻量级实现,性能比synchronized好。
5.volatile不会造成线程的阻塞,synchronized可能会造成线程的阻塞

1540

被折叠的 条评论
为什么被折叠?



