ReentrantLock 使用
简介
ReentrantLock的实现不仅可以替代隐式的synchronized关键字,而且能够提供超过关键字本身的多种功能。
这里提到一个锁获取的公平性问题,如果在绝对时间上,先对锁进行获取的请求一定被先满足,那么这个锁是公平的,反之,是不公平的,也就是说等待时间最长的线程最有机会获取锁,也可以说锁的获取是有序的。ReentrantLock这个锁提供了一个构造函数,能够控制这个锁是否是公平的。
而锁的名字也是说明了这个锁具备了重复进入的可能,也就是说能够让当前线程多次的进行对锁的获取操作,这样的最大次数限制是Integer.MAX_VALUE,约21亿次左右。
事实上公平的锁机制往往没有非公平的效率高,因为公平的获取锁没有考虑到操作系统对线程的调度因素,这样造成JVM对于等待中的线程调度次序和操作系统对线程的调度之间的不匹配。对于锁的快速且重复的获取过程中,连续获取的概率是非常高的,而公平锁会压制这种情况,虽然公平性得以保障,但是响应比却下降了,但是并不是任何场景都是以TPS作为唯一指标的,因为公平锁能够减少“饥饿”发生的概率,等待越久的请求越是能够得到优先满足。
ReentrantLock是一个互斥的同步器,其实现了接口Lock,里面的功能函数主要有:
1. lock() -- 阻塞模式获取资源
2. lockInterruptibly() -- 可中断模式获取资源
3. tryLock() -- 尝试获取资源
4. tryLock(time) -- 在一段时间内尝试获取资源
5. unlock() -- 释放资源
公平性和非公平性对比测试
package demo; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.junit.Test; public class ReentrantLockTest { private static Lock fairLock = new ReentrantLock(true); private static Lock unfairLock = new ReentrantLock(); @Test public void fair() { System.out.println("fair version"); for (int i = 0; i < 5; i++) { Thread thread = new Thread(new Job(fairLock)); thread.setName("" + i); thread.start(); } try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } @Test public void unfair() { System.out.println("unfair version"); for (int i = 0; i < 5; i++) { Thread thread = new Thread(new Job(unfairLock)); thread.setName("" + i); thread.start(); } try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } private static class Job implements Runnable { private Lock lock; public Job(Lock lock) { this.lock = lock; } @Override public void run() { for (int i = 0; i < 5; i++) { lock.lock(); try { System.out.println("Lock by:" + Thread.currentThread().getName()); } finally { lock.unlock(); } } } } }
unfair version Lock by:1 Lock by:1 Lock by:1 Lock by:1 Lock by:1 Lock by:0 Lock by:0 Lock by:3 Lock by:3 Lock by:3 Lock by:3 Lock by:3 Lock by:0 Lock by:0 Lock by:0 Lock by:2 Lock by:2 Lock by:2 Lock by:2 Lock by:2 Lock by:4 Lock by:4 Lock by:4 Lock by:4 Lock by:4 fair version Lock by:0 Lock by:0 Lock by:0 Lock by:0 Lock by:0 Lock by:1 Lock by:4 Lock by:2 Lock by:3 Lock by:1 Lock by:4 Lock by:2 Lock by:3 Lock by:1 Lock by:4 Lock by:2 Lock by:3 Lock by:1 Lock by:4 Lock by:2 Lock by:3 Lock by:1 Lock by:4 Lock by:2 Lock by:3
仔细观察返回的结果(其中每个数字代表一个线程),非公平的结果一个线程连续获取锁的情况非常多,而公平的结果连续获取的情况基本没有。
可中断模式测试
public class Buffer { private Object lock; public Buffer() { lock = this; } public void write() { synchronized (lock) { long startTime = System.currentTimeMillis(); System.out.println("开始往这个buff写入数据…"); // 模拟要处理很长时间 for(;;) { if(System.currentTimeMillis() - startTime > Integer.MAX_VALUE) { break; } } System.out.println("终于写完了"); } } public void read() { synchronized(lock) { System.out.println("从这个buff读数据"); } } public static void main(String[] args) { Buffer buff = new Buffer(); final Writer writer = new Writer(buff); final Reader reader = new Reader(buff); writer.start(); reader.start(); new Thread(new Runnable() { @Override public void run() { long start = System.currentTimeMillis(); for (;;) { // 等5秒钟去中断读 if(System.currentTimeMillis() - start > 5000) { System.out.println("不等了,尝试中断"); reader.interrupt(); break; } } } }).start(); } } class Writer extends Thread { private Buffer buff; public Writer(Buffer buff) { this.buff = buff; } @Override public void run() { buff.write(); } } class Reader extends Thread { private Buffer buff; public Reader(Buffer buff) { this.buff = buff; } @Override public void run() { buff.read();//这里估计会一直阻塞 System.out.println("读结束"); } }
开始往这个buff写入数据…
不等了,尝试中断
我们期待“读”这个线程能退出等待锁,可是事与愿违,一旦读这个线程发现自己得不到锁,就一直开始等待了,就算它等死,也得不到锁,因为写线程要21亿秒才能完成,即使我们中断它,它都不来响应下,看来真的要等死了。
这个时候,ReentrantLock给了一种机制让我们来响应中断, 让“读”能伸能屈,勇敢放弃对这个锁的等待。我们来改写Buffer这个类,就叫InterruptiblyBuffer吧,可中断缓存。
public class InterruptiblyBuffer { private ReentrantLock lock = new ReentrantLock(); public void write() { lock.lock(); try { long startTime = System.currentTimeMillis(); System.out.println("开始往这个buff写入数据…"); // 模拟要处理很长时间 for (;;) { if(System.currentTimeMillis() - startTime > Integer.MAX_VALUE) { break; } } System.out.println("终于写完了"); }finally { lock.unlock(); } } public void read() throws InterruptedException { // 可中断模式获取资源 lock.lockInterruptibly(); try { System.out.println("从这个buff读数据"); } finally { lock.unlock(); } } public static void main(String args[]) { InterruptiblyBuffer buff = new InterruptiblyBuffer(); final Writer writer = new Writer(buff); final Reader reader = new Reader(buff); writer.start(); reader.start(); new Thread(new Runnable() { @Override public void run() { long start = System.currentTimeMillis(); for (;;) { if(System.currentTimeMillis() - start > 5000) { System.out.println("不等了,尝试中断"); reader.interrupt(); break; } } } }).start(); } } class Reader extends Thread { private InterruptiblyBuffer buff; public Reader(InterruptiblyBuffer buff) { this.buff = buff; } @Override public void run() { // 可以收到中断的异常,从而有效退出 try { buff.read(); } catch (InterruptedException e) { System.out.println("我不读了"); } System.out.println("读结束"); } } class Writer extends Thread { private InterruptiblyBuffer buff; public Writer(InterruptiblyBuffer buff) { this.buff = buff; } @Override public void run() { buff.write(); } }
开始往这个buff写入数据…
不等了,尝试中断
我不读了
读结束
可重入锁的使用场景
场景1:如果发现该操作已经在执行中则不再执行(有状态执行)
1. 用在定时任务时,如果任务执行时间可能超过下次计划执行时间,确保该有状态任务只有一个正在执行,忽略重复触发。
2. 用在界面交互时点击执行较长时间请求操作时,防止多次点击导致后台重复执行(忽略重复触发)。
以上两种情况多用于进行非重要任务防止重复执行,(如:清除无用临时文件,检查某些资源的可用性,数据备份操作等)
public class Demo { private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { // 如果已经被lock,则立即返回false不会等待,达到忽略操作的效果 if(lock.tryLock()) { try { //操作 } finally { lock.unlock(); } } } }
场景2:如果发现该操作已经在执行,等待一个一个执行(同步执行,类似synchronized)
这种比较常见大家也都在用,主要是防止资源使用冲突,保证同一时间内只有一个操作可以使用该资源。
但与synchronized的明显区别是性能优势(伴随jvm的优化这个差距在减小)。同时Lock有更灵活的锁定方式,公平锁与不公平锁,而synchronized永远是公平的。
这种情况主要用于对资源的争抢(如:文件操作,同步消息发送,有状态的操作等)
ReentrantLock默认情况下为不公平锁
public class Demo { private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { try { // 如果被其它资源锁定,会在此等待锁释放,达到暂停的效果 lock.lock(); //操作 } finally { lock.unlock(); } } }
场景3:如果发现该操作已经在执行,则尝试等待一段时间,等待超时则不执行(尝试等待执行)
这种其实属于场景2的改进,等待获得锁的操作有一个时间的限制,如果超时则放弃执行。
用来防止由于资源处理不当长时间占用导致死锁情况(大家都在等待资源,导致线程队列溢出)。
public class Demo { private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { try { // 如果已经被lock,尝试等待5s,看是否可以获得锁,如果5s后仍然无法获得锁则返回false继续执行 if(lock.tryLock(5, TimeUnit.SECONDS)) { try { //操作 } finally { lock.unlock(); } } } catch (InterruptedException e) { // 当前线程被中断时(interrupt),会抛InterruptedException e.printStackTrace(); } } }
场景4:如果发现该操作已经在执行,等待执行。这时可中断正在进行的操作立刻释放锁继续下一操作。
synchronized与Lock在默认情况下是不会响应中断(interrupt)操作,会继续执行完。lockInterruptibly()提供了可中断锁来解决此问题。(场景2的另一种改进,没有超时,只能等待中断或执行完毕)
这种情况主要用于取消某些操作对资源的占用。如:(取消正在同步运行的操作,来防止不正常操作长时间占用造成的阻塞)
public class Demo { private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { try { lock.lockInterruptibly(); //操作 } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }
若一个程序或子程序可以“安全的被并行执行(Parallel computing)”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,可以再次进入并执行它(并行执行时,个别的执行结果,都符合设计时的预期)。可重入概念是在单线程操作系统的时代提出的。
参考资料:
摘自: 并发编程网 – ifeve.com本文链接地址: ReentrantLock(重入锁)以及公平性
摘自:http://www.cnblogs.com/jianwei-dai/p/6004657.html
摘自:https://my.oschina.net/noahxiao/blog/101558
浙公网安备 33010602011771号