当前位置: 首页 > news >正文

Java中的锁思想

原子锁、对象锁、偏向锁、轻量级锁、重量级锁、公平锁、非公平锁、死锁、乐观锁、悲观锁,萌新看到这些应该有点懵吧!不要着急看完这篇文章让你了解Java中的锁到底是什么东东。

开始先了解一下什么是锁,锁是在时间维度的重量级操作形式,就是在并发情况下某个时间下只能有一个业务线程在执行(避免共享资源的争夺造成并发安全),非常耽误我们宝贵的业务时间,所以出现的很多优化锁新能的方法:偏向锁、轻量级锁、重量级锁、公平锁、非公平锁、乐观锁 

无锁、偏向锁、轻量级锁、重量级锁这四种锁的情况就是根据并发情况的成度来判断对synchronized对象锁的使用,只有到重量级锁时才会真正使用到了synchronized锁。

公平锁、非公平锁是ReentrantLock底层的不同实现方式,这里涉及到AQS(抽象队列同步器):通过volatile、CAS、同步队列来实现对并发线程的控制。

这里我只介绍什么是公平、什么是非公平的概念。AQS的详情后续会做介绍。

上面提到AQS里有维护一个先进先出的同步队列来存放并发的线程。

公平锁:产生并发的线程乖乖去队列里排队等待获取锁资源

非公平锁:产生并发之后不第一时间去队列里排队,而是直接尝试获取锁,如果刚好持有锁的线程把锁资源释放了,他可能会抢到锁就不用去排队了。若失败就得老老实实去队伍后面排队。 (线程的等待、唤醒要涉及到上下文切换、用户态内核态的转换,如果直接获取到锁就可以避免这部分的资源损耗)

1. 原子锁(Atomic Lock)

  • 定义:通常指基于 CAS(Compare-And-Swap) 实现的锁(如AtomicInteger),属于 乐观锁在代码层面的一种实现

  • 特点:无阻塞同步,通过硬件指令保证原子性。

  • 场景:简单变量的线程安全操作(如计数器)。

2. 乐观锁 vs 悲观锁

  • 乐观锁(无锁化编程,降低使用锁的开销:默认不产生并发情况,并发产生之后通过自旋补偿操作)是一种广义的并发策略,认为冲突概率低,先尝试操作,失败再重试(如CAS、版本号机制)。

    • 场景:读多写少(如数据库UPDATE ... WHERE version=old_version)。

  • 悲观锁(直接加锁,默认有并发产生synchronized 、Lock假设有冲突,先加锁再操作(如synchronized)。

    • 场景:写操作频繁。


3. 对象锁:偏向锁、轻量级锁、重量级锁synchronized/Monitor Lock)

  • 定义:与Java对象关联的内置锁synchronized关键字)

  • 特点:独占锁,进入同步代码块前需获取对象的Monitor。

Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。

锁升级锁状态、线程ID都会存储在对象头里mark word(对象头由标记字)
无锁(01)在无竞争的情况下,线程可以自由地访问共享数据,无需任何锁机制。
偏向锁Biased Locking(01):只被一个线程持有当只有一个线程访问共享数据时,使用偏向锁可以减少同步的开销。偏向锁会偏向于第一个获取锁的线程,将对象头标记为偏向锁,并将线程ID记录在对象头中。此后,该线程再次访问同步块时,无需竞争,直接获取锁。偏向锁的目标是提供低延迟的锁操作。
轻量级锁Lightweight Lock(00):不同线程交替持有锁线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性。一旦锁发生了竞争,都会升级为重量级锁
重量级锁Heavyweight Lock(10):多线程竞争锁底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。

原理实现

实现原理: JVM 是通过进入、退出 对象监视器(Monitor) 来实现对方法、同步块的同步的,而对象监视器的本质依赖于底层操作系统的 互斥锁(Mutex Lock) 实现。

具体实现是在编译之后在同步方法调用前加入一个monitor.enter指令,在退出方法和异常处插入monitor.exit的指令。

对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程monitor.exit之后才能尝试继续获取锁。

synchronized实现原理采用互斥的方式让同一时刻至多只有一个线程能持有
在编译之后在同步方法调用前加入monitor.enter指令进入对象监视器(Monitor)
获取锁的线程执行monitor.exit指令退出 对象监视器(Monitor):其他等待的线程竞争
对象监视器(Monitor)Monitor内部具体的存储结构
Ownerowner是关联的获得锁的线程,并且只能关联一个线程
EntryListentrylist关联的是处于阻塞状态的线程
WaitSetwaitset关联的是处于Waiting状态的线程

4.公平锁、非公平锁 ReentrantLock

公平锁:N个线程去申请锁时,会按照先后顺序进入一个队列当中去排队,依次按照先后顺序获取锁。先来的先占用厕所,后来的只能老老实实排队。

非公平锁:N个线程去申请锁,会直接去竞争锁,若能获取锁就直接占有,获取不到锁,再进入队列排队顺序等待获取锁。同样以排队上厕所打比分,这时候,后来的线程会先尝试插队看看能否抢占到锁资源,若能插队抢占成功,就能使用锁,若失败就得老老实实去队伍后面排队。

公平锁和非公平锁在ReentrantLock类当中锁怎样实现的。

ReentrantLock内部实现的公平锁类是FairSync,非公平锁类是NonfairSync

当ReentrantLock以无参构造器创建对象时,默认生成的是非公平锁对象NonfairSync,只有带参且参数为true的情况下FairSync,才会生成公平锁,若传参为false时,生成的依然是非公平锁,两者构造器源码结果如下

在实际开发当中,关于ReentrantLock的使用案例,一般是这个格式

 class X {    private final ReentrantLock lock = new ReentrantLock();    // ...      public void m() {      lock.lock();  // block until condition holds      try {        // ... method body      } finally {        lock.unlock()      }    }  }

这时的lock指向的其实是NonfairSync对象,即非公平锁。

当使用lock.lock()对临界区进行占锁操作时,最终会调用到NonfairSync对象的lock()方法。根据图1可知,NonfairSync和FairSync两者的lock方法实现逻辑是不一样的,而体现其锁是否符合公平与否的地方,就是在两者的lock方法里。


5.synchronized与Lock

5.1.synchronized的缺陷

        synchronized是java中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢?

        如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

  1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;

  2)线程执行发生异常,此时JVM会让线程自动释放锁。

  那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能等待,试想一下,这多么影响程序执行效率。因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。

        再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。

        但是采用synchronized关键字来实现同步的话,就会导致一个问题:如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。

        另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。

总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:

  1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;

  2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

5.2.synchronized和lock区别

1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;

而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

5)Lock可以提高多个线程进行读操作的效率。

        在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

6. 死锁(Deadlock)

6.1死锁产生的条件

        以下这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

死锁:产生条件(4个)指两个或两个以上的进程(线程)在运行过程中因争夺资源而造成的一种僵局。
互斥即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
不可剥夺进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
请求与保持条件进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
循环等待条件存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。
6.2处理死锁的方法
处理死锁的方法预防、避免、检测、解除
预防死锁设置限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来防止死锁的发生。
避免死锁在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免死锁的发生。
检测死锁设置检测机构及时检测死锁的发生,并采取适当措施加以清除。
解除死锁当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来。
6.2.1 预防死锁

“互斥”条件是无法破坏的。

破坏“不可抢占”条件就是允许对资源实行抢夺。 方法一:占有某些资源的同时再请求被拒绝,则该进程必须释放已占有的资源,如果有必要,可再次请求这些资源和另外的资源。 方法二:设置进程优先级,优先级高的可以抢占资源。

破坏“占有并等待”条件,就是在系统中不允许进程在已获得某种资源的情况下,申请其他资源。即要想出一个办法,阻止进程在持有资源的同时申请其他资源。 方法一:一次申请所需的全部资源,即 “ 一次性分配”。 方法二:要求每个进程提出新的资源申请前,释放它所占有的资源。即“先释放后申请”。

破坏“循环等待”条件: 将系统中的所有资源统一编号,所有进程必须按照资源编号顺序提出申请。

6.2.2 避免死锁

加锁顺序:线程按照一定的顺序加锁。 加锁时限:线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁。 死锁检测:每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。

6.2.3 检测死锁

一般来说,由于操作系统有并发,共享以及随机性等特点,通过预防和避免的手段达到排除死锁的目的是很困难的。这需要较大的系统开销,而且不能充分利用资源。为此,一种简便的方法是系统为进程分配资源时,不采取任何限制性措施,但是提供了检测和解脱死锁的手段:能发现死锁并从死锁状态中恢复出来。因此,在实际的操作系统中往往采用死锁的检测与恢复方法来排除死锁。

6.2.4 解除死锁

资源剥夺法:挂起某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但应防止被挂起的进程长时间得不到资源,而处于资源匮乏的状态。 撤销进程法:强制撤销部分、甚至全部死锁进程并剥夺这些进程的资源。撤销的原则可以按进程优先级和撤销进程代价的高低进行。 进程回退法:让一(多)个进程回退到足以回避死锁的地步,进程回退时自愿释放资源而不是被剥夺。要求系统保持进程的历史信息,设置还原点。

http://www.dtcms.com/a/263205.html

相关文章:

  • Java开发者转型AI时代的路径
  • js代码04
  • (LeetCode 面试经典 150 题) 135. 分发糖果 (贪心)
  • vue3 el-table 列增加 自定义排序逻辑
  • 青少年 Python AI 科普小游戏设计方案
  • 成像光谱遥感技术中的AI革命:ChatGPT在遥感领域中的应用
  • 【windows上VScode开发STM32】
  • 【Debian】2-1 frp内网穿透原理
  • 第25天:高级数据库学习笔记1
  • WTL 之trunk技术学习
  • Compose入门1 - 高仿抖音 上下滑动播放视频
  • 深入解析JADX:专业Android逆向工程的利器
  • Oracle 进阶语法实战:从多维分析到数据清洗的深度应用​(第四课)
  • 大模型在多发性硬化预测及治疗方案制定中的应用研究
  • Stable Diffusion 项目实战落地:从0到1 掌握ControlNet 第三篇: 打造光影字形的创意秘技-文字与自然共舞
  • Java:Json反序列化自定义类
  • 计算机网络(一)层
  • 【基于Nest.js+React的全栈项目-00篇】开篇目录:25年新开系列文章,望多多支持~
  • 06_Americanas精益管理项目_数据分析
  • 卡片跳转到应用页面(router事件)
  • 阿里云-Docker的使用
  • 手动续期证书后自动上传到阿里云
  • 9.6 视觉专家模块+1536超清解析!智谱CogVLM-9B多模态模型中文场景实战评测,性能炸裂吊打LLaVA
  • 笨方法学python -练习6
  • MySQL 慢查询日志详解
  • Arduino IDE ESP8266连接0.96寸SSD1306 IIC单色屏显示北京时间
  • 第81题:搜索旋转排序数组Ⅱ
  • PHP:历经岁月沉淀的Web开发利器
  • 如何查看服务器的运行日志?
  • mysql 分组后时间没有按照最新时间倒序