Java线程死锁示例

时间:2012-03-20 11:12:13

标签: java concurrency

我一直在努力阅读Herlihy和Shavit的多处理器编程艺术。在第二章中,他们用两种不正确的锁实现来激励Peterson的算法。我试图了解LockTwo类的问题,并以下面的代码结束了死锁。在代码之后给出示例输出,其中线程在死锁之前不打印从1到100的所有值。为了在任一线程中发生死锁,while(victim ==i){}必须永远运行。但是如果两个线程都在运行这个循环,那么其中一个必须退出,因为victim不能同时为0和1。我错过了缓存相关的东西吗?

class Counter
{
    private int value;
    private int maxValue;
    public Counter(int value, int maxValue)
    {
        this.value = value;
        this.maxValue = maxValue;
    }

    public int getValue()
    {
        return value;
    }

    public int getMaxValue()
    {
        return maxValue;
    }

    public void incrementValue()
    {
        value++;
    }
}

class ThreadID 
{
  private static volatile int nextID = 0;
  private static ThreadLocalID threadID = new ThreadLocalID();

  public static int get() 
  {
    return threadID.get();
  }

  public static void reset() 
  {
    nextID = 0;
  }

  private static class ThreadLocalID extends ThreadLocal<Integer> 
  {
    protected synchronized Integer initialValue() 
    {
      return nextID++;
    }
  }
}

class IncrementThread implements Runnable
{
    private static Counter c = new Counter(0,100);
    private static LockTwo lock2 = new LockTwo();
    public IncrementThread()
    {
    }

    public void run()
    {
        int j = ThreadID.get();
        String m = "Thread ID " + j;
        //System.out.println(m);                        
        while(c.getValue() < c.getMaxValue())
        {
            lock2.lock();
            c.incrementValue();
            String s = "Value in counter is " + c.getValue() + " in thread " + j;
            System.out.println(s);                        
            lock2.unlock();
        }
    }
}

interface Lock
{
    public void lock();
    public void unlock();
}


class LockTwo implements Lock
{
    private int victim;
    public LockTwo() {};
    public void lock()
    {
        int i = ThreadID.get();
        victim = i;
        System.out.println("Trying to acquire lock in thread " + i +" with victim " + victim);
        while(victim == i) {} 
        System.out.println("Lock acquired in thread " + i + " with victim " + victim);
    }

    public void unlock()
    {
        int i = ThreadID.get();
        //victim = i;
        System.out.println("Lock released in thread " + i + " with victim " + victim);
    }
}

public class SharedCounter
{
    public static void main(String[] args) throws InterruptedException
    {
        Thread thread[] = new Thread[2];
        for (int i = 0; i < thread.length; i++)
        {
            thread[i] = new Thread(new IncrementThread());
            thread[i].start();
        }
    }
}

示例输出 $java SharedCounter

Trying to acquire lock in thread 0 with victim 1
Trying to acquire lock in thread 1 with victim 1
Lock acquired in thread 0 with victim 1
Value in counter is 1 in thread 0
Lock released in thread 0 with victim 1
Trying to acquire lock in thread 0 with victim 0
Lock acquired in thread 1 with victim 0
Value in counter is 2 in thread 1
Lock released in thread 1 with victim 0
Trying to acquire lock in thread 1 with victim 1
Lock acquired in thread 0 with victim 1
Value in counter is 3 in thread 0
Lock released in thread 0 with victim 1
Trying to acquire lock in thread 0 with victim 0

2 个答案:

答案 0 :(得分:3)

我怀疑问题是victim不易变 java中的变量可以在本地缓存到线程中。

这意味着每个线程都有自己的受害者视图,每个视图都有自己的id 即线程0有victim == 0,Thread1有victim == 1

使用volatile告诉jvm变量将在线程之间使用,并且不应该缓存它。

答案 1 :(得分:0)

这个答案建立在Jim的答案以及与之相关的评论之上。

  

如果缺少volatile关键字,则线程应立即死锁,它们永远不能递增计数器。 - sarva

这是不正确的,没有办法保证写入的值何时传播到其他线程。

  

顺便说一句,ArtofMP中的LockTwo实施具有与volatile关联的victim关键字,但勘误表要求删除此关键字。 - sarva

这只是因为与同一章中提出的其他算法的一致性。 Pragma 2.3.1表示victimlabel等等需要在实践中声明volatile - 但是在图2.5中victim被声明为volatile反正。

  

如果我将volatile关键字添加到victim,那么当一个线程通过将计数器递增到100而另一个线程在{{1}中等待完成执行时,最后会出现死锁没有改变受害者的价值。 - sarva

这是您想要的行为。考虑一个单线程程序,程序中的以下行没有意义:

while(victim == i) {}

如果没有额外的线程,这将是一个无限循环。

当只有一个线程尝试获取锁(即调用victim = i; while (victim == i) {} 方法)时,我们可以在双线程程序中获得基本相同的情况。

如果两个线程都调用lock方法,我们会得到您在最后发现死锁的行为:

  1. 第一个线程调用lock并将lock设置为0并开始循环
  2. 第二个线程调用victim并将lock设置为1,然后开始循环
  3. 第一个帖子看到victim现在等于1并进入关键部分
  4. 如果第一个线程再也没有调用victim方法,则第二个线程将永远停留在循环中,等待lock变为1
  5. 因此,如果两个线程分别多次调用victim方法,那么(在某种意义上)最后一次调用将无法完成。