21.Java中的锁

一个锁是一个线程同步机制,像同步块除了锁可以是更复杂的比java的同步块。锁(以及其他的更先进的同步机制)都是使用同步块被创建的。

从Java 5开始这个java.util.concurrent.locks包包含了几个锁的实现,以至于你可能不需要去实现你自己的锁。但是你仍然需要知道怎么样使用它们,并且知道他们背后实现的原理仍然是有用的。对于更多的信息,看我的教程在:java.util.concurrent.locks.Lock接口。

一个简单的锁

让我们通过看一个Java代码的同步锁来开始:

public class Counter{

  private int count = 0;

  public int inc(){
    synchronized(this){
      return ++count;
    }
  }
}
注意在inc方法中的synchronized(this)这个块。这个块确保一次只能一个线程执行这个return ++count操作。在同步块中的代码可能有更高级的,但是这个简单的方式已经可以达到效果了。

这个Counter类被代替像下面这样写了,使用一个Lock代替一个同步块:

public class Counter{

  private Lock lock = new Lock();
  private int count = 0;

  public int inc(){
    lock.lock();
    int newCount = ++count;
    lock.unlock();
    return newCount;
  }
}
这个lock方法锁住这个Lock示例以至于调用lock方法的所有线程被锁住了直到unlock方法被执行了。

这里有一个简单的Lock实现:

public class Lock{

  private boolean isLocked = false;

  public synchronized void lock()
  throws InterruptedException{
    while(isLocked){
      wait();
    }
    isLocked = true;
  }

  public synchronized void unlock(){
    isLocked = false;
    notify();
  }
}

注意这个while(isLocked)循环,这个也被称之为“自旋锁”。自旋锁以及这个wait方法和notify方法在线程信号文章中有更详细的描述。当isLocked为true的时候,调用lock方法的这个线程就会被挂起等待在wait方法调用。假设这个线程没有接收到一个notify调用而意外的从wait方法返回(又称之为伪唤醒),这个线程重新检查这个isLocked条件去看是否它是安全的去执行,而不只是假设被唤醒意味着就会去安全的执行。如果isLocked是false,这个线程退出while循环,并且设置这个isLocked为true,去锁住对于调用lock方法其他线程的Lock实例。

当这个线程在代码的临界区域(在lock方法和unlock方法之间的代码)被执行的时候,这个线程调用unlock方法。执行unlock方法设置isLocked会false,并且唤醒等待在wait调用中的线程中的一个,如果可以的话。

重入锁

在Java中的同步块是可重入的。这就意味着,如果一个Java线程进入一个代码的同步块,并且从而持有一个监控对象的锁,这个线程也可以进入在相同的监控对象上的其他的Java同步块中。这里有一个例子:

public class Reentrant{

  public synchronized outer(){
    inner();
  }

  public synchronized inner(){
    //do something
  }
}
注意这个outer方法和inner方法是怎么样被同步的声明的,这个在Java中是等同于synchronized(this)块的。如果一个线程调用outer方法,没有问题在outer方法内部去调用inner方法,因为两个方法都是被相同的监控对象同步了。如果一个线程已经持有在这个监控对象的锁了,它就可以访问在相同的监控对象上的所有的同步块了。这个就被称之为重入锁。这个线程可以重新进入任何一个它已经持有这个锁的代码块。

前面展示的锁的实现是不能重入的。如果我们像下面这样重写这个Reentrant类,调用outer方法这个线程将会被在inner方法的lock.lock方法的内部锁住。

public class Reentrant2{

  Lock lock = new Lock();

  public outer(){
    lock.lock();
    inner();
    lock.unlock();
  }

  public synchronized inner(){
    lock.lock();
    //do something
    lock.unlock();
  }
}

调用outer方法的一个线程将会首先锁住这个Lock示例,然后它将要调用inner方法。在inner方法的内部这个线程将会再一次尝试着锁住这个Lock示例。这个将会失败(意味着这个线程将会被锁住),因为这个Lock示例已经在outer方法中被锁定了。

在第二次它调用lock方法这个线程被锁定的原因,当我们看这个lock方法的实现就很明显了:

public class Lock{

  boolean isLocked = false;

  public synchronized void lock()
  throws InterruptedException{
    while(isLocked){
      wait();
    }
    isLocked = true;
  }

  ...
}

它是这个while循环(自旋锁)内部的条件决定了一个线程是否允许离开这个lock方法。当前的这个条件isLocked必须为false才能被允许的,不管哪个线程锁住了它。

为了使得Lock类可重入的,我们需要做一个小的改变:

public class Lock{

  boolean isLocked = false;
  Thread  lockedBy = null;
  int     lockedCount = 0;

  public synchronized void lock()
  throws InterruptedException{
    Thread callingThread = Thread.currentThread();
    while(isLocked && lockedBy != callingThread){
      wait();
    }
    isLocked = true;
    lockedCount++;
    lockedBy = callingThread;
  }


  public synchronized void unlock(){
    if(Thread.curentThread() == this.lockedBy){
      lockedCount--;

      if(lockedCount == 0){
        isLocked = false;
        notify();
      }
    }
  }

  ...
}

注意这个while循环(自旋锁)现在也会把锁住这个Lock实例的线程加入到考虑的条件中。如果这个锁是未锁定的(isLocked=false)或者调用的线程就是锁定这个Lock实例的线程,那么这个while循环不会执行,并且调用lock方法的这个线程允许去退出这个方法。

另外,我们需要计算别相同线程锁住的次数。另外,一个对于unlock方法单独的调用去释放这个锁,甚至如果这个锁被锁定多次了。我们不想这个锁被释放直到锁定它的这个线程已经执行unlock方法的次数跟lock方法调用的次数一样。

这个Lock类是可重入的。

公平锁

Java的同步块不能保证线程的执行顺序。因此,如果许多线程在不断的竞争相同的同步块的访问权限,这里有一个风险,一个或者多个线程永远不会获取到访问权限。这个也称之为饥饿。为了避免这个,锁应该是公平的。因为在这个文本中显示的Lock实现内部使用同步块,他们不会保证公平。饥饿和公平在“饥饿和公平”文章中有更详细的讨论。

在Finally子句中调用unlock方法

当使用Lock守卫一个关键部分的时候,并且这个关键部分可能会抛出异常,在finally子句内部调用unlock方法是重要的。这样做可以确保Lock可以被解锁的以至于其他的线程可以锁定它。这里有一个例子:

lock.lock();
try{
  //do critical section code, which may throw exception
} finally {
  lock.unlock();
}

这个小的构造可以确保一旦一个异常抛出,这个Lock就可以被解锁了。如果unlock方法没有在finally子句中被调用,并且一个异常被抛出,这个Lock将会永远会保持锁定的,导致调用lock方法的所有线程将会永远的停在那里了。


翻译地址:http://tutorials.jenkov.com/java-concurrency/locks.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值