一道有深度的面试题:本地悲观锁实现计数器需要加 volatile 吗?

故事背景

团队内部前几天讨论了一个面试题,在本地用乐观锁和悲观锁实现计数器需要volatile关键字吗?毫无疑问,使用乐观锁一定是需要的。但使用悲观锁需要呢?

张三:不需要吧,每次不都是一个线程访问变量吗?

李四:还是需要的,加锁只是保证了该变量被一个线程独占,但是不能保证拿到变量最新的值,因为可能上个线程操作后数据还在线程本地内存里,导致本线程读取的数据是脏数据!

张三:嘶,好像有点道理,不过如果加锁的话,这个本地内存的数据什么时候刷到主内存呢?会不会加锁后就直接读取到最新数据了?

李四:诶?问得好,这个得研究研究。

预备知识

Hppens-Before 规则

Java Memory Model(JMM) 里定义了一些跨线程操作的 Happens-Before 关系,并据此来决定线程间一些操作的相对顺序。如果说操作 A “Happens-Before” B,则有两个含义:

  1. 可见性:A 的操作对 B 可见
  2. 顺序性:A 要在 B 之前执行

Happens-before 规则有多条,本文只借助几条来进行解释

  • 程序顺序规则:如果程序中操作 A 在操作 B 之前,那么在线程中操作 A Happens-Before 操作 B
  • 监视器锁规则:监视器上的 unlock 操作 Happens-Before 同一个监视器的 lock 操作
  • volatile 变量规则:写入 volatile 变量 Happens-Before 读取该变量
  • 传递性:如果 hb(A, B)hb(B, C),则 hb(A, C)

volatile 的内存语义

从内存语义的角度来说,volatile的写-读与锁的释放-获取有相同的内存效果。

volatile 写的内存语义:当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中写 volatile 前所有的共享变量刷新到主内存中,并让其他 core 的缓存失效,不管这些变量是否volatile,不仅仅只是 volatile 变量本身。

volatile 读的内存语义:当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

关于 volatile 写的说明:https://jenkov.com/tutorials/java-concurrency/volatile.html

synchronized 实现

synchronized 可见性原理

Synchronized 的 Happens-Before 规则,即监视器锁规则:对同一个监视器的解锁,Happens-Before 于对该监视器的加锁。

在这里插入图片描述

图中每一个箭头连接的两个节点就代表之间的 Happens-Before 关系,红色的为监视器锁规则推导而出:线程A释放锁 Happens-Before 线程B加锁;蓝色的则是通过程序顺序规则和监视器锁规则推测出来 Happens-Before 关系,通过传递性规则进一步推导的 Happens-Before 关系。

根据 Happens-Before 规则的程序顺序规则:如果 A Happens-Before B,则 A 的执行结果对 B 可见,并且 A 的执行顺序先于 B。因此,如果线程 A 修改了计数器的值,对线程 B 是可见的。

synchronized 验证代码

public class TestSynchronizedCounter {
    private static int count = 0;

    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                synchronized (TestSynchronizedCounter.class) {
                    count++;
                }
            }
            System.out.println("thread1 finish, sum = " + count);
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                synchronized (TestSynchronizedCounter.class) {
                    count++;
                }
            }
            System.out.println("thread2 finish, sum = " + count);
        }).start();
    }
}

Reentrantlock 实现

Reentrantlock 可见性原理

ReentrantLock 也可以起到和 Synchronized 关键字同样的效果,在 Lock 接口的注释中有如下描述。这段描述的意思是说所有的 Lock 接口实现必须在内存可见性上具有和内置监视器锁(Synchronized)相同的语义。

Memory Synchronization
All Lock implementations must enforce the same memory synchronization semantics as provided by the built-in monitor lock, as described in The Java Language Specification (17.4 Memory Model) :
A successful lock operation has the same memory synchronization effects as a successful Lock action.
A successful unlock operation has the same memory synchronization effects as a successful Unlock action.

ReentrantLock 通过内部的 Sync 类来完成锁的功能,Sync 类扩展了 AQS,重用 AQS 的各项同步功能。众所周知:Reentrantlock 的 lock 和 unlock 都需要读取并用 CAS 方式修改被 volatile 修饰的变量 state。

需要注意的是,volatile 写操作会把之前的共享变量更新一并发布出去,而不只是 volatile 变量本身。

在这里插入图片描述

假设线程a通过调用lock方法获取到锁,此时线程b也调用了lock方法,因为a尚未释放锁,b只能等待。a在获取锁的过程中会先读state,再写state。当a释放掉锁并唤醒b,b会尝试获取锁,也会先读state,再写state。

根据 Hppens-Before 规则的 volatile 变量规则:写入 volatile 变量 Happens-Before 读取该变量以及 volatile 写的内存语义。可以推测出,线程a在写入state变量之前的任何操作结果对线程b都是可见的。

再次说明:volatile 写操作会把之前的共享变量更新一并发布出去,而不只是 volatile 变量本身。

以公平锁为例,我们看看 ReentrantLock 获取锁 & 释放锁的关键代码:

private volatile int state; // 关键 volatile 变量
protected final int getState() {
    return state;
}

// 获取锁
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState(); // 重要!!!读 volatile 变量
    ... // 竞争获取锁逻辑,省略   
}

// 释放锁
protected final boolean tryRelease(int releases) {
    boolean free = false;
    ... // 根据状态判断是否成功释放,省略
    setState(c); // 重要!!!写 volatile 变量
    return free;
}

简单来说就是对于每一个进入到锁的临界区域的线程,都会做三件事情:

  • 获取锁,读取 volatile 变量;
  • 执行临界区代码,针对本文是对 count 做自增;
  • 写 volatile 变量 (即发布所有写操作),释放锁。

Reentrantlock 验证代码

public class TestLockCounter {
    private final static ReentrantLock LOCK = new ReentrantLock();
    private static int count = 0;

    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 1000000; i++) {
                try {
                    LOCK.lock();
                    count++;
                } finally {
                    LOCK.unlock();
                }
            }
            System.out.println("thread1 finish, sum = " + count);
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 1000000; i++) {
                try {
                    LOCK.lock();
                    count++;
                } finally {
                    LOCK.unlock();
                }
            }
            System.out.println("thread2 finish, sum = " + count);
        }).start();
    }
}

相关原理

内存屏障

内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个:

  • 保证特定操作的执行顺序
  • 二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。

Intel硬件提供了一系列的内存屏障,Java内存模型屏蔽了底层硬件平台的差异,由 JVM来为不同的平台生成相应的机器码。 JVM中提供了四类内存屏障指令:

屏障类型指令示例说明
LoadLoadLoad1; LoadLoad; Load2在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕
StoreStoreStore1; StoreStore; Store2在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见
LoadStoreLoad1; LoadStore; Store2在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕
StoreLoadStore1; StoreLoad; Load2在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见

Happens-Before 底层实现原理

Happens-Before 是对 Java 内存模型(JMM)中所规定的可见性的更高级的语言层面的描述,程序员可以用这个原则解决并发环境下两个操作之间的可见性问题,而不需要陷入 Java 内存模型苦涩难懂的定义中。

一个 Happens-Before 规则对应于一个或多个编译器和处理器重排序规则,Happens-Before 与JMM的关系如下图所示:

在这里插入图片描述

volatile 底层实现原理

JVM的实现会在 volatile 读写前后均加上内存屏障,实现了可见性和有序性。如下所示:

LoadLoadBarrier
volatile 读操作
LoadStoreBarrier

StoreStoreBarrier
volatile 写操作
StoreLoadBarrier

总结

本地用悲观锁实现计数器不需要加 volatile ,synchronized 关键字和 Lock 接口都具有 Happens-Before 规则:

  • synchronized 关键字遵循监视器锁规则,从而实现了代码临界区内变量的可见性。
  • ReentrantLock 及其它 Lock 接口实现类借助了 volatile 关键字间接地实现了可见性。

参考资料

  • Java 并发知识:https://lotabout.me/books/Java-Concurrency/Happens-Before/index.html
  • Happens-Before 原则深入解读:https://xie.infoq.cn/article/d0f4d9e812ee03b6a32265686
  • Java Volatile Keyword:https://jenkov.com/tutorials/java-concurrency/volatile.html
  • 关键字: synchronized 详解:https://pdai.tech/md/java/thread/java-thread-x-key-synchronized.html
  • 深度好文 | Java 可重入锁内存可见性分析:https://cloud.tencent.com/developer/article/1142546
  • ReentrantLock 是如何保证内存的可见性的:https://zhuanlan.zhihu.com/p/80929454

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/454001.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

什么是字节码?采用字节码的好处是什么?

在 Java 中&#xff0c;JVM 可以理解的代码就叫做字节码&#xff08;即扩展名为 .class 的文件&#xff09;&#xff0c;字节码是一种中间代码&#xff0c;它是由源代码经过编译生成的一种二进制表示形式。字节码通常不针对特定的硬件平台&#xff0c;而是针对虚拟机设计的&…

antd vue Tabs控件的使用

Ant Design Vue-------Tabs标签页 今天就讲讲Ant Design Vue下的控件----tabs 标签页 结合项目中的需求&#xff0c;讲一下该控件如何使用&#xff0c;需求&#xff1a; &#xff08;1&#xff09;竖排样式 &#xff08;2&#xff09;如何使用v-for绑定数据源 &#xff08;3…

蓝桥杯专题 bfs习题详解

1.离开中山路 #include<iostream> #include<cstring> #include<queue> #include<algorithm> #include<string> using namespace std; int x1,x2,y1,y2; int n,n1,m1; const int N1010;typedef pair<int,int> PII; queue<PII> q;int …

CTP-API开发系列之九:行情登录及订阅代码

CTP-API开发系列之九&#xff1a;行情登录及订阅代码 前情回顾全局配置参数行情初始化代码行情登录行情订阅行情接收注意事项 前情回顾 CTP-API开发系列之一&#xff1a;各版本更新说明&#xff08;持续更新&#xff09; CTP-API开发系列之二&#xff1a;问题汇总&#xff08;…

(done) NLP “bag-of-words“ 方法 (带有二元分类和多元分类两个例子)词袋模型、BoW

一个视频&#xff1a;https://www.bilibili.com/video/BV1mb4y1y7EB/?spm_id_from333.337.search-card.all.click&vd_source7a1a0bc74158c6993c7355c5490fc600 这里有个视频&#xff0c;讲解得更加生动形象一些 总得来说&#xff0c;词袋模型(Bow, bag-of-words) 是最简…

fs模块 文件写入 之 流式写入

一、流式写入&#xff08;createWriteStream &#xff09;与 文件的同步异步写入&#xff08;writeFile &#xff09;的区别&#xff1a; 1》程序打开一个文件是需要耗费资源的&#xff0c;流式写入可以减少打开关闭文件的次数。 2》文件的流式写入方式适用于大文件写入或者频…

ChatGPT国内能用吗?中国用户怎么才能使用ChatGPT?

与ChatGPT类似的国内网站&#xff0c;他们都能提供和ChatGPT相似的能力&#xff0c;而且可以在国内直接使用。 点击直达方式 百科GPT官网&#xff1a;baikegpt.cn ChatGPT是基于GPT-3.5架构的语言模型的一个实例&#xff0c;由OpenAI开发。以下是ChatGPT的发展历史&#xff1…

《ElementPlus 与 ElementUI 差异集合》el-button 属性 type=“text“ 被删除

差异 element-ui el-button中&#xff0c;属性 type"text" 定义文字按钮&#xff0c;也是链接按钮&#xff1b;element-plus el-button中&#xff0c;改为新增属性 link 并与其它 type 值配合使用&#xff1b; // element-ui <el-button type"text"&g…

(Linux学习九)管道、重定向介绍

FD:文件描述符。 0,1,2,3&#xff0c;&#xff0c;&#xff0c;。进程打开文件所用。 0标准输入 1 标准输出 2 标准错误输出 3普通文件 一、管道 | 命令 | tee | xargs | 命令1的输出&#xff0c;作为命令2的输入&#xff0c;命令2的输出作为命令3的输入 | tee 三通&#xff…

Qt+FFmpeg+opengl从零制作视频播放器-3.解封装

解封装:如下图所示,就是将FLV、MKV、MP4等文件解封装为视频H.264或H.265压缩数据,音频MP3或AAC的压缩数据,下图为常用的基本操作。 ffmpeg使用解封装的基本流程如下: 在使用FFmpeg API之前,需要先注册API,然后才能使用API。当然,新版本ffmpeg库不需要再调用下面的方法…

原型模式(Clone)——创建型模式

原型模式(clone)——创建型模式 什么是原型模式&#xff1f; 原型模式是一种创建型设计模式&#xff0c; 使你能够复制已有对象&#xff0c; 而又无需依赖它们所属的类。 总结&#xff1a;需要在继承体系下&#xff0c;实现一个clone接口&#xff0c;在这个方法中以本身作为拷…

技术方案|某工业集团PaaS容灾方案

在当今快速发展的数字化时代&#xff0c;业务的连续性和稳定性已成为企业核心竞争力的重要组成部分。然而&#xff0c;由于各种原因&#xff0c;企业常常面临着数据丢失、系统瘫痪等潜在风险。因此&#xff0c;制定一套科学、高效的容灾方案至关重要。本文将围绕某全球领先的工…

WRF模型运行教程(ububtu系统)--III.运行WRF模型(官网案例)

创建DATA目录 1、创建一个DATA目录用于存放数据&#xff08;一般为fnl数据&#xff0c;放在Build_WRF目录下&#xff09;。 mkdir DATA 2、将数据放在DATA文件夹里。 3、链接数据 cd ~/Build_WRF/WPS/ ./link_grib.csh ~/Build_WRF/DATA/data/fnl ln -sf ungrib/Variab…

数据结构02:线性表 顺序表习题01[C++]

图源&#xff1a;文心一言 考研笔记整理~&#x1f95d;&#x1f95d; 之前的博文链接在此&#xff1a;数据结构02&#xff1a;线性表[顺序表链表]_线性链表-CSDN博客~&#x1f95d;&#x1f95d; 本篇作为线性表的代码补充&#xff0c;供小伙伴们参考~&#x1f95d;&#x1…

(C语言)strcat函数详解与模拟实现与strncat函数详解

目录 1. strcat函数详解 1. strcat函数模拟实现 3. strcat函数的危险性 4. strncat函数详解 4.1 strncat函数的特殊情况验证 1. strcat函数详解 头文件<string.h> 该函数是用来对字符串末尾追加字符串的&#xff0c;有两个参数&#xff0c;destination是要被追加的字…

LVS 负载均衡-DR模式

一 . DR 模式 直接路由 &#xff1a; 1.介绍&#xff1a; 直接路由&#xff08;Direct Routing&#xff09;&#xff1a;简称 DR 模式&#xff0c;采用半开放式的网络结构&#xff0c;与 TUN 模式的结构类似&#xff0c;但各节点并不是分散在各地&#xff0c;而是与调度器位…

基于SpringBoot+MYSQL的旅游网站

目录 1、前言介绍 2、主要技术 3、系统流程分析 1、登录流程图如下&#xff1a; 2、管理员后台管理流程图如下&#xff1a; 3. 修改密码流程图如下&#xff1a; 4、系统设计 4.1、系统结构设计 4.2 数据库概述 4.2.1 数据库概念设计 4.2.2 数据库逻辑设计 5、运行截…

alibabacloud学习笔记08(小滴课堂)

讲解JDK⼀些基础知识科普 介绍什么是微服务的网关和应用场景 介绍网关SpringCloud Gateway 创建SpringCloud网关项目和依赖添加 1.添加依赖&#xff1a; 2.创建启动类&#xff1a; 3.配置配置文件&#xff1a; 启动验证&#xff1a; 启动网关以及对应的订单服务&#xff1a; …

一个简单的微信小程序表单提交样式模板

没什么东西&#xff0c;只是方便自己直接复制使用 .wxml <view class"box"><form bindsubmit"formSubmit"><view class"form-item"><text class"head">姓名&#xff1a;</text><input class"…

luatos框架中LVGL如何使用中文字体〈二〉编写脚本设置中文字体

本节内容&#xff0c;将和大家一同学习&#xff0c;在luatos环境中&#xff0c;使用lvgl库&#xff0c;一步步的编译固件、编写脚本&#xff0c;最终实现中文字体的显示。 芯片&#xff1a;AIR101 LCD屏&#xff1a;ST7789 上一节&#xff0c;我们一同学习了&#xff0c;硬件引…