乐观锁、悲观锁及死锁

乐观锁、悲观锁

1.概念

  • 悲观锁(悲观锁定):具有强烈的独占和排他特性。在整个执行过程中,将处于锁定状态。悲观锁在持有数据的时候总会把资源或者数据锁住,这样其他线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止. (例如:Synchronized和ReetrantLock)

  • 乐观锁(乐观锁定):乐观锁机制采取了更加宽松的加锁机制。乐观锁在读取时不会上锁,但是乐观锁在进行写入操作的时候会判断当前数据是否被修改过。

    (例如:JAVA中的Stamp锁定和原子整型)

2.锁的使用----读写

ReentranLock

保证了只有一个线程可以执行临界区代码

问题:任何时刻,只允许一个线程执行,不是读线程,就是写线程

👏改进一下:允许多个线程同时读,但只有一个线程在写,其他线程就必须等待

ReadWriteLock

  • 只允许一个线程写入(其他线程既不能写入,也不能读取)

  • 没有写入时,多个线程允许同时读(提高性能)

java 复制代码
 public class ReadWriteLockTest {
     private final ReadWriteLock lock = new ReentrantReadWriteLock();
     private final Lock readLock = lock.readLock();
     private final Lock writeLock = lock.writeLock();
 ​
     private int[] counts = new int[10];
     public void inc(int index){
         writeLock.lock();
         try {
             counts[index]+=1;
         } finally {
             writeLock.unlock();
         }
     }
     public int[] get(){
         readLock.lock();
         try {
             return Arrays.copyOf(counts,counts.length);
         } finally {
             readLock.unlock();
         }
     }
 }

在读取时,多个线程可以同时获取读锁,大大提高了并发读的执行效率。

问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写.

👏改进一下:读的过程中也允许获取写锁写入!

StampedLock

读的数据就可能不一致,需要一点额外的代码来判断读的过程中是否有写入。所以,这种读锁是一种 乐观锁

java 复制代码
public class Test2 {
     private final StampedLock stampedLock = new StampedLock();
     private double x;
     private double y;
     public void move(double deltaX,double deltaY){
         long stamp = stampedLock.writeLock();
 ​
         try {
             x += deltaX;
             y += deltaY;
         } finally {
             stampedLock.unlock(stamp);
         }
     }
     public double distanceFromOrigin(){
         //假设下面两行代码不是原子操作
         //假设x,y =(100,200)
 ​
         //获取读锁(乐观锁)
         long stamp = stampedLock.tryOptimisticRead();
 ​
         double currentX =x;
         //此处已读取到x=100,如果没有写入,读取是正确的(100,200)
 ​
         double currentY =y;
         //此处已读取到y,如果没有写入,读取是正确的(100,200)
         //如果有写入,读取是错误的(100,400)
 ​
         //检查乐观锁的版本号值(stamp)是否一致
         if(!stampedLock.validate(stamp)){
             //获取读锁(悲观锁)
             stamp = stampedLock.tryReadLock();
             //重新获取
             try {
                 currentY=y;
                 currentX=x;
             } finally {
                 stampedLock.unlock(stamp);
             }
         }
         //计算
         return Math.sqrt(currentX*currentX+currentY*currentY);
     }
 }

和ReadWriteLock相比,写入的加锁是完全一样的,不同的是读取

步骤:

1.通过Try OptimisTicRead()获取一个乐观读锁,并返回版本号。

2.进行读取,读取完成后,我们通过验证validate()去验证版本号,如果在读取过程中没有写入,版本号不变,验证成功,继续后续操作.如果在读取过程中有写入,版本号会发生变化,验证将失败。

3.当验证失败时,再通过ReadLock()获取悲观锁再次读取。(由于写入的概率不高,程序在绝大部分情况下可以通过乐观读锁获取数据,极少数情况下使用悲观读锁获取数据)。

所以,StampeLock把读锁细分为乐观读和悲观读,能进一步提升并发效率。

但这也是有代价的:

一是代码更加复杂。

二是Stamp锁定是不可重入锁,不能在一个线程中反复获取同一个锁。

死锁

1.概念

多个线程在运行中,都需要获取对方线程所持有的锁(资源),导致处于长期无限等待的状态。

死锁发生后,只能通过强制结束JVM进程来解决死锁。

java 复制代码
public class Test3 {
     public static void main(String[] args) {
         DeadLock deadLock = new DeadLock();
         Thread t1 = new Thread(() -> {
             try {
                 deadLock.add();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         });
         Thread t2 = new Thread(() -> {
                 deadLock.dec();
         });
         t1.start();
         t2.start();
 ​
     }
 }
 class DeadLock{
     //两把锁
     private static Object lockA = new Object();
     private static  Object lockB = new Object();
 ​
     public void add() throws InterruptedException {
         synchronized (lockA){//获得lockA的锁
             Thread.sleep(100);//线程休眠
             synchronized (lockB){
                 System.out.println("执行add()");
             }//释放lockB的锁
         }//释放lockA的锁
     }
     public void dec(){
         synchronized (lockB){//获得lockB的锁
             synchronized (lockA){//获得lockA的锁
                 System.out.println("执行dec()");
             }//释放lockA的锁
         }//释放lockB的锁
     }
 }

2.死锁的条件

产生死锁有四个必要条件:

**1.资源互斥:**对所分配的资源进行排它性控制,锁在同一时刻只能被一个线程使用;

**2.不可剥夺:**线程已获得的资源在未使用完之前,不能被剥夺,只能等待占有者自行释放锁:

**3.请求等待:**当线程因请求资源而阻塞时,对已获得的资源保持不放.

**4.循环等待:**线程之间的相互等待

3.排查及定位死锁

4.如何避免死锁

1.每次只占用不超过1个锁.

2.按照相同的顺序申请锁.

3.使用信号量

相关推荐
头发还在的女程序员9 小时前
医院陪诊管理系统怎么选择?——2026 年选型避坑与架构参考
java·开发语言·陪诊系统·陪诊app·医院陪诊陪护
CodeStats9 小时前
【Spring事务】Spring事务注解 @Transactional 完整体系:从 MySQL 隔离级别到 MyBatis 原理详解
java·spring·mybatis·事务·transactional
我命由我1234510 小时前
Android 开发问题:为 PDFView 设置一个带有黑色边框的背景 drawable,但边框没有生效
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
都叫我大帅哥12 小时前
从Python到Java:为什么企业级Agent最终会选择Java?
java·ai编程
wanderist.12 小时前
Lambda表达式在算法竞赛中的应用
java·开发语言·算法
腻害兔14 小时前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:IM 即时通讯模块,一个被低估的「全功能聊天系统」
java·前端·vue.js·产品经理·ai编程
心念枕惊14 小时前
.NET CORE 授权进阶-角色、策略与动态权限实现
java·前端·.netcore
景同学16 小时前
把 AI 用到线上运维:可行、有效,前提是喂足信息——一次 Full GC 排障实录
java·人工智能·后端
C++、Java和Python的菜鸟16 小时前
第7章 后端Web实战(Tlias系统)
java
憧憬成为java架构高手的小白16 小时前
黑马八股--spring框架学习
java·学习·spring