Java内存模型<二> _ volatile/synchronized/final内存语义

目录

 

一、volatile的内存语义 

1. happens-before关系

2. volatile写-读内存语义

3. volatile内存语义实现

        a. 禁止编译器重排序

        b. 禁止处理器重排序

二、synchronized的内存语义

1. happens-before关系

2. synchronized锁的释放-获取内存语义 

3. synchronized内存语义实现

        a. ReentrantLock源码分析

        b. synchronized内存语义实现

三、final的内存语义

四、参考资料


一、volatile的内存语义 

1. happens-before关系

package com.cmmon.instance;

/**
 * @description volatile示例代码
 * @author tcm
 * @version 1.0.0
 * @date 2022/2/28 10:10
 **/
public class VolatileExample {

    public int a = 0;
    public volatile boolean flag = false;

    public void writer() {
        a = 1;                   // 1
        flag = true;             // 2
    }

    public void reader() {
        if (flag) {              // 3
            int i = a;           // 4
        }
    }

}

        如上代码所示,假设线程A执行writer()方法后,线程B执行reader()方法。根据happens-before规则,有以下关系:

1)根据程序顺序规则:1 happens-before 2;3 happens-before 4;
2)根据volatile变量规则:2 happens-before 3;
3)根据传递性规则:1 happens-before 4。

        如下图所示happens-before关系图。线程A写一个volatile变量后,线程B读同一个volatile变量,存在:1 happens-before 4。线程A在写volatile变量之前所有可见的共享变量,在线程B读同一个volatile变量后,将立即变得对线程B可见

happens-before关系图

2. volatile写-读内存语义

        以上节代码和执行线程为例,两个线程的本地内存中的flag=false和a=0都是初始值或默认值。当线程A写flag变量后,本地内存A中被线程A更新过的两个共享变量的值被刷新到主内存中。此时,本地内存A和主内存中的共享变量的值是一致,如下图所示。

        当线程B读volatile变量时,JMM会把线程B对应的本地内存置为无效。线程B将从主内存中读取共享变量。

类型内存语义
volatile写

1. 本地内存共享变量刷新到主内存中;

2. 写线程向将要读volatile变量的线程发出消息。

volatile读

1. 读线程接收了写线程发出的消息;

2. 本地内存共享变量设置为无效;

3. 从主内存中读取共享变量。

JMM

1. 写线程经过主内存向读线程发送消息

(JMM决定一个线程对共享变量的写入何时对另一个线程可见)

3. volatile内存语义实现

        a. 禁止编译器重排序

        JMM对源码生成字节码时,对volatile变量做了重排序规则。如下表所示,可以得出结论:

  • 第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。确保volatile写之前的操作不会被编译器重排序到volatile写之后。
  • 第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。确保volatile读之后的操作不会被编译器重排序到volatile读之前。
  • 第一个操作是volatile写,第二个操作是volatile读时,不能重排序

        b. 禁止处理器重排序

        JMM编译器生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。如下表所示:

操作类型内存屏障
volatile写该写操作前面插入一个StoreStore屏障
volatile写该写操作后面插入一个StoreLoad屏障
volatile读该读操作后面插入一个LoadLoad屏障
volatile读该读操作后面插入一个LoadStore屏障

        如下图所示,StoreStore屏障将保证上面所有的普通写在volatile写之前刷新到主内存volatile写后面的StoreLoad屏障,作用是避免volatile写与后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile写的后面是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即return)。为了保证能正确实现volatile的内存语义,JMM在采取了保守策略:在每个volatile写的后面,或者在每个volatile读的前面插入一个StoreLoad屏障。 

二、synchronized的内存语义

1. happens-before关系

package com.cmmon.instance;

/**
 * @description volatile示例代码
 * @author tcm
 * @version 1.0.0
 * @date 2022/2/28 13:48
 **/
public class MonitorExample {

    public int a = 0;

    public synchronized void writer() {    // 1
        a++;                               // 2
    }                                      // 3

    public synchronized void reader() {    // 4
        int i = a;                         // 5
    }                                      // 6

}

         如上代码所示,假设线程A执行writer()方法后,线程B执行reader()方法。根据happens-before规则,有以下关系:

1)根据程序顺序规则:1 happens-before 2,2 happens-before 3;4 happens-before 5,5 happens-before 6;
2)根据synchronized锁规则:3 happens-before 4;
3)根据传递性规则:2 happens-before 5。

        如下图所示happens-before关系图。线程A释放了锁之后,随后线程B获取同一个锁,存在:2 happens-before 5。因此,线程A在释放锁之前所有可见的共享变量,在线程B获取同一个锁之后,将立刻变得对线程B可见

happens-before关系图

2. synchronized锁的释放-获取内存语义 

        以上节代码和执行线程为例,两个线程的本地内存中的a=0都是初始值或默认值。线程A释放锁时,JMM会把线程A对应的本地内存中的共享变量刷新到主内存中。此时,本地内存A和主内存中的共享变量的值是一致,如下图所示。

        当线程B获取锁时,JMM会把线程B对应的本地内存置为无效。线程B从而使得被Monitor保护的代码区内的共享变量必须从主内存中读取共享变量。

类型内存语义

释放锁

(释放Monitor对象)

1. 本地内存共享变量刷新到主内存中;

2. 释放锁线程向将要获取锁的线程发出消息。

获取锁

(获取Monitor对象)

1. 获取锁线程接收了释放锁线程发出的消息;

2. 本地内存共享变量设置为无效;

3. 从主内存中读取共享变量。

JMM

1. 释放锁线程经过主内存向获取锁线程发送消息

(JMM决定一个线程对共享变量的写入何时对另一个线程可见)

注意锁释放与volatile写、获取锁与volatile读,两者有相同的内存语义

3. synchronized内存语义实现

        a. ReentrantLock源码分析

        下图是ReentrantLock类图,其依赖于Java同步器框架AbstractQueuedSynchronizer。其使用一个整型的volatile变量(private volatile int state;)来维护同步状态。如下表所示是公平锁和非公平锁的释放锁、获取锁的对比。

锁类型方法调用轨迹

公平锁

(FairSync)

获取锁step1:ReentrantLock:lock()
step2:FairSync:lock()
step3:AbstractQueuedSynchronizer:acquire(int arg)
step4:ReentrantLock:tryAcquire(int acquires)
释放锁step1:ReentrantLock:unlock()
step2:AbstractQueuedSynchronizer:release(int arg)
step3:Sync:tryRelease(int releases)

非公平锁

(NonfairSync)

获取锁step1:ReentrantLock:lock()
step2:NonfairSync:lock()
step3:AbstractQueuedSynchronizer:compareAndSetState(int expect,int update)
释放锁step1:ReentrantLock:unlock()
step2:AbstractQueuedSynchronizer:release(int arg)
step3:Sync:tryRelease(int releases)
注意

1. 公平锁与非公平锁的释放锁是相同的;

2. 非公平锁获取锁是通过unsafe.compareAndSwapInt(this, stateOffset, expect, update),即:CAS操作来获取锁(Lock CMPXCHG);

3. 公平锁和非公平锁释放时,最后都要写一个volatile变量state;
4. 公平锁获取时,首先会去读volatile变量;
5. 非公平锁获取时,首先会用CAS更新volatile变量,CAS操作同时具有volatile读和volatile写的内存语义。

        b. synchronized内存语义实现

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);    // 释放锁的最后,写volatile变量state
    return free;
}

        以上代码所示是公平锁和非公平锁的释放锁的代码。在释放锁时,最后都要写一个volatile变量state。根据volatile的happens-before规则,释放锁的线程在写volatile变量之前的共享变量,在获取锁的线程读取同一个volatile变量后共享变量则可见。

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

        以上代码非公平锁的获取锁的代码。AbstractQueuedSynchronizer:compareAndSetState(int expect,int update) 方法以原子操作的方式更新state变量,即:CAS操作。程序在多CPU处理下,cmpxchg指令加上Lock前缀(Lock CMPXCHG)实现总线锁或缓存锁。CAS操作同时实现volatile读和volatile写的内存语义。

        锁释放-获取的内存语义的实现至少有下面两种方式:

  • 利用volatile变量的写-读所具有的内存语义;
  • 利用CAS所附带的volatile读和volatile写的内存语义。
     

三、final的内存语义

package com.cmmon.instance;

/**
 * @description final示例代码
 * @author tcm
 * @version 1.0.0
 * @date 2022/2/28 17:10
 **/
public class FinalExample {

    public int i;                        // 普通变量
    public final int j;                  // final变量
    public static FinalExample obj;
    public FinalExample () {             // 构造函数
        i = 1;                           // 写普通域
        j = 2;                           // 写final域
    }

    public static void writer () {       // 写线程A执行
        obj = new FinalExample ();
    }

    public static void reader () {       // 读线程B执行
        FinalExample object = obj;       // 读对象引用
        int a = object.i;                // 读普通域
        int b = object.j;                // 读final域
    }

}

·        写final域的重排序规则会要求编译器在final域的写之后,构造函数return之前插入一个StoreStore障屏;读final域的重排序规则要求编译器在读final域的操作前面插入一个LoadLoad屏障。如下表总结所示。

类型重排序规则

写final

编译器重排序JMM禁止编译器把final域的写重排序到构造函数之外
处理器重排序

编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障

作用为:禁止处理器把final域的写重排序到构造函数之外

读final

编译器重排序---------
处理器重排序

编译器会在读final域操作的前面插入一个LoadLoad屏障

作用:线程初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序

注意

1. 写final域的重排序规则“限定”在了构造函数之内完成

2. 少数处理器允许对存在间接依赖关系的操作做重排序,所以读final域操作前面插入LoadLoad屏障

        只要保证写final预在构造函数中没有“逸出”,那么不需要使用同步(指lock和volatile的使用)就可以保证任意线程都能读final域在构造函数中被初始化之后的值。下面代码示例写final域“逸出”

package com.cmmon.instance;

/**
 * @description 写final域在构造函数内溢出
 * @author tcm
 * @version 1.0.0
 * @date 2022/2/28 17:35
 **/
public class FinalReferenceEscapeExample {

    public final int i;
    public static FinalReferenceEscapeExample obj;
    
    public FinalReferenceEscapeExample () {
        i = 1;                                // 1写final域
        obj = this;                           // 2 this引用在此"逸出"
    }
    
    public static void writer() {
        new FinalReferenceEscapeExample ();
    }
    
    public static void reader() {
        if (obj != null) {                    // 3
            int temp = obj.i;                 // 4
        }
    }

}

四、参考资料

Java内存模型<一> _ 基础_爱我所爱0505-CSDN博客

volatile与synchronized实现原理_爱我所爱0505-CSDN博客

Volatile使用原理及作用_huangwei18351的博客-CSDN博客_volatile的作用和原理

详解synchronized与Lock的区别与使用_brickworkers的博客-CSDN博客_synchronized和lock的区别

让你彻底理解Synchronized - 简书

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值