【Java多线程】分析线程加锁导致的死锁问题以及解决方案

简介: 【Java多线程】分析线程加锁导致的死锁问题以及解决方案

1、线程加锁


其中 locker 可以是任意对象,进入 synchronized 修饰的代码块, 相当于加锁,退出 synchronized 修饰的代码块, 相当解锁。


如果一个线程,针对一个对象加上锁之后,其他线程也尝试对这个对象加锁,就会导致锁竞争进而引起阻塞(BLOCKED),这个阻塞会一直持续到上一个线程释放锁为止。


如果是两个线程分别针对不同的对象进行加锁,此时不会由锁竞争,也就不会阻塞。


出现锁竞争进而引起阻塞状态,这个阻塞会一直持续到下一个线程释放锁为止。


但是,设想一个场景,共有AB两个线程,此时A线程因为锁竞争进入阻塞状态,而如果此时B线程恰巧也正在阻塞状态,由于AB线程都进入了阻塞状态,此时进程无法运行,出现死锁问题。下面针对死锁问题的出现以及解决方法展开讨论。


2、死锁问题的三种经典场景

2.1、一个线程一把锁

public static void main(String[] args) {
    Object locker = new Object();
    Thread t = new Thread(() -> {
        synchronized (locker) {   //两次加锁,加的是同一把锁
            synchronized (locker) {   //两次加锁,加的是同一把锁
                System.out.println("hello synchronized");
            }
        }
    });
    t.start();
}

需要注意的是,这里最直观的感觉是进行了两次加锁,会发生锁冲突。第一次针对locker加锁之后,在还没释放锁的时候又尝试对locker加锁,理论会出现锁冲突。


至于事实上是否会出现所冲突进而出现死锁,需要分情况讨论:


1、如果是不可重入锁,则就会出现锁竞争引起死锁。


2、如果是可重入锁,则不会出现锁竞争引起死锁,Java中的锁就是可重入锁,因此可以正常打印。


可以把这种情况理解成:【屋钥匙锁在了屋里】


2.2、两个线程两把锁

package thread;
public class ThreadDemo22 {
    public static void main(String[] args) {
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                // sleep一下, 给 t2 时间, 让 t2 也能拿到 B
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 尝试获取 B, 并没有释放 A
                synchronized (B) {
                    System.out.println("t1 拿到了两把锁!");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (A) {
                // sleep一下, 给 t1 时间, 让 t1 能拿到 A
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 尝试获取 A, 并没有释放 B
                synchronized (B) {
                    System.out.println("t2 拿到了两把锁!");
                }
            }
        });
        t1.start();
        t2.start();
    }
}
package thread;
public class ThreadDemo22 {
    public static void main(String[] args) {
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                // sleep一下, 给 t2 时间, 让 t2 也能拿到 B
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 尝试获取 B, 并没有释放 A
                synchronized (B) {
                    System.out.println("t1 拿到了两把锁!");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (A) {
                // sleep一下, 给 t1 时间, 让 t1 能拿到 A
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 尝试获取 A, 并没有释放 B
                synchronized (B) {
                    System.out.println("t2 拿到了两把锁!");
                }
            }
        });
        t1.start();
        t2.start();
    }
}


两个线程,两把锁。线程A获取到锁A,线程B获取到锁B,在没释放锁AB的前提下,线程A尝试获取锁B,线程B尝试获取锁A,就会出现死锁。


可以把这种情况理解成:【屋钥匙锁在了车里,车钥匙锁在了屋里】


2.3、N个线程M把锁(哲学家就餐问题)

首先假设一个场景,一张圆桌上坐着五个人,每个人面前都有一碗面条,桌子上一共有五根筷子(不是五双),而将五根筷子分别摆放在两人各自之间,如下图。



       要想吃面条,需要拿起自己身旁的两根筷子(左右两根,只能拿身边的这两根)。假设此时A拿起了左右筷子吃面条,此时B就无法吃,因为A正在使用B的左筷子,B目前只能拿起一根右筷子,并且开始等待,等待A放下筷子,再拿起左筷子吃面条(此处的等待只有拿到另外一根筷子后才会停止,并且等待的同时不会放下已经拿起的筷子)。同理E也一样。


       此处讨论的问题中N等于M。我们将线程比作人,筷子比作锁,此时B所处的状态可以比作锁竞争引起的阻塞状态。大家可以试着想想各种其他不同的情况,始终都能保证桌上5个人至少有一人正在吃面条,除了一种特殊的极端情况下:


       极端情况下,会出现所有人同时都拿了同一侧的筷子(例如都拿了左筷子),导致所有人都不能拿起另一侧的筷子而都进入阻塞,等待着别人放下筷子后自己再拿起来。但是此时又因为没有一个人能吃的上面条,因此永远不会有人放下筷子,出现死锁。


       这个问题也被人称之为:哲学家就餐问题。


3、解决死锁问题

要想解决死锁情况,就得先讨论产生死锁的原因:


死锁产生的四个必要条件(缺一不可)


由于是必要条件,只需要破坏其中一种条件,就可以让死锁解开。


  1. 互斥使用。一个线程拿到了这把锁,另一个线程也想获取,就需要阻塞等待,这是锁最基本的特性,不好破坏。
  2. 不可抢占。一个线程拿到了锁之后,只能主动解锁,不能让别的线程强行把锁抢走,这也是锁最基本的特性,不好破坏。
  3. 请求保持。一个线程拿到了锁A,在持有锁A的前提下,尝试获取锁B。这些场景下必须需要这样使用,也不好破坏。
  4. 循环等待/环路等待,是一种代码结构,是最容易破坏。

由上述分析可以得知,想要解决死锁问题,要从破坏循环等待/环路等待入手。


引入加锁顺序的规则就是很好破解循环等待的办法,即给每一个锁编号,规定只能按照锁的序号顺序拿起,就能打破循环等待。


举例说明:


       依然是是上面的哲学家就餐问题,此时给筷子编号序号之后,要求只能按照顺序由小到大拿起,此时就算是所有人同时拿起筷子,C先拿1,B先拿2,A先拿3,E先拿4,此时D按照规定应该拿起1,但是此时C正拿着1,因此此时D还没有机会拿起5,就直接进入阻塞状态。此场景下E就能拿起5开始吃面,E放下筷子A就接着吃,依此类推,就将可能出现的死锁问题破解了。


目录
相关文章
|
2月前
|
监控 Java Unix
6个Java 工具,轻松分析定位 JVM 问题 !
本文介绍了如何使用 JDK 自带工具查看和分析 JVM 的运行情况。通过编写一段测试代码(启动 10 个死循环线程,分配大量内存),结合常用工具如 `jps`、`jinfo`、`jstat`、`jstack`、`jvisualvm` 和 `jcmd` 等,详细展示了 JVM 参数配置、内存使用、线程状态及 GC 情况的监控方法。同时指出了一些常见问题,例如参数设置错误导致的内存异常,并通过实例说明了如何排查和解决。最后附上了官方文档链接,方便进一步学习。
194 4
|
13天前
|
Java 数据库连接 API
互联网大厂校招 JAVA 工程师笔试题解析及常见考点分析
本文深入解析互联网大厂校招Java工程师笔试题,涵盖基础知识(数据类型、流程控制)、面向对象编程(类与对象、继承与多态)、数据结构与算法(数组、链表、排序算法)、异常处理、集合框架、Java 8+新特性(Lambda表达式、Stream API)、多线程与并发、IO与NIO、数据库操作(JDBC、ORM框架MyBatis)及Spring框架基础(IoC、DI、AOP)。通过技术方案讲解与实例演示,助你掌握核心考点,提升解题能力。
54 2
|
1月前
|
人工智能 Java
Java参数传递分析
本文详细探讨了Java中参数传递的机制,明确指出Java采用的是值传递而非引用传递。通过基本数据类型(如int)和引用类型(如Map、自定义对象People)的实例测试,证明方法内部对参数的修改不会影响原始变量。即使在涉及赋值返回的操作中,表面上看似引用传递,实际仍是值传递的结果。文中结合代码示例与执行结果,深入解析了值传递的本质及容易引起混淆的情形,帮助读者准确理解Java参数传递的核心概念。
|
30天前
|
缓存 安全 Java
【高薪程序员必看】万字长文拆解Java并发编程!(3-1):并发共享问题的解决与分析
活锁:多个线程相互影响对方退出同步代码块的条件而导致线程一直运行的情况。例如,线程1的退出条件是count=5,而线程2和线程3在其代码块中不断地是count进行自增自减的操作,导致线程1永远运行。内存一致性问题:由于JIT即时编译器对缓存的优化和指令重排等造成的内存可见性和有序性问题,可以通过synchronized,volatile,并发集合类等机制来解决。这里的线程安全是指,多个线程调用它们同一个实例的方法时,是线程安全的,但仅仅能保证当前调用的方法是线程安全的,不同方法之间是线程不安全的。
37 0
|
30天前
|
Java 程序员
【高薪程序员必看】万字长文拆解Java并发编程!(3-2):并发共享问题的解决与分析
wait方法和notify方法都是Object类的方法:让当前获取锁的线程进入waiting状态,并进入waitlist队列:让当前获取锁的线程进入waiting状态,并进入waitlist队列,等待n秒后自动唤醒:在waitlist队列中挑一个线程唤醒:唤醒所有在waitlist队列中的线程它们都是之间协作的手段,只有拥有对象锁的线程才能调用这些方法,否则会出现IllegalMonitorStateException异常park方法和unpark方法是LockSupport类中的方法。
40 0
|
10月前
|
存储 监控 Java
Java多线程优化:提高线程池性能的技巧与实践
Java多线程优化:提高线程池性能的技巧与实践
271 1
|
设计模式 监控 Java
Java多线程基础-11:工厂模式及代码案例之线程池(一)
本文介绍了Java并发框架中的线程池工具,特别是`java.util.concurrent`包中的`Executors`和`ThreadPoolExecutor`类。线程池通过预先创建并管理一组线程,可以提高多线程任务的效率和响应速度,减少线程创建和销毁的开销。
568 2
|
Java 数据库
【Java多线程】对线程池的理解并模拟实现线程池
【Java多线程】对线程池的理解并模拟实现线程池
120 1
|
10月前
|
安全 算法 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(下)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
130 6
|
10月前
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(中)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
146 5

热门文章

最新文章