Java 并发编程之死锁

动态的锁顺序死锁

这是接着上一篇的写。为了方便又贴了一遍代码,因为第二天我发现,这个通过锁顺序来避免死锁的程序依旧有问题。

我的问题是:

一个对象的Object的Hashcode的值 应该是固定的,那么。虽然这个代码通过hashcode规范了锁顺序,当两个人互相往对方的账户里面转账的时候。不还是变成了

public void transferMoney(Account formaccaount, Account toaccount,
			DollarAmount amount) {
		synchronized (formaccaount) {
			synchronized (toaccount) {
				if (formaccaount.getBalance.compareTo(amount) < 0) {
					throw new InsufficientFundsExcetion();
				} else {
					fromaccount.debit(amount);
					toaccount.credit(amount);
				}
			}
		}
	}

这样的代码。因为还是Fromacct和toacct互换了一个位置啊。后来仔细一想,哈哈确实想少了,因为Fromacct和toacct互换了位置,那么其实里面的嵌套锁也换啦,虽然名字上看来不一样,但是如果两个人都互相往对方转账的话,锁的顺序是一样的。

    public class TestLock {
        private static final Object tieLock = new Object();  

        private Account fromacct;
        private Account toacct;  

        public void transferMoney(final Account fromacct, final Account toacct,
                final DollarAmount amount) {
            this.fromacct = fromacct;
            this.toacct = toacct;
            class Helper {
                public void transfer() {
                    if (fromacct.getBalance().compareTo(amount) < 0) {
                        throw new InsufficientFundsExcetion();
                    } else {
                        fromacct.debit(amount);
                        toacct.credit(amount);
                    }
                }
            }
            int fromHash = System.identityHashCode(fromacct);
            int toHash = System.identityHashCode(toacct);
            if (fromHash < toHash) {
                synchronized (fromacct) {
                    synchronized (toacct) {
                        new Helper().transfer();
                    }
                }
            } else if (fromHash > toHash) {
                synchronized (toacct) {
                    synchronized (fromacct) {
                        new Helper().transfer();
                    }
                }
            } else {
                synchronized (tieLock) {
                    synchronized (fromacct) {
                        synchronized (toacct) {
                            new Helper().transfer();
                        }
                    }
                }
            }
        }  

        private class DollarAmount {  

        }  

        private abstract class Account {
            Comparable<DollarAmount> getBalance() {
                return new Comparable<DollarAmount>() {  

                    @Override
                    public int compareTo(DollarAmount o) {
                        // TODO Auto-generated method stub
                        return 0;
                    }
                };
            }  

            abstract void debit(DollarAmount a);  

            abstract void credit(DollarAmount a);
        }
    }  

在协作对象之间发生的死锁

以出租车调试系统为例

import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.image.ImageObserver;
import java.awt.image.ImageProducer;
import java.util.HashSet;
import java.util.Set;

class Taxi {
	private Point location, destination;
	private final Dispather dispather;

	public Taxi(Dispather dispather) {
		super();
		this.dispather = dispather;
	}

	/**
	 * @return the location
	 */
	public synchronized Point getLocation() {
		return location;
	}

	/**
	 * @param location
	 *            the location to set
	 */
	public synchronized void setLocation(Point location) {
		this.location = location;
		if (location.equals(destination)) {
			dispather.notifyAvailable(this);
		}
	}

}

class Dispather {
	private final Set<Taxi> taxis;
	private final Set<Taxi> availableTaxis;

	public Dispather() {
		taxis = new HashSet<Taxi>();
		availableTaxis = new HashSet<Taxi>();
	}

	public synchronized void notifyAvailable(Taxi taxi) {
		availableTaxis.add(taxi);
	}

	public synchronized Image getImage() {
		Image image = new Image();
		for (Taxi t : taxis) {
			image.drawMarker(t.getLocation());
		}
		return image;
	}
}

锁顺序很隐蔽:

其实最后还是简单的锁顺序死锁,对于synchronized可能比较熟悉的是synchronized(object){}的使用方法,对于synchronized方法不太熟悉,对于每个对象都有一个锁,可以这样理解synchronized(class test{ private int a,b..;});所以对象内的一个方法被调用的时候 ,这个对象的锁就被持有了,无论调用这个对象的哪个方法都需要等待正在被调用的方法执行完毕。

下面可以解释协作对象之间的死锁成因了:

Taxi调用setLocation的同时Dispather调用getImage。这个可以同时发生:

setLocation先获取taxi的锁,然后获取Dispather的锁(因为它调用了notifyAvailable方法)

同时

getImage先获取Dispather的锁,然后依次获取taxi的锁,而当获取到那个正在被调用setLocation的taxi时,死锁就发生了。

解决方法:采用类似客户端加锁的办法,把接口改为开放调用。

import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.image.ImageObserver;
import java.awt.image.ImageProducer;
import java.util.HashSet;
import java.util.Set;

class Taxi {
	private Point location, destination;
	private final Dispather dispather;

	public Taxi(Dispather dispather) {
		super();
		this.dispather = dispather;
	}

	/**
	 * @return the location
	 */
	public synchronized Point getLocation() {
		return location;
	}

	/**
	 * @param location
	 *            the location to set
	 */
	public void setLocation(Point location) {
		boolean reached = false;
		synchronized (this) {
			this.location = location;
			reached = location.equals(destination);
		}
		if (reached) {
			dispather.notifyAvailable(this);
		}

	}
}

class Dispather {
	private final Set<Taxi> taxis;
	private final Set<Taxi> availableTaxis;

	public Dispather() {
		taxis = new HashSet<Taxi>();
		availableTaxis = new HashSet<Taxi>();
	}

	public synchronized void notifyAvailable(Taxi taxi) {
		availableTaxis.add(taxi);
	}

	public 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;
	}
}

效果:

将两个锁减为了只竞争Dispather一个锁。Dispather从不持有taxi的锁。它调用的是taxis的副本。

因为getImage只用来获取数据,并不执行set类的方法。

此外收缩同步代码块的保护范围还可以提高可伸缩性。

死锁的避免与诊断

尽管是一个并发高手可能也会因为大意便程序发生死锁,而死锁的原因也很多,像上面那种因为协作对象之间而发生的死锁是非常难发现的。所以我们在写程序的过程中应该尽量避免锁的交互。同时应尽可能的使用开放调用。

支持定时的锁

这一项技术可以检测死锁并从死锁中恢复过来。即显式使用lock类的定时trylock功能来代替内置锁机制。显式锁可以设置一个超时时限。

在lock类里找到了这个接口 :

 boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

先了解一下,以后会细说

通过线程转储信息来分析死锁

JVM通过线程转储来帮助识别死锁的发生。

UNIX可以向jvm的进程发送SIGQUIT信号,或者在UNIX平台按下ctrl-\。windows平台按下ctrl+break(这个break键笔记本是没有的。只有87键及以上的键盘有。一般用来调试服务器。)

其它活跃性危险

饥饿

就是要执行任务的线程因为优先级等原因被迫等待,而造成的饥饿性等待。

你经常能发现某个程序会在一些奇怪的地方调用Thread.sleep和Thread.yield,这是因为该程序试图克服优先级调整问题或者响应性问题,并让低优先级的线程执行更多时间

要避免使用线程优先级,因为这会增加平台依赖性。并可能导致活跃性问题

糟糕的响应性

和饥饿是一类的问题,如果GUI应用程序中使用了后台线程,那么这种问题是很常见的。

活锁

活锁是另一种形式的活跃性问题,该问题尽管不用阻塞线程,但也不能继续执行,因为线程将不断重复执行相同的操作。而且总会失败。

活锁通常发生在处理事务消息的应用程序中,如果不能成功的处理某个消息,那么消息处理机制将回滚整个事务。并将它重新放到队列的开头。所以程序不会死掉。但也不能继续执行了。

书上给了一个很形象的栗子,两个人走路遇见了,然后相互避让,结果又在另一条路上遇见,不断重复。

解决问题的办法是在重试机制中引入随机性。如,两台机器尝试使用相同的载波(就是频率,如果相同的话会发生混叠,使传输信号失真)来发送数据包,那么这些数据包就会发生冲突。并又双双重试。引入随机的概念后,让它们在等待随机的时间段后再重试。以太协议定义了重复发生冲突时采用指数方式回退机制,从而降低在多台存在冲突的机器之间发生拥塞和反复失败的风险。

在并发应用中,我们可以通过让程序等待随机长度的时间来避免活锁的发生。

时间: 2024-10-09 17:53:14

Java 并发编程之死锁的相关文章

【Java并发编程】并发编程大合集-值得收藏

http://blog.csdn.net/ns_code/article/details/17539599这个博主的关于java并发编程系列很不错,值得收藏. 为了方便各位网友学习以及方便自己复习之用,将Java并发编程系列内容系列内容按照由浅入深的学习顺序总结如下,点击相应的标题即可跳转到对应的文章    [Java并发编程]实现多线程的两种方法    [Java并发编程]线程的中断    [Java并发编程]正确挂起.恢复.终止线程    [Java并发编程]守护线程和线程阻塞    [Ja

【Java并发编程】并发编程大合集

转载自:http://blog.csdn.net/ns_code/article/details/17539599 为了方便各位网友学习以及方便自己复习之用,将Java并发编程系列内容系列内容按照由浅入深的学习顺序总结如下,点击相应的标题即可跳转到对应的文章    [Java并发编程]实现多线程的两种方法    [Java并发编程]线程的中断    [Java并发编程]正确挂起.恢复.终止线程    [Java并发编程]守护线程和线程阻塞    [Java并发编程]Volatile关键字(上)

Java并发编程

synchronized是Java中的关键字,在并发编程中被称为内置锁或者监视器锁.当用它来修饰一个方法或者一个代码块的时候能够保证同一时刻最多只有一个线程执行该段代码. Java的内置锁相当于一种互斥锁,最多只有一个线程能够持有这种锁,故而由这个锁保护的同步代码块会以原子方式执行,多个线程在执行该代码时就不会相互干扰. 但由于被锁保护的同步块代码是以串行形式来访问的,即多个线程以独占的方式访问对象,而这也导致如果被锁保护的同步代码块的作用范围过大,会导致并发不良. 这里有必要简单讲一下内置锁的

Java 并发编程之任务取消(九)

Jvm关闭 jvm可正常关闭也可强行关闭,正常关闭有多种触发方式: 当最后一个正常(非守护,下面会讲到什么是守护线程)线程结束时 当调用system.exit时,或者通过其他特定于平台的方法关闭时(例如发送了SIGINT信号或键入Ctrl-c) 通过其他特定平台的方法关闭jvm,调用Runtime.halt或者在操作系统当中杀死JVM进程(例如发送sigkill)来强行关闭jvm. 关闭钩子 在正常关闭中,jvm首先调用所有已注册的关闭钩子,关闭钩子是指通过 Runtime.addShutdow

JAVA并发编程艺术 一(并发编程的挑战)

从今天起开始java并发编程艺术的学习,每一章学习完以后再这里记录下内容的重点,做个笔记,加深印象. 并发编程的目的是为了让程序运行的更快,但是,并不是启动更多的线程就能让程序最大限度地并发执行.在进行并发是,如果希望通过多现场执行任务让程序运行得更快,会面临非常多的挑战,比如上下文切换的问题,死锁的问题,以及受限于硬件和软件的资源限制问题,本章会介绍几种并发编程的挑战以及解决方案 1.上下问切换 即使是单核处理器也支持多线程执行代码,cpu通过给每个线程分配cpu时间片来实现这个机制.时间片是

5、Java并发编程:Lock

Java并发编程:Lock 在上一篇文章中我们讲到了如何使用关键字synchronized来实现同步访问.本文我们继续来探讨这个问题,从Java 5之后,在java.util.concurrent.locks包下提供了另外一种方式来实现同步访问,那就是Lock. 也许有朋友会问,既然都可以通过synchronized来实现同步访问了,那么为什么还需要提供Lock?这个问题将在下面进行阐述.本文先从synchronized的缺陷讲起,然后再讲述java.util.concurrent.locks包

《Java并发编程实战》第八章 线程池的使用 读书笔记

一.在任务与执行策略之间的隐性解耦 有些类型的任务需要明确地指定执行策略,包括: . 依赖性任务.依赖关系对执行策略造成约束,需要注意活跃性问题.要求线程池足够大,确保任务都能放入. . 使用线程封闭机制的任务.需要串行执行. . 对响应时间敏感的任务. . 使用ThreadLocal的任务. 1. 线程饥饿死锁 线程池中如果所有正在执行任务的线程都由于等待其他仍处于工作队列中的任务而阻塞,这种现象称为线程饥饿死锁. 2. 运行时间较长的任务 Java提供了限时版本与无限时版本.例如Thread

【Java并发编程实战】—– AQS(三):阻塞、唤醒:LockSupport

在上篇博客([Java并发编程实战]-– AQS(二):获取锁.释放锁)中提到,当一个线程加入到CLH队列中时,如果不是头节点是需要判断该节点是否需要挂起:在释放锁后,需要唤醒该线程的继任节点 lock方法,在调用acquireQueued(): if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; 在acquireQueued()中调用parkAndCheckIn

《Java并发编程实战》要点笔记及java.util.concurrent 的结构介绍

买了<java并发编程实战>这本书,看了好几遍都不是很懂,这个还是要在实战中找取其中的要点的,后面看到一篇文章笔记做的很不错分享给大家!! 原文地址:http://blog.csdn.net/cdl2008sky/article/details/26377433 Subsections  1.线程安全(Thread safety) 2.锁(lock) 3.共享对象 4.对象组合 5.基础构建模块 6.任务执行 7.取消和关闭 8.线程池的使用 9.性能与可伸缩性 10.并发程序的测试 11.显