系列文章目录
前言
阅读该文章之前要了解,锁策略是为了解决什么问题
多线程带来的的风险-线程安全的问题的简单实例-线程不安全的原因
提示:以下是本篇文章正文内容,下面案例可供参考
一、六大"有锁策略"
锁冲突是指两个线程对一个对象加锁,产生了阻塞等待。
1. 乐观锁——悲观锁
乐观锁
-
假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
-
预测接下来的锁冲突不大(一般消耗的资源少,效率高点)
悲观锁
-
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
-
预测接下来的锁冲突很大(一般消耗的资源多,效率低点)
举个例子
大学里的期末的最后一门考试结束后,当天,辅导员就会通知假期就开始了,大家可以离校了。
-
同学A(乐观锁)认为:反正,每次都是考试完,就可以直接走了,于是他就直接收拾行李,不等通知,直接当时就回家了,出现意外再说。
-
同学B(悲观锁)认为:万一辅导员说这次放假延迟,大家都留校等领导通知,于是他就在宿舍一直等到辅导员通知,才开始收拾行李,出发回家。
此时,在B等待的时间里,A可能已经到家了。(即A的回家效率高于B)
2. 轻量级锁——重量级锁
该文中出现的“乐观锁”“偏向锁”“等都会在后面介绍,读者不必先理解,可先大致有个印象
轻量级锁
-
加锁解锁,过程更快更高效
-
轻量级锁在Java中是一种乐观锁的方式,使用CAS(比较和交换)实现,它是通过在对象头中标记为“偏向锁”来实现的。当一个线程获得该偏向锁时,它就可以直接访问被锁定的对象,而不用执行任何额外的同步操作。如果有其他线程来访问该对象,轻量级锁就会自动退化为重量级锁。
重量级锁
-
加锁解锁,过程更慢更低效
-
重量级锁在Java中是一种悲观锁的方式,使用互斥锁(Mutex Lock)实现,它需要操作系统的支持。当有多个线程同时访问一个共享资源时,重量级锁会把其他线程阻塞,直到当前线程执行完毕,释放锁。这种方式的效率较低,因为线程的上下文切换和系统调用开销较大。
总结
-
轻量级锁适用于竞争不激烈的情况,而重量级锁适用于竞争激烈的情况。在实际开发中,我们需要根据具体场景选择合适的锁机制,以达到最佳的性能。
-
同时,乐观锁可能是轻量级锁,悲观锁可能是重量级锁(不绝对)
3. 自旋锁——挂起等待锁
自旋锁
- 一直占用CPU,不涉及线程阻塞和调度,持续不断的请求锁,一但锁被释放,就能立即得到,忙等
- 如果其他线程一直不释放锁,那它就一直持续消耗CPU资源(该代码通常是纯用户态,不会设置很长的时间)
挂起等待锁
-
当它发现没有锁的时候,就会进入挂起等待状态(挂机),挂起等待的时候是
不消耗 CPU的 -
它等待操作系统的通知唤醒,但是可能其他线程刚释放了锁,就被一直不断请求的自旋锁线程给枪走了,所以它只能继续等待,具体拿到锁的时机,还得听从操作系统的安排(该锁一般是内核机制,可能会等待较长的时间)
对照前文
-
自旋锁是轻量级锁的一种典型实现
-
挂起等待锁是重量级锁的一种典型实现
4. 互斥锁——读写锁
互斥锁(例如:synchronized)
只有两个操作:
-
进入代码块,给该代码块加锁。
-
出代码块,解锁该带代码块。
-
互斥锁常用于保护共享数据结构的访问,如队列、链表、散列表等。需要注意的是,互斥锁使用不当可能会带来锁竞争、死锁等问题,
读写锁(例如:ReentrantReadWriteLock)
-
给读操作加锁。(读锁,是一种共享锁,可被多个线程同时拥有。当读锁被占用时,其他读锁可以继续被占用。共享性。)
-
给写操作加锁。(写锁,写锁是一种排他锁,只能被一个线程占用,当写锁被占用时,其他任何锁都不能被占用。原子性。)
-
解锁。
-
多个线程同时读取一个变量,不会涉及到线程安全问题。读写锁适用于对共享资源的读操作频繁,写操作较少的情况,如高并发读,比如缓存、数据维护等。读写锁可以提高读取效率,避免了互斥锁的性能开销。同时,写操作的排他特性避免了并发写操作对共享资源的影响,保证数据的正确性和一致性。
在读锁和写锁之间,约定:
-
读锁和读锁之间,不会锁竞争,不会产生阻塞等待。(不会影响执行速度)
-
写锁和写锁之间,有锁竞争。(不会影响执行速度)
-
读锁和写锁之间,有锁竞争。(会影响速度,但是保证线程安全)
5. 可重入锁——不可重入锁
可重入锁,又名递归锁(例如:synchronized)
-
如果一个锁,在一个线程中,连续加锁两次,不死锁,就叫做可重入锁,死锁了,就叫不可重入锁。即允许同一个线程多次获取同一把锁,而不会产生死锁。
-
这种锁能够保证同一线程多次访问同一资源时不会发生冲突。
-
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() {}
}
不可重入锁
-
同一线程第二次加锁的时候, 会阻塞等待。直到第一次的锁被释放, 才能获取到第二个锁。 但是释放第一个锁也是由该线程来完成, 结果这个线程已经阻塞了, 也就无法进行解锁操作.。这时候就会死锁。
-
即在同一线程再次请求获得该锁时,会造成死锁。因为该锁只能被获得一次,并且只有获得锁的线程才能释放锁。
-
Linux系统提供的 mutex是不可重入锁.
6. 公平锁——非公平锁
公平锁
-
是指多个线程按照申请锁的顺序来获取锁,即先到先得的策略。(公不公平是由自己对公平的定义决定,Java中定义先到先得为公平,synchronized为非公平锁,它遵循等概率竞争规则)
-
公平锁的优点是可以避免饥饿现象,即线程在获取锁时会受到先来先服务的原则,公平性是保证锁最大程度分配给等待时间最长的线程,缺点是其效率较低,因为需要保存大量的线程状态。
非公平锁
-
多个线程获取锁的顺序是不确定的,有可能后申请锁的线程先获取到锁,这种方式可能造成某些线程一直无法获取到锁。
-
在Java中,ReentrantLock默认就是非公平锁。与公平锁相比,非公平锁调度的效率要高,但是不公平的分配策略可能会导致某些线程一直无法获取到锁,从而产生“饥饿”的现象。
-
在Java中,ReentrantLock默认是非公平锁,可以通过它的构造函数改为公平锁。
二、Synchronized——ReentrantLock
Synchronized的特点(JDK1.8)
-
开始时是乐观锁,如果锁冲突频繁,就转换为悲观锁。
-
开始时轻量级锁,如果锁被持有时间较长,就转换为重量级锁。
-
轻量级锁大概率基于自旋实现,重量级锁大概率基于挂起等待实现。
-
不是读写锁。
-
是可重入锁。
-
是非公平锁。
Synchronized的锁升级策略
都是尽可能减少锁带来的的开销
-
无锁
-
偏向锁(非必要不加锁)
即线程对锁有个标记,没有竞争就不加锁,倘若有别的线程竞争,就立即加锁,即高效又安全
-
自旋锁 / 轻量级锁(遇到了锁竞争,但是目前线程较少,就让它自旋一会,说不定很快就拿到了 )
-
重量级锁(线程竞争激烈,多个线程都在自旋,大量占用cpu资源,直接升级锁,调用系统内核阻塞等待)
主流的JVM只能锁升级,不能降级,不是实现不了,可能需要付出更大的代价,于是干脆就不降级了
ReentrantLock的特点
-
可重入:同一个线程可以多次获取锁,避免了死锁的发生。
-
公平锁和非公平锁:ReentrantLock可以通过参数指定是公平锁还是非公平锁。
-
条件变量:ReentrantLock可以通过维护条件变量来实现线程间的协调。
-
中断响应:ReentrantLock支持线程中断,即在等待锁的过程中,可以响应中断信号。
-
限时等待:ReentrantLock支持线程等待一定时间,如果在指定时间内还未获取到锁,就会放弃等待。
Synchronized和ReentranLock对比
-
ReentranLock是可重入锁,提供lock()和unlock()独立方法(即需要手动释放),来进行加锁解锁,synchronized也是可重入锁(基于代码块的方式来控制加锁解锁),它在第二次加锁之前,会判定当前锁的拥有者是否是同一个线程,如果是,则直接放行,不必再加一次锁
-
synchronized是非公平的,若想要公平,需要手动加个优先级队列来记录顺序。ReentrantLock提供公平和非公平两种工作模式,默认是非公平锁, 在构造方法中传入true,开启公平锁。
-
synchronized搭配Object的wait和notify进行等待唤醒,如果多个线程wait()同一个对象,notify()随机唤醒一个。ReentrantLock需要搭配Condition这个类,这个类也能起到等待通知的作用,能够精准唤醒某个线程, 功能更强大。
-
synchronized是一个关键字, 是 JVM内部实现的(大概率是基于 C++ 实现). ReentrantLock是标准库的一个类, 在 JVM 外实现的(基于Java实现)
-
synchronized在申请锁失败时, 会死等. ReentrantLock可以通过 trylock()的方式等待一段时间就放弃, 不会阻塞,而是返回false(让用户自己决定后续操作)。
三、锁消除——锁粗化
锁消除
-
非必要不加锁(不滥用synchronized)
-
编译器+JVM就会会作出优化,检测当前代码是否是多线程执行 / 是否有必要加锁,如果没必要,就自动把锁去掉。
例如:StringBuilder和StringBuffer,后者带锁,但是如果单线程使用后者,就自动将后者优化为前者。(该手段十分保守,只有保证消除是可靠的,才会启动,宁愿什么也不做,也不愿意犯错)
锁粗化
-
锁的粒度,synchronized代码块,包含代码的多少(代码越多,粒度越粗。代码越少,粒度越细)文章来源:https://www.toymoban.com/news/detail-615012.html
-
一般写代码,多数情况下,希望粒度小一些。(串行执行的代码少,并发执行的代码多)但是如果某个场景,频繁的加锁/解锁,此时编译器就会把它优化为一个更粗粒度的锁。文章来源地址https://www.toymoban.com/news/detail-615012.html
到了这里,关于【六大锁策略-各种锁的对比-Java中的Synchronized锁和ReentrantLock锁的特点分析-以及加锁的合适时机】的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!