多线程Synchronized原理及扩展

CAS

什么是CAS?

CAS:Compare and Swap 比较并交换,乐观锁的一种实现。

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

1. 比较 A 与 V 是否相等。(比较)

2. 如果比较相等,将 B 写入 V。(交换)

3. 返回操作是否成功

当多个线程同时对某个资源进行CAS操作,只有一个线程操作成功,但不会阻塞其他线程,其他线程只会收到操作失败的信号。

CAS实现原理

针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:

  • java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
  • unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
  • Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子 性。
  1. java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
  2. unsafe的cas基于操作系统+cpu提供的机制实现

简而言之,是因为硬件予以了支持,软件层面才能做到

CAS的ABA问题

概述问题:

假设存在两个线程 t1 和 t2. 有一个共享变量 num, 初始值为 A.

线程 t1 想使用 CAS 把 num 值改成 Z, 那么就需要

  • 先读取 num 的值, 记录到 oldNum 变量中.
  • 使用 CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z.

但是, 在 t1 执行这两个操作之间, t2 线程可能把 num 的值从 A 改成了 B, 又从 B 改成了 A

线程 t1 的 CAS 是期望 num 不变就修改. 但是 num 的值已经被 t2 给改了. 只不过又改成 A 了. 这 个时候 t1 究竟是否要更新 num 的值为 Z 呢?

ABA引发的问题:

大部分的情况下, t2 线程这样的一个反复横跳改动, 对于 t1 是否修改 num 是没有影响的. 但是不排除一 些特殊情况

假设 滑稽老哥 有 100 存款. 滑稽想从 ATM 取 50 块钱. 取款机创建了两个线程, 并发的来执行 -50 操作.

我们期望一个线程执行 -50 成功, 另一个线程 -50 失败.

如果使用 CAS 的方式来完成这个扣款过程就可能出现问题.

正常的过程

1. 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100,

期望更新为 50.

2. 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.

3. 轮到线程2 执行了, 发现当前存款为 50, 和之前读到的 100 不相同, 执行失败.

异常的过程

1. 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100,

期望更新为 50.

2. 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.

3. 在线程2 执行之前, 滑稽的朋友正好给滑稽转账 50, 账户余额变成 100 !!

4. 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 再次执行扣款操作

解决方案:引入版本号

给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.

  • CAS 操作在读取旧值的同时, 也要读取版本号.
  • 真正修改的时候,
    • 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
    • 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了)

了解

在 Java 标准库中提供了 AtomicStampedReference 类. 这个类可以对某个类进行包装, 在内部就提 供了上面描述的版本管理功能

可以通过这个对象的方法保存我们要修改的变量,这个对象本身也包含版本号的字段

CAS的应用

实现自旋锁

基于CAS实现的更灵活的;

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;
    }
}

实现原子类

标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的.

典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作

注意:

  • CAS 是直接读写内存的, 而不是操作寄存器.
  • CAS 的读内存, 比较, 写内存操作是一条硬件指令, 是原子的
AtomicIneteger atomicInteger = new AtomicInteger(0);
//i++;
atomicInteger.getAndIncrement();

synchronized 原理(重点)

加锁工作过程

JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级

以对象头加锁的方式,实现线程安全;申请同一个对象锁时,多线程之间时同步互斥的;

对象中有一个对象头,其中有一个状态的字段:无锁,偏向锁,轻量级锁,重量级锁;

  • synchronized 申请锁成功,是后三种锁状态之一;
  • synchronized 编译为字节码之后,里面存了一个对象头的monitor机制(监视器)
    • monitorenter 执行synchronized 这行代码

偏向锁

第一个尝试加锁的线程,优先进入偏向锁状态。

偏向锁不是真的 "加锁", 只是给对象头中做一个 "偏向锁的标记", 记录这个锁属于哪个线程.

如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)

如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态.

偏向锁本质上相当于 "延迟加锁" . 能不加锁就不加锁, 尽量来避免不必要的加锁开销.

但是该做的标记还是得做的, 否则无法区分何时需要真正加锁.

同一个线程可重入方式申请锁,此时使用偏向锁

轻量级锁

随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁).

此处的轻量级锁就是通过 CAS 来实现.

  • 通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
  • 如果更新成功, 则认为加锁成功
  • 如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).

自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源.

因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了.

也就是所谓的 "自适应"

重量级锁

如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁

此处的重量级锁就是指用到内核提供的 mutex .

  • 执行加锁操作, 先进入内核态.
  • 在内核态判定当前锁是否已经被占用
  • 如果该锁没有占用, 则加锁成功, 并切换回用户态.
  • 如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒.
  • 经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒 这个线程, 尝试重新获取锁

其他的优化操作

锁消除

编译器+JVM 判断锁是否可消除. 如果可以, 就直接消除.

局部变量只有当前线程持有(不可能其他线程持有,此时不存在线程安全)

StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");

此时每个 append 的调用都会涉及加锁和解锁. 但如果只是在单线程中执行这个代码, 那么这些加 锁解锁操作是没有必要的, 白白浪费了一些资源开销

锁的粗化

一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化

实际开发过程中, 使用细粒度锁, 是期望释放锁的时候其他线程能使用锁.

但是实际上可能并没有其他线程来抢占这个锁. 这种情况 JVM 就会自动把锁粗化, 避免频繁申请释 放锁

总结synchronized 的特性:

  • 开始乐观锁,若冲突频繁,转换为悲观锁
  • 开始轻量锁,若持有时间较长,转为重量级锁
  • 实现轻量级锁是大概率用自旋锁策略
  • 不公平锁
  • 可重入锁
  • 不是读写锁

最难不过坚持!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值