【六大锁策略-各种锁的对比-Java中的Synchronized锁和ReentrantLock锁的特点分析-以及加锁的合适时机】

系列文章目录



前言

阅读该文章之前要了解,锁策略是为了解决什么问题

多线程带来的的风险-线程安全的问题的简单实例-线程不安全的原因


提示:以下是本篇文章正文内容,下面案例可供参考

一、六大"有锁策略"

锁冲突是指两个线程对一个对象加锁,产生了阻塞等待。

1. 乐观锁——悲观锁

乐观锁

  • 假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。

  • 预测接下来的锁冲突不大(一般消耗的资源少,效率高点)

悲观锁

  • 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

  • 预测接下来的锁冲突很大(一般消耗的资源多,效率低点)


举个例子

大学里的期末的最后一门考试结束后,当天,辅导员就会通知假期就开始了,大家可以离校了。

  • 同学A乐观锁)认为:反正,每次都是考试完,就可以直接走了,于是他就直接收拾行李,不等通知,直接当时就回家了,出现意外再说。

  • 同学B悲观锁)认为:万一辅导员说这次放假延迟,大家都留校等领导通知,于是他就在宿舍一直等到辅导员通知,才开始收拾行李,出发回家。

此时,在B等待的时间里,A可能已经到家了。(即A的回家效率高于B


2. 轻量级锁——重量级锁

该文中出现的“乐观锁”“偏向锁”“等都会在后面介绍,读者不必先理解,可先大致有个印象

轻量级锁

  • 加锁解锁,过程更快更高效

  • 轻量级锁在Java中是一种乐观锁的方式,使用CAS(比较和交换)实现,它是通过在对象头中标记为“偏向锁”来实现的。当一个线程获得该偏向锁时,它就可以直接访问被锁定的对象,而不用执行任何额外的同步操作。如果有其他线程来访问该对象,轻量级锁就会自动退化为重量级锁。


重量级锁

  • 加锁解锁,过程更慢更低效

  • 重量级锁在Java中是一种悲观锁的方式,使用互斥锁(Mutex Lock)实现,它需要操作系统的支持。当有多个线程同时访问一个共享资源时,重量级锁会把其他线程阻塞,直到当前线程执行完毕,释放锁。这种方式的效率较低,因为线程的上下文切换和系统调用开销较大。

总结

  1. 轻量级锁适用于竞争不激烈的情况,而重量级锁适用于竞争激烈的情况。在实际开发中,我们需要根据具体场景选择合适的锁机制,以达到最佳的性能。

  2. 同时,乐观锁可能是轻量级锁,悲观锁可能是重量级锁(不绝对)


3. 自旋锁——挂起等待锁

自旋锁

  • 一直占用CPU,不涉及线程阻塞和调度,持续不断的请求锁,一但锁被释放,就能立即得到,忙等
  • 如果其他线程一直不释放锁,那它就一直持续消耗CPU资源(该代码通常是纯用户态,不会设置很长的时间)

挂起等待锁

  • 当它发现没有锁的时候,就会进入挂起等待状态(挂机),挂起等待的时候是
    不消耗 CPU的

  • 它等待操作系统的通知唤醒,但是可能其他线程刚释放了锁,就被一直不断请求的自旋锁线程给枪走了,所以它只能继续等待,具体拿到锁的时机,还得听从操作系统的安排(该锁一般是内核机制,可能会等待较长的时间)

对照前文

  1. 自旋锁是轻量级锁的一种典型实现

  2. 挂起等待锁是重量级锁的一种典型实现


4. 互斥锁——读写锁

互斥锁(例如:synchronized)

只有两个操作:

  1. 进入代码块,给该代码块加锁。

  2. 出代码块,解锁该带代码块。

  3. 互斥锁常用于保护共享数据结构的访问,如队列、链表、散列表等。需要注意的是,互斥锁使用不当可能会带来锁竞争、死锁等问题,

读写锁(例如:ReentrantReadWriteLock)

  1. 给读操作加锁。(读锁,是一种共享锁,可被多个线程同时拥有。当读锁被占用时,其他读锁可以继续被占用。共享性。)

  2. 给写操作加锁。(写锁,写锁是一种排他锁,只能被一个线程占用,当写锁被占用时,其他任何锁都不能被占用。原子性。)

  3. 解锁。

  4. 多个线程同时读取一个变量,不会涉及到线程安全问题。读写锁适用于对共享资源的读操作频繁,写操作较少的情况,如高并发读,比如缓存、数据维护等。读写锁可以提高读取效率,避免了互斥锁的性能开销。同时,写操作的排他特性避免了并发写操作对共享资源的影响,保证数据的正确性和一致性。

在读锁和写锁之间,约定:

  • 读锁和读锁之间,不会锁竞争,不会产生阻塞等待。(不会影响执行速度)

  • 写锁和写锁之间,有锁竞争。(不会影响执行速度)

  • 读锁和写锁之间,有锁竞争。(会影响速度,但是保证线程安全)


5. 可重入锁——不可重入锁

可重入锁,又名递归锁(例如:synchronized)

  1. 如果一个锁,在一个线程中,连续加锁两次,不死锁,就叫做可重入锁,死锁了,就叫不可重入锁。即允许同一个线程多次获取同一把锁,而不会产生死锁。

  2. 这种锁能够保证同一线程多次访问同一资源时不会发生冲突。

  3. Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。

代码示例

Object locker = new Object();
synchronized(locker) {
    synchronized(locker) {
        //连续加锁两次    
    }
}

//或者
//这也是两次加锁,针对this
class BlockingQueue {
    synchronized void put(int elem) {
        this.size();
    }
    
    synchronized int size() {}
}

不可重入锁

  1. 同一线程第二次加锁的时候, 会阻塞等待。直到第一次的锁被释放, 才能获取到第二个锁。 但是释放第一个锁也是由该线程来完成, 结果这个线程已经阻塞了, 也就无法进行解锁操作.。这时候就会死锁。

  2. 即在同一线程再次请求获得该锁时,会造成死锁。因为该锁只能被获得一次,并且只有获得锁的线程才能释放锁。

  3. Linux系统提供的 mutex是不可重入锁.


6. 公平锁——非公平锁

公平锁

  1. 是指多个线程按照申请锁的顺序来获取锁,即先到先得的策略。(公不公平是由自己对公平的定义决定,Java中定义先到先得为公平,synchronized为非公平锁,它遵循等概率竞争规则)

  2. 公平锁的优点是可以避免饥饿现象,即线程在获取锁时会受到先来先服务的原则,公平性是保证锁最大程度分配给等待时间最长的线程,缺点是其效率较低,因为需要保存大量的线程状态。

非公平锁

  1. 多个线程获取锁的顺序是不确定的,有可能后申请锁的线程先获取到锁,这种方式可能造成某些线程一直无法获取到锁。

  2. 在Java中,ReentrantLock默认就是非公平锁。与公平锁相比,非公平锁调度的效率要高,但是不公平的分配策略可能会导致某些线程一直无法获取到锁,从而产生“饥饿”的现象。

  3. 在Java中,ReentrantLock默认是非公平锁,可以通过它的构造函数改为公平锁。


二、Synchronized——ReentrantLock

Synchronized的特点(JDK1.8)

  1. 开始时是乐观锁,如果锁冲突频繁,就转换为悲观锁。

  2. 开始时轻量级锁,如果锁被持有时间较长,就转换为重量级锁。

  3. 轻量级锁大概率基于自旋实现,重量级锁大概率基于挂起等待实现。

  4. 不是读写锁。

  5. 是可重入锁。

  6. 是非公平锁。

Synchronized的锁升级策略

都是尽可能减少锁带来的的开销

在这里插入图片描述

  • 无锁

  • 偏向锁(非必要不加锁

即线程对锁有个标记,没有竞争就不加锁,倘若有别的线程竞争,就立即加锁,即高效又安全

  • 自旋锁 / 轻量级锁(遇到了锁竞争,但是目前线程较少,就让它自旋一会,说不定很快就拿到了 )

  • 重量级锁(线程竞争激烈,多个线程都在自旋,大量占用cpu资源,直接升级锁,调用系统内核阻塞等待)

主流的JVM只能锁升级,不能降级,不是实现不了,可能需要付出更大的代价,于是干脆就不降级了

ReentrantLock的特点

  1. 可重入:同一个线程可以多次获取锁,避免了死锁的发生。

  2. 公平锁和非公平锁:ReentrantLock可以通过参数指定是公平锁还是非公平锁。

  3. 条件变量:ReentrantLock可以通过维护条件变量来实现线程间的协调。

  4. 中断响应:ReentrantLock支持线程中断,即在等待锁的过程中,可以响应中断信号。

  5. 限时等待:ReentrantLock支持线程等待一定时间,如果在指定时间内还未获取到锁,就会放弃等待。

Synchronized和ReentranLock对比

  1. ReentranLock是可重入锁,提供lock()unlock()独立方法(即需要手动释放),来进行加锁解锁,synchronized也是可重入锁(基于代码块的方式来控制加锁解锁),它在第二次加锁之前,会判定当前锁的拥有者是否是同一个线程,如果是,则直接放行,不必再加一次锁

  2. synchronized是非公平的,若想要公平,需要手动加个优先级队列来记录顺序。ReentrantLock提供公平和非公平两种工作模式,默认是非公平锁, 在构造方法中传入true,开启公平锁。

  3. synchronized搭配Objectwaitnotify进行等待唤醒,如果多个线程wait()同一个对象,notify()随机唤醒一个。ReentrantLock需要搭配Condition这个类,这个类也能起到等待通知的作用,能够精准唤醒某个线程, 功能更强大。

  4. synchronized是一个关键字, 是 JVM内部实现的(大概率是基于 C++ 实现). ReentrantLock是标准库的一个类, 在 JVM 外实现的(基于Java实现)

  5. synchronized在申请锁失败时, 会死等. ReentrantLock可以通过 trylock()的方式等待一段时间就放弃, 不会阻塞,而是返回false(让用户自己决定后续操作)。

三、锁消除——锁粗化

锁消除

  • 非必要不加锁(不滥用synchronized)

  • 编译器+JVM就会会作出优化,检测当前代码是否是多线程执行 / 是否有必要加锁,如果没必要,就自动把锁去掉。

例如:StringBuilder和StringBuffer,后者带锁,但是如果单线程使用后者,就自动将后者优化为前者。(该手段十分保守,只有保证消除是可靠的,才会启动,宁愿什么也不做,也不愿意犯错

锁粗化

在这里插入图片描述

  • 锁的粒度,synchronized代码块,包含代码的多少(代码越多,粒度越粗。代码越少,粒度越细)

  • 一般写代码,多数情况下,希望粒度小一些。(串行执行的代码少,并发执行的代码多)但是如果某个场景,频繁的加锁/解锁,此时编译器就会把它优化为一个更粗粒度的锁。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值