上一节讲到Java线程和synchronized关键字的使用。下面就开始介绍JDK中的一些好用的并发控制工具。
先来看ReetrantLock类,他可用来替换synchronized关键字,而且比synchronized关键字更为强大和灵活。
一、ReetrantLock简单示例
先看代码:
1public class ReeterLock implements Runnable { 2 static ReentrantLock lock = new ReentrantLock(); 3 static int i = 0; 4 5 @Override 6 public void run() { 7 for (int k = 0; k < 1000000; k++) { 8 lock.lock(); 9 try { 10 i++; 11 } finally { 12 lock.unlock(); 13 } 14 } 15 } 16 17 public static void main(String[] args) throws InterruptedException { 18 ReeterLock reeterLock = new ReeterLock(); 19 Thread t1 = new Thread(reeterLock); 20 Thread t2 = new Thread(reeterLock); 21 t1.start(); 22 t2.start(); 23 t1.join(); 24 t2.join(); 25 System.out.println(i); 26 } 27 28}
可以看到重入锁有lock()和unlock()方法,使用上比synchronized关键字要灵活很多。
还记得上章节中关于区域控制权的例子么?这里lock就是获得控制权,unlock就是交出控制权,这样的话江湖有了规矩,大家都好办事儿。
这里说一下性能问题,在Java5的早期版本,ReentrantLock的性能会比synchronized强很多;在Java6之后,synchronized关键字获得了优化,性能基本和ReentrantLock相差不大。
二、ReetrantLock的中断响应
第一章中讲到线程中断机制对于同步阻塞的情况并不能做到收放自如,同步阻塞不会收到异常。
对应上一节的例子来看,如果lock.lock();这一行出现了同步等待,即使调用了线程的interrupt()的方法,lock.lock();也收不到异常。为了解决这个问题,ReentrantLock提供了lockInterruptibly()方法。
使用这个方法,就可以响应中断了。
三、锁申请等待限时
如果你排队买肯德基,时间超过10分钟,也许你就失去耐心、悻悻而归了。
有时候获得锁也一样,要获得一个锁,如果等待时间过长,那么也可以选择放弃。
1public class TimeLock implements Runnable { 2 static ReentrantLock lock = new ReentrantLock(); 3 4 @Override 5 public void run() { 6 try { 7 if (lock.tryLock(1, TimeUnit.SECONDS)) { 8 Thread.sleep(1111); 9 } else { 10 System.out.println("try lock failed"); 11 } 12 } catch (InterruptedException e) { 13 e.printStackTrace(); 14 } finally { 15 if (lock.isHeldByCurrentThread()) { 16 lock.unlock(); 17 } 18 } 19 } 20 21 public static void main(String[] args) throws InterruptedException { 22 TimeLock reeterLock = new TimeLock(); 23 Thread t1 = new Thread(reeterLock); 24 Thread t2 = new Thread(reeterLock); 25 t1.start(); 26 t2.start(); 27 } 28 29}
t2线程要去获得锁,他的耐心只有1秒钟,如果排队了1秒还获取不到,那么就失败,可以去先做别的事儿。
tryLock()也可以无参,意思就是试着获得锁,如果得不到就返回false。复杂情况下使用tryLock()而不是lock()可以有效避免死锁。
四、公平锁
在操作系统中,关于进程该如何竞争CPU的使用时间是个经典问题。其中有一种算法叫抢占式调度算法,他会让进程去抢占CPU,然后占用CPU一段固定时间,当占用时段结束,则该进程被挂起,让其他进程有可运行的时间。有点类似一人一口苹果的感觉。
那么在Java并发中,如果多个线程差之毫厘的去请求锁,默认的策略是什么呢?默认的策略会是系统随机挑选一个来线程来获得锁。
如果现在你想要保证公平性,也就是说规定先到先得,该怎么办?幸好ReentrantLock提供了这种机制,叫做公平锁的机制。
只需要ReentrantLock的构造函数入参是true即可:
1public class FairLock implements Runnable { 2 static ReentrantLock lock = new ReentrantLock(true); 3 4 @Override 5 public void run() { 6 while (true) { 7 try { 8 lock.lock(); 9 System.out.println(Thread.currentThread().getName() + " 获得锁!"); 10 } finally { 11 lock.unlock(); 12 } 13 } 14 } 15 16 public static void main(String[] args) throws InterruptedException { 17 FairLock reeterLock = new FairLock(); 18 Thread t1 = new Thread(reeterLock, "t"); 19 Thread t2 = new Thread(reeterLock, "---t"); 20 t1.start(); 21 t2.start(); 22 } 23 24}
默认的策略非常高效,但非公平;公平锁的话比较公平,效率稍有降低。
五、重入锁的好搭档:Condition
Condition,是条件的意思,可以看做是ReentrantLock的一种扩展,利用好Condition对象,我们可以让线程在合适的时间进行等待,或者在某一个特定的时刻得到通知,取消等待,继续执行。
Condition中的主要方法有:
await()使当前线程等待,同时释放当前锁。当其他线程中使用signal()或signalAll()方法时,线程会重新获得锁并继续执行awaitUninterruptibly()与await()方法相同,但不会在等待过程中响应中断singal()用于唤醒一个在等待中的线程
例如警匪片中,警察前往歹徒毒品交易的地点,卧底发来消息:交易时间变更,请等待我的通知(调用了await()方法)。警察原地待命,30分钟后卧底发来通知:出发(调用了singal()方法),警察继续行动。
Condition和ReentrantLock是配合使用的:
1public class ReentrantLockCondition implements Runnable { 2 static ReentrantLock lock = new ReentrantLock(true); 3 static Condition condition = lock.newCondition(); 4 5 @Override 6 public void run() { 7 try { 8 lock.lock(); 9 condition.await(); 10 System.out.println("going on"); 11 } catch (InterruptedException e) { 12 e.printStackTrace(); 13 } finally { 14 lock.unlock(); 15 } 16 } 17 18 public static void main(String[] args) throws InterruptedException { 19 ReentrantLockCondition reeterLock = new ReentrantLockCondition(); 20 Thread t1 = new Thread(reeterLock); 21 t1.start(); 22 Thread.sleep(2000); 23 lock.lock(); 24 condition.signal(); 25 lock.unlock(); 26 } 27 28}
这里要注意一点,condition.signal()调用的时候一定要保证当前线程重新获得锁(也就是最好先调用lock.lock()方法),否则会报IllegalMonitorStateException异常。
JDK中的并发容器ArrayBlockingQueue中就使用到ReentrantLock和Condition的配方,ArrayBlockingQueue在后面我们会讲到。如果你现在就非常感兴趣的话,可以看下ArrayBlockingQueue的源码:
1 /** Main lock guarding all access */ 2 final ReentrantLock lock; 3 4 /** Condition for waiting takes */ 5 private final Condition notEmpty; 6 7 /** Condition for waiting puts */ 8 private final Condition notFull; 9 10 public ArrayBlockingQueue(int capacity, boolean fair) { 11 if (capacity <= 0) 12 throw new IllegalArgumentException(); 13 this.items = new Object[capacity]; 14 lock = new ReentrantLock(fair); 15 notEmpty = lock.newCondition(); 16 notFull = lock.newCondition(); 17 } 18 19 public E take() throws InterruptedException { 20 final ReentrantLock lock = this.lock; 21 lock.lockInterruptibly(); 22 try { 23 while (count == 0) 24 notEmpty.await(); 25 return dequeue(); 26 } finally { 27 lock.unlock(); 28 } 29 } 30 31 public void put(E e) throws InterruptedException { 32 checkNotNull(e); 33 final ReentrantLock lock = this.lock; 34 lock.lockInterruptibly(); 35 try { 36 while (count == items.length) 37 notFull.await(); 38 enqueue(e); 39 } finally { 40 lock.unlock(); 41 } 42 } 43 44 /** 45 * Inserts element at current put position, advances, and signals. 46 * Call only when holding lock. 47 */ 48 private void enqueue(E x) { 49 // assert lock.getHoldCount() == 1; 50 // assert items[putIndex] == null; 51 final Object[] items = this.items; 52 items[putIndex] = x; 53 if (++putIndex == items.length) 54 putIndex = 0; 55 count++; 56 notEmpty.signal(); 57 }
先在构造函数中初始化了lock和两个condition,这里我们先拿notEmpty condition来看一下用法。
ArrayBlockingQueue是一个阻塞队列,他的工作方式是:取出元素的时候,如果队列中没有元素,则线程进行等待,直到队列中有元素,才从队列中取值。
我们看到take()方法中,如果队列元素数为0,则notEmpty condition进行等待。直到put进来一个元素,调用enqueue()方法后,会调用notEmpty.signal()方法,这时take()方法才能解除阻塞、继续执行。
这可真是ReentrantLock和Condition的经典配合。