Basic Of Concurrency(十四: Java中的锁)

Lock跟Java中的synchronized关键字同样,都是用于线程的同步机制。不一样的是Lock相比synchronized关键字提供更加丰富的功能和灵活性。html

从Java5开始,java.util.concurrent.locks包中提供了几种不一样的Lock实现,所以咱们不须要本身去实现锁。但咱们仍然须要知道怎么使用它们,以及它们的底层原理。java

一个简单的Lock

咱们来看一下这样一个同步块:安全

public class Counter {
    private int count = 0;

    public int increment() {
        synchronized (this) {
            return ++count;
        }
    }
}
复制代码

咱们能够注意到increment()方法中的同步块。这个同步块确保了每次只会有一个线程执行++count;同步块的中的代码相对比较简单,如今咱们只须要关注++count();这一行代码便可。post

上面的例子用Lock来替换同步块:this

public class Counter {
    private Lock lock = new Lock();
    private int count = 0;

    public int increment() {
        lock.lock();
        return ++count;
        lock.unlock();
    }
}
复制代码

increment()方法中,当有线程取得Lock实例后,其余线程都会在调用lock()后被阻塞,直到取得Lock实例的线程调用了unlock()为止。spa

下面是一个简单的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时,线程会进入循环内部,从而调用wait()方法进入等待状态。实例中,在其余线程没有调用notify()发送信号唤醒的状况下,线程如果意外唤醒会从新检查isLocked条件是否为false以此来肯定线程是否能够安全的执行,若仍然为true则线程从新调用wait()方法进入等待状态。所以旋转锁能够保护意外唤醒带来的潜在风险。若线程检查isLocked为false则退出while循环,将isLocked置换为true,以此来标记得到当前Lock实例。让其余线程阻塞在lock()调用上。code

当线程执行完临界区代码(位于lock()和unlock()调用之间的代码)后,会调用unlock()方法释放Lock()实例。调用unlock()方法会将isLocked置换为false.并唤醒调用了lock()方法中的wait()方法进入等待状态的线程,若是有的话。htm

可重入锁

Java中的synchronized同步块是可重入的。这意味着,若是一个Java线程进入一个同步代码块取得当前对象锁,那么它能够再进入其余所用与之相同对象锁的其余同步代码块。以下所示:对象

public class Reentrant{
    public synchronized void outer() {
        inner();
    }

    public synchronized void inner() {
        // do something you like
    }
}
复制代码

咱们能够注意到outer()和inner()方法签名中都有synchronized声明,这在Java中等同于synchronized(this)同步代码块。当多个方法都是以"this"即当前对象做为监控对象时,那么一个线程在调用完outer()方法后,天然能够在outer()方法内部调用inner()方法。一个线程将一个对象做为监控对象即取得该对象锁后,它能够进入其余使用相同对象做为对象锁的同步代码块。这种状况咱们称之为可重入。线程能够反复进入它所取得监控对象的所有同步代码块。

上文中给出的Lock实现并非可重入的。若是咱们将上文的Reentrant.class改写成以下代码所示的Reentrant2.class,那么线程在调用完outer()方法后,将在调用inner()方法中的lock.lock()方法后阻塞。

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

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

    public synchronized void inner() {
        lock.lock();
        //do something
        lock.unlock();
    }
}
复制代码

线程在调用outer()时会先锁住Lock实例。而后再调用inner()方法。在inner()方法内部,线程会再一次尝试锁住Lock实例,但注定会失败。由于lock实例在调用outer()方法时已经被锁住。

在尚未调用unlock()方法释放Lock()实例的时候,再次调用lock()的时候会让线程阻塞在调用lock()方法上。以下lock()方法:

public class Lock{
    boolean isLocked = false;
  
    public synchronized void lock() throws InterruptedException {
        while (isLocked) {
            wait();
        }
        isLocked = true;
    }
}
复制代码

旋转锁中的条件,决定了线程允不容许退出lock()方法。当isLocked为false时,意味着线程容许退出lock()方法,即容许锁住Lock实例。

让Lock.class支持可重入,咱们须要做出一点改动:

public class Lock {
    private boolean isLocked = false;
    private Thread lockingThread;
    int lockedCount = 0;

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

    public void unlock() {
        Thread callingThread = Thread.currentThread();
        if (lockingThread != null && lockingThread == callingThread) {
            lockedCount--;
            if (lockedCount == 0) {
                isLocked = false;
                notify();
            }
        }
    }
}
复制代码

咱们能够注意到while循环(旋转锁)中多了一个条件,即当前线程是否为持有锁实例的线程。当只有两个条件都符合时线程才能退出循环和退出lock()方法调用。

咱们须要对线程锁住Lock实例的次数进行记录。不然的话,调用一次unlock()方法就会让当前线程释放掉锁尽管实际上它调用了屡次lock()。所以咱们须要保障lock()和unlock()方法的调用次数是一致的,即每调用一次lock()即必须调用一次unlock()。

如今的Lock.class已是可重入的了。

公平锁

Java的synchronized同步代码块不能保证线程按照必定的顺序进入同步代码块。所以存在必定的风险,有一或者几个线程一直没法进入同步代码块,由于其余线程一直代替它们进入到同步代码块。咱们称这种状况为肌饿现象。

为了解决这个问题,咱们实现的锁须要保证必定的公平性,但上文给出的Lock实现并不能保证线程的公平性。饥饿和公平一文中详细讨论了这种状况。

在finally语句中调用unlock()

当使用Lock来保证临界区代码的同步时,临界区中的代码可能会抛出异常。因此颇有必要在finally语句中来调用unlock()方法。不管线程执行的代码异常与否,始终可以释放它所持有的锁,以让其余线程能正常执行。以下所示:

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

咱们使用这个小结构来保证即便临界区代码抛出异常,线程也能正常释放掉所持有的锁。若是咱们没有这么作,一旦临界区代码出现异常,线程将没法释放锁,以致于其余调用了该锁的lock()方法的线程将会永远等待下去。

该系列博文为笔者复习基础所著译文或理解后的产物,复习原文来自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial

上一篇: Slipped Conditions
下一篇: Java中的读写锁

相关文章
相关标签/搜索