- 笔记主要参考《Java并发编程的艺术》并且基于JDK1.8的源码进行的刨析,此篇只分析独占模式,后续在ReentrantReadWriteLock和 CountDownLatch中 会重点分析AQS的共享模式
复制代码 一丶Lock
锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁可以防止多个线程同时访问共享资源(这种锁称为独占锁,排他锁)但是有些锁可以允许多个线程并发访问共享资源,比如读写锁
1.Lock接口的方法:
方法作用void lock()获取锁,调用该方法的线程将会获取锁,当锁获得之后从该方法返回void lockInterruptibly()可中断地获取锁,该方法会响应中断,在锁的获取可以中断当前线程,如果在获取锁之前设置了中断标志,or获取锁的中途被中断or其他线程中断该线程则抛出InterruptedException并清除当前线程的中断状boolean tryLock()尝试非阻塞的获取锁,调用方法会立即返回,如果可以获取到锁返回trueboolean tryLock(long time, TimeUnit unit) throws InterruptedException超时获取锁,从当前返回有三种情况
1.超时时间内获取到锁
2.当前线程在超时时间内被中断3.超时间结束没有获取到锁,返回falsevoid unLock释放锁Condition newCondition()获取等待通知的组件,该组件和当前锁绑定,只有获取到锁调用wait方法后当前线程将放弃锁,后续被其他线程signal继续争抢锁2.Lock相比synchronized具备的特性
- synchronized相比于Lock 更加简单,更不容易犯错,但是不够灵活
复制代码 3.使用Lock的经典范式
获取锁的过程不要写在try中,避免获取锁失败最后finally释放其他线程持有的锁
二丶AbstractQueuedSynchronizer队列同步器
使用一个int成员变量state表示同步状态,内置的FIFO队列来完成资源的获取和线程的排队工作,支持独占也支持共享的获取同步状态。
三个变量被volatile修饰,保证其线程可见性
1.队列同步器可以被重写的方法
方法说明protected boolean tryAcquire(int arg)独占的获取锁,需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS改变同步状态protected boolean tryRelease(int arg)独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态protected int tryAcquireShared(int arg)共享式的获取同步状态,放回大于等于0()的值表示成功,反之失败protected boolean tryReleaseShared(int arg)共享式释放同步状态protected boolean isHeldExclusively()当前队列同步器释放再独占模式下被线程占用,一般表示当前线程是否独占2.队列同步器提供的模板方法
方法说明void acquire(int arg)独占式获取同步状态,如果获取成功那么直接返回,反之进入同步队列中等待,void acquireInterruptibly(int arg)和acquire,但是此方法支持在获取锁的过程中响应中断,如果当前线程被中断那么抛出InterruptedExceptionboolean tryAcquireNanos(int arg, long nanosTimeout)在acquireInterruptibly的基础上增加了超时限制,如果在指定时间内没有获取到同步状态那么返回false反之truevoid acquireShared(int arg)共享式获取同步状态,如果没有获取到那么进入等待队列等待,和acquire不同的式支持同一个时刻多个线程获取同步状态void acquireSharedInterruptibly(int arg)和acquireShared类似但是支持响应中断boolean tryAcquireSharedNanos(int arg, long nanosTimeout)在acquireSharedInterruptibly新增了超时限制boolean release(int arg)独占式释放同步资源,在释放同步状态后唤醒后继线程boolean releaseShared(int arg)共享式释放同步状态Collection getQueuedThreads()获取等待在同步队列上的线程们3.同步队列的节点属性
属性描述int waitStatus等待状态Node pre前驱节点Node next后继节点Node nextWaiter等待队列中的后继节点,如果当前节点式共享模式,那么这个节点是SHARED常量,也就是说节点类型和等待中后继节点是公用一个字段Thread thread获取同步状态的线程等待状态是一个枚举,具备下列可选的值
- CANCELLED(1)线程获取锁超时or被中断,需要从同步队列中取消中断,节点进入改状态后状态不再改变
- SIGNAL(-1)后继节点线程处于等待状态,而当前节点的线程如果释放共享资源或者被取消会通知后继节点,使后继线程被唤醒继续执行
- CONDITION(-2)节点在等待队列中,节点中的线程在Condition上进行等待,需要等待其他线程调用Condition的singal or singalAll进行唤醒,该节点会从等待队列移动到同步队列,进行共享资源的争夺
- PROPAGATE(-3)表示下一次共享式同步状态的获取将无条件的传播下去
- 0 初始状态,节点假如到同步队列时候的状态
4.AQS怎么维护同步队列
AQS中包含两个节点类型引用:头节点和尾节点。当一个线程获取到同步状态的时候,其他线程无法获取,将被放入到同步队列中,加入队列这个过程为了保证线程安全而采用CAS。同步队列遵守FIFO,头节点是获取到同步状态的线程,释放同步状态将会唤醒后继线程,后继节点获取到同步状态后将被设置为头节点
三丶ReentrantLock可重入锁
1.ReentrantLock简介
支持公平和非公平和重入的独占式锁
- 重入表示已经获得锁的线程可以对共享资源重复加锁
- 公平锁,支持先来后到,像在公司排队上厕所,先来的人肯定优先获取到茅坑,先来的线程肯定先获取到共享资源
- 独占式,同一时间只允许一个线程操作共享资源
2.公平锁和非公平锁比较
公平锁在头节点释放同步资源的时候需要unpark后续节点,并切换线程执行上下文,导致效率并不如非公平锁,但是公平锁可以减少饥饿,因为非公平锁好像A在排队,A获取到共享资源需要进行唤醒和上下文切换,而导致需要更多时间,这时候流氓B刚好进厕所门,上来就是一个CAS,很快抢占了厕所这一共享资源,导致A处于饥饿——迟迟得不到厕所(共享资源)的操作资源。
3.ReentrantLock的可重入
实现可重入需要解决两个问题
- 线程再次获得锁,锁需要去识别当前线程释放是当前占据锁的线程,如果是那么直接加锁成功
- 锁的最终释放,加锁多少次,就需要释放多少次,完全解锁后其他线程才可以获取到锁。
第一个问题ReentrantLock通过获取当前线程和独占锁线程的`==1判断来实现,第二个问题ReentrantLock通过对AQS中的共享资源state增加和减少来实现
四丶结合ReentrantLock分析加锁解锁的流程
1.ReentranLock
ReentrantLock的公平和非公平就是由于sync引用指向了的不同实现,其lock unlock等操作也是一律交由到sync
2.ReentrantLock的非公平模式
2.1非公平加锁——lock方法
加锁的大致流程
- 无论是非公平还是公平在加锁成功后都会通过setExclusiveOwnerThread设置当前线程为独占锁的线程,这个方法会记住当前线程,这是后面实现可重入的关键、
- acquire 方法会调用tryAcquire方法,这个方法由AQS的子类实现,NonfairSync这里会调用nonfairTryAcquire方法
2.1.1不公平的尝试获取共享资源nonfairTryAcquire
- 如果nonfairTryAcquire返回true表示当前线程获取到了锁,那么皆大欢喜,当前线程可以继续运行
- 返回false的情况
- 共享资源是0,但是同一个时间多个线程抢占,当前这个线程CAS失败了
- 共享资源不是0,当前线程也不是独占的线程
这两种情况都需要继续执行AQS的acquire方法
2.1.2AQS的acquire 方法
独占模式获取共享资源,对中断不敏感,或者说不响应中断——获取共享资源失败的线程将会进入到同步队列,后续对此线程进行中断操作,线程不会从同步队列中移出
1.执行流程
2.将当前线程包装成Node加入到队列尾addWaiter
- 快速入队
下面这段代码值得品一品- Node pred = tail;
- if (pred != null) {
- //当前线程的前置设置为尾,这一步那么多个线程执行这一步也是无关紧要的
- //只是把当前节点的前置改变了,不是改变pred的next指向
- node.prev = pred;
- //CAS设置尾节点 为当前节点,这个自选操作compareAndSetTail是线程安全,同一时间只有一个线程可以设置自己为尾节点
- if (compareAndSetTail(pred, node)) {
- //注意 如果原尾节点是S,线程A设置成功 那么尾巴被修改为了A,假如A执行下面一行的时候消耗完了时间片,线程B进来了,这时候线程B拿到的tail就是A,所以不会存在线程安全问题
- pred.next = node;
- return node;
- }
- }
复制代码 - 完整入队
!
完整入队和快速入队差不多,就是多了一个初始化的逻辑
那么为什么不直接完整入队,也许是for循环比if多更多的字节码需要执行?也许Doug Lea测试多次后发现快速入队后完整入队,比直接完整入队效率更高
3.尝试出队acquireQueued
- 如何从自旋中退出
前继节点是头节点,头节点是当前获取到共享资源的节点,且获取共享资源tryAcquire成功
- 挂起当前线程避免无休止的自选
自选是cpu操作,无限制的自选是很浪费cpu资源的
如果shouldParkAfterFailedAcquire放回true 表示当前线程需要被挂起,会继续执行parkAndCheckInterrupt,这个方法很简单只有两行- private final boolean parkAndCheckInterrupt() {
- //挂起当前线程
- LockSupport.park(this);
-
- //返回中断状态,并且清除中断标识
- return Thread.interrupted();
- }
复制代码 如果parkAndCheckInterrupt 返回了true 表示当前线程被中断过,并且会让外层的acquireQueued返回true,会导致acquire执行当前线程的自我中断
理解这一段代码需要对java中断机制具备一定理解
java线程中断机制
- 调用Thread的interrupt方法
- 如果线程处于Running状态那么只是修改Thread内部的中断标识值为true
- 如果线程由于sleep,wait,join等方法进入等待状态,会直接抛出中断异常并清楚中断标识
- 如果线程由于LockSupport.park进入等待状态,调用该线程的interrupt方法只会让LockSupport.park返回
- interrupt,interrupted,isInterrupted三个方法比较
- interrupt 见上⬆
- interrupted 返回当前线程的中断标识并且充值中断标识
- isInterrupted返回中断标识
我们继续说为什么当前线程在获取锁的途中被中断,需要自我中断以下- acquire的"需求":
- 独占模式获取共享资源,对中断不敏感,或者说不响应中断——获取共享资源失败的线程将会进入到同步队列,后续对此线程进行中断操作,线程不会从同步队列中移出
复制代码 线程获取同步状态的时候被中断会发生什么——从LockSupport.park(this)中返回继续拿锁,这就是为什么说acquire的对中断不敏感。- LockSupport.park();不会抛出受检查异常,当出现被打断的情况下,线程被唤醒后,我们可以通过Interrupt的状态来判断,我们的线程是不是被interrupt的还是被unpark或者到达指定休眠时间
复制代码 假如我们写如下这样的代码执行
存在一个调度线程中断了上面的线程,但是上面的线程还在抢夺锁,并且被park了,这时候上面线程的park会返回,并且清除中断标识,如果不进行自我中断,那么下面while内容还是会进行,那么我们调度线程的中断就无效了
3.ReentrantLock的公平模式
3.1公平的获取锁——lock方法
公平锁的lock方法直接调用AQS的acquire方法,上面我们分析的acquire方法它会先去调用tryAcquire,这个tryAcquire被FairSync重写
- protected final boolean tryAcquire(int acquires) {
- final Thread current = Thread.currentThread();
- int c = getState();
- //共享状态当前空闲
- if (c == 0) {
- //前面没有节点 这就是公平是怎么实现的
- //且cas成功 那么拿到锁
- if (!hasQueuedPredecessors() &&
- compareAndSetState(0, acquires)) {
- setExclusiveOwnerThread(current);
- return true;
- }
- }
- //实现重入 和 公平锁一样
- else if (current == getExclusiveOwnerThread()) {
- int nextc = c + acquires;
- if (nextc < 0)
- throw new Error("Maximum lock count exceeded");
- setState(nextc);
- return true;
- }
- return false;
- }
- }
复制代码 源码没什么很难的点,就是通过判断前面时候还有节点(标识是否由线程比当前线程先到)如果没有那么再去拿锁,如果共享状态不是0且当前线程不是独占的线程那么就会执行acquireQueued方法,在acquireQueued里面自选获取锁会判断前一个节点是否是头节点且调用tryAcquire
4.释放锁
释放锁直接调用AQS的release方法,其中tryRelease方法由ReentrantLock中Sync自己实现(公平or非公平都一样)- public final boolean release(int arg) {
- //完全的释放资源
- if (tryRelease(arg)) {
- Node h = head;
- //头节点初始化的时候才为0,但是后面如果由节点加入到同步队列会把前置节点的状态设置为Singnal
- if (h != null && h.waitStatus != 0)
- //唤醒后继节点
- unparkSuccessor(h);
- return true;
- }
- return false;
- }
复制代码 4.1 tryRelease
- protected final boolean tryRelease(int releases) {
- //重入了n次,当前释放m次 c=n-m
- int c = getState() - releases;
- //如果不是独占锁的线程 那么抛出异常
- if (Thread.currentThread() != getExclusiveOwnerThread())
- throw new IllegalMonitorStateException();
- //是否完全的释放了锁
- boolean free = false;
- //只有剩下的为0 才是完全释放锁
- if (c == 0) {
- //置为true
- free = true;
- //独占线程设置为null
- setExclusiveOwnerThread(null);
- }
- //修改state
- setState(c);
- return free;
- }
复制代码 需要注意的是只有完全的释放了共享资源,在ReentrantLock里就是加锁n次解锁n次,才返回true,才会去唤醒后继节点
4.2 unparkSuccessor
[code]private void unparkSuccessor(Node node) { int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; //从尾巴开始找到队列最前面的且需要通知的节点 为什么要从尾巴开始找? for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus spinForTimeoutThreshold,剩余时间大于阈值(1000)才会挂起,如果小于的化还是进行自旋,因为非常短的超时时间无法做到十分精确(挂起和唤醒也是需要时间的)如果还是进行超时等待反而会表现得不精确</p>
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |