Lock跟Java中的synchronized
关键字同样,都是用于线程的同步机制。不一样的是Lock相比synchronized
关键字提供更加丰富的功能和灵活性。html
从Java5开始,java.util.concurrent.locks
包中提供了几种不一样的Lock实现,所以咱们不须要本身去实现锁。但咱们仍然须要知道怎么使用它们,以及它们的底层原理。java
咱们来看一下这样一个同步块:安全
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实现并不能保证线程的公平性。饥饿和公平一文中详细讨论了这种状况。
当使用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