Java锁

时间:2020-01-09 10:35:51  来源:igfitidea点击:

锁是一种类似于同步块的线程同步机制,除了锁比Java的同步块更复杂之外。锁(以及其他更高级的同步机制)是使用同步块创建的,因此我们无法完全摆脱synchronized关键字。

从Java 5开始,软件包java.util.concurrent.locks包含多个锁实现,因此我们可能不必实现自己的锁。但是我们仍然需要知道如何使用它们,并且了解实现它们背后的理论仍然很有用。

一个简单的锁

让我们开始看一下Java代码的同步块:

public class Counter{

  private int count = 0;

  public int inc(){
    synchronized(this){
      return ++count;
    }
  }
}

注意inc()方法中的synchronized(this)块。这个块确保了只有一个线程一次可以执行return ++ count。同步块中的代码本来可以更高级,但是简单的" ++ count"足以说明要点。

可以使用Lock而不是同步块来编写Counter类,如下所示:

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"为真时,调用" lock()"的线程将停在" wait()"调用中。如果线程应该在没有接收到" notify()"调用(又称为"虚假唤醒")的情况下从wait()调用中意外返回,则线程会重新检查" isLocked"条件以查看是否安全进行,而不是仅仅假设被唤醒就意味着可以安全进行。如果isLocked为假,则线程退出while(isLocked)循环,并将isLocked设置回true,以锁定其他调用Lock()的线程的Lock实例。

当线程使用关键部分中的代码(" lock()"和" unlock()"之间的代码)完成时,线程将调用" unlock()"。执行unlock()会将isLocked设置为false,并通知(唤醒)在lock()方法中的wait()调用中等待的线程之一。

重锁

Java中的同步块是可重入的。这意味着,如果Java线程输入了同步的代码块,从而锁定了同步该块的监视对象,则该线程可以输入在同一监视对象上同步的其他Java代码块。这是一个例子:

public class Reentrant{

  public synchronized outer(){
    inner();
  }

  public synchronized inner(){
    //do something
  }
}

注意,如何将" outer()"和" inner()"都声明为已同步,这在Java中等效于" synchronized(this)"块。如果线程调用outer(),则从outer()内部调用inner()是没有问题的,因为两个方法(或者块)都在同一个监视对象(" this")上同步。如果线程已经拥有监视对象上的锁,则它可以访问在同一监视对象上同步的所有块。这称为重入。线程可以重新输入已经为其持有锁的任何代码块。

前面显示的锁实现不是可重入的。如果我们像下面这样重写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()而没有在其间调用unlock()时被阻塞的原因很明显:

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()将会解锁该锁,即使该锁已被多次锁定。我们不希望在锁定它的线程执行与lock()调用相同数量的unlock()调用之前将其解锁。

锁类现在是可重入的。

公平锁

Java的同步块无法保证尝试进入线程的线程被授予访问权限的顺序。因此,如果许多线程一直在争夺对同一同步块的访问权,则存在一个风险,即一个或者多个线程永远不会被授予访问权,而该访问权总是授予其他线程。这称为饥饿。为了避免这种情况,Lock应该是公平的。由于本文中显示的Lock实现在内部使用同步块,因此它们不能保证公平。饥饿和公平在文本饥饿和公平中有更详细的讨论。

从最后一个子句中调用unlock()

当用"锁"保护关键部分时,关键部分可能会引发异常,从"最终"子句内部调用" unlock()"方法很重要。这样做可以确保"锁定"已解锁,以便其他线程可以锁定它。这是一个例子:

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

这个小结构可以确保在关键部分的代码中抛出异常时,可以解锁" Lock"。如果未从"最终"子句中调用" unlock()",并且从关键部分引发了异常,则" Lock"将永远保持锁定状态,从而导致所有线程在该" Lock"上调用" lock()"。实例无限期停止。