2023.10.28 关于 synchronized 原理 及 ReentrantLock 介绍

目录

synchronized 特性

synchronized 优化机制

锁升级(锁膨胀)

其他优化机制

锁消除

锁粗化

ReentrantLock

加锁方式

相关问题

优势方面

优势一

优势二

优势三


synchronized 特性

  • 开始时是乐观锁,如果锁冲突频繁,就转为悲观锁
  • 开始是轻量级锁,如果锁被持有的时间较长,就转化成重量级锁
  • 实现轻量级锁的时候大概率用到的自旋锁策略
  • 是一个不公平锁
  • 是一种可重入锁
  • 不是读写锁

synchronized 优化机制

锁升级(锁膨胀)

  • JVM 将 synchronized 分为以下四个状态,会根据情况,依次进行升级
  1. 无锁
  2. 偏向锁
  3. 轻量级锁
  4. 重量级锁

无锁状态

  • 即代码还未执行到加锁的代码块中,该状态称为无锁状态

偏向锁状态

  • 偏向锁的获取和释放操作开销非常低,几乎可以忽略不记,因为它不需要进行线程间的竞争和同步
  • 当一个线程第一次尝试获取锁时,会将锁的标记设置为偏向锁,并将线程ID 记录在锁的元数据中
  • 当同一个线程再次尝试获取锁时,无需竞争,直接获取到锁,不需要进行任何同步操作
  • 如果在整个使用锁的过程中,都没有出现锁竞争,那在 synchronized 执行完之后,取消偏向锁即可
  • 如果期间另一个线程尝试获取锁,偏向锁会自动撤销,升级为真正的加锁状态,从而另一个线程也就只能阻塞等待了

注意

  • 偏向锁的使用是 JVM 自动进行的,开发人员无需显式地使用偏向锁
  • JVM 会根据锁的竞争情况自动选择使用偏向锁、轻量级锁、重量级锁,以优化锁的性能和吞吐量

轻量级锁状态

  • 当 synchronized 发生锁竞争的时候,就会从偏向锁,升级成轻量级锁
  • 此时 synchronized 相当于通过 CAS 操作,以不断自旋的方式来进行加锁
  • 如果别人很快就释放锁了,自旋是划算的,但是如果迟迟拿不到锁,一直自旋显然是不划算的,因为会长期占用的 CPU 资源,造成性能损失

重量级锁状态

  • 当然 synchronized 自旋不是无休止的自旋,自旋到一定程度之后,就会再次升级成 重量级锁(挂起等待锁)
  • 如果线程进行了重量级锁的加锁,并且发生锁竞争,此时线程就会被放到阻塞队列中,暂时不参与 CPU 调度了
  • 然后直到锁被释放,这个线程才有机会被调度到,并且有机会获取到锁
  • 一旦 线程被切换出 CPU,此时就会变得比较低效了

注意:

  • 在 JVM 主流实现中,只有锁升级,没有锁降级

其他优化机制

锁消除

  • 编译器智能的判定,看当前的代码是否真的要加锁
  • 如果这个场景不需要加锁,程序员也加了,就自动把锁给干掉

实例理解

  • StringBuffer 中的关键方法都带有 synchronized 
  • 如果在单线程中使用 StringBuffer,synchronized 加了也白加,此时编译器就会直接把这些加锁操作消除掉了

锁粗化

锁的粒度

  • synchronized 包含的代码越多,粒度就越粗
  • 包含的代码越少,粒度就越细

通常理解

  • 一般情况下,认为锁的粒度细一点是比较好的
  • 加锁部分的代码,是不能并发执行的
  • 锁的粒度越细能并发的代码就越多,反之越少
  • 但有些情况下,锁的粒度粗一些反而更好

  • 如上图,这里的间隙非常小,就算并发了,也没啥太大效果
  • 然而每次加锁都是带有开销的
  • 此时并发节省的时间,反而不如加锁的开销大
  • 所以我们不如将其转变为直接加一把大锁

  • 上述过程就相当于锁粗化

ReentrantLock

  • ReentrantLock 是标准库给我们提供的另一种锁,顾名思义,也是一把 可重入锁

加锁方式

  • 相比于 synchronized 直接基于代码块的方式来加锁解锁
  • ReentrantLock 更传统,使用 lock方法 和 unlock方法 加锁解锁
import java.util.concurrent.locks.ReentrantLock;

public class ThreadDemo31 {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        reentrantLock.lock();
//        加锁区间
        reentrantLock.unlock();
    }
}

相关问题

  • 当然这样的写法,所带来的最大问题就是 unlock 可能会执行不到

实例理解

import java.util.concurrent.locks.ReentrantLock;

public class ThreadDemo31 {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        reentrantLock.lock();

//      1. 使用 return 提前返回
        boolean b = true;
        if (true){
            return;
        }
//      2. 抛出了一个异常导致 代码提前中断执行
        int[] arr = new int[5];
//        此处抛出 ArrayIndexOutOfBoundsException 异常
        int a = arr[6];
        
//        ....
        reentrantLock.unlock();
    }
}

建议用法

  • 把 unlock 放到 finally 中
import java.util.concurrent.locks.ReentrantLock;

public class ThreadDemo31 {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        reentrantLock.lock();
        try {
            boolean b = true;
            if (true){
                return;
            } 
            
            int[] arr = new int[5];
            int a = arr[6];
        } finally {
            reentrantLock.unlock();
        }
    }
}

优势方面

  • 上述为 ReentrantLock 的劣势,但其也是具有优势的

优势一

  • ReentrantLock 提供了公平锁版本的实现
//        在构造 ReentrantLock 对象时,将其参数填入 true 即该对象为公平锁
//        其参数填入 false 即该对象为非公平锁
        ReentrantLock reentrantLock = new ReentrantLock(true);

优势二

  • 相比于 synchronized 提供的锁操作为 死等,只要获取不到锁,就一直阻塞等待
  • ReentrantLock 提供了更灵活的等待方式:tryLock 
        ReentrantLock reentrantLock = new ReentrantLock();
//        这里的 tryLock 会返回一个布尔类型的值
//        如果加上锁了,则返回 true,否则返回 false
        boolean result = reentrantLock.tryLock();
        try {
            if(result) {
//                加锁区间
            }else {
//                没加上锁 啥都不做
            }
        } finally {
            if(result) {
                reentrantLock.unlock();
            }
        }
  • tryLock 无参数版本,能加锁就加,加不上就放弃
  • tryLock 有参数版本,指定了超时时间,加不上锁就等待一会,如果等一会时间到了也没加上,就放弃

优势三

  • ReentrantLock 提供了一个更强大,更方便的 等待通知机制
  • synchronized 搭配的是 wait 和 notify,notify 的时候是随机唤醒一个 wait 的线程
  • ReentrantLock 搭配了一个 Condition 类,进行唤醒的时候可以唤醒指定的线程

注意:

  • 虽然 ReentrantLock 有一定的优势,但是实际开发中,大部分情况下还是使用的synchronized
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

茂大师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值