• synchronized常见锁策略


    目录

    乐观锁和悲观锁

    乐观锁如何检测冲突???

    synchronized属于哪种???

    普通的互斥锁和读写锁

    synchronized属于哪种???

    重量级锁和轻量级锁

    synchronized属于哪种???

    自旋锁和挂起等待锁

    自旋锁的优缺点

    公平锁和非公平锁

    synchronized是哪种锁 ???

    可重入锁和不可重入锁

     synchronized是哪种锁 ???

    synchronized常见锁策略总结

    CAS

    CAS的应用场景

    CAS中的ABA问题

    synchronized锁升级过程

    锁的其他优化

    锁消除

    锁粗化


    乐观锁和悲观锁

    悲观锁 : 悲观锁对发生并发冲突总假设最坏情况,预测锁冲突概率会很大,所以每次拿数据时都要进行加锁,如果其他线程想要获取此资源就要进行阻塞等待.----悲观锁往往要进入内核态--使当前线程阻塞等待.

    乐观锁: 乐观锁则认为一般情况下不会发生并发冲突,而是当需要进行更新数据时,才来检测是否发生锁冲突,如果发生冲突交给用户处理----(纯用户态操作)

    乐观锁和悲观锁举例 :

    比如有小李和小明刚刚高考完,小李下了考场就感觉这次考试凉了,立马就想要复读(典型的悲观锁),而小明则认为是正常发挥,不管咋地高考完了先玩几天,等考试结果出来了再做打算(典型的乐观锁)

    乐观锁如何检测冲突???

    乐观锁一般是利用版本号来检测当前乐观锁是否发生冲突.

    乐观锁的策略 : 当提交版本的版本号大于当前记录的版本号则更新成功,否则操作失败

     当线程1和线程2进行修改操作时,同时版本号也增加1,线程1修改时发现内存里版本号为1,则可以更新,当线程2再次写回内存时就会发现版本号一致则无法更新,操作失败,发生冲突.

    synchronized属于哪种???

    synchronized既属于乐观锁又属于悲观锁,是一种自适应锁.

    刚开始锁冲突/竞争小时为乐观锁,随着锁冲突/竞争激烈,就变为悲观锁

    普通的互斥锁和读写锁

    互斥锁:像synchronized就是一种普通的互斥锁,当加锁时,另外的线程想要获取资源就要阻塞等待,等待释放锁.

    读写锁 : 读写锁是将读和写分离开来;

    • 读加锁和读加锁之间不会产生互斥
    • 读加锁和写加锁之间会产生互斥
    • 写加锁和写加锁之间会产生互斥

    读写锁适合频繁的读操作.

    synchronized属于哪种???

    synchronized不是读写锁,是普通的互斥锁

    重量级锁和轻量级锁

    •  CPU为操作系统提供了一些原子操作的指令
    • 操作系统为JVM提供了mutex互斥锁---重量级锁
    • JVM为Java代码提供了两把锁 ReentractLock 和synchronized

    重量级锁 : 重量级锁一般是指操作系统提供的mutex互斥锁,需要在内核态和用户态之间频繁的切换,容易引发线程调度.  ---- 常常是悲观锁

    轻量级锁 : 一般是在用户态中进行操作,很少使用mutex互斥锁 ----常常会是乐观锁

    synchronized属于哪种???

    synchronized既是重量级锁又是轻量级锁.

    刚开始为轻量级锁,随着锁竞争越激烈就转为重量级锁---自适应锁.

    自旋锁和挂起等待锁

    轻量级锁的内部实现是自旋锁. 重量级锁的内部实现是挂起等待锁'

    自旋锁 : 当发生锁冲突时,获取锁失败后,会继续尝试获取锁(处于自旋等待),无限循环,直到锁被释放后,第一时间获取到锁.

    挂起等待锁 : 当发生锁冲突时,会阻塞等待,不会第一时间释放锁

    自旋锁的优缺点

    当锁释放的时候,自旋锁会第一时间获取到锁,缺点是在自旋等待时,时刻占CPU,会导致大量CPU资源浪费

    而挂起等待锁,在锁释放的时候不会第一时间获取到锁,但是在阻塞等待时,会释放CPU资源

    公平锁和非公平锁

    公平锁 : 这里的公平锁遵守 "先来后到"的原则,谁先加锁的就谁先获取到锁

    非公平锁 : 这里的非公平 指的是 "同等竞争" 

    synchronized是哪种锁 ???

    synchronized是非公平锁.

    没有任何限制的锁都是非公平锁,如果想要变为公平锁就需要额外的数据结构.

    可重入锁和不可重入锁

    可重入锁 : 表面意思可以理解为再次进入,"某个线程可以加多把锁",当加锁的线程与持有锁的线程是同一个的时候,不阻塞等待,可以直接获取锁,锁内部会记录是哪个线程加了锁,同时内部还有一个计数器来记录加锁的次数,通过这个计数器也可以知道什么时候释放锁.

    不可重入锁 : 一个线程不能加锁多次,如果加了锁的线程和持有锁线程是同一个就会导致死锁状态.

    即第一个锁的释放依赖第二个锁的获取,第二个锁的获取依赖第一个锁的释放--导致死循环

     synchronized是哪种锁 ???

    synchronized是可重入锁

    synchronized常见锁策略总结

    • synchronized既是乐观锁也是悲观锁,最开始为乐观锁,随着锁冲突/竞争激烈就会转为悲观锁
    • synchronized既是重量级锁也是轻量级锁,最开始为轻量级锁,随着锁冲突激烈,转为重量级锁
    • 轻量级锁的内部实现是基于自旋锁,重量级锁内部实现基于挂起等待锁
    • synchronized是普通的互斥锁,不是读写锁
    • synchronized是可重入锁
    • synchronized是非公平锁

    CAS

    什么是CAS ???

    CAS(compare and swap)是一种原子的操作指令,有操作系统/硬件为JVM提供的一种硬件指令.

    CAS伪代码 :

    1. boolean CAS(address, expectValue, swapValue) {
    2. if (&address == expectedValue) {
    3. &address = swapValue;
    4. return true;
    5. }
    6. return false;
    7. }

    CAS的会执行如下的操作:

    • 将旧的预期值与内存中原有数据进行比较
    • 如果相等,就将新的值与内存数据交换.
    • 返回更新成功 否则返回失败

    注意 : CAS上述3个操作都是原子的.---是一种硬件指令

    多个线程操作CAS时只会有一个线程成功,其余线程不会阻塞,但是都会接收操作失败的信号.

    CAS的应用场景

    • CAS实现原子类

    伪代码如下 :

    1. class AtomicInteger {
    2. private int value;
    3. public int getAndIncrement() {
    4. int oldValue = value;
    5. while ( CAS(value, oldValue, oldValue+1) != true) {
    6. oldValue = value;
    7. }
    8. return oldValue;
    9. }
    10. }

    • 线程1先进行操作,如果与内存原有数据相等就+1之后与内存数据交换.
    • 线程2在进行操作,与内存数据进行比较,发现不相等就进入循环,把内存数据赋值给线程2.
    • 线程2再次进行操作,与内存数据进行比较,发现相等,就对线程2的值+1然后与内存数据交换.

    到此就完成了两个线程都对内存执行了两次++操作.

    在Java类中也有原子类,能进行原子的++,--操作------>AtomicInteger

    1. public static void main(String[] args) {
    2. AtomicInteger atomicInteger = new AtomicInteger();
    3. atomicInteger.getAndIncrement();//后置++;
    4. atomicInteger.incrementAndGet();//前置++;
    5. atomicInteger.decrementAndGet();//前置--;
    6. atomicInteger.getAndIncrement();//后置--
    7. }
    •  CAS实现自旋锁

    伪代码如下 :

    1. public class SpinLock {
    2. private Thread owner = null;
    3. public void lock(){
    4. while(!CAS(this.owner, null, Thread.currentThread())){
    5. }
    6. }
    7. public void unlock (){
    8. this.owner = null;
    9. }
    10. }

    刚开始Thread线程对象owner为null;然后一直与null值进行比较.

    如果为null,证明当前线程没有占用锁,就退出循环,把owner设为当前持有锁线程

    如果不为null,证明当前线程占用了锁,进入循环,自旋等待.

    CAS中的ABA问题

    我们在执行CAS的操作时,就会拿A与内存中的数据进行比较,如果相等就进行交换/更新,但是这里就会出现问题,我们拿的A是始终是A,还是从A->B->A,CAS就会认为A始终没有变化,这就会导致问题.

    举个例子

    比如小明现金有1000元,小明要取钱,一不小心按下了两次取钱操作,这时就会出现两个线程,线程1与银行卡的前进行比较1000与1000相同,这时就扣款成功银行卡中只剩下500,此时线程2的CAS又对其执行扣款,线程2原来有1000与500进行比较扣款失败,此时没有问题,符合预期,但是当线程2读取时,又有小明的朋友转了500元,有来了个线程3,线程3中的500与银行卡中的500进行比较相等然后打钱成功,此时银行卡又剩下1000元,线程2读取时发现1000与1000相等此时又进行了一次扣款. 这就导致扣款两次,导致500元转丢了.我们并不知道银行卡中的500.始终是500还是500->1000->500

    那如何解决CAS中ABA问题呢???

    我们可以通过引入版本号来解决此问题.比较值的同时也要比较版本号,每次进行修改时,都要对版本号进行+1操作,如果版本号不相同则操作失败

    此时引入版本号,再次进行推演上个例子

    还是小明有1000元(版本号为1),此时要取款,线程1进行比较,1000(版本号为1)与1000(版本号为1),发现相等就扣款成功此时银行卡中有500(版本号2),线程2在读取时,线程3又对其打钱500,此时变为1000(版本号3),在线程2读取时又要扣款,线程2(版本号1,1000元)与线程3(版本号3,1000)不一致则扣款失败.

    CAS还会有一些问题 :

    循环时间开销大;

    只能保证一个共享变量的原子操作.

    synchronized锁升级过程

    JVM有时会根据情况对synchronized进行锁升级;

    基本的升级过程为  无锁===>偏向锁===>轻量级锁===>重量级锁

    • 无锁

    没有加锁的状态

    • 偏向锁

    当要进行加锁时,从无锁转为偏向锁.

    注意理解偏向锁 :

    偏向锁不是真正的加锁,而是给对象头标记为偏向锁状态.如果接下来不会产生锁冲突,就不会进行锁升级.偏向锁的目的就是尽量能不加锁就不加锁,延迟加锁,避免了加锁的开销.当发生锁冲突时,就取消偏向锁状态,进行锁升级--升级为轻量级锁.

    • 轻量级锁

    这个轻量级锁时基于CAS实现的,CAS为其更新内存,更新成功,则加锁成功,否则处于自旋状态(始终占CPU).

    • 重量级锁

    当竞争在激烈时就会转为重量级锁,此时会进入内核态,会看当前锁是否被占用,如果没有被占用就切换为用户态.如果被占用就需要阻塞等待,等待其他线程都释放锁之后,才尝试加锁


    • 总结

    刚开始为无锁状态,随着要进行加锁,进入偏向锁状态,偏向锁不是真正的加锁,只是在对象头上做了个标记,标记为偏向锁状态,如果不产生锁冲突就一直保持当前状态,如果产生锁冲突,就需要进行锁升级,升级为轻量级锁.此轻量级锁基于CAS实现的,CAS会为其更新内存,如果更新内存成功,则加锁成功,否则进入自旋等待状态.此时自旋状态相当于CPU空转,浪费CPU资源.随着锁冲突/竞争激烈时,又转为重量级锁,此时会进入内核态.会看当前锁是否被占用,如果没有被占用,则切换为用户态加锁成功,如果当前锁被占用,会一直阻塞等待,等待其他线程释放锁,才尝试获取锁.


    锁的其他优化

    锁消除

    比如StringBuilder的append方法是使用了synchronized来保证线程安全状态,而当我们在单线程使用append方法时,不涉及到线程安全问题,此时JVM和编译器就会对其优化,将其锁消除

    锁粗化

    这里是表示synchronized代码块的范围.

    当代码块的范围越大时,锁的粒度就越粗.当代码块的范围越小时,锁的粒度就越细.

    有时代码范围越小就导致反复的加锁释放锁,这时就会扩大代码块的范围,避免反复的加锁释放锁.

    举例 :

    比如向领导汇报工作,我是三件事情分成三次时间向领导汇报工作这样做不仅我烦,领导也会很烦,所以我就一次性的把三件事情全部汇报清楚.

  • 相关阅读:
    Android 免杀教程
    Windows 权限维持手法
    Django 的国际化与本地化详解
    Java零基础(第十三章)---》方法覆盖和多态
    Python Basics with Numpy(吴恩达课程)
    隐藏在背后的真相——数据存储的方式(上)
    Elasticsearch:使用 Open AI 和 Langchain 的 RAG - Retrieval Augmented Generation (二)
    Linux命令
    线程池(治理线程的法宝)
    TS的类型转换
  • 原文地址:https://blog.csdn.net/m0_61210742/article/details/126272778