理解并正确使用synchronized和volatile

简介: 线程安全需要同时满足两个条件: 线程可见性 操作的原子性 volatile能保证其修饰的变量的线程可见性但无法保证操作原子性,只能用于"多个变量之间或者某个变量的当前值与修改后值之间没有约束"的场景。

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

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

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

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

    1. 重排序时不能把后面的指令重排序到内存屏障之前的位置
    1. 将当前处理器缓存行的数据会写回到系统内存。
    1. 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。

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

什么场景下别用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
        }
    }
}

这是一个典型的读写冲突例子,引文中提到"如果Floor引用不是 volatile 类型,doWork() 中的代码在解除对Floor的引用时,将会得到一个不完全构造的Floor"。我对这句理解的是:thread2执行完步骤2后释放对Floor的引用时,thread1可能正在调用init方法初始化floor,此时thread2拿到的是还未被thread1完全初始化的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修饰的是数组引用并非数组元素!)。

引用文章

目录
相关文章
|
3月前
|
缓存 Java 编译器
volatile与synchronized
volatile与synchronized
34 0
|
9月前
|
缓存 Java 编译器
|
10月前
|
缓存 安全 Java
Java并发编程中的四个关键字:ThreadLocal、Volatile、Synchronized和Atomic
Java并发编程中的四个关键字:ThreadLocal、Volatile、Synchronized和Atomic
226 0
java多线程关键字volatile、lock、synchronized
volatile写和volatile读的内存语义: 1. 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所在修改的)消息。 2. 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。 3. 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。
258 0
|
SQL
volatile的正确使用(七)
volatile的正确使用(七)
86 0
volatile的正确使用(七)
|
Java 编译器
volatile与synchronized 区别
volatile与synchronized 区别
|
存储 缓存 算法
volatile synchronized cas
之前写了《熔断》,以及其中使用的《计数器算法》;本来是要接着再写不通过定时器清理计数环的计数器算法,看了下我司亿级网关的计数器,百行的代码,但却是满满bug。不得穿插一下并发的基础知识 处理并发,最基本的元件就这三样 1. synchronized 这个关键字不必讲,从开始多线程,它就进入你的视线 2. volatile 在jdk5之后大放异彩 3. cas 在J.U.C中大量使用,他与volatile组合是J.U.C的基石
126 0
volatile synchronized cas
volatile和synchronized的区别
volatile和synchronized的区别
96 0
|
缓存
volatile
volatile
77 0