之前总结了部分无锁机制的多线程基础,理想的状态当然是利用无锁同步解决多线程程序设计的问题。但是实际碰到的问题使得很多情况下,我们不得不借助锁同步来保证线程安全。自从JDK5开始,有两种机制来屏蔽代码块在并行访问的干扰,synchronized关键字已经介绍过了部分内容,所以这次简单的说说另一种锁机制:ReentrantLock。
对于synchronized的缺点之前也简单的说了一些,实际使用中比较烦扰的几点是:a.只有一个"条件"与锁相关联,这对于大量并发线程的情况是很难管理(等待和唤醒);b.多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断。这种情况对于大量的竞争线程会造成性能的下降等后果。JDK5以后提供了ReentrantLock的同步机制对于前面提的两种情况有相对的改善。下面我还是写个小例子分析一下:
在这个例子中,辅助类SampleSupport提供一个倒计时的功能startTheCountdown(),这里倒计时5s左右。SampleSupport1,SampleSupport2继承其并分别的具有doSomething()方法,任何进入方法的线程会运行5s左右之后counter++然后离开方法释放锁。SampleSupport1是使用ReentrantLock机制,SampleSupport2是使用synchronized机制。
testSynchronized()和testReentrantLock()都分别开启两个线程执行测试方法executeTest(),这个方法会让一个线程先启动,另一个过100ms左右启动,并且隔1s左右试图中断后者。结果正如之前提到的第二点:interrupt()对于synchronized是没有作用的,它依然会等待5s左右获得锁执行counter++;而ReentrantLock机制可以保证在线程还未获得并且试图获得锁时如果发现线程中断,则抛出异常清除中断标记退出竞争。所以testReentrantLock()中second线程不会继续去竞争锁,执行异常内的打印语句后线程运行结束。
这里我是用了ReentrantLock的lockInterruptibly()方法,在SampleSupport1的代码(1)处。这个方法保证了中断线程的响应,如果仅仅是lock()则不会有此功能。但是不管怎么说ReentrantLock提供了解决方案。至于提到的第一点“多条件”的机制我通过java.util.concurrent.ArrayBlockingQueue(源码参考1.6.0.17内的实现)简单的介绍一下:
这里notEmpty和notFull作为lock的两个条件是可以分别负责管理想要加入元素的线程和想要取出元素的线程的wait和notify分别通过await()和signal(),signalAll()方法,有效的分离了不同职责的线程。例如put()方法在元素个数达到最大限制时会使用notFull条件把试图继续插入元素的线程都扔到等待集中,而执行了take()方法时如果顺利进入extract()则会空出空间,这时notFull负责随机的通知被其扔到等待集中的线程执行插入元素的操作。这样的设计使得线程按照功能行为职责管理成为了现实。
通过上述的总结,对于ReentrantLock的优点有了一定的认识,但是它也是实现了与synchronized相同语义和行为的可重用完全互斥锁,所以在竞争机制上不会有什么性能提高,功能倒是强大了不少。不过使用它要配合try{...}finally{...}显式的释放锁,这点是决定如果业务实现没有需要使用其特有的功能,更好的方式是使用synchronized。后者毕竟不用自己去释放锁,降低了开发的失误率。当然在java.util.concurrent.locks包内还一个很有意思的锁:ReentrantReadWriteLock,其提供了部分互斥的锁实现,以后的总结会有介绍。
对于synchronized的缺点之前也简单的说了一些,实际使用中比较烦扰的几点是:a.只有一个"条件"与锁相关联,这对于大量并发线程的情况是很难管理(等待和唤醒);b.多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断。这种情况对于大量的竞争线程会造成性能的下降等后果。JDK5以后提供了ReentrantLock的同步机制对于前面提的两种情况有相对的改善。下面我还是写个小例子分析一下:
import java.util.concurrent.locks.ReentrantLock;
/**
* @author: yanxuxin
* @date: 2010-1-4
*/
public class ReentrantLockSample {
public static void main(String[] args) {
testSynchronized();
testReentrantLock();
}
public static void testReentrantLock() {
final SampleSupport1 support = new SampleSupport1();
Thread first = new Thread(new Runnable() {
public void run() {
try {
support.doSomething();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread second = new Thread(new Runnable() {
public void run() {
try {
support.doSomething();
}
catch (InterruptedException e) {
System.out.println("Second Thread Interrupted without executing counter++,beacuse it waits a long time.");
}
}
});
executeTest(first, second);
}
public static void testSynchronized() {
final SampleSupport2 support2 = new SampleSupport2();
Runnable runnable = new Runnable() {
public void run() {
support2.doSomething();
}
};
Thread third = new Thread(runnable);
Thread fourth = new Thread(runnable);
executeTest(third, fourth);
}
/**
* Make thread a run faster than thread b,
* then thread b will be interruted after about 1s.
* @param a
* @param b
*/
public static void executeTest(Thread a, Thread b) {
a.start();
try {
Thread.sleep(100);
b.start(); // The main thread sleep 100ms, and then start the second thread.
Thread.sleep(1000);
// 1s later, the main thread decided not to allow the second thread wait any longer.
b.interrupt();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
abstract class SampleSupport {
protected int counter;
/**
* A simple countdown,it will stop after about 5s.
*/
public void startTheCountdown() {
long currentTime = System.currentTimeMillis();
for (;;) {
long diff = System.currentTimeMillis() - currentTime;
if (diff > 5000) {
break;
}
}
}
}
class SampleSupport1 extends SampleSupport {
private final ReentrantLock lock = new ReentrantLock();
public void doSomething() throws InterruptedException {
lock.lockInterruptibly(); // (1)
System.out.println(Thread.currentThread().getName() + " will execute counter++.");
startTheCountdown();
try {
counter++;
}
finally {
lock.unlock();
}
}
}
class SampleSupport2 extends SampleSupport {
public synchronized void doSomething() {
System.out.println(Thread.currentThread().getName() + " will execute counter++.");
startTheCountdown();
counter++;
}
}
在这个例子中,辅助类SampleSupport提供一个倒计时的功能startTheCountdown(),这里倒计时5s左右。SampleSupport1,SampleSupport2继承其并分别的具有doSomething()方法,任何进入方法的线程会运行5s左右之后counter++然后离开方法释放锁。SampleSupport1是使用ReentrantLock机制,SampleSupport2是使用synchronized机制。
testSynchronized()和testReentrantLock()都分别开启两个线程执行测试方法executeTest(),这个方法会让一个线程先启动,另一个过100ms左右启动,并且隔1s左右试图中断后者。结果正如之前提到的第二点:interrupt()对于synchronized是没有作用的,它依然会等待5s左右获得锁执行counter++;而ReentrantLock机制可以保证在线程还未获得并且试图获得锁时如果发现线程中断,则抛出异常清除中断标记退出竞争。所以testReentrantLock()中second线程不会继续去竞争锁,执行异常内的打印语句后线程运行结束。
这里我是用了ReentrantLock的lockInterruptibly()方法,在SampleSupport1的代码(1)处。这个方法保证了中断线程的响应,如果仅仅是lock()则不会有此功能。但是不管怎么说ReentrantLock提供了解决方案。至于提到的第一点“多条件”的机制我通过java.util.concurrent.ArrayBlockingQueue(源码参考1.6.0.17内的实现)简单的介绍一下:
public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
...
/** Main lock guarding all access */
private final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;
...
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = (E[]) new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
final E[] items = this.items;
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
try {
while (count == items.length)
notFull.await();
} catch (InterruptedException ie) {
notFull.signal(); // propagate to non-interrupted thread
throw ie;
}
insert(e);
} finally {
lock.unlock();
}
}
private void insert(E x) {
items[putIndex] = x;
putIndex = inc(putIndex);
++count;
notEmpty.signal();
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
try {
while (count == 0)
notEmpty.await();
} catch (InterruptedException ie) {
notEmpty.signal(); // propagate to non-interrupted thread
throw ie;
}
E x = extract();
return x;
} finally {
lock.unlock();
}
}
private E extract() {
final E[] items = this.items;
E x = items[takeIndex];
items[takeIndex] = null;
takeIndex = inc(takeIndex);
--count;
notFull.signal();
return x;
}
...
}
这里notEmpty和notFull作为lock的两个条件是可以分别负责管理想要加入元素的线程和想要取出元素的线程的wait和notify分别通过await()和signal(),signalAll()方法,有效的分离了不同职责的线程。例如put()方法在元素个数达到最大限制时会使用notFull条件把试图继续插入元素的线程都扔到等待集中,而执行了take()方法时如果顺利进入extract()则会空出空间,这时notFull负责随机的通知被其扔到等待集中的线程执行插入元素的操作。这样的设计使得线程按照功能行为职责管理成为了现实。
通过上述的总结,对于ReentrantLock的优点有了一定的认识,但是它也是实现了与synchronized相同语义和行为的可重用完全互斥锁,所以在竞争机制上不会有什么性能提高,功能倒是强大了不少。不过使用它要配合try{...}finally{...}显式的释放锁,这点是决定如果业务实现没有需要使用其特有的功能,更好的方式是使用synchronized。后者毕竟不用自己去释放锁,降低了开发的失误率。当然在java.util.concurrent.locks包内还一个很有意思的锁:ReentrantReadWriteLock,其提供了部分互斥的锁实现,以后的总结会有介绍。