没了解死锁怎么能行?进来看看,一文带你拿下死锁产生的原因、死锁的解决方案。

简介: 没了解死锁怎么能行?进来看看,一文带你拿下死锁产生的原因、死锁的解决方案。




一、死锁是什么

死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

举个例子理解死锁

滑稽老哥和女神一起去饺子馆吃饺子,吃饺子需要酱油和醋。

滑稽老哥抄起了酱油瓶, 女神抄起了醋瓶。

滑稽: 你先把醋瓶给我, 我用完了就把酱油瓶给你。

女神: 你先把酱油瓶给我, 我用完了就把醋瓶给你。

如果这俩人彼此之间互不相让, 就构成了死锁.

酱油和醋相当于是两把锁, 这两个人就是两个线程。

为了进一步阐述死锁的形成, 很多资料上也会谈论到 "哲学家就餐问题"。

二、哲学家就餐问题

  • 有个桌子, 围着一圈哲学家, 桌子中间放着一盘意大利面。每个哲学家两两之间, 放着一根筷子。

每个哲学家只做两件事: 思考人生或者吃面条。思考人生的时候就会放下筷子,吃面条就会拿起左

右两边的筷子(先拿起左边, 再拿起右边)。

如果哲学家发现筷子拿不起来了(被别人占用了), 就会阻塞等待。

  • [关键点] 假设同一时刻, 五个哲学家同时拿起左手边的筷子, 然后再尝试拿右手的筷子, 就会发现右手的筷子都被占用了。由于哲学家们互不相让, 这个时候就形成了 死锁

死锁是一种严重的 BUG!! 导致一个程序的线程 "卡死", 无法正常工作!

三、如何避免死锁

3.1 死锁产生的四个必要条件

  • 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用。
  • 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  • 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
  • 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

其中最容易破坏的就是 "循环等待"。

3.2 破除循环等待

最常用的一种死锁阻止技术就是锁排序。假设有N 个线程尝试获取M 把锁, 就可以针对M 把锁进行编号 (1, 2, 3...M)。

N 个线程尝试获取锁的时候, 都按照固定的按编号由小到大顺序来获取锁. 这样就可以避免环路等待。

可能产生环路等待的代码:

两个线程对于加锁的顺序没有约定, 就容易产生环路等待。

Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread() {
    @Override
    public void run() {
        synchronized (lock1) {
            synchronized (lock2) {
                // do something...
           }
       }
   }
};
t1.start();
Thread t2 = new Thread() {
    @Override
    public void run() {
        synchronized (lock2) {
            synchronized (lock1) {
                // do something...
           }
       }
   }
};
t2.start();

约定好先获取 lock1, 再获取 lock2 , 就不会环路等待。

Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread() {
    @Override
    public void run() {
        synchronized (lock1) {
            synchronized (lock2) {
                // do something...
           }
       }
   }
};
t1.start();
Thread t2 = new Thread() {
    @Override
    public void run() {
        synchronized (lock1) {
            synchronized (lock2) {
                // do something...
           }
       }
   }
};
t2.start();
}

四、关于多线程的一些问题

4.1 谈谈 volatile关键字的用法?

volatile 能够保证内存可见性,强制从主内存中读取数据。此时如果有其他线程修改被 volatile 修饰的变量, 可以第一时间读取到最新的值。

4.2 Java多线程是如何实现数据共享的?

JVM 把内存分成了这几个区域:

方法区, 堆区, 栈区, 程序计数器。

其中堆区这个内存区域是多个线程之间共享的。

只要把某个数据放到堆内存中, 就可以让多个线程都能访问到。

4.3 Java创建线程池的接口是,参数 LinkedBlockingQueue 的作用

创建线程池主要有两种方式:

  • 通过 Executors 工厂类创建,创建方式比较简单, 但是定制能力有限。
  • 通过 ThreadPoolExecutor 创建,创建方式比较复杂, 但是定制能力强。

LinkedBlockingQueue 表示线程池的任务队列,用户通过 submit / execute 向这个任务队列中添加任务, 再由线程池中的工作线程来执行任务。

4.4  Java线程共有几种状态?状态之间怎么切换的?

  • NEW: 安排了工作, 还未开始行动,新创建的线程, 还没有调用 start 方法时处在这个状态。
  • RUNNABLE: 可工作的。又可以分成正在工作中和即将开始工作,调用 start 方法之后, 并正在CPU 上运行/在即将准备运行的状态。
  • BLOCKED: 使用 synchronized 的时候, 如果锁被其他线程占用, 就会阻塞等待, 从而进入该状态。
  • WAITING: 调用 wait 方法会进入该状态。
  • TIMED_WAITING: 调用 sleep 方法或者 wait(超时时间) 会进入该状态。
  • TERMINATED: 工作完成了,当线程 run 方法执行完毕后, 会处于这个状态。

4.5 在多线程下,如果对一个数进行叠加,该怎么做?

  • 使用 synchronized / ReentrantLock 加锁
  • 使用 AtomInteger 原子操作

4.6 ThreadRunnable的区别和联系?

  • Thread 类描述了一个线程。
  • Runnable 描述了一个任务。
  • 在创建线程的时候需要指定线程完成的任务, 可以直接重写 Thread 的 run 方法, 也可以使用Runnable 来描述这个任务。

4.7 多次start一个线程会怎么样

  • 第一次调用 start 可以成功调用
  • 后续再调用 start 会抛出 java.lang.IllegalThreadStateException 异常

4.8 synchronized两个方法,两个线程分别同时用这个方法,会发生什么?

synchronized 加在非静态方法上, 相当于针对当前对象加锁。

如果这两个方法属于同一个实例:

线程1 能够获取到锁, 并执行方法。线程2会阻塞等待, 直到线程1 执行完毕, 释放锁, 线程2 获取到锁之后才能执行方法内容。

如果这两个方法属于不同实例:

两者能并发执行, 互不干扰

4.9 进程和线程的区别

  • 进程是包含线程的,每个进程至少有一个线程存在,即主线程。
  • 进程和进程之间不共享内存空间,同一个进程的线程之间共享同一个内存空间。
  • 进程是系统分配资源的最小单位,线程是系统调度的最小单位。

🌈🌈🌈好啦,今天的分享就到这里!

🛩️🛩️🛩️希望各位看官读完文章后,能够有所提升。

🎉🎉🎉创作不易,还希望各位大佬支持一下!

✈️✈️✈️点赞,你的认可是我创作的动力!

⭐⭐⭐收藏,你的青睐是我努力的方向!

✏️✏️✏️评论:你的意见是我进步的财富!

目录
相关文章
|
3月前
|
资源调度 算法
深入理解网络中的死锁和活锁现象
【8月更文挑战第24天】
128 0
|
3月前
|
Java 开发者
解锁Java并发编程的秘密武器!揭秘AQS,让你的代码从此告别‘锁’事烦恼,多线程同步不再是梦!
【8月更文挑战第25天】AbstractQueuedSynchronizer(AQS)是Java并发包中的核心组件,作为多种同步工具类(如ReentrantLock和CountDownLatch等)的基础。AQS通过维护一个表示同步状态的`state`变量和一个FIFO线程等待队列,提供了一种高效灵活的同步机制。它支持独占式和共享式两种资源访问模式。内部使用CLH锁队列管理等待线程,当线程尝试获取已持有的锁时,会被放入队列并阻塞,直至锁被释放。AQS的巧妙设计极大地丰富了Java并发编程的能力。
44 0
|
6月前
|
算法 Java
Java多线程基础-13:一文阐明死锁的成因及解决方案
死锁是指多个线程相互等待对方释放资源而造成的一种僵局,导致程序无法正常结束。发生死锁需满足四个条件:互斥、请求与保持、不可抢占和循环等待。避免死锁的方法包括设定加锁顺序、使用银行家算法、设置超时机制、检测与恢复死锁以及减少共享资源。面试中可能会问及死锁的概念、避免策略以及实际经验。
102 1
|
Java 开发者
解锁Java多线程编程中的死锁之谜
解锁Java多线程编程中的死锁之谜
57 0
|
6月前
|
存储 算法 安全
操作系统基础:死锁
操作系统基础:死锁
|
Java
死锁不可怕
死锁不可怕
97 0
死锁的发生原因和怎么避免,写的明明白白
死锁的发生原因和怎么避免,写的明明白白
死锁的发生原因和怎么避免,写的明明白白
|
SQL 监控 关系型数据库
死锁分析延续
可知上一篇【死锁分析】,又重新表达了一些图片,图画更了
153 0
死锁分析延续
Synchronized 升级到重量级锁之后就下不来了?你错了!
Synchronized 升级到重量级锁之后就下不来了?你错了!
Synchronized 升级到重量级锁之后就下不来了?你错了!