锁是一种线程同步机制,与同步代码块不同,锁显得更加精致。当然锁底层还是离不开synchronized
关键字。
从Java5开始,在包java.util.concurrent.locks
就提供了几种锁的实现。
来看一个简单的锁
先看用同步代码块是怎么实现的:
public class Counter {
private int count = 0;
public int inc () {
synchronized (this) {
return ++count;
}
}
}
复制代码
注意到synchronized (this)
在inc()
方法内,确保了同一时间只有一个线程可以执行return ++count
。
如果使用锁的话,就是以下写法:
public class Counter {
private Lock lock = new Lock();
private int count = 0;
public int inc () {
lock.lock();
int newCount = ++count;
lock.unlock();
return newCount;
}
}
复制代码
锁这个类是怎么实现的呢?一个简单的写法是:
public class Lock {
private boolean isLocked = false;
public synchronized void lock() thrwos InterruptedException {
while (isLocked) {
wait();
}
isLocked = true;
}
public synchronized void unlock() {
isLocked = false;
notify();
}
}
复制代码
当加锁时会进行循环等待isLocked
变为true
,这一过程也被称为自旋,因此这种锁也被称为自旋锁。
当isLocked
为true
时,调用lock()
的线程将停在wait()
调用中。万一线程在此时没有收到notify()
就挂了,线程会重新检查isLocked
条件以查看继续执行是否安全,而不是说被唤醒了就意味线程是安全的。如果isLocked
为false
,则线程退出while (isLocked)
循环,并将isLocked
设置为true
,其他调用lock()
的线程将锁定Lock
实例。
可重入锁
Java中的同步块是可重入的。这意味着,如果Java线程进入了同步的代码块,从而锁定了同步块的监视器,则该线程可以进入在同一监视对象上其他Java同步代码块。 这是一个例子:
public class Reentrant {
public synchronized outer () {
inner();
}
public synchronzied inner () {
// do something
}
}
复制代码
如果线程调用了outer()
,那么也会调用inner()
,因为两个方法由一个监视器同步。如果线程已经拥有监视器上的锁,则它可以访问在同一监视器上的所有同步块。这称为重入,线程可以重新进入已经为其持有锁的任何代码块。
但是,同步块可重入不代表类可重入。改写一下类,调用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();
}
}
复制代码
想让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();
}
}
}
...
}
复制代码
这里自旋时考虑到了锁实例,如果锁被释放或者调用线程为锁住类实例的线程,就不会自旋等待。
此外,我们需要计算锁被同一线程锁定的次数。 否则,即使已多次锁定该锁,一次对unlock()
的调用也将解除该锁的锁定。 我们不希望在锁定该锁的线程执行与lock()
调用相同数量的unlock()
调用之前将其解锁。
锁的公平性
Java的同步块无法保证进入的线程顺序。因此,如果多线程一直在争夺同一同步块的访问权限,则存在一个或多个线程永远不会访问权的风险,这称为饥饿。为了避免这种情况,锁应该是公平的。
锁释放
当锁住的代码块引发异常,就必须从finally
子句内部调用unlock()
。这样做可以确保锁定已解锁,以便其他线程可以锁定它。这是一个例子:
lock.lock();
try{
//do critical section code, which may throw exception
} finally {
lock.unlock();
}
复制代码
这个小结构可以确保在关键部分的代码中引发异常时,可以解除锁定。如果未从finally子句内部调用unlock()
,并且从关键部分引发了异常,则锁将永远保持锁定状态,从而导致在该实例上调用lock()
的所有线程无限期地停止。