【Java并发】活跃性问题-死锁
死锁
死锁的经典场景“哲学家进餐”:每个人都拥有其他人需要的资源,同时又等待其他人已经拥有的资源,并且每个人在获得所需要的资源之前都不会放弃已经拥有的资源。
数据库系统解决死锁问题:当检测到一组事务发生死锁时(通过在表示等待关系的有向图中搜索循环),将选择一个牺牲者并放弃这个事务。
但是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 活锁:不会阻塞线程,但也不能继续执行,因为线程将重复执行相同的操作,而且总会失败。通常发生在处理事务消息的应用中。要解决这种活锁问题,需要在重试机制中引入随机性。