栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

多线程——CAS和synchronized的优化过程

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

多线程——CAS和synchronized的优化过程

目录

1.CAS

1.1什么是CAS

1.2CAS是怎样实现

1.3CAS的应用

1.3.1实现原子类

1.3.2实现自旋锁

1.3.3CAS引发的ABA问题

2.synchronized关键字背后的锁升级流程

 2.1偏向锁

2.2轻量级锁

2.3重量级锁

2.4其他优化操作

2.4.1锁消除

2.4.2锁粗化


1.CAS

1.1什么是CAS

CAS:compare and swap,字面意思就是“比较并交换”。

CAS一般操作如下:

当多个线程同时对某个资源进行 CAS 操作,只能有一个线程操作成功,但是并不会阻塞其他线程 , 其他线程只会收到操作失败的信号。 可以将CAS理解为乐观锁的一种实现方式。线程并不会阻塞,不断尝试。

 

 

1.2CAS是怎样实现 针对不同的操作系统, JVM 用到了不同的 CAS 实现原理,简单来讲:         java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;         unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg ;         Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子 性。 简而言之,是因为 硬件予以了支持,软件层面才能做到 。

1.3CAS的应用

1.3.1实现原子类 标准库中提供了 java.util.concurrent.atomic 包 , 里面的类都是基于这种方式来实现的, 典型的就是 AtomicInteger 类 . 其中的 getAndIncrement 相当于 i++ 操作 。

 

 

 

1.3.2实现自旋锁 基于 CAS 实现更灵活的锁 , 获取到更多的控制权。 来看伪代码:
public class SpinLock {
    private Thread owner = null;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有. 
        // 如果这个锁已经被别的线程持有, 那么就自旋等待. 
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
       }
   }
    public void unlock (){
        this.owner = null;
   }
}

1.3.3CAS引发的ABA问题

 

解决方案:

给要修改的值 , 引入版本号 . 在 CAS 比较数据当前值和旧值的同时 , 也要比较版本号是否符合预期 CAS 操作在读取旧值的同时 , 也要读取版本号。 真正修改的时候 ,         如果当前版本号和读到的版本号相同, 则修改数据 , 并把版本号 + 1.         如果当前版本号高于读到的版本号. 就操作失败 ( 认为数据已经被修改过了 ).

2.synchronized关键字背后的锁升级流程

 2.1偏向锁 第一个尝试加锁的线程 , 优先进入偏向锁状态 .         偏向锁不是真的 " 加锁 ", 只是给对象头中做一个 " 偏向锁的标记 ", 记录这个锁属于哪个线程 . 如果后续没有其他线程来竞争该锁 , 那么就不用进行其他同步操作了 ( 避免了加锁解锁的开销 ) 如果后续有其他线程来竞争该锁 ( 刚才已经在锁对象中记录了当前锁属于哪个线程了 , 很容易识别 当前申请锁的线程是不是之前记录的线程 ), 那就取消原来的偏向锁状态 , 进入一般的轻量级锁状态 . 偏向锁本质上相当于 " 延迟加锁 " . 能不加锁就不加锁 , 尽量来避免不必要的加锁开销 . 但是该做的标记还是得做的 , 否则无法区分何时需要真正加锁 . 举例理解:

2.2轻量级锁 随着其他线程进入竞争 , 偏向锁状态被消除 , 进入轻量级锁状态 ( 自适应的自旋锁 )。 此处的轻量级锁就是通过 CAS 来实现 .通过 CAS 检查并更新一块内存 ( 比如 null => 该线程引用 ) 如果更新成功 , 则认为加锁成功, 如果更新失败 , 则认为锁被占用 , 继续自旋式的等待 ( 并不放弃 CPU).。

2.3重量级锁 如果竞争进一步激烈 , 自旋不能快速获取到锁状态 , 就会膨胀为重量级锁 此处的重量级锁就是指用到内核提供的 mutex .          执行加锁操作 , 先进入内核态 . 在内核态判定当前锁是否已经被占用 如果该锁没有占用 , 则加锁成功 , 并切换回用户态 . 如果该锁被占用 , 则加锁失败 . 此时线程进入锁的等待队列 , 挂起 . 等待被操作系统唤醒 . 经历了一系列的沧海桑田 , 这个锁被其他线程释放了 , 操作系统也想起了这个挂起的线程 , 于是唤醒 这个线程 , 尝试重新获取锁 .

2.4其他优化操作

2.4.1锁消除 编译器 +JVM 判断锁是否可消除 . 如果可以 , 就直接消除 . 什么是 " 锁消除 " 有些应用程序的代码中 , 用到了 synchronized, 但其实没有在多线程环境下 . ( 例如 StringBuffer)
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");
此时每个 append 的调用都会涉及加锁和解锁 . 但如果只是在单线程中执行这个代码 , 那么这些加 锁解锁操作是没有必要的 , 白白浪费了一些资源开销

2.4.2锁粗化 一段逻辑中如果出现多次加锁解锁 , 编译器 + JVM 会自动进行锁的粗化 . 锁的粒度 : 粗和细 实际开发过程中 , 使用细粒度锁 , 是期望释放锁的时候其他线程能使用锁 . 但是实际上可能并没有其他线程来抢占这个锁 . 这种情况 JVM 就会自动把锁粗化 , 避免频繁申请释 放锁 .

 

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1021058.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号