在一个村子里面,有一口井水,水质很是的好,村民们都想打井里的水。这井只有一口,村里的人那么多,因此得出个打水的规则才行。村长绞尽脑汁,最终想出了一个比较合理的方案,我们来仔细的看看聪明的村长大人的智慧。java
井边安排一个看井人,维护打水的秩序。编程
打水时,以家庭为单位,哪一个家庭任何人先到井边,就能够先打水,并且若是一个家庭占到了打水权,其家人这时候过来打水不用排队。而那些没有抢占到打水权的人,一个一个挨着在井边排成一队,先到的排在前面。打水示意图以下 :多线程
是否是感受很和谐,若是打水的人打完了,他会跟看井人报告,看井人会让第二我的接着打水。这样你们总都可以打到水。是否是看起来挺公平的,先到的人先打水,固然不是绝对公平的,本身看看下面这个场景 :并发
看着,一个有娃的父亲正在打水,他的娃也到井边了,因此女凭父贵直接排到最前面打水,羡煞旁人了。
以上这个故事模型就是所谓的公平锁模型,当一我的想到井边打水,而如今打水的人又不是自家人,这时候就得乖乖在队列后面排队。函数
事情总不是那么一路顺风的,总会有些人想走捷径,话说看井人年纪大了,有时候,眼力不是很好,这时候,人们开始打起了新主意。新来打水的人,他们看到有人排队打水的时候,他们不会那么乖巧的就排到最后面去排队,反之,他们会看看如今有没有人正在打水,若是有人在打水,没辄了,只好排到队列最后面,但若是这时候前面打水的人刚刚打完水,正在交接中,排在队头的人尚未完成交接工做,这时候,新来的人能够尝试抢打水权,若是抢到了,呵呵,其余人也只能睁一只眼闭一只眼,由于你们都默认这个规则了。这就是所谓的非公平锁模型。新来的人不必定总得乖乖排队,这也就形成了原来队列中排队的人可能要等好久好久。
工具
ReentrantLock支持两种获取锁的方式,一种是公平模型,一种是非公平模型。在继续以前,我们先把故事元素转换为程序元素。源码分析
我们先来讲说公平锁模型:性能
初始化时, state=0,表示无人抢占了打水权。这时候,村民A来打水(A线程请求锁),占了打水权,把state+1,以下所示:学习
线程A取得了锁,把 state原子性+1,这时候state被改成1,A线程继续执行其余任务,而后来了村民B也想打水(线程B请求锁),线程B没法获取锁,生成节点进行排队,以下图所示:测试
初始化的时候,会生成一个空的头节点,而后才是B线程节点,这时候,若是线程A又请求锁,是否须要排队?答案固然是否认的,不然就直接死锁了。当A再次请求锁,就至关因而打水期间,同一家人也来打水了,是有特权的,这时候的状态以下图所示:
此处可能有人会问 在代码里边怎么理解这种可重入锁的形态呢?
public static ReentrantLock lock = new ReentrantLock(); public static int i = 0; public void run() { for (int j = 0;j<100000;j++) { lock.lock(); lock.lock(); try { i++; }finally { lock.unlock(); lock.unlock(); } } }
为何须要使用可重入锁 在故事描述完后进行具体说明;
到了这里,相信你们应该明白了什么是可重入锁了吧。就是一个线程在获取了锁以后,再次去获取了同一个锁,这时候仅仅是把状态值进行累加。若是线程A释放了一次锁,就成这样了:
仅仅是把状态值减了,只有线程A把此锁所有释放了,状态值减到0了,其余线程才有机会获取锁。当A把锁彻底释放后,state恢复为0,而后会通知队列唤醒B线程节点,使B能够再次竞争锁。固然,若是B线程后面还有C线程,C线程继续休眠,除非B执行完了,通知了C线程。注意,当一个线程节点被唤醒而后取得了锁,对应节点会从队列中删除。
非公平锁模型
若是你已经明白了前面讲的公平锁模型,那么非公平锁模型也就很是容易理解了。当线程A执行完以后,要唤醒线程B是须要时间的,并且线程B醒来后还要再次竞争锁,因此若是在切换过程中,来了一个线程C,那么线程C是有可能获取到锁的,若是C获取到了锁,B就只能继续乖乖休眠了。这里就再也不画图说明了。
java5中添加了一个并发包, java.util.concurrent,里面提供了各类并发的工具类,经过此工具包,能够在java当中实现功能很是强大的多线程并发操做。对于每一个java攻城狮,我以为很是有必要了解这个包的功能。虽然作不到一步到位,但慢慢虚心学习,沉下心来,总能慢慢领悟到java多线程编程的精华。
本问故事情节转载自其余博客,原文地址:https://blog.csdn.net/yanyan19880509/article/details/52345422/
ReentrantLock 是一个可重入的互斥(/独占)锁,又称为“独占锁”。
ReentrantLock经过自定义队列同步器(AQS-AbstractQueuedSychronized,是实现锁的关键)来实现锁的获取与释放。
其能够彻底替代 synchronized 关键字。JDK 5.0 早期版本,其性能远好于 synchronized,但 JDK 6.0 开始,JDK 对 synchronized 作了大量的优化,使得二者差距并不大。
“独占”,就是在同一时刻只能有一个线程获取到锁,而其它获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才可以获取锁。
“可重入”,就是支持重进入的锁,它表示该锁可以支持一个线程对资源的重复加锁。
该锁还支持获取锁时的公平和非公平性选择。“公平”是指“不一样的线程获取锁的机制是公平的”,而“不公平”是指“不一样的线程获取锁的机制是非公平的”。
对于 synchronized 来讲,若是一个线程在等待锁,那么结果只有两种状况,得到这把锁继续执行,或者线程就保持等待。
而使用重入锁,提供了另外一种可能,这就是线程能够被中断。也就是在等待锁的过程当中,程序能够根据须要取消对锁的需求。
下面的例子中,产生了死锁,但得益于锁中断,最终解决了这个死锁:
public class IntLock implements Runnable{ public static ReentrantLock lock1 = new ReentrantLock(); public static ReentrantLock lock2 = new ReentrantLock(); int lock; /** * 控制加锁顺序,产生死锁 */ public IntLock(int lock) { this.lock = lock; } public void run() { try { if (lock == 1) { lock1.lockInterruptibly(); // 若是当前线程未被 中断,则获取锁。 try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } lock2.lockInterruptibly(); System.out.println(Thread.currentThread().getName()+",执行完毕!"); } else { lock2.lockInterruptibly(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } lock1.lockInterruptibly(); System.out.println(Thread.currentThread().getName()+",执行完毕!"); } } catch (InterruptedException e) { e.printStackTrace(); } finally { // 查询当前线程是否保持此锁。 if (lock1.isHeldByCurrentThread()) { lock1.unlock(); } if (lock2.isHeldByCurrentThread()) { lock2.unlock(); } System.out.println(Thread.currentThread().getName() + ",退出。"); } } public static void main(String[] args) throws InterruptedException { IntLock intLock1 = new IntLock(1); IntLock intLock2 = new IntLock(2); Thread thread1 = new Thread(intLock1, "线程1"); Thread thread2 = new Thread(intLock2, "线程2"); thread1.start(); thread2.start(); Thread.sleep(1000); thread2.interrupt(); // 中断线程2 } }
上述例子中,线程 thread1 和 thread2 启动后,thread1 先占用 lock1,再占用 lock2;thread2 反之,先占 lock2,后占 lock1。这便造成 thread1 和 thread2 之间的相互等待。
代码 56 行,main 线程处于休眠(sleep)状态,两线程此时处于死锁的状态,代码 57 行 thread2 被中断(interrupt),故 thread2 会放弃对 lock1 的申请,同时释放已得到的 lock2。这个操做致使 thread1 顺利得到 lock2,从而继续执行下去。
执行代码,输出以下:
除了等待外部通知(中断操做 interrupt )以外,限时等待也能够作到避免死锁。
一般,没法判断为何一个线程迟迟拿不到锁。也许是由于产生了死锁,也许是产生了饥饿。但若是给定一个等待时间,让线程自动放弃,那么对系统来讲是有意义的。可使用 tryLock() 方法进行一次限时的等待。
public class TimeLock implements Runnable{ public static ReentrantLock lock = new ReentrantLock(); public void run() { try { if (lock.tryLock(5, TimeUnit.SECONDS)) { Thread.sleep(6 * 1000); }else { System.out.println(Thread.currentThread().getName()+" get Lock Failed"); } } catch (InterruptedException e) { e.printStackTrace(); }finally { // 查询当前线程是否保持此锁。 if (lock.isHeldByCurrentThread()) { System.out.println(Thread.currentThread().getName()+" release lock"); lock.unlock(); } } } /** * 在本例中,因为占用锁的线程会持有锁长达6秒,故另外一个线程没法再5秒的等待时间内得到锁,所以请求锁会失败。 */ public static void main(String[] args) { TimeLock timeLock = new TimeLock(); Thread t1 = new Thread(timeLock, "线程1"); Thread t2 = new Thread(timeLock, "线程2"); t1.start(); t2.start(); } }
上述例子中,因为占用锁的线程会持有锁长达 6 秒,故另外一个线程没法在 5 秒的等待时间内得到锁,所以,请求锁失败。
ReentrantLock.tryLock()方法也能够不带参数直接运行。这种状况下,当前线程会尝试得到锁,若是锁并未被其余线程占用,则申请锁成功,当即返回 true。不然,申请失败,当即返回 false,当前线程不会进行等待。这种模式不会引发线程等待,所以也不会产生死锁。
·默认状况下,锁的申请都是非公平的。也就是说,若是线程 1 与线程 2,都申请得到锁 A,那么谁得到锁不是必定的,是由系统在等待队列中随机挑选的。这就比如,买票的人不排队,售票姐姐只能随机挑一我的卖给他,这显然是不公平的。而公平锁,它会按照时间的前后顺序,保证先到先得。公平锁的特色是:不会产生饥饿现象。
重入锁容许对其公平性进行设置。构造函数以下:
public ReentrantLock(boolean fair)
public class FairLock implements Runnable{ public static ReentrantLock fairLock = new ReentrantLock(true); public void run() { while (true) { try { fairLock.lock(); System.out.println(Thread.currentThread().getName()+",得到锁!"); }finally { fairLock.unlock(); } } } public static void main(String[] args) { FairLock fairLock = new FairLock(); Thread t1 = new Thread(fairLock, "线程1"); Thread t2 = new Thread(fairLock, "线程2"); t1.start();t2.start(); } }
测试结果:
1.当参数设置为 true 时:线程1 和 线程2 交替进行 公平竞争 交替打印
线程1,得到锁! 线程2,得到锁! 线程1,得到锁! 线程2,得到锁! 线程1,得到锁! 线程2,得到锁! 线程1,得到锁! 线程2,得到锁! 线程1,得到锁! 线程2,得到锁! 线程1,得到锁! 线程2,得到锁! 线程1,得到锁! 线程2,得到锁! 线程1,得到锁!
2.当参数设置为 false 时: 此时能够看到线程1 能够持续拿到锁 等线程1 执行完后 线程2 才能够拿到线程 而后屡次执行 ; 这就是使用 可重入锁后 是非公平机制 线程能够优先屡次拿到执行权
线程1,得到锁! 线程1,得到锁! 线程1,得到锁! 线程1,得到锁! 线程1,得到锁! 线程1,得到锁! 线程1,得到锁! 线程1,得到锁! 线程1,得到锁! 线程1,得到锁! 线程1,得到锁! 线程1,得到锁! 线程2,得到锁! 线程2,得到锁! 线程2,得到锁! 线程2,得到锁!
修改重入锁是否公平,观察输出结果,若是公平,输出结果始终为两个线程交替的得到锁,若是是非公平,输出结果为一个线程占用锁很长时间,而后才会释放锁,另个线程才能执行。
引出第二个问题:为何公平锁例子中出现,公平锁线程是不断切换的,而非公平锁出现同一线程连续获取锁的状况?
重进入是指任意线程在获取到锁以后可以再次获取该锁而不会被锁阻塞,该特性的实现须要解决如下两个问题:
以非公平锁源码分析:
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
acquireQueued 方法增长了再次获取同步状态的处理逻辑:经过判断当前线程是否为获取锁的线程,来决定获取操做是否成功,若是获取锁的线程再次请求,则将同步状态值进行增长并返回 true,表示获取同步状态成功。
成功获取锁的线程再次获取锁,只是增长了同步状态值,也就是要求 ReentrantLock 在释放同步状态时减小同步状态值,释放锁源码以下:
public void unlock() { sync.release(1); } public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }
若是锁被获取 n 次,那么前 (n-1) 次 tryRelease(int releases) 方法必须返回 false,只有同步状态彻底释放了,才能返回 true。该方法将同步状态是否为 0 做为最终释放的条件,当同步状态为 0 时,将占有线程设置为 null,并返回 true,表示释放成功。
经过对获取与释放的分析,就能够解释,以上两个例子中出现的两个问题:为何 ReentrantLock 锁可以支持一个线程对资源的重复加锁?为何公平锁例子中出现,公平锁线程是不断切换的,而非公平锁出现同一线程连续获取锁的状况?
对上面ReentrantLock的几个重要方法整理以下:
对于其实现原理,下篇博文将详细分析,其主要包含三个要素: