【操作系统之二】死锁
1、概念
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
概括来说:
当前线程拥有其他线程需要的资源;
当前线程等待其他线程已拥有的资源;
都不放弃自己拥有的资源;
2、产生条件
1)互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放
2)请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
3)不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用
4)循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。
3、三种锁
(1)锁顺序死锁
package com; public class LeftRightDeadlock { private final Object left = new Object(); private final Object right = new Object(); public void leftRight() { // 得到left锁 synchronized (left) { // 得到right锁 synchronized (right) { System.out.println("do something...."); } } } public void rightLeft() { // 得到right锁 synchronized (right) { // 得到left锁 synchronized (left) { System.out.println("do something...."); } } } }
线程是交错执行的,那么有可能出现:
线程A调用leftRight()方法,得到left锁;
线程B调用rightLeft()方法,得到right锁;
线程A和线程B都继续执行,此时线程A需要right锁才能继续往下执行,线程B需要left锁才能继续往下执行。
但是:线程A的left锁并没有释放,线程B的right锁也没有释放-->死锁
(2)动态锁顺序死锁
// 转账 public static void transferMoney(Account fromAccount, Account toAccount,DollarAmount amount) throws InsufficientFundsException { // 锁定转出账户 synchronized (fromAccount) { // 锁定转入账户 synchronized (toAccount) { // 判余额是否大于0 if (fromAccount.getBalance().compareTo(amount) < 0) { throw new InsufficientFundsException(); } else { // 汇账账户减钱 fromAccount.debit(amount); // 来账账户增钱 toAccount.credit(amount); } } } }
如果两个线程A和B同时调用transferMoney():
线程A从X账户向Y账户转账;
线程B从账户Y向账户X转账
那么就会发生死锁。
(3)协作对象之间发生死锁
public class CooperatingDeadlock { // Warning: deadlock-prone! class Taxi { @GuardedBy("this") private Point location, destination; private final Dispatcher dispatcher; public Taxi(Dispatcher dispatcher) { this.dispatcher = dispatcher; } public synchronized Point getLocation() { return location; } // setLocation 需要Taxi内置锁 public synchronized void setLocation(Point location) { this.location = location; if (location.equals(destination)) // 调用notifyAvailable()需要Dispatcher内置锁 dispatcher.notifyAvailable(this); } public synchronized Point getDestination() { return destination; } public synchronized void setDestination(Point destination) { this.destination = destination; } } class Dispatcher { @GuardedBy("this") private final Set<Taxi> taxis; @GuardedBy("this") private final Set<Taxi> availableTaxis; public Dispatcher() { taxis = new HashSet<Taxi>(); availableTaxis = new HashSet<Taxi>(); } public synchronized void notifyAvailable(Taxi taxi) { availableTaxis.add(taxi); } // 调用getImage()需要Dispatcher内置锁 public synchronized Image getImage() { Image image = new Image(); for (Taxi t : taxis) // 调用getLocation()需要Taxi内置锁 image.drawMarker(t.getLocation()); return image; } } class Image { public void drawMarker(Point p) { } } }
上面的getImage()和setLocation(Point location)都需要获取两个锁;
并且在操作途中是没有释放锁的;
这就是隐式获取两个锁(对象之间协作),这种方式也很容易就造成死锁。
4、预防死锁
针对产生条件
资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
(1)固定锁顺序避免死锁(针对锁顺序死锁)
public class InduceLockOrder { // 额外的锁、避免两个对象hash值相等的情况(即使很少) private static final Object tieLock = new Object(); public void transferMoney(final Account fromAcct, final Account toAcct, final DollarAmount amount) throws InsufficientFundsException { class Helper { public void transfer() throws InsufficientFundsException { if (fromAcct.getBalance().compareTo(amount) < 0) throw new InsufficientFundsException(); else { fromAcct.debit(amount); toAcct.credit(amount); } } } // 得到锁的hash值 int fromHash = System.identityHashCode(fromAcct); int toHash = System.identityHashCode(toAcct); // 根据hash值来上锁 if (fromHash < toHash) { synchronized (fromAcct) { synchronized (toAcct) { new Helper().transfer(); } } } else if (fromHash > toHash) {// 根据hash值来上锁 synchronized (toAcct) { synchronized (fromAcct) { new Helper().transfer(); } } } else {// 额外的锁、避免两个对象hash值相等的情况(即使很少) synchronized (tieLock) { synchronized (fromAcct) { synchronized (toAcct) { new Helper().transfer(); } } } } } }
得到对应的hash值来固定加锁的顺序,这样就不会发生死锁!
(2)开放调用(针对对象之间协作造成的死锁)
在协作对象之间发生死锁的例子中,主要是因为在调用某个方法时就需要持有锁,并且在方法内部也调用了其他带锁的方法!
如果在调用某个方法时不需要持有锁,那么这种调用被称为开放调用!
改造:
同步代码块最好仅被用于保护那些涉及共享状态的操作!
class CooperatingNoDeadlock { @ThreadSafe class Taxi { @GuardedBy("this") private Point location, destination; private final Dispatcher dispatcher; public Taxi(Dispatcher dispatcher) { this.dispatcher = dispatcher; } public synchronized Point getLocation() { return location; } public synchronized void setLocation(Point location) { boolean reachedDestination; // 加Taxi内置锁 synchronized (this) { this.location = location; reachedDestination = location.equals(destination); } // 执行同步代码块后完毕,释放锁 if (reachedDestination) // 加Dispatcher内置锁 dispatcher.notifyAvailable(this); } public synchronized Point getDestination() { return destination; } public synchronized void setDestination(Point destination) { this.destination = destination; } } @ThreadSafe class Dispatcher { @GuardedBy("this") private final Set<Taxi> taxis; @GuardedBy("this") private final Set<Taxi> availableTaxis; public Dispatcher() { taxis = new HashSet<Taxi>(); availableTaxis = new HashSet<Taxi>(); } public synchronized void notifyAvailable(Taxi taxi) { availableTaxis.add(taxi); } public Image getImage() { Set<Taxi> copy; // Dispatcher内置锁 synchronized (this) { copy = new HashSet<Taxi>(taxis); } // 执行同步代码块后完毕,释放锁 Image image = new Image(); for (Taxi t : copy) // 加Taix内置锁 image.drawMarker(t.getLocation()); return image; } } class Image { public void drawMarker(Point p) { } } }
使用开放调用是非常好的一种方式,应该尽量使用它.
(3)使用定时锁-->tryLock()
tryLock 是防止自锁的一个重要方式。tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
package com; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Test2 { public static void main(String[] args) { System.out.println("开始"); final Lock lock = new ReentrantLock(); new Thread() { @Override public void run() { String tName = Thread.currentThread().getName(); if (lock.tryLock()) { System.out.println(tName + "获取到锁!"); } else { System.out.println(tName + "获取不到锁!"); return; } try { for (int i = 0; i < 5; i++) { System.out.println(tName + ":" + i); } Thread.sleep(5000); } catch (Exception e) { System.out.println(tName + "出错了!!!"); } finally { System.out.println(tName + "释放锁!!"); lock.unlock(); } } }.start(); new Thread() { @Override public void run() { String tName = Thread.currentThread().getName(); if (lock.tryLock()) { System.out.println(tName + "获取到锁!"); } else { System.out.println(tName + "获取不到锁!"); return; } try { for (int i = 0; i < 5; i++) { System.out.println(tName + ":" + i); } } catch (Exception e) { System.out.println(tName + "出错了!!!"); } finally { System.out.println(tName + "释放锁!!"); lock.unlock(); } } }.start(); System.out.println("结束"); } }
运行结果:
开始 Thread-0获取到锁! Thread-0:0 Thread-0:1 Thread-0:2 Thread-0:3 Thread-0:4 结束 Thread-1获取不到锁! Thread-0释放锁!!
5、死锁检测
死锁示例:
package com; public class Test2 { public static void main(String[] args) { final Object a = new Object(); final Object b = new Object(); Thread threadA = new Thread(new Runnable() { public void run() { synchronized (a) { try { System.out.println("now i in threadA-locka"); Thread.sleep(1000l); synchronized (b) { System.out.println("now i in threadA-lockb"); } } catch (Exception e) { // ignore } } } }); Thread threadB = new Thread(new Runnable() { public void run() { synchronized (b) { try { System.out.println("now i in threadB-lockb"); Thread.sleep(1000l); synchronized (a) { System.out.println("now i in threadB-locka"); } } catch (Exception e) { // ignore } } } }); threadA.start(); threadB.start(); } }
(1)Jstack命令
jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息。 Jstack工具可以用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
第一步:jps 找到死锁程序PID
C:\Users\Administrator>jps
8256 Test2
7188
11244 Jps
Test2 进程号是8256
第二步:使用jstack打印进程8256堆栈信息
C:\Users\Administrator>jstack -F 8256 Attaching to process ID 8256, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.171-b11 Deadlock Detection: Found one Java-level deadlock: ============================= "Thread-0": waiting to lock Monitor@0x000000001763a5a8 (Object@0x00000000d5fbffd0, a java/ lang/Object), which is held by "Thread-1" "Thread-1": waiting to lock Monitor@0x0000000017637d18 (Object@0x00000000d5fbffc0, a java/ lang/Object), which is held by "Thread-0" Found a total of 1 deadlock.
对比:使用jstack -l 8256,输出:
Found one Java-level deadlock: ============================= "Thread-1": waiting to lock monitor 0x0000000057827bb8 (object 0x00000000d5fbffc0, a java. lang.Object), which is held by "Thread-0" "Thread-0": waiting to lock monitor 0x000000005782a4f8 (object 0x00000000d5fbffd0, a java. lang.Object), which is held by "Thread-1" Java stack information for the threads listed above: =================================================== "Thread-1": at com.Test2$2.run(Test2.java:31) - waiting to lock <0x00000000d5fbffc0> (a java.lang.Object) - locked <0x00000000d5fbffd0> (a java.lang.Object) at java.lang.Thread.run(Thread.java:748) "Thread-0": at com.Test2$1.run(Test2.java:15) - waiting to lock <0x00000000d5fbffd0> (a java.lang.Object) - locked <0x00000000d5fbffc0> (a java.lang.Object) at java.lang.Thread.run(Thread.java:748) Found 1 deadlock.
上面红色部分表示死锁发生位置。
(2)JConsole工具
Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。而且本身占用的服务器内存很小,甚至可以说几乎不消耗。
第一步:输入命令jconsole 回车
C:\Users\Administrator>jconsole
弹出窗体:
选择对应线程8256的java应用,点击“连接”:
第二步:点击 “不安全的连接”
第三步:线程 选项卡,点击下面的“检测死锁”
参考:
多线程之死锁就是这么简单