volatile关键字 和 可见性
内容纲要
volatile关键字
volatile是什么
- volatile是一种同步机制 ,比synchronized或者Lock相关类更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为。
- 如果一个变量修饰成volatile ,那么JVM就知道了这个变量可能会被并发修改。
- 但是开销小,相应的能力也小,虽然说volatile是用来同步的保证线程安 全的,但是volatile做不到synchronized那样的原子保护, volatile仅在很有限的场景下才能发挥作用。
volatile的适用场合
- 不适用: a++
- 适用场合1 : boolean flag ,如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。
- 适用场合2 :作为刷新之前变量的触发器
volatile的作用:可见性、禁止重排序
- 可见性:读一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一 个volatile属性会立即刷入到主内存。
- 禁止指令重排序优化:解决单例双重锁乱序问题
volatile和synchronized的关系?
- volatile在这方面可以看做是轻量版的synchronized 轻量版的synchronized:如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。
volatile小结
- volatile修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如boolean flag;或者作为触发器,实现轻量级同步。
- volatile属性的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
- volatile只能作用于属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排序。
- volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile 属性不会被线程缓存,始终从主存中读取。
- volatile提供了happens-before保证, 对volatile变量V的写入happens-before所有其他线程后续对v的读操作。
- volatile可以使得long和double的赋值是原子的,后面马上会讲long和double的原子性。
能保证可见性的措施
- 除了volatile可以让变量保证可见性外, synchronized、Lock、并发集合、Thread.join()和Thread.start() 等都可以保证的可见性
- 具体看happens-before原则的规定
升华:对synchronized可见性的正确理解
- synchronized不仅保证了原子性,还保证了可见性
- synchronized不仅让被保护的代码安全,还近朱者赤
原则性
什么是原子性
- 系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一 半的情况,是不可分割的。
- ATM里取钱
- i++ 不是原子性
- 用synchronized实现原子性
- 原子性并不是单指单一的操作,也可以一系列操作,当着一系列操作满足要么全部执行成功,要么全部不执行,不会出现执行一 半的情况,也是具有原子性
Java中的原子操作有哪些?
- 除long和double之外的基本类型(int, byte, boolean, short, char,float)的赋值操作
- 所有引|用reference的赋值操作,不管是32位的机器还是64位的机器
- java.concurrent.Atomic.*包中所有类的原子操作
long和double的原子性
- 问题描述:官方文档、对于64位的值的写入,可以分为两个32位的操作进行写入、读取错误、使用volatile解决
- 结论:在32位上的JVM.上, long和double的操作不是原子的,但是在64位的JVM.上是原子的
原子操作+原子操作!=原子操作
- 简单地把原子操作组合在一起,并不能保证整体依然具有原子性
- 比如我去ATM机两次取钱是两次独立的原子操作,但是期间有可能银行卡被借给女朋友,也就是被其他线程打断并被修改。
- 全同步的HashMap也不完全安全
单列模式的8种写法
- 饿汉式(静态常量)可用]
/**
* 描述: 饿汉式(静态常量)(可用)
*/
public class Singleton1 {
private final static Singleton1 INSTANCE = new Singleton1();
private Singleton1() {
}
public static Singleton1 getInstance() {
return INSTANCE;
}
}
-
饿汉式(静态代码块) [可用]
/** * 描述: 饿汉式(静态代码块)(可用) */ public class Singleton2 { private final static Singleton2 INSTANCE; static { INSTANCE = new Singleton2(); } private Singleton2() { } public static Singleton2 getInstance() { return INSTANCE; } }
- 懒汉式(线程安全,同步方法)[不推荐用]
/**
* 描述: 懒汉式(线程不安全)
*/
public class Singleton3 {
private static Singleton3 instance;
private Singleton3() {
}
public static Singleton3 getInstance() {
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
}
- 懒汉式(线程不安全)[不可用]
/**
* 描述: 懒汉式(线程安全)(不推荐)
*/
public class Singleton4 {
private static Singleton4 instance;
private Singleton4() {
}
public synchronized static Singleton4 getInstance() {
if (instance == null) {
instance = new Singleton4();
}
return instance;
}
}
- 懒汉式(线程安全,同步方法)[不推荐用]
/**
* 描述: 懒汉式(线程不安全)(不推荐)
*/
public class Singleton5 {
private static Singleton5 instance;
private Singleton5() {
}
public static Singleton5 getInstance() {
if (instance == null) {
synchronized (Singleton5.class) {
instance = new Singleton5();
}
}
return instance;
}
}
-
双重检查[推荐用,面试, Spring源码中有使用]
- 优点:线程安全;延迟加载;效率较高。
- 为什么要double-check
- 线程安全
- 单check行不行?
- 性能问题
- 为什么要用volatile
- 新建对象实际上有3个步骤(可能会被jvm指令重排序)
- 新建一个空的Person对象
- 把这个对象的地址指向p
- 执行Person的构造函数
- 重排序会带来NPE
- 防止重排序
/**
- 描述: 双重检查(推荐面试使用)
*/
public class Singleton6 {
// 加上volatile,保证可见性
private volatile static Singleton6 instance;
private Singleton6() {
}
public static Singleton6 getInstance() {
if (instance == null) {
synchronized (Singleton6.class) {
if (instance == null) {
instance = new Singleton6();
}
}
}
return instance;
}
} -
静态内部类[推荐用,懒汉模式]
/**
* 描述: 静态内部类方式,可用
*/
public class Singleton7 {
private Singleton7() {
}
private static class SingletonInstance {
private static final Singleton7 INSTANCE = new Singleton7();
}
public static Singleton7 getInstance() {
return SingletonInstance.INSTANCE;
}
}
- 枚举[推荐用]
/**
* 描述: 枚举单例
*/
public enum Singleton8 {
INSTANCE;
public void whatever() {
}
}
Singleton8.INSTANCE.whatever();
不同对比
- 饿汉:简单,但是没有lazy loading
- 懒汉:有线程安全问题
- 静态内部类:可用
- 双重检查:面试用
- 枚举:最好
用哪种单例的实现方案最好?
- Joshua Bloch大神在《Effective Java)》中明确表达过的观点: "使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。
- 写法简单
- 线程安全有保障
- 避免反序列化破坏单例
各种写法的适用场合
- 最好的方法是利用枚举,因为还可以防止反序列化重新创建新的对象;
- 非线程同步的方法不能使用;
- 如果程序一开始要加载的资源太多, 那么就应该使用懒加载;
- 饿汉式如果是对象的创建需要配置文件就不适用。
- 懒加载虽然好,但是静态内部类这种方式会引|入编程复杂性
共有 0 条评论