【操作系统之二】死锁

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....");
            }
        }
    }

}
View Code

线程是交错执行的,那么有可能出现:
线程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);
                }
            }
        }
    }
View Code

如果两个线程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) {
        }
    }
}
View Code

上面的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();
                    }
                }
            }
        }
    }
}
View Code

得到对应的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) {
        }
    }

}
View Code

使用开放调用是非常好的一种方式,应该尽量使用它.


(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("结束");
    }
}
View Code

运行结果:

开始
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();
    }

    
}
View Code

(1)Jstack命令

jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息。 Jstack工具可以用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。

第一步:jps 找到死锁程序PID

C:UsersAdministrator>jps
8256 Test2
7188
11244 Jps

Test2 进程号是8256

第二步:使用jstack打印进程8256堆栈信息

C:UsersAdministrator>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:UsersAdministrator>jconsole

弹出窗体:

选择对应线程8256的java应用,点击“连接”:

第二步:点击 “不安全的连接”

第三步:线程 选项卡,点击下面的“检测死锁”

参考:
多线程之死锁就是这么简单 

本地win下JConsole监控远程linux下的JVM

原文地址:https://www.cnblogs.com/cac2020/p/11753738.html