Java多线程学习之Lock与ReentranLock详解
1 public interface Lock { 2 void lock(); 3 void lockInterruptibly() throws InterruptedException; 4 Condition newCondition(); 5 boolean tryLock(); 6 boolean tryLock(long time, TimeUnit unit) throws InterruptedException; 7 void unlock(); 8 }
1 // 创建锁 2 Lock lock = new ReentranLock(); 3 ... 4 5 lock.lock(); 6 try{ 7 // 进行必要操作 8 // 捕获异常,并在必要时恢复不变性条件 9 }finally{ 10 //释放锁 11 lock.unlock(); 12 }
注意,使用Lock时一定要在finally语句里面释放锁,否则发生异常时可能会导致锁无法被释放,导致程序奔溃。
2、轮询锁和定时锁
下面代码要实现统计两个资源的数量总和操作:使用tryLock尝试同时获取两个资源的锁,如果不能同时获取两个资源的锁,则退出重试。如果在规定时间内不能同时获取两对象的锁并完成操作,则返回-1作为失败的标识。
1 // 资源类 2 public class Resource { 3 //资源总和 4 private int resourceNum; 5 // 显示锁 6 public Lock lock = new ReentrantLock(); 7 8 public Resource(int resourceNum){ 9 this.resourceNum = resourceNum; 10 } 11 //返回此资源的总量 12 public int getResourceNum(){ 13 return resourceNum; 14 } 15 }
1 public class LockTest1 { 2 //传入两个资源类和预期操作时间,在此期间内返回两个资源的数量总和 3 public int getResource(Resource resourceA, Resource resourceB, long timeout, TimeUnit unit) 4 throws InterruptedException { 5 // 获取当前时间,算出操作截止时间 6 long stopTime = System.nanoTime() + unit.toNanos(timeout); 7 8 while(true){ 9 try { 10 // 尝试获得资源A的锁 11 if (resourceA.lock.tryLock()) { 12 try{ 13 // 如果获得资源A的锁,尝试获得资源B的锁 14 if(resourceB.lock.tryLock()){ 15 //同时获得两资源的锁,进行相关操作后返回 16 return getSum(resourceA, resourceB); 17 } 18 }finally { 19 resourceB.lock.unlock(); 20 } 21 } 22 }finally { 23 resourceA.lock.unlock(); 24 } 25 26 // 判断当前是否超时,规定-1为错误标识 27 if(System.nanoTime() > stopTime) 28 return -1; 29 30 //睡眠1秒,继续尝试获得锁 31 TimeUnit.SECONDS.sleep(1); 32 } 33 } 34 35 // 获得资源总和 36 public int getSum(Resource resourceA,Resource resourceB){ 37 return resourceA.getResourceNum()+resourceB.getResourceNum(); 38 } 39 }
- 如果不休眠,让线程在一次获取锁失败后立即进行下一轮获取尝试,可以获得很好的响应速度,但是这也会让线程长时间占用CPU时钟周期直到成功获得两个锁。如果该锁在很长时间后才都可用,这会造成CPU资源浪费,服务器性能降低。
因此,需要在响应速度和服务器性能之间做出权衡。
1 public class InterruptedLockTest implements Runnable{ 2 public synchronized void doCount(){ 3 //使用死循环表示此操作要进行很长的一段时间才能结束 4 while(true){} 5 } 6 7 @Override 8 public void run() { 9 doCount(); 10 } 11 }
1 public static void main(String[] args) throws InterruptedException { 2 InterruptedLockTest test = new InterruptedLockTest(); 3 4 Thread t1 = new Thread(test); 5 Thread t2 = new Thread(test); 6 7 t1.start(); 8 t2.start(); 9 10 //等待两秒,尝试中断线程t2的等待 11 TimeUnit.SECONDS.sleep(2); 12 t2.interrupt(); 13 14 //等待1秒,让 t2.interrupt(); 执行生效 15 TimeUnit.SECONDS.sleep(1); 16 System.out.println("线程t1是否存活:" + t1.isAlive()); 17 System.out.println("线程t2是否存活:" + t2.isAlive()); 18 }
1 public class InterruptedLockTest2 implements Runnable{ 2 Lock lock = new ReentrantLock(); 3 4 public void doCount() throws InterruptedException { 5 //可中断的锁等待机制,会抛出中断异常 6 lock.lockInterruptibly(); 7 try { 8 while (true) {} 9 }finally { 10 lock.unlock(); 11 } 12 } 13 14 @Override 15 public void run() { 16 try { 17 doCount(); 18 } catch (InterruptedException e) { 19 System.out.println("被中断...."); 20 } 21 } 22 }
1 public static void main(String[] args) throws InterruptedException { 2 InterruptedLockTest2 test = new InterruptedLockTest2(); 3 4 Thread t1 = new Thread(test); 5 Thread t2 = new Thread(test); 6 7 t1.start(); 8 t2.start(); 9 10 TimeUnit.SECONDS.sleep(2); 11 t2.interrupt(); 12 13 //等待1秒,让 t2.interrupt(); 执行生效 14 TimeUnit.SECONDS.sleep(1); 15 System.out.println("线程t1是否存活:" + t1.isAlive()); 16 System.out.println("线程t2是否存活:" + t2.isAlive()); 17 }
1 // 也可以指定公平性 2 public ReentrantLock(boolean fair) { 3 sync = fair ? new FairSync() : new NonfairSync(); 4 } 5 //默认创建非公平锁 6 public ReentrantLock() { 7 sync = new NonfairSync(); 8 }
在公平性的ReentranLock中,如果有一个线程在持有这个锁或是有线程在阻塞队列中等待这个锁,那么新请求的线程会被放入队列中等待。非公平性锁中,只当锁被某个线程占领时,才会把新请求的线程放入阻塞队列中。
在竞争激烈的环境中,公平性锁的性能会比非公平性锁差很多。如果没有特殊的需求,不推荐使用公平锁,因为在公平锁中,恢复一个被挂起的线程与该线程真正开始执行之间存在严重的迟延。假如线程A持有一个锁,线程B请求这个锁,由于这个锁已经被持有,所以B会放入阻塞对类中。如果A释放该锁,B将被唤醒,一次会尝试再次请求该锁。与此同时,如果线程C也请求该锁,那么C很可能在B被完全唤醒之前持有、使用和释放该锁。这样,B既没有延迟使用该锁,C还利用其中间隙完成自己的操作,这是一个双赢的局面。
因此,只有在内置锁无法满足需求的情况下,比如,需要使用:可定时的、可轮询的和可中断的锁获取机制,公平队列。才会考虑使用ReentranLock。否则,优先使用synchronized内置锁。