前言
直接写第7篇的原因是锁是一个十分重要的概念,而管程嵌套锁死却并没有经常地出现在我所刷的面试题中。在前面的几篇中,我们也涉及到了简单的锁、公平锁的示例。这一篇中我们会更细致地来介绍锁。
和synchronized
同步块一样,锁也是一种线程同步机制,但更为复杂,锁是基于synchronized
同步块实现的,从Java5开始,java.util.concurrent.locks
中实现了几种锁,因此,我们并没有必要自行实现锁,但是为了更好地掌握,还是有必要探究一下它们的原理。
1. 一个简单的锁
我们先看一个简单的同步代码块:
public class Counter{
private int count=0;
public int incr(){
synchronized(this){
return ++count;
}
}
}
这里的synchronized(this)
可以保证在同一时间只有一个线程可以执行同一Counter
实例的return ++count;
,我们使用锁可以达到上述相同的效果:
public class Counter{
private Lock lock=new Lock();
private int count=0;
public int incr(){
lock.lock();
int newCount=++count;
lock.unlock();
return newCount;
}
}
这里lock
对象会对Lock
实例进行加锁,所有调用该实例lock()
方法的实例对象都将被阻塞,直到该对象执行unlock()
方法。下面我们看它的简单实现:
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)
循环,它被称为自旋锁,线程在被唤醒之后需要检查isLocked
状态决定是运行还是保持继续等待,只有isLocked
是false
,它才能够退出循环,并再次将isLocked
置为true
,以便其他线程能够调用lock()
方法在Lock
实例上加锁。
而在临界区代码执行完成后则会调用unlock()
方法,将isLocked
置为false
,并通过notify()
唤醒一个因为调用了lock()
而处在wait()
状态的线程。
2. 锁的可重入性
Java中的synchronized代码块是可重入的,这意味着一个线程如果进入了 synchronized 同步块,并且因此获得了该同步块对应的同步对象对应的管程上的锁,那么这个线程可以进入由同一个管程对象,例如:
public class Reentrant{
public synchronized outer(){
inner();
}
public synchronized inner(){
//do something
}
}
这个类中的outer()
和inner()
都被声明为synchronized
,等价于在方法内部使用synchronized(this)
,如果一个线程调用了outer()
,那么调用内部的inner()
方法也是没有问题的,因为这两个方法由同一个管程对象this
同步。如果一个线程已经拥有了一个管程对象上的锁,那么它就有权访问被这个管程对象同步的所有代码块,这就是可重入。线程可以进入任何一个它已经拥有的锁所同步着的代码块。
我们在上面实现的简单的锁是不具备可重入性的,如果,我们使用上述的Lock来实现Reentrant类,如下:
public class Reentrant{
Lock lock=new Lock();
public outer(){
lock.lock();
inner();
lock.unlock();
}
public synchronized inner(){
lock.lock();
//do something
lock.unlock();
}
}
当线程调用outer()
时,就会阻塞在inner()
中的lock.lock()上,因为此时lock
实例已经在outer()
中被锁住了,原因是显然的,在lock()
方法中,一个线程能否退出lock()
方法是由while(isLocked)
决定的,更准确地说由isLocked
决定,而我们在两次调用lock()
的间隔中没有调用过unlock()
方法。
我们可以对它进行一些小改实现可重入性:
public class Lock{
private 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.currentThread==lockedBy){
lockedCount--;
if(lockedCount==0){
isLocked=false;
notify();
}
}
}
}
注意到现在的自旋锁也考虑到了Lock
实例已被锁住的情景:如果当前的锁对象没有被加锁(isLocked=false),或者当前线程已经对Lock
实例加了锁,那么while循环就不会执行。这样调用lock()
的线程就可以退出该方法,而不会调用wait()
导致阻塞。
除此之外,我们还需要记录一个同一线程对同一锁对象加锁的次数,否则一次unlock()
就会导致整个锁被释放,即使当前锁已经被加过很多次。在 unlock()调用没有达到对应 lock()调用的次数之前,我们不希望锁被解除。
这样,我们就实现了锁的可重入性。
3. 锁的公平性
Java的synchronized
并不保证线程进入的顺序。因此,如果多个线程同时访问一个synchronized
块,这就存在一种风险,某一个或者某几个线程可能永远得不到访问权。为了避免这种问题,我们需要实现锁的公平性,我们上述的锁是通过在内部使用synchronized
来实现,因此也难以保证公平性。
4. 在finally
块中调用unlock()
如果用Lock
来保护临界区,并且临界区中可能会抛出异常,那么就需要用try...catch
来处理异常,这时,在finally
中调用unlock()
,这样可以保证这个锁对象可以被解锁以便其它线程可以继续对其加锁,否则,当临界区抛出异常时,Lock
实例就会永远处于锁住的状态,这会导致其它所有在该Lock
实例lock()
方法的线程阻塞。