当前位置: 首页 > news >正文

volatile,synchronized,reentranlock,CAS详解

1. volatile

1.1 64位写入的原子性(Half Write)

例如,对于一个long型变量的赋值和取值操作而言,在多线程场景下,线程A调用set(100),线程B调用get(),在某些场景下,返回值可能不是100。

public class MyClass {private long a = 0;// 线程A调用set(100)public void set(long a) {this.a = a;}// 线程B调用get(),返回值一定是100吗?public long get() {return this.a;}
}

因为JVM的规范并没有要求64位的long或者double的写入是原子的。在32位的机器上,一个64位变量的写入可能被拆分成两个32位的写操作来执行。这样一来,读取的线程就可能读到“一半的值”解决办法也很简单,在long前面加上volatile关键字。

1.2 重排序:DCL问题

单例模式的线程安全的写法不止一种,常用写法为DCL(Double Checking Locking),如下所示:

public class Singleton {private static Singleton instance;public static Singleton getInstance() {if (instance == null) {synchronized(Singleton.class) {if (instance == null) {// 此处代码有问题instance = new Singleton();}}}return instance;}
}

上述的 instance = new Singleton(); 代码有问题:其底层会分为三个操作:

  1. 分配一块内存。
  2. 在内存上初始化成员变量。
  3. 把instance引用指向内存。


在这三个操作中,操作2和操作3可能重排序,即先把instance指向内存,再初始化成员变量,因为二者并没有先后的依赖关系。此时,另外一个线程可能拿到一个未完全初始化的对象。这时,直接访问里面的成员变量,就可能出错。这就是典型的“构造方法溢出”问题。

解决办法也很简单,就是为instance变量加上volatile修饰。
volatile的三重功效:64位写入的原子性、内存可见性和禁止重排序。

1.3 volatile实现原理

由于不同的CPU架构的缓存体系不一样,重排序的策略不一样,所提供的内存屏障指令也就有差异。

这里只探讨为了实现volatile关键字的语义的一种参考做法:

  1. 在volatile写操作的前面插入一个StoreStore屏障。保证volatile写操作不会和之前的写操作重排序。
  2. 在volatile写操作的后面插入一个StoreLoad屏障。保证volatile写操作不会和之后的读操作重排序。
  3. 在volatile读操作的后面插入一个LoadLoad屏障+LoadStore屏障。保证volatile读操作不会和之后的读操作、写操作重排序。


具体到x86平台上,其实不会有LoadLoad、LoadStore和StoreStore重排序,只有StoreLoad一种重排序(内存屏障),也就是只需要在volatile写操作后面加上StoreLoad屏障。

1.4 4 JSR-133对volatile语义的增强

在JSR -133之前的旧内存模型中,一个64位long/ double型变量的读/ 写操作可以被拆分为两个32位的读/写操作来执行。从JSR -133内存模型开始 (即从JDK5开始),仅仅只允许把一个64位long/double
型变量的写操作拆分为两个32位的写操作来执行,任意的读操作在JSR -133中都必须具有原子性(即任意读操作必须要在单个读事务中执行)。

这也正体现了Java对happen-before规则的严格遵守。

2. synchronized

2.1 锁的对象

synchronized关键字“给某个对象加锁”,示例代码:

public Class MyClass {public void synchronized method1() {// ...}public static void synchronized method2() {// ...}
}

等价于:

public class MyClass {public void method1() {synchronized(this) {// ...}}public static void method2() {synchronized(MyClass.class) {// ...}}
}

实例方法的锁加在对象myClass上;静态方法的锁加在MyClass.class上。

2.2 锁的本质

如果一份资源需要多个线程同时访问,需要给该资源加锁。加锁之后,可以保证同一时间只能有一个线程访问该资源。资源可以是一个变量、一个对象或一个文件等。

加锁
锁是一个“对象”,作用如下:

  1. 这个对象内部得有一个标志位(state变量),记录自己有没有被某个线程占用。最简单的情况是这个state有0、1两个取值,0表示没有线程占用这个锁,1表示有某个线程占用了这个锁。
  2. 如果这个对象被某个线程占用,记录这个线程的thread ID。
  3. 这个对象维护一个thread id list,记录其他所有阻塞的、等待获取拿这个锁的线程。在当前线程释放锁之后从这个thread id list里面随机取一个线程唤醒

要访问的共享资源本身也是一个对象,例如前面的对象myClass,这两个对象可以合成一个对象。代码就变成synchronized(this) {…},要访问的共享资源是对象a,锁加在对象a上。
当然,也可以另外新建一个对象,代码变成synchronized(obj1) {…}。这个时候,访问的共享资源是对象a,而锁加在新建的对象obj1上。

资源和锁合二为一,使得在Java里面,synchronized关键字可以加在任何对象的成员上面。这意味
着,这个对象既是共享资源,同时也具备“锁”的功能!

2.3 实现原理

锁如何实现?

在对象头里,有一块数据叫Mark Word。在64位机器上,Mark Word是8字节(64位)的,这64位中有2个重要字段:锁标志位和占用该锁的thread ID。因为不同版本的JVM实现,对象头的数据结构会有各种差异。

在 Java 中,每个对象都隐式包含⼀个 monitor(监视器)对象,加锁的过程其实就是竞争 monitor 的过程,当线程进⼊字节码 monitorenter 指令之后,线程将持有 monitor 对象, 执⾏ monitorexit 时释放 monitor 对象,当其他线程没有拿到 monitor 对象时,则需要阻塞 等待获取该对象。

2.4 锁的升级

  1. 偏向锁 在 JDK1.8 中,其实默认是轻量级锁,但如果设定了 -XX:BiasedLockingStartupDelay = 0,那在对⼀个 Object 做 syncronized 的时候,会⽴即上⼀把偏向锁。当处于偏向锁状态时,markwork 会记录当前线程 ID 。
  2. 升级到轻量级锁 当下⼀个线程参与到偏向锁竞争时,会先判断 markword 中保存的线程 ID 是否与这个 线程 ID 相等,如果不相等,会⽴即撤销偏向锁,升级为轻量级锁。每个线程在⾃⼰的线 程栈中⽣成⼀个 LockRecord ( LR ),然后每个线程通过 CAS (⾃旋)的操作将锁对象头 中的 markwork设置为指向⾃⼰的 LR 的指针,哪个线程设置成功,就意味着获得锁。 关于 synchronized 中此时执⾏的 CAS 操作是通过 native 的调⽤ HotSpot 中 bytecodeInterpreter.cpp ⽂件 C++ 代码实现的,有兴趣的可以继续深挖。
  3. 升级到重量级锁 如果锁竞争加剧(如线程⾃旋次数或者⾃旋的线程数超过某阈值, JDK1.6 之后,由JVM ⾃⼰控制该规则),就会升级为重量级锁。此时就会向操作系统申请资源,线程挂起,进 ⼊到操作系统内核态的等待队列中,等待操作系统调度,然后映射回⽤户态。在重量级 锁中,由于需要做内核态到⽤户态的转换,⽽这个过程中需要消耗较多时间,也就 是"重"的原因之⼀。

3. reentranlock

3.1 ReentrantLock的特点

  1. 可重⼊
    ReentrantLock 和 syncronized 关键字⼀样,都是可重⼊锁,不过两者实现原理稍有差别, RetrantLock 利⽤ AQS 的的 state 状态来判断资源是否已锁,同⼀线程重⼊加锁, state 的状态 +1 ; 同⼀线程重⼊解锁, state 状态 -1 (解锁必须为当前独占线程,否则异 常); 当 state 为 0 时解锁成功。
  2. 需要⼿动加锁、解锁
    synchronized 关键字是⾃动进⾏加锁、解锁的,⽽ ReentrantLock 需要lock() 和 unlock() ⽅法配合 try/finally 语句块来完成,来⼿动加锁、解锁。
  3. ⽀持设置锁的超时时间
    synchronized 关键字⽆法设置锁的超时时间,如果⼀个获得锁的线程内部发⽣死锁,那 么其他线程就会⼀直进⼊阻塞状态,⽽ ReentrantLock 提供 tryLock ⽅法,允许设置线 程获取锁的超时时间,如果超时,则跳过,不进⾏任何操作,避免死锁的发⽣。
  4. ⽀持公平/⾮公平锁
    synchronized 关键字是⼀种⾮公平锁,先抢到锁的线程先执⾏。⽽ReentrantLock 的 构造⽅法中允许设置 true/false 来实现公平、⾮公平锁,如果设置为 true ,则线程获取 锁要遵循"先来后到"的规则,每次都会构造⼀个线程 Node ,然后到双向链表的"尾巴"后⾯排队,等待前⾯的 Node 释放锁资源。
  5. 可中断锁
    ReentrantLock 中的 lockInterruptibly() ⽅法使得线程可以在被阻塞时响应中断,⽐如⼀个线程 t1 通过 lockInterruptibly() ⽅法获取到⼀个可重⼊锁,并执⾏⼀个⻓时间 的任务,另⼀个线程通过 interrupt()⽅法就可以⽴刻打断 t1 线程的执⾏,来获取t1持 有的那个可重⼊锁。⽽通过 ReentrantLock 的 lock()⽅法或者 Synchronized 持有锁 的线程是不会响应其他线程的interrupt() ⽅法的,直到该⽅法主动释放锁之后才会响应 interrupt() ⽅法。

4. CAS

CAS全称是Compare And Swap,即比较替换,是实现并发应用到的一种技术。
操作包含三个操作数:内存位置(V)、预期原值(A)和新值(B)
如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。
CAS存在三大问题:ABA问题,循环时间长开销大,以及只能保证一个共享变量的原子操作。

CAS是compare and swap的缩写,即我们所说的⽐较交换。
cas是⼀种基于锁的操作,⽽且是乐观锁。

在java中锁分为乐观锁和悲观锁。悲观锁是将资源 锁住,等⼀个之前获得锁的线程释放锁之后,下⼀个线程才可以访问。
⽽乐观锁采取了⼀种宽 泛的态度,通过某种⽅式不加锁来处理资源,⽐如通过给记录加version来获取数据,性能较 悲观锁 有很⼤的提⾼。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址⾥ ⾯的值和A的值是⼀样的,那么就将内存⾥⾯的值更新成B。CAS是通过⽆限循环来获取数据 的,若果在第⼀轮循环中,a线程获取地址⾥⾯的值被b线程修改了,那么a线程需要⾃旋,到 下次循环才有可能机会执⾏。

java.util.concurrent.atomic 包下的类⼤多是使⽤CAS操作来实现的(AtomicInteger,AtomicBoolean,AtomicLong)。

5. ReentrantLock和synchronized区别

ReentrantLock 提供了比 synchronized 更加灵活和强大的锁机制。
首先他们肯定具有相同的功能和内存语义。

  1. 与 synchronized 相比,ReentrantLock提供了更多、更加全面的功能、具备更强的扩展性。例如:时间锁等候,可中断锁等候,锁投票。
  2. ReentrantLock 还提供了条件 Condition,对线程的等待、唤醒操作更加详细和灵活,所以在多个条件变量和高度竞争锁的地方,ReentrantLock更加适合(以后会阐述Condition)。
  3. ReentrantLock 提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下次运行时处理,而 synchronized 则一旦进入锁请求要么成功要么阻塞,所以相比 synchronized 而言,ReentrantLock会不容易产生死锁些。
  4. ReentrantLock 支持更加灵活的同步代码块,但是使用 synchronized 时,只能在同一个synchronized 块结构中获取和释放。注意,ReentrantLock的锁释放一定要在 fina1ly 中处理,否则可能会产生严重的后果。
  5. ReentrantLock 支持中断处理,且性能较 synchronized 会好些。

6. synchronized关键字和volatile的区别

  1. volatile关键字是线程同步的轻量级实现,所以volatile性能⽐synchronized关键字要好。但是volatile关键字只能⽤于变量⽽synchronized关键字可以修饰⽅法以及代码块。
  2. 多线程访问volatile关键字不会发⽣阻塞,⽽synchronized关键字可能会发⽣阻塞。 volatile关键字主要⽤于解决变量在多个线程之间的可⻅性,⽽ synchronized关键字解决的 是多个线程之间访问资源的同步性。
  3. volatile关键字能保证数据的可⻅性,但不能保证数据的原⼦性。synchronized关键字两者都 能保证。

相关文章:

  • go env 命令详解
  • TouchGFX之Button
  • JavaEE企业级分布式高级架构师课程
  • python知识点总结(十)
  • Chrome 插件 storage API 解析
  • 类的定义与实例化
  • AI大模型学习:AI大模型在特定领域的应用
  • 华为OD七日集训第5期 - 按算法分类,由易到难,循序渐进,玩转OD
  • 工业无线网关在汽车制造企业的应用效果和价值-天拓四方
  • C# 多态 派生类 abstract virtual new
  • JSP基础
  • 登录拦截器
  • unity无法使用道路生成插件Road Architect(ctrl和shift无法标点)
  • SAP_MMQM模块-采购收货质量控制
  • 【八股】泛型
  • [PHP内核探索]PHP中的哈希表
  • axios请求、和返回数据拦截,统一请求报错提示_012
  • IE报vuex requires a Promise polyfill in this browser问题解决
  • Netty 4.1 源代码学习:线程模型
  • Spring核心 Bean的高级装配
  • Three.js 再探 - 写一个跳一跳极简版游戏
  • vue自定义指令实现v-tap插件
  • 限制Java线程池运行线程以及等待线程数量的策略
  • 携程小程序初体验
  • 硬币翻转问题,区间操作
  • 原生 js 实现移动端 Touch 滑动反弹
  • 东超科技获得千万级Pre-A轮融资,投资方为中科创星 ...
  • $.ajax()方法详解
  • (01)ORB-SLAM2源码无死角解析-(56) 闭环线程→计算Sim3:理论推导(1)求解s,t
  • (SpringBoot)第七章:SpringBoot日志文件
  • (超详细)2-YOLOV5改进-添加SimAM注意力机制
  • (附源码)spring boot智能服药提醒app 毕业设计 102151
  • (转)Spring4.2.5+Hibernate4.3.11+Struts1.3.8集成方案一
  • (转)从零实现3D图像引擎:(8)参数化直线与3D平面函数库
  • ./和../以及/和~之间的区别
  • .NET MVC、 WebAPI、 WebService【ws】、NVVM、WCF、Remoting
  • .net使用excel的cells对象没有value方法——学习.net的Excel工作表问题
  • [ 云计算 | Azure 实践 ] 在 Azure 门户中创建 VM 虚拟机并进行验证
  • []T 还是 []*T, 这是一个问题
  • [04] Android逐帧动画(一)
  • [AI]ChatGPT4 与 ChatGPT3.5 区别有多大
  • [Angular] 笔记 18:Angular Router
  • [Angularjs]asp.net mvc+angularjs+web api单页应用之CRUD操作
  • [BZOJ1178][Apio2009]CONVENTION会议中心
  • [CSS]文字旁边的竖线以及布局知识
  • [CVPR 2023:3D Gaussian Splatting:实时的神经场渲染]
  • [IE技巧] 如何让IE 启动的时候不加载任何插件
  • [Java性能剖析]Sun JDK基本性能剖析工具介绍
  • [jquery]this触发自身click事件,当前控件向上滑出
  • [MySQL复制异常]Cannot execute statement: impossible to write to binary log since statement is in row for
  • [MySQL数据库部署及初始化相关]
  • [NEWS] J2SE5.0来了
  • [NOI2014]购票
  • [Nuget]使用Nuget管理工具包
  • [Real world Haskell] 中文翻译:第二章 类型与函数