并发学习笔记 (5)

tutorials sitehtml

Locks in java

Locks (and other more advanced synchronization mechanisms) are created using synchronized blocks, so it is not like we can get totally rid of the synchronized keyword.
锁的实现是利用synchonized, wait(),notify()方法实现的。因此不能够认为锁能够彻底脱离synchonized实现。java

Java包 JUC java.util.concurrent.locks 包括了不少lock接口的实现了类,这些类足够使用。
可是须要知道如何使用它们,以及这些类背后的理论。JUC包教程安全

用synchonized:能够保证在同一时间只有一个线程能够执行 return ++count:多线程

public class Counter{
  private int count = 0;

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

如下的Counter类用Lock代替synchronized 达到一样的目的:
lock() 方法会对 Lock 实例对象进行加锁,所以全部其余对该对象调用 lock() 方法的线程都会被阻塞,直到该 Lock 对象的 unlock() 方法被调用。函数

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类是怎么设计的?this


Lock 类的设计

一个Lock类的简单实现:线程

javapublic 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) 循环, 又被称为spin lock自旋锁。当 isLockedtrue 时,调用 lock() 的线程在 wait() 调用上阻塞等待。为防止该线程没有收到 notify() 调用也从 wait() 中返回(也称做虚假唤醒),这个线程会从新去检查 isLocked 条件以决定当前是否能够安全地继续执行仍是须要从新保持等待,而不是认为线程被唤醒了就能够安全地继续执行了。若是 isLocked 为 false,当前线程会退出 while(isLocked) 循环,并将 isLocked 设回 true,让其它正在调用 lock() 方法的线程可以在 Lock 实例上加锁。设计

当线程完成了临界区(位于 lock() 和 unlock() 之间)中的代码,就会调用 unlock()。执行 unlock() 会从新将 isLocked 设置为 false,而且通知(唤醒)其中一个(如有的话)在 lock() 方法中调用了 wait() 函数而处于等待状态的线程。code

锁的可重入性

synchronized 同步块是可重入的。这意味着: 若是一个java线程进入了代码中的同步块synchonzied block,并所以得到了该同步块使用的同步对象对应的管程monitor object上的锁那么这个线程能够进入由同一个管程对象所同步的另外一个 java 代码块htm

前面的Lock的设计就不是可重入的:

javapublic class Reentrant2{
    Lock lock = new Lock();

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

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

一个线程是否被容许退出 lock() 方法是由 while 循环(自旋锁)中的条件决定的。当前的判断条件是只有当 isLocked 为 false 时 lock 操做才被容许,而没有考虑是哪一个线程锁住了它。
因此须要对Lock的设计作出以下修改,才能可重入。

javapublic 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() 的线程就能够退出该方法(译者注:“被容许退出该方法” 在当前语义下就是指不会调用 wait() 而致使阻塞)。

除此以外,咱们须要记录同一个线程重复对一个锁对象加锁的次数。不然,一次 unblock() 调用就会解除整个锁,即便当前锁已经被加锁过屡次。在 unlock() 调用没有达到对应 lock() 调用的次数以前,咱们不但愿锁被解除。

如今这个 Lock 类就是可重入的了。

锁的公平性

Starvation and Fairness 饥饿和公平

一个线程由于其余线程长期占有CPU而本身得到不到,这种状态称为Starvation. 解决线程饥饿的方法是公平机制fairness公平机制,让全部线程都能公平的有机会去得到CPU。

致使饥饿的缘由

  1. 高优先级的线程占有了全部CPU处理时间,这样低优先级的线程得到不到;
  2. 处于阻塞状态的线程无限期被阻塞
    Java 的同步代码区也是一个致使饥饿的因素。Java 的同步代码区对哪一个线程容许进入的次序没有任何保障。这就意味着理论上存在一个试图进入该同步区的线程处于被永久堵塞的风险,由于其余线程老是能持续地先于它得到访问,这便是 “饥饿” 问题,而一个线程被 “饥饿致死” 正是由于它得不到 CPU 运行时间的机会

Java's synchronized code blocks can be another cause of starvation.

  1. 处于等待状态的对象无限期等待
    若是多个线程处在 wait() 方法执行上,而对其调用 notify() 不会保证哪个线程会得到唤醒,任何线程都有可能处于继续等待的状态。所以存在这样一个风险:一个等待线程历来得不到唤醒,由于其余等待线程老是能被得到唤醒。

这里细说一下:多线程经过共享一个object对象,来调用对象的wait/notifyAll 来致使线程等待或者唤醒; 每次一个线程进入同步块,其余全部线程陷入等待状态;而后active线程调用notifyALL()函数唤醒全部等待线程,全部线程竞争,只有一个线程竞争成功,得到CPU执行。竞争失败的线程处于就绪状态,长期竞争失败的线程就会饥饿。

线程之间的对资源(object)竞争致使的饥饿,为了不竞争,因此想办法一次唤醒一个线程。也就是下面讲的FairLock 公平锁机制。

Implementing Fairness in Java

使用锁lock来代替同步块synchonized block

每个调用 lock() 的线程都会进入一个队列,当解锁后,只有队列里的第一个线程 (队首)被容许锁住 Fairlock 实例,全部其它的线程都将处于等待状态,直到他们处于队列头部。
公平锁实现机制:为每个线程建立一个专属锁对象(而非多个线程共享一个对象,来wait/notify()),而后用一个队列来管理这些锁对象,尝试加锁的线程会在各自的对象上等待,当一个线程unlock的时候,只通知队列头的锁对象,以唤醒其对应的线程

为了让这个 Lock 类具备可重入性,咱们须要对它作一点小的改动:

javapublic class FairLock {
    private boolean           isLocked       = false;
    private Thread            lockingThread  = null;
    private List<QueueObject> waitingThreads =
            new ArrayList<QueueObject>();

  public void lock() throws InterruptedException{
    QueueObject queueObject           = new QueueObject();
    boolean     isLockedForThisThread = true;
    synchronized(this){
        waitingThreads.add(queueObject);
    }

    while(isLockedForThisThread){
      synchronized(this){
        isLockedForThisThread =
            isLocked || waitingThreads.get(0) != queueObject;
        if(!isLockedForThisThread){
          isLocked = true;
           waitingThreads.remove(queueObject);
           lockingThread = Thread.currentThread();
           return;
         }
      }
      try{
        queueObject.doWait();
      }catch(InterruptedException e){
        synchronized(this) { waitingThreads.remove(queueObject); }
        throw e;
      }
    }
  }

  public synchronized void unlock(){
    if(this.lockingThread != Thread.currentThread()){
      throw new IllegalMonitorStateException(
        "Calling thread has not locked this lock");
    }
    isLocked      = false;
    lockingThread = null;
    if(waitingThreads.size() > 0){
      waitingThreads.get(0).doNotify();
    }
  }
}
相关文章
相关标签/搜索