首页 > 编程语言 >【Java】并发编程的艺术:悲观锁、乐观锁与死锁管理

【Java】并发编程的艺术:悲观锁、乐观锁与死锁管理

时间:2024-09-24 13:49:28浏览次数:3  
标签:Java 编程 读锁 获取 死锁 线程 悲观 public

目录

一、乐观锁和悲观锁

二、ReadWriteLock

三、StampedLock

四、Semaphore

五、死锁的条件

六、如何发现死锁

七、如何避免死锁


一、乐观锁和悲观锁

        悲观锁(Pessimistic Locking)具有强烈的独占和排他特性。它指的是对数据被外界修改持保守态度。因此,在整个执行过程中,将处于锁定状态。所以,悲观锁是一种悲观思想,它总认为最坏的情况可能会出现,它认为数据很可能会被其他人所修改,所以悲观锁在持有数据的时候总会把资源 或者 数据 锁住,这样其他线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止。Java中的Synchronized 和 ReentrantLock 是一种悲观锁思想的实现,因为Synchronzied 和ReetrantLock 不管是否持有资源,它都会尝试去加锁。

        乐观锁(Optimistic Locking ) 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。乐观锁的思想与悲观锁的思想相反,它总认为资源和数据不会被别人所修改,所以读取不会上锁,但是乐观锁在进行写入操作的时候会判断当前数据是否被修改过。Java中的StampedLock和 AtomicInteger 是一种乐观锁思想的实现。

二、ReadWriteLock

  • 只允许一个线程写入(其他线程既不能写入也不能读取)
  • 没有写入时,多个线程允许同时读(提高性能)
  • 用 ReadwriteLock 实现这个功能十分容易。创建一个 ReadwriteLock 实例,然后分别获取读锁和写锁:
// 读写互斥
public class Counter {
    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();
        }
    }
}

        把读写操作分别用读锁和写锁来加锁,在读取时,多个线程可以同时获得读锁,这样就大大提高了并发读的执行效率。

        如果我们深入分析 ReadwriteLock,会发现它有个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写。 

三、StampedLock

        Java 8进一步提升并发执行效率,引入了新的读写锁:StampedLock。

        StampedLock和 ReadwriteLock 相比,改进之处在于:读的过程中也允许获取写锁写入!这样一来,我们读的数据就可能不一致,需要一点额外的代码来判断读的过程中是否有写入。所以,这种读锁是一种乐观锁

public class Point {
    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.unlockWrite(stamp);  // 释放写锁
        }

    }

    // 读
    public double distanceFromOrigin() {

        // 获取读锁(乐观锁)
        long stamp = stampedLock.tryOptimisticRead();

        double currentX = x;

        double currentY = y;

        // 检查乐观锁的版本号(stamp)是否一致
        if (!stampedLock.validate(stamp)){
            // 获取读锁(悲观锁)
            stamp = stampedLock.tryReadLock();

            //重新获取
            try{
                currentX=x;
                currentY=y;
            }finally{
                stampedLock.unlockRead(stamp);  // 释放悲观读锁
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

}

        和 ReadwriteLock 相比,写入的加锁是完全一样的,不同的是读取。注意到首先我们通过 tryoptimisticRead()获取一个乐观读锁,并返回版本号。接着进行读取,读取完成后,我们通过 validate()去验证版本号,如果在读取过程中没有写入,版本号不变,验证成功,我们就可以放心地继续后续操作。如果在读取过程中有写入,版本号会发生变化,验证将失败。在失败的时候,我们再通过 readLock()获取悲观读锁再次读取。由于写入的概率不高,程序在绝大部分情况下可以通过乐观读锁获取数据,极少数情况下使用悲观读锁获取数据。

        所以,StampedLock 把读锁细分为乐观读悲观读,能进一步提升并发效率。但这也是有代价的:一是代码更加复杂,二是 StampedLock 是不可重入锁,不能在一个线程中反复获取同一个锁。

四、Semaphore

        通过各种锁的实现,我们会发现锁的目的是保护一种受限资源,保证同一时刻只有一个线程能访问(ReentrantLock),或者只有一个线程能写入(ReadWriteLock)。

        还有一种受限资源,它需要保证同一时刻最多有 N个线程能访问,比如同一时刻最多创
建 100 个数据库连接,最多允许10个用户下载等。

        这种限制数量的锁,可以用 Lock 数组来实现,但是非常麻烦。类似需求常见更适合 sema
phore 信号量。 semaphore 本质上就是一个信号计数器,用于限制同一时间的最大访问数量

        例如,最多允许3个线程同时访问:

public class AccessLimitControl {
    // 任意时刻只允许最多3个线程获取许可
    final Semaphore semaphore = new Semaphore(3);

    public void access() throws InterruptedException {
        // 超过数量的其他线程等待
        semaphore.acquire();

        // 2秒内获取许可
        if (semaphore.tryAcquire(2, TimeUnit.SECONDS))
        try{
            System.out.println(UUID.randomUUID().toString());
        }finally{
            semaphore.release();
        }
    }
}

        使用 semaphore 先调用 acquire()获取,然后通过 try ...finally 保证在 finally中释放。调用acquire()可能会进入等待,直到满足条件为止。也可以使用 tryAcquire()指定等待时间。

五、死锁

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

        例如:

// 死锁
public class Test {
    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){  //  获得locakA的锁
            Thread.sleep(1000); // 休眠,不释放锁
            synchronized(lockB){  //  获得locakB的锁
                System.out.println("执行add");
            }  // 释放locakB的锁
        }  // 释放locakA的锁
    }

    public void dec(){
        synchronized(lockB){  // 获得locakB的锁
            synchronized(lockA){  //  获得locakA的锁
                System.out.println("执行dec");
            }  // 释放locakA的锁
        }  // 释放locakB的锁
    }
}

        在获取多个锁的时候,不同线程获取多个不同对象的锁可能导致死锁。对于上述代码,线程1和线程2如果分别执行 add()和 dec()方法时:

                线程1:进入 add(),获得 lockA;
                线程2:进入 dec(),获得lockB;                

        随后:
        
                线程1:准备获得 lockB,失败,等待中;
                线程2:准备获得 lockA,失败,等待中。

        此时,两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等
下去,这就是死锁。
        
        死锁发生后,没有任何机制能解除死锁,只能强制结束VM 进程

五、死锁的条件

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

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

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

        3. 请求等待:当线程因请求资源而阻塞时,对已获得的资源保持不放;
        
        4. 循环等待:线程之间的相互等待。      

六、如何发现死锁

1. 通过 jps 命令,显示本地所有 JVM进程,查找当前 JVM 进程的进程号。

2. 通过 jstack 命令,显示当前 JVM 虚拟机的栈信息,查找产生死锁的线程。

3. 通过jconsole命令,利用终端进入JConsole查看线程活动。

  JConsole 是一种 Java 监控和管理控制台工具,可以用于监视 Java 虚拟机(JVM)的性能资源利用情况。它提供了一种图形化界面,可以实时查看 JVM 的运行状态、内存使用情况、线程活动、垃圾回收等信息,以及执行一些管理操作。 

        查看死锁:

七、如何避免死锁

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

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

        3. 使用信号量。

标签:Java,编程,读锁,获取,死锁,线程,悲观,public
From: https://blog.csdn.net/weixin_71491685/article/details/142391715

相关文章

  • java中的静态方法
    /*1.被static修饰的成员,称之为类成员,在对象创建之前就存在于方法区中静态区2.被static修饰的成员,可以通过类名直接访问使用,非静态的成员必须要通过对象去调用3.static可以修饰成员变量和成员方法4.非静态的成员方法既可以访问静态的成员[变量或方法],也......
  • JavaScript 对象的基本操作及相关知识点详解
    在JavaScript中,对象是一种基本的数据结构,以键值对形式保存数据且数据没有顺序,它可以包含多种数据类型的属性和方法。1.创建对象的方法字面量写法: let自定义对象名={}构造函数写法:let自定义对象名=newObject();//字面量写法letperson={};//构造函数......
  • 云服务器(华为云)安装java环境。
    这篇文章主要是介绍如何搭建华为云服务器中的java环境,也就是jdk的安装。这里华为云服务器使用的是liunx系统。uname-aLinux操作系统的版本信息。具体来说,它表明使用的是Ubuntu系统,内核版本是5.15.0,构建于2023年1月20日,运行在x86_64架构的硬件上。不过这里要介绍一下jdk......
  • Python知识点:如何使用Python与Java进行互操作(Jython)
    开篇,先说一个好消息,截止到2025年1月1日前,翻到文末找到我,赠送定制版的开题报告和任务书,先到先得!过期不候!Jython是一种完全兼容Java的Python实现,它将Python代码编译成Java字节码,这样就可以在Java虚拟机(JVM)上运行。使用Jython,你可以无缝地调用Java类库和P......
  • Python函数艺术:掌握编程中的“乐高积木”
    引言函数是程序设计的基本单元之一,它使得代码模块化,提高了重用性和可读性。无论是处理数据、操作文件还是实现特定业务逻辑,掌握好函数的设计与使用都是至关重要的技能。在Python中,定义一个函数非常直观且强大,这使得即使是初学者也能快速上手,并随着经验积累不断发掘其深层价......
  • 大学生网页制作期末作业——html+css+javascript+jquery旅游官网6页 html大学生网站开
    ......
  • 【期末大作业】基于HTML+CSS+JavaScript南京大学网页校园教育网站html模板(3页) (1)
    ......
  • Java中Map接口的学习
    Map接口目录Map接口HashMap(掌握)TreeMap(掌握)LinkedHashMapConcurrentHashMapidentityHashMapHashTable(过时)HashMap(掌握)Java中的HashMap是一种基于哈希表实现的Map接口的非同步集合,它提供了快速的查找、插入和删除操作。以下是HashMap的特点、底层数据结构以及常用方法的详细解......
  • 第二天:Java练习
    1,BMI体质指数测试BMI=体重(kg)/(身高*身高),接收输入的身高和体重,然后输出结果:过轻:低于18.5正常:18.5~22.9偏胖:23~24.9肥胖:25~29.9重度肥胖:高于30packagejava4;importjava.util.Scanner;publicclasspractise{publicstaticvoidmain(String[]args){......
  • 07 Java 类与对象(pta)
    函数题##1classTest{publicintsum(double...values)//接受若干个,最后一个为valus{intresult=0;for(doublei:values){result+=i;}returnresult;}}##2classPoint{i......