锁像synchronized同步块一样,是一种线程同步机制,但比java中的synchronized同步块更复杂。因为锁(以及其它更高级的线程同步机制)是由synchronized同步块的方式实现的,所以我们还不能完全摆脱synchronized关键字(译者注:这说的是java 5之前的情况)。
以下是本文所涵盖的主题:
<a href="http://ifeve.com/locks/#simplelock">一个简单的锁</a>
<a href="http://ifeve.com/locks/#lockreentrance">锁的可重入性</a>
<a href="http://ifeve.com/locks/#lockfairness">锁的公平性</a>
<a href="http://ifeve.com/locks/#finallyunlock">在finally语句中调用unlock()</a>
让我们从java中的一个同步块开始:
可以看到在inc()方法中有一个synchronized(this)代码块。该代码块可以保证在同一时间只有一个线程可以执行return
++count。虽然在synchronized的同步块中的代码可以更加复杂,但是++count这种简单的操作已经足以表达出线程同步的意思。
以下的counter类用lock代替synchronized达到了同样的目的:
lock()方法会对lock实例对象进行加锁,因此所有对该对象调用lock()方法的线程都会被阻塞,直到该lock对象的unlock()方法被调用。
这里有一个lock类的简单实现:
<code></code>
这个线程会重新去检查islocked条件以决定当前是否可以安全地继续执行还是需要重新保持等待,而不是认为线程被唤醒了就可以安全地继续执行了。如果
islocked为false,当前线程会退出while(islocked)循环,并将islocked设回true,让其它正在调用lock()方法
的线程能够在lock实例上加锁。
java中的synchronized同步块是可重入的。这意味着如果一个java线程进入了代码中的synchronized同步块,并因此获得了该同
步块使用的同步对象对应的管程上的锁,那么这个线程可以进入由同一个管程对象所同步的另一个java代码块。下面是一个例子:
注意outer()和inner()都被声明为synchronized,这在java中和synchronized(this)块等效。如果一个
线程调用了outer(),在outer()里调用inner()就没有什么问题,因为这两个方法(代码块)都由同一个管程对象(”this”)所同步。
如果一个线程已经拥有了一个管程对象上的锁,那么它就有权访问被这个管程对象同步的所有代码块。这就是可重入。线程可以进入任何一个它已经拥有的锁所同步
着的代码块。
前面给出的锁实现不是可重入的。如果我们像下面这样重写reentrant类,当线程调用outer()时,会在inner()方法的lock.lock()处阻塞住。
调用outer()的线程首先会锁住lock实例,然后继续调用inner()。inner()方法中该线程将再一次尝
试锁住lock实例,结果该动作会失败(也就是说该线程会被阻塞),因为这个lock实例已经在outer()方法中被锁住了。
两次lock()之间没有调用unlock(),第二次调用lock就会阻塞,看过lock()实现后,会发现原因很明显:
一个线程是否被允许退出lock()方法是由while循环(自旋锁)中的条件决定的。当前的判断条件是只有当islocked为false时lock操作才被允许,而没有考虑是哪个线程锁住了它。
为了让这个lock类具有可重入性,我们需要对它做一点小的改动:
这个简单的结构可以保证当临界区抛出异常时lock对象可以被解锁。如果不是在finally语句中调用的unlock(),当临界区抛出异常时,lock对象将永远停留在被锁住的状态,这会导致其它所有在该lock对象上调用lock()的线程一直阻塞。