美文网首页
【Java并发】活跃性问题-死锁

【Java并发】活跃性问题-死锁

作者: Rockie_h | 来源:发表于2018-03-20 23:53 被阅读23次

    死锁

    死锁的经典场景“哲学家进餐”:每个人都拥有其他人需要的资源,同时又等待其他人已经拥有的资源,并且每个人在获得所需要的资源之前都不会放弃已经拥有的资源。
    数据库系统解决死锁问题:当检测到一组事务发生死锁时(通过在表示等待关系的有向图中搜索循环),将选择一个牺牲者并放弃这个事务。
    但是JVM遇到死锁将无计可施,除了终止程序并重启。
    示例一:简单的锁顺序死锁:

    public class LeftRightDeadLock
    {
        private final Object left = new Object();
        private final Object right = new Object();
        
        public void leftRight()
        {
            synchronized(left){
                synchronized(right){
                    doSomething();
                }
            }
        }
        
        public void rightLeft()
        {
            synchronized(right){
                synchronized(left){
                    doSomethingElse();
                }
            }
        }
    }
    

    如果所有线程都以固定顺序来获得锁,那么程序中就不会出现锁顺序死锁的问题。
    示例二:动态的锁顺序死锁:

    public class DeadLockDemo 
    {
        private static final int NUM_ACCOUNTS = 10;
        private static final int NUM_THREADS = 20;
        private static final int NUM_ITERATIONS = 10000;
        private static int count = 0;
    
        public static void main(String args[]) 
        {
            Account[] accounts = new Account[NUM_ACCOUNTS];
            Random rnd = new Random();
            for (int i = 0; i < NUM_ACCOUNTS; i++) 
            {
                Account account = new Account(i, rnd.nextInt(1000));
                accounts[i] = account;
            }
            class TransferThread extends Thread
            {
                public void run()
                {
                    long starttime = System.currentTimeMillis();
                    for (int i = 0; i < NUM_ITERATIONS; i++) 
                    {
                        int fromAcct = rnd.nextInt(NUM_ACCOUNTS);
                        int toAcct = rnd.nextInt(NUM_ACCOUNTS);
                        transfer(accounts[fromAcct], accounts[toAcct], rnd.nextInt(1000));
                    }
                    System.out.println("cost: " + (System.currentTimeMillis() - starttime));
                }
                private void transfer(Account fromAccount, Account toAccount, int transferAmount)
                {
                    synchronized (fromAccount) 
                    {
                        synchronized (toAccount)    
                        {
                            fromAccount.withDraw(transferAmount);
                            toAccount.deposit(transferAmount);
                            count++;
                            //System.out.println("run count = " + count);
                        }
                    }
                }
                
            }
            
            for(int i = 0; i < NUM_THREADS; i++)
            {
                new TransferThread().start();
            }
            
        }
    }
    

    示例二中,当每个线程“转账”操作达到一定数量时,发生死锁的概率就大大提升。当某个线程从账号A向账号B转账,同时另一个线程从账号B向账号A转账,死锁就发生了。
    示例三:协作对象间发生的死锁:

    /**
     * 协作对象间发生的死锁
     * @author Administrator
     *
     */
    public class Taxi 
    {
        private Point location;
        private Point destination;
        private final Dispatcher dispatcher;
        
        public Taxi(Dispatcher disp)
        {
            this.dispatcher = disp;
        }
        
        public synchronized Point getLocation()
        {
            return this.location;
        }
        
        /**
         * 更新位置,如果已经到达目的地,则通知调度系统:
         * 某个线程调用此方法,则会持有Taxi的锁,然后再持有Dispatcher的锁
         * @param newLoc
         */
        public synchronized void setLocation(Point newLoc)
        {
            this.location = newLoc;
            if(this.location.equals(this.destination))
            {
                dispatcher.notifyAvailable(this);
            }
        }
    }
    public class Dispatcher 
    {
        /**
         * 所有的出租车集合
         */
        private final Set<Taxi> taxis;
        
        /**
         * 可用的出租车集合
         */
        private final Set<Taxi> availableTaxis;
        
        public Dispatcher()
        {
            taxis = new HashSet<Taxi>();
            this.availableTaxis = new HashSet<Taxi>();
        }
    
        public synchronized void notifyAvailable(Taxi taxi) 
        {
            this.availableTaxis.add(taxi);
        }
        
        /**
         * 拍照:
         * 某个线程调用此方法,会先持有Dispatcher的锁,再持有Taxi的锁
         * @return
         */
        public synchronized Image getImage()
        {
            Image image = new Image();
            for(Taxi t: taxis)
            {
                image.drawMarker(t.getLocation());
            }
            return image;
        }
    }
    

    如果在持有锁的情况下调用某个外部方法,就需要警惕死锁。因为在这个外部方法中可能会获取其他所,或者阻塞时间过长,导致其他线程无法及时获得当时被持有的锁。
    示例四:通过开放调用来避免在相互协作的对象之间产生死锁:

        public synchronized void setLocation(Point newLoc)
        {
            boolean haveReached;
            synchronized(this)
            {
                //先同步更新定位坐标,然后发出通知
                this.location = newLoc;
                haveReached = location.equals(destination);
            }
            if(haveReached)
            {
                dispatcher.notifyAvailable(this);
            }
        }
        public synchronized Image getImage()
        {
            Set<Taxi> copy;
            synchronized(this)
            {
                //先同步持有集合副本,对副本进行操作
                copy = new HashSet<Taxi>(taxis);
            }
            Image image = new Image();
            for(Taxi t: copy)
            {
                image.drawMarker(t.getLocation());
            }
            return image;
        }
    

    开放调用:如果在调用某个方法时不需要持有锁,则称为开放调用。这需要使同步代码块仅用于保护那些涉及共享状态的操作。
    资源死锁:当多个线程在相同的资源集合上等待时,也会发生死锁。比如,两个不同数据库的资源池,极端情况下,每个资源池只有一个连接,一个任务获得A数据的唯一连接,并请求B数据的连接,同时,另一个任务获得B的唯一连接,请求A的唯一连接,就会发生死锁。因此,一般资源池越大,这种情况可能性越小。
    因此,有界线程池/资源池与相互依赖的任务不能一起使用。

    死锁的避免与诊断

    如果一个程序每次之多只能获得一个锁,就不会产生所顺序死锁。如果必须获取多个锁,那么设计时就必须考虑锁顺序:尽量减少潜在的加锁交互数量,将获取锁时需要遵循的协议写入正是文档并始终遵循。
    1 采用定时锁:即显式使用Lock中的定时tryLock功能来代替内置锁,下篇专门分析;
    2 通过线程转储信息分析死锁;

    其他活跃性问题:

    1 饥饿:线程无法访问它所需要的资源而无法继续执行时,就发生“饥饿(starvation)”。引发饥饿最常见的资源就是CPU时钟周期。
    要避免使用线程优先级,这会增加平台依赖性,并可能导致活跃性问题。在大多数并发应用程序中,都可以使用默认的线程优先级。
    2 糟糕的响应性:前台请求线程和后台运算线程。此时,可以考虑使用线程优先级来提高前台线程的优先级;
    3 活锁:不会阻塞线程,但也不能继续执行,因为线程将重复执行相同的操作,而且总会失败。通常发生在处理事务消息的应用中。要解决这种活锁问题,需要在重试机制中引入随机性。

    相关文章

      网友评论

          本文标题:【Java并发】活跃性问题-死锁

          本文链接:https://www.haomeiwen.com/subject/ppyiqftx.html