理解并正确使用synchronized和volatile

理解并正确使用volatile

理解volatile原理前,有必要了解JMM。JMM - Java Memory Model,即Java内存模型。由方法区、堆、程序计数器、虚拟机栈和本地方法栈组成。
JMM

工作内存和主内存分别可理解为”客户端缓存-持久化数据”模型中的”缓存”和”持久化数据”。

  • 工作内存为线程私有数据,存储了当前方法的本地变量信息。
  • 线程运行期间创建的实例对象都在主内存中,无论该实例是成员变量or局部变量(如Object obj = new String(“test”);)、类信息、常量以及静态变量。

当方法内调用如StringBuilder sb = new StringBuilder()之类的堆内存分配语句时,JVM在主内存申请内存空间后将其引用传递给共享变量,线程执行完修改该内存数据后将工作内存中的数据刷回主内存。如果多个线程同时操作主存的同一段内存数据,则产生线程安全问题。

线程安全需要同时满足三个条件:

  • 原子性
    某个操作是不可中断的,且要么全部做完要么没有执行。
  • 可见性
    通过volatile关键字修饰变量实现。读取volatile变量时,先失效本地缓存再读取主存中的最新值;更新volatile变量会立即将最新值刷回主存。
  • 有序性
    JMM的happens-before原则。

happens-before原则

作为synchronize和volatile的辅助规则,该原则是判断数据存在竞争以及线程安全的一句,其内容如下:

  • 一个线程内代码按顺序执行
  • 同一个锁的unlock必须在lock前
  • volatile写先于读,保证工作内存里读取到的volatile变量值均为最新值
  • A先于B ,B先于C 那么A必然先于C
  • 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
  • 线程的所有操作先于线程的终结,即线程终结前会将修改内容刷回主存。
  • 对象终结规则 对象的构造函数执行,结束先于finalize()方法

volatile实现可见性的原理

先看下volatile的内存语意:

  • 修改volatile变量后,其余线程拿到的volatile变量值均为最新值
  • 禁止指令重排序优化(延迟加载版本的双重检测单例问题)

被volatile修饰的变量在进行写操作时,处理器会插入一条lock前缀的汇编代码,做了层内存屏障,其作用为:

  • 重排序时不能把后面的指令重排序到内存屏障之前的位置
  • 将当前处理器缓存行的数据会写回到系统内存
  • 当读取volatile变量时,先失效工作内存的变量值,只能从主存读取volatile变量的最新值

通过处理器之间的缓存一致性协议,当(处理器本)地缓存过期后会失效本地缓存,当更新该缓存时处理器重新从主存load数据到本地缓存并执行计算逻辑。

双重检测单例问题的实质

JVM对非volatile变量进行指令重排序。正常的对象实例化流程为:内存分配->初始化->返回对象引用,但指令重排序后其流程可能为:内存分配->返回对象引用->初始化,通过getInstance()返回的引用指向了一个未初始化的不完整对象。

volatile能保证其修饰的变量的线程可见性但无法保证操作原子性,
只能用于"多个变量之间或者某个变量的当前值与修改后值之间没有约束"的场景。
在实现计数器(++count)和由多个变量组成的不变表达式方面,volatile无法胜任。

什么场景下别用volatile?

非线程安全的计数器类

自增操作并不是原子的,比如”++count”操作就是三个原子操作的集合:
1. 读取count旧值
2. 线程上下文汇总设置count新值: count+1
3. 将count新值刷回主存并失效其他线程上下文的count值

假设thread1和thread2均执行”++count”计数操作,thread1和thread2均执行完2但未执行3,此时thread1和thread2先后将count新值刷回主存,这就产生了线程不安全的现象。

只有在状态真正独立于程序内其他内容时才能使用volatile。

  • 引用
volatile操作不会像锁一样造成阻塞,因此,在能够安全使用volatile的情况下,volatile 可以提供一些优于锁的可伸缩特性。如果读操作的次数要远远超过写操作,与锁相比,volatile 变量通常能够减少同步的性能开销。

非线程安全的数值范围类

// 代码来源于Brian Goetz的《正确使用 Volatile 变量》一文,本文稍作修改
@NotThreadSafe 
public class NumberRange {
    private volatile int lower, upper;

    public int getLower() { return lower; }
    public int getUpper() { return upper; }

    public void setLower(int value) { 
        if (value > upper) 
            throw new IllegalArgumentException(...);
        lower = value;
    }

    public void setUpper(int value) { 
        if (value < lower) 
            throw new IllegalArgumentException(...);
        upper = value;
    }
}

假设NumberRange初始化后lower/upper分别为0和4,即数值范围为[0,4]。此时thread1和thread2分别执行setLower(3)和setUpper(2),最终lower/upper分别被thread1和thread2设置为3和2,数值范围被更新为[3,2],某种意义上看是一个无效的数值范围。

什么场景下可以使用volatile?

1、对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
2、该变量没有包含在具有其他变量的不变式中。

Case1: 状态标志

这种类型的状态标记的一个公共特性是:通常只有一种状态转换。

// 代码来源于Brian Goetz的《正确使用 Volatile 变量》一文,本文稍作修改
volatile boolean shutdownRequested;
public void shutdown() { shutdownRequested = true; }
public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

Case2: 一次性安全发布 - One-time Safe Publication

// 代码来源于Brian Goetz的《正确使用 Volatile 变量》一文,本文稍作修改
public class Floor {
    public Floor() {
        // initialization...
    }
}
public class Loader {
    public volatile Floor floor;
    public void init() {
        // this is the only write to floor
        floor = new Floor();
        // initialize floor object here ...
    }

    public Floor getFloor() {
        return this.floor;
    }
}

public class SomeOtherClass {
private Loader loader;
    public void doWork() {
        while (true) { 
            // use "floor" only if it is ready
            if (loader != null)  // 步骤1
                doSomething(loader.getFloor()); // 步骤2
        }
    }
}

这是一个典型的读写冲突例子,引文中提到“如果 theFlooble 引用不是 volatile 类型,doWork() 中的代码在解除对 theFlooble 的引用时,将会得到一个不完全构造的 Flooble”。我对这句理解的是:thread2执行步骤2时,thread1可能正在调用init方法初始化floor,如果doWork内部主动解除对floor对象的引用,则可能拿到未初始化完全的floor对象的引用。

即使floor对象完成初始化,对floor成员域的修改仍然是线程不可见的。

volatile引用可以保证任意线程都可以看到这个对象引用的最新值,但不保证能看到被引用对象的成员域的最新值。

因为volatile修饰的是floor对象的引用,如果thread1执行到步骤1时,thread3修改了floor成员域,其修改对thread1并不可见。思考:如果floor成员域均被volatile锁修饰,其成员域的修改是否对thread3可见

Case3: 多个独立观察结果的发布

// 代码来源于Brian Goetz的《正确使用 Volatile 变量》一文,本文稍作修改
public class UserManager {
    public volatile String lastUser;
    public boolean authenticate(String user, String password) {
        boolean valid = passwordIsValid(user, password);
        if (valid) {
            User u = new User();
            activeUsers.add(u);
            lastUser = user;
        }
        return valid;
    }
}

这个模式要求被发布的值(lastUser)是有效不可变的 —— 即值的状态在发布后不会更改。与Case1中更新floor对象成员域不同,对String类的操作是在新的String实例上进行的,String对象本身的状态并未改变。String类的这种实现方式天然地提供了线程隔离性。

volatile并非用来解决高并发场景下数据竞争冲突的方案,它只是实现线程可见性的一种方式!如果多个线程同时更新volatile变量,需要采用同步机制解决数据竞争,如CAS或者锁等。

Case4: “volatile bean” 模式

该模式中,Java Bean成员变量均被volatile修饰,且引用类型的成员变量也必须是有效不可变(Collection的子类如List, Set, Queue等有数组值的成员变量,volatile修饰的是数组引用并非数组元素!)。

引用文章

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值