如果您对Java并发之AQS详解感兴趣,那么本文将是一篇不错的选择,我们将为您详在本文中,您将会了解到关于Java并发之AQS详解的详细内容,我们还将为您解答java中并发的相关问题,并且为您提供关于
如果您对Java 并发之 AQS 详解感兴趣,那么本文将是一篇不错的选择,我们将为您详在本文中,您将会了解到关于Java 并发之 AQS 详解的详细内容,我们还将为您解答java中并发的相关问题,并且为您提供关于Java JUC 并发之 JMM 原理详解、Java 中的锁原理、锁优化、CAS、AQS 详解、Java 中的锁原理、锁优化、CAS、AQS 详解!、Java 中的队列同步器 AQS的有价值信息。
本文目录一览:- Java 并发之 AQS 详解(java中并发)
- Java JUC 并发之 JMM 原理详解
- Java 中的锁原理、锁优化、CAS、AQS 详解
- Java 中的锁原理、锁优化、CAS、AQS 详解!
- Java 中的队列同步器 AQS
Java 并发之 AQS 详解(java中并发)
一、概述
谈到并发,不得不谈 ReentrantLock;而谈到 ReentrantLock,不得不谈 AbstractQueuedSynchronizer(AQS)!
类如其名,抽象的队列式的同步器,AQS 定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的 ReentrantLock/Semaphore/CountDownLatch...。
二、框架
它维护了一个 volatile int state(代表共享资源)和一个 FIFO 线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里 volatile 是核心关键词,具体 volatile 的语义,在此不述。state 的访问方式有三种:
- getState()
- setState()
- compareAndSetState()
AQS 定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如 ReentrantLock)和 Share(共享,多个线程可同时执行,如 Semaphore/CountDownLatch)。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队 / 唤醒出队等),AQS 已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
- isHeldExclusively ():该线程是否正在独占资源。只有用到 condition 才需要去实现它。
- tryAcquire (int):独占方式。尝试获取资源,成功则返回 true,失败则返回 false。
- tryRelease (int):独占方式。尝试释放资源,成功则返回 true,失败则返回 false。
- tryAcquireShared (int):共享方式。尝试获取资源。负数表示失败;0 表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared (int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回 true,否则返回 false。
以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock () 时,会调用 tryAcquire () 独占该锁并将 state+1。此后,其他线程再 tryAcquire () 时就会失败,直到 A 线程 unlock () 到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。
再以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown () 一次,state 会 CAS 减 1。等到所有子线程都执行完后 (即 state=0),会 unpark () 主调用线程,然后主调用线程就会从 await () 函数返回,继续后余动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现 tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared 中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如 ReentrantReadWriteLock。
三、源码详解
本节开始讲解 AQS 的源码实现。依照 acquire-release、acquireShared-releaseShared 的次序来。
3.1 acquire(int)
此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。这也正是 lock () 的语义,当然不仅仅只限于 lock ()。获取到资源后,线程就可以去执行其临界区代码了。下面是 acquire () 的源码:
1 public final void acquire(int arg) { 2 if (!tryAcquire(arg) && 3 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 4 selfInterrupt(); 5 }
函数流程如下:
-
- tryAcquire () 尝试直接去获取资源,如果成功则直接返回;
- addWaiter () 将该线程加入等待队列的尾部,并标记为独占模式;
- acquireQueued () 使线程在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回 true,否则返回 false。
- 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断 selfInterrupt (),将中断补上。
这时单凭这 4 个抽象的函数来看流程还有点朦胧,不要紧,看完接下来的分析后,你就会明白了。就像《大话西游》里唐僧说的:等你明白了舍生取义的道理,你自然会回来和我唱这首歌的。
3.1.1 tryAcquire(int)
此方法尝试去获取独占资源。如果获取成功,则直接返回 true,否则直接返回 false。这也正是 tryLock () 的语义,还是那句话,当然不仅仅只限于 tryLock ()。如下是 tryAcquire () 的源码:
1 protected boolean tryAcquire(int arg) { 2 throw new UnsupportedOperationException(); 3 }
什么?直接 throw 异常?说好的功能呢?好吧,还记得概述里讲的 AQS 只是一个框架,具体资源的获取 / 释放方式交由自定义同步器去实现吗?就是这里了!!!AQS 这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过 state 的 get/set/CAS)!!!至于能不能重入,能不能加塞,那就看具体的自定义同步器怎么去设计了!!!当然,自定义同步器在进行资源访问时要考虑线程安全的影响。
这里之所以没有定义成 abstract,是因为独占模式下只用实现 tryAcquire-tryRelease,而共享模式下只用实现 tryAcquireShared-tryReleaseShared。如果都定义成 abstract,那么每个模式也要去实现另一模式下的接口。说到底,Doug Lea 还是站在咱们开发者的角度,尽量减少不必要的工作量。
3.1.2 addWaiter(Node)
此方法用于将当前线程加入到等待队列的队尾,并返回当前线程所在的结点。还是上源码吧:
1 private Node addWaiter(Node mode) { 2 //以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享) 3 Node node = new Node(Thread.currentThread(), mode); 4 5 //尝试快速方式直接放到队尾。 6 Node pred = tail; 7 if (pred != null) { 8 node.prev = pred; 9 if (compareAndSetTail(pred, node)) { 10 pred.next = node; 11 return node; 12 } 13 } 14 15 //上一步失败则通过enq入队。 16 enq(node); 17 return node; 18 }
不用再说了,直接看注释吧。
3.1.2.1 enq(Node)
此方法用于将 node 加入队尾。源码如下:
1 private Node enq(final Node node) { 2 //CAS"自旋",直到成功加入队尾 3 for (;;) { 4 Node t = tail; 5 if (t == null) { // 队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。 6 if (compareAndSetHead(new Node())) 7 tail = head; 8 } else {//正常流程,放入队尾 9 node.prev = t; 10 if (compareAndSetTail(t, node)) { 11 t.next = node; 12 return t; 13 } 14 } 15 } 16 }
如果你看过 AtomicInteger.getAndIncrement () 函数源码,那么相信你一眼便看出这段代码的精华。CAS 自旋 volatile 变量,是一种很经典的用法。还不太了解的,自己去百度一下吧。
3.1.3 acquireQueued(Node, int)
OK,通过 tryAcquire () 和 addWaiter (),该线程获取资源失败,已经被放入等待队列尾部了。聪明的你立刻应该能想到该线程下一部该干什么了吧:进入等待状态休息,直到其他线程彻底释放资源后唤醒自己,自己再拿到资源,然后就可以去干自己想干的事了。没错,就是这样!是不是跟医院排队拿号有点相似~~acquireQueued () 就是干这件事:在等待队列中排队拿号(中间没其它事干可以休息),直到拿到号后再返回。这个函数非常关键,还是上源码吧:
1 final boolean acquireQueued(final Node node, int arg) { 2 boolean failed = true;//标记是否成功拿到资源 3 try { 4 boolean interrupted = false;//标记等待过程中是否被中断过 5 6 //又是一个“自旋”! 7 for (;;) { 8 final Node p = node.predecessor();//拿到前驱 9 //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)。 10 if (p == head && tryAcquire(arg)) { 11 setHead(node);//拿到资源后,将head指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null。 12 p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了! 13 failed = false; 14 return interrupted;//返回等待过程中是否被中断过 15 } 16 17 //如果自己可以休息了,就进入waiting状态,直到被unpark() 18 if (shouldParkAfterFailedAcquire(p, node) && 19 parkAndCheckInterrupt()) 20 interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true 21 } 22 } finally { 23 if (failed) 24 cancelAcquire(node); 25 } 26 }
到这里了,我们先不急着总结 acquireQueued () 的函数流程,先看看 shouldParkAfterFailedAcquire () 和 parkAndCheckInterrupt () 具体干些什么。
3.1.3.1 shouldParkAfterFailedAcquire(Node, Node)
此方法主要用于检查状态,看看自己是否真的可以去休息了(进入 waiting 状态,如果线程状态转换不熟,可以参考本人上一篇写的 Thread 详解),万一队列前边的线程都放弃了只是瞎站着,那也说不定,对吧!
1 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { 2 int ws = pred.waitStatus;//拿到前驱的状态 3 if (ws == Node.SIGNAL) 4 //如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了 5 return true; 6 if (ws > 0) { 7 /* 8 * 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。 9 * 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)! 10 */ 11 do { 12 node.prev = pred = pred.prev; 13 } while (pred.waitStatus > 0); 14 pred.next = node; 15 } else { 16 //如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢! 17 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); 18 } 19 return false; 20 }
整个流程中,如果前驱结点的状态不是 SIGNAL,那么自己就不能安心去休息,需要去找个安心的休息点,同时可以再尝试下看有没有机会轮到自己拿号。
3.1.3.2 parkAndCheckInterrupt()
如果线程找好安全休息点后,那就可以安心去休息了。此方法就是让线程去休息,真正进入等待状态。
1 private final boolean parkAndCheckInterrupt() { 2 LockSupport.park(this);//调用park()使线程进入waiting状态 3 return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。 4 }
park () 会让当前线程进入 waiting 状态。在此状态下,有两种途径可以唤醒该线程:1)被 unpark ();2)被 interrupt ()。(再说一句,如果线程状态转换不熟,可以参考本人写的 Thread 详解)。需要注意的是,Thread.interrupted () 会清除当前线程的中断标记位。
3.1.3.3 小结
OK,看了 shouldParkAfterFailedAcquire () 和 parkAndCheckInterrupt (),现在让我们再回到 acquireQueued (),总结下该函数的具体流程:
- 结点进入队尾后,检查状态,找到安全休息点;
- 调用 park () 进入 waiting 状态,等待 unpark () 或 interrupt () 唤醒自己;
- 被唤醒后,看自己是不是有资格能拿到号。如果拿到,head 指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,继续流程 1。
3.1.4 小结
OKOK,acquireQueued () 分析完之后,我们接下来再回到 acquire ()!再贴上它的源码吧:
1 public final void acquire(int arg) { 2 if (!tryAcquire(arg) && 3 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 4 selfInterrupt(); 5 }
再来总结下它的流程吧:
- 调用自定义同步器的 tryAcquire () 尝试直接去获取资源,如果成功则直接返回;
- 没成功,则 addWaiter () 将该线程加入等待队列的尾部,并标记为独占模式;
- acquireQueued () 使线程在等待队列中休息,有机会时(轮到自己,会被 unpark ())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回 true,否则返回 false。
- 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断 selfInterrupt (),将中断补上。
由于此函数是重中之重,我再用流程图总结一下:
至此,acquire () 的流程终于算是告一段落了。这也就是 ReentrantLock.lock () 的流程,不信你去看其 lock () 源码吧,整个函数就是一条 acquire (1)!!!
3.2 release(int)
上一小节已经把 acquire () 说完了,这一小节就来讲讲它的反操作 release () 吧。此方法是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即 state=0), 它会唤醒等待队列里的其他线程来获取资源。这也正是 unlock () 的语义,当然不仅仅只限于 unlock ()。下面是 release () 的源码:
1 public final boolean release(int arg) { 2 if (tryRelease(arg)) { 3 Node h = head;//找到头结点 4 if (h != null && h.waitStatus != 0) 5 unparkSuccessor(h);//唤醒等待队列里的下一个线程 6 return true; 7 } 8 return false; 9 }
逻辑并不复杂。它调用 tryRelease () 来释放资源。有一点需要注意的是,它是根据 tryRelease () 的返回值来判断该线程是否已经完成释放掉资源了!所以自定义同步器在设计 tryRelease () 的时候要明确这一点!!
3.2.1 tryRelease(int)
此方法尝试去释放指定量的资源。下面是 tryRelease () 的源码:
1 protected boolean tryRelease(int arg) { 2 throw new UnsupportedOperationException(); 3 }
跟 tryAcquire () 一样,这个方法是需要独占模式的自定义同步器去实现的。正常来说,tryRelease () 都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即可 (state-=arg),也不需要考虑线程安全的问题。但要注意它的返回值,上面已经提到了,release () 是根据 tryRelease () 的返回值来判断该线程是否已经完成释放掉资源了!所以自义定同步器在实现时,如果已经彻底释放资源 (state=0),要返回 true,否则返回 false。
3.2.2 unparkSuccessor(Node)
此方法用于唤醒等待队列中下一个线程。下面是源码:
1 private void unparkSuccessor(Node node) { 2 //这里,node一般为当前线程所在的结点。 3 int ws = node.waitStatus; 4 if (ws < 0)//置零当前线程所在的结点状态,允许失败。 5 compareAndSetWaitStatus(node, ws, 0); 6 7 Node s = node.next;//找到下一个需要唤醒的结点s 8 if (s == null || s.waitStatus > 0) {//如果为空或已取消 9 s = null; 10 for (Node t = tail; t != null && t != node; t = t.prev) 11 if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。 12 s = t; 13 } 14 if (s != null) 15 LockSupport.unpark(s.thread);//唤醒 16 }
这个函数并不复杂。一句话概括:用 unpark () 唤醒等待队列中最前边的那个未放弃线程,这里我们也用 s 来表示吧。此时,再和 acquireQueued () 联系起来,s 被唤醒后,进入 if (p == head && tryAcquire (arg)) 的判断(即使 p!=head 也没关系,它会再进入 shouldParkAfterFailedAcquire () 寻找一个安全点。这里既然 s 已经是等待队列中最前边的那个未放弃线程了,那么通过 shouldParkAfterFailedAcquire () 的调整,s 也必然会跑到 head 的 next 结点,下一次自旋 p==head 就成立啦),然后 s 把自己设置成 head 标杆结点,表示自己已经获取到资源了,acquire () 也返回了!!And then, DO what you WANT!
3.2.3 小结
release () 是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即 state=0), 它会唤醒等待队列里的其他线程来获取资源。
3.3 acquireShared(int)
此方法是共享模式下线程获取共享资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。下面是 acquireShared () 的源码:
1 public final void acquireShared(int arg) { 2 if (tryAcquireShared(arg) < 0) 3 doAcquireShared(arg); 4 }
这里 tryAcquireShared () 依然需要自定义同步器去实现。但是 AQS 已经把其返回值的语义定义好了:负值代表获取失败;0 代表获取成功,但没有剩余资源;正数表示获取成功,还有剩余资源,其他线程还可以去获取。所以这里 acquireShared () 的流程就是:
-
- tryAcquireShared () 尝试获取资源,成功则直接返回;
- 失败则通过 doAcquireShared () 进入等待队列,直到获取到资源为止才返回。
3.3.1 doAcquireShared(int)
此方法用于将当前线程加入等待队列尾部休息,直到其他线程释放资源唤醒自己,自己成功拿到相应量的资源后才返回。下面是 doAcquireShared () 的源码:
1 private void doAcquireShared(int arg) { 2 final Node node = addWaiter(Node.SHARED);//加入队列尾部 3 boolean failed = true;//是否成功标志 4 try { 5 boolean interrupted = false;//等待过程中是否被中断过的标志 6 for (;;) { 7 final Node p = node.predecessor();//前驱 8 if (p == head) {//如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的 9 int r = tryAcquireShared(arg);//尝试获取资源 10 if (r >= 0) {//成功 11 setHeadAndPropagate(node, r);//将head指向自己,还有剩余资源可以再唤醒之后的线程 12 p.next = null; // help GC 13 if (interrupted)//如果等待过程中被打断过,此时将中断补上。 14 selfInterrupt(); 15 failed = false; 16 return; 17 } 18 } 19 20 //判断状态,寻找安全点,进入waiting状态,等着被unpark()或interrupt() 21 if (shouldParkAfterFailedAcquire(p, node) && 22 parkAndCheckInterrupt()) 23 interrupted = true; 24 } 25 } finally { 26 if (failed) 27 cancelAcquire(node); 28 } 29 }
有木有觉得跟 acquireQueued () 很相似?对,其实流程并没有太大区别。只不过这里将补中断的 selfInterrupt () 放到 doAcquireShared () 里了,而独占模式是放到 acquireQueued () 之外,其实都一样,不知道 Doug Lea 是怎么想的。
跟独占模式比,还有一点需要注意的是,这里只有线程是 head.next 时(“老二”),才会去尝试获取资源,有剩余的话还会唤醒之后的队友。那么问题就来了,假如老大用完后释放了 5 个资源,而老二需要 6 个,老三需要 1 个,老四需要 2 个。老大先唤醒老二,老二一看资源不够,他是把资源让给老三呢,还是不让?答案是否定的!老二会继续 park () 等待其他线程释放资源,也更不会去唤醒老三和老四了。独占模式,同一时刻只有一个线程去执行,这样做未尝不可;但共享模式下,多个线程是可以同时执行的,现在因为老二的资源需求量大,而把后面量小的老三和老四也都卡住了。当然,这并不是问题,只是 AQS 保证严格按照入队顺序唤醒罢了(保证公平,但降低了并发)。
3.3.1.1 setHeadAndPropagate(Node, int)
1 private void setHeadAndPropagate(Node node, int propagate) { 2 Node h = head; 3 setHead(node);//head指向自己 4 //如果还有剩余量,继续唤醒下一个邻居线程 5 if (propagate > 0 || h == null || h.waitStatus < 0) { 6 Node s = node.next; 7 if (s == null || s.isShared()) 8 doReleaseShared(); 9 } 10 }
此方法在 setHead () 的基础上多了一步,就是自己苏醒的同时,如果条件符合(比如还有剩余资源),还会去唤醒后继结点,毕竟是共享模式!
doReleaseShared () 我们留着下一小节的 releaseShared () 里来讲。
3.3.2 小结
OK,至此,acquireShared () 也要告一段落了。让我们再梳理一下它的流程:
-
- tryAcquireShared () 尝试获取资源,成功则直接返回;
- 失败则通过 doAcquireShared () 进入等待队列 park (),直到被 unpark ()/interrupt () 并成功获取到资源才返回。整个等待过程也是忽略中断的。
其实跟 acquire () 的流程大同小异,只不过多了个自己拿到资源后,还会去唤醒后继队友的操作(这才是共享嘛)。
3.4 releaseShared()
上一小节已经把 acquireShared () 说完了,这一小节就来讲讲它的反操作 releaseShared () 吧。此方法是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源。下面是 releaseShared () 的源码:
1 public final boolean releaseShared(int arg) { 2 if (tryReleaseShared(arg)) {//尝试释放资源 3 doReleaseShared();//唤醒后继结点 4 return true; 5 } 6 return false; 7 }
此方法的流程也比较简单,一句话:释放掉资源后,唤醒后继。跟独占模式下的 release () 相似,但有一点稍微需要注意:独占模式下的 tryRelease () 在完全释放掉资源(state=0)后,才会返回 true 去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式下的 releaseShared () 则没有这种要求,共享模式实质就是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。例如,资源总量是 13,A(5)和 B(7)分别获取到资源并发运行,C(4)来时只剩 1 个资源就需要等待。A 在运行过程中释放掉 2 个资源量,然后 tryReleaseShared (2) 返回 true 唤醒 C,C 一看只有 3 个仍不够继续等待;随后 B 又释放 2 个,tryReleaseShared (2) 返回 true 唤醒 C,C 一看有 5 个够自己用了,然后 C 就可以跟 A 和 B 一起运行。而 ReentrantReadWriteLock 读锁的 tryReleaseShared () 只有在完全释放掉资源(state=0)才返回 true,所以自定义同步器可以根据需要决定 tryReleaseShared () 的返回值。
3.4.1 doReleaseShared()
此方法主要用于唤醒后继。下面是它的源码:
1 private void doReleaseShared() { 2 for (;;) { 3 Node h = head; 4 if (h != null && h != tail) { 5 int ws = h.waitStatus; 6 if (ws == Node.SIGNAL) { 7 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) 8 continue; 9 unparkSuccessor(h);//唤醒后继 10 } 11 else if (ws == 0 && 12 !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) 13 continue; 14 } 15 if (h == head)// head发生变化 16 break; 17 } 18 }
3.5 小结
本节我们详解了独占和共享两种模式下获取 - 释放资源 (acquire-release、acquireShared-releaseShared) 的源码,相信大家都有一定认识了。值得注意的是,acquire () 和 acquireSahred () 两种方法下,线程在等待队列中都是忽略中断的。AQS 也支持响应中断的,acquireInterruptibly ()/acquireSharedInterruptibly () 即是,这里相应的源码跟 acquire () 和 acquireSahred () 差不多,这里就不再详解了。
四、简单应用
通过前边几个章节的学习,相信大家已经基本理解 AQS 的原理了。这里再将 “框架” 一节中的一段话复制过来:
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队 / 唤醒出队等),AQS 已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
- isHeldExclusively ():该线程是否正在独占资源。只有用到 condition 才需要去实现它。
- tryAcquire (int):独占方式。尝试获取资源,成功则返回 true,失败则返回 false。
- tryRelease (int):独占方式。尝试释放资源,成功则返回 true,失败则返回 false。
- tryAcquireShared (int):共享方式。尝试获取资源。负数表示失败;0 表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared (int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回 true,否则返回 false。
OK,下面我们就以 AQS 源码里的 Mutex 为例,讲一下 AQS 的简单应用。
4.1 Mutex(互斥锁)
Mutex 是一个不可重入的互斥锁实现。锁资源(AQS 里的 state)只有两种状态:0 表示未锁定,1 表示锁定。下边是 Mutex 的核心源码:
1 class Mutex implements Lock, java.io.Serializable { 2 // 自定义同步器 3 private static class Sync extends AbstractQueuedSynchronizer { 4 // 判断是否锁定状态 5 protected boolean isHeldExclusively() { 6 return getState() == 1; 7 } 8 9 // 尝试获取资源,立即返回。成功则返回true,否则false。 10 public boolean tryAcquire(int acquires) { 11 assert acquires == 1; // 这里限定只能为1个量 12 if (compareAndSetState(0, 1)) {//state为0才设置为1,不可重入! 13 setExclusiveOwnerThread(Thread.currentThread());//设置为当前线程独占资源 14 return true; 15 } 16 return false; 17 } 18 19 // 尝试释放资源,立即返回。成功则为true,否则false。 20 protected boolean tryRelease(int releases) { 21 assert releases == 1; // 限定为1个量 22 if (getState() == 0)//既然来释放,那肯定就是已占有状态了。只是为了保险,多层判断! 23 throw new IllegalMonitorStateException(); 24 setExclusiveOwnerThread(null); 25 setState(0);//释放资源,放弃占有状态 26 return true; 27 } 28 } 29 30 // 真正同步类的实现都依赖继承于AQS的自定义同步器! 31 private final Sync sync = new Sync(); 32 33 //lock<-->acquire。两者语义一样:获取资源,即便等待,直到成功才返回。 34 public void lock() { 35 sync.acquire(1); 36 } 37 38 //tryLock<-->tryAcquire。两者语义一样:尝试获取资源,要求立即返回。成功则为true,失败则为false。 39 public boolean tryLock() { 40 return sync.tryAcquire(1); 41 } 42 43 //unlock<-->release。两者语文一样:释放资源。 44 public void unlock() { 45 sync.release(1); 46 } 47 48 //锁是否占有状态 49 public boolean isLocked() { 50 return sync.isHeldExclusively(); 51 } 52
同步类在实现时一般都将自定义同步器(sync)定义为内部类,供自己使用;而同步类自己(Mutex)则实现某个接口,对外服务。当然,接口的实现要直接依赖 sync,它们在语义上也存在某种对应关系!!而 sync 只用实现资源 state 的获取 - 释放方式 tryAcquire-tryRelelase,至于线程的排队、等待、唤醒等,上层的 AQS 都已经实现好了,我们不用关心。
除了 Mutex,ReentrantLock/CountDownLatch/Semphore 这些同步类的实现方式都差不多,不同的地方就在获取 - 释放资源的方式 tryAcquire-tryRelelase。掌握了这点,AQS 的核心便被攻破了!
Java JUC 并发之 JMM 原理详解

转:
Java JUC 并发之 JMM 原理详解
一、什么是 JMM?
JMM 指的是 Java 内存模型,即 Java Memory Model
- Java 内存模型并不是一种实际存在的东西,而是一种人为形成的约定,是一种概念。
关于 JMM,我们需要了解一些相关的同步约定 :
- 线程在解锁前,必须将线程中的工作内存中存储的值即时刷新到主内存中的共享变量!
- 线程在加锁前,必须读取主存中的最新值到工作内存中!
- 加锁和解锁是同一把锁!
线程中操作的数据要从主内存中读取,并备份到线程自己的工作内存中,作为副本,主存并不会主动向线程更新数据。
线程的八种内存交互操作:
-
- lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock(解锁) :作用于主内存的变量,把一个处于锁定状态的共享变量释放
- read(读取):作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中
- load(加载):作用于工作内存的变量,把通过 read 操作获取的变量值放入工作内存中
- use(使用):作用于工作内存的变量,把工作内存中的变量传输给执行引擎,每当虚拟机遇到需要使用到变量的值,就会使用到这个指令
- assign(赋值):作用于工作内存的变量,把执行引擎传输过来的值放入工作内存
- store(存储):作用于主内存的变量,把一个从线程中的工作内存的变量值传送到主内存中,以便后续的 write 操作
- write(写入):作用于主内存的变量,将 store 操作从工作内存获取的变量值放入主内存中
JMM 对以上八种内存操作指令做出了如下约束:
-
- read 和 load、user 和 assign、store 和 write、lock 和 unlock 必须成对出现,不允许单独操作其中一条指令
- 不允许线程丢弃离它最近的 assign 操作,即 工作内存中的变量值改变之后,必须告知主内存
- 不允许一个线程将没有 assign 过的数据从工作内存同步会主内存
- 一个新的变量必须在主内存中产生,不允许工作内存私自初始化一个变量来作为共享变量,即 实施 use 和 store 操作之前 , 必须经过 load 和 assign 操作
- 同一变量同一时间只允许一个线程对其进行 lock 操作;多次 lock 之后,必须执行相同次数的 unlock 对其解锁
- 如果对一个变量进行 lock 操作,会清空所有工作内存中此变量的值, 即 每次获得锁的线程,加锁前必须要重新读取主内存中的变量值,才能提交给执行引擎进行 use 操作
- 如果一个变量没有被 lock,就不能对其进行 unlock 操作,也不能对一个被其他线程锁住的变量进行 unlock
- 对一个变量加锁之前,必须把工作内存中的变量值同步回主内存
存在问题:
假设现在有一个 main 线程和一个普通线程,普通线程执行的操作是:当 num 为 0 时 ,一直循环下去;此时 main 线程给 num 赋值为 1 ,普通线程并不知道 num 已经被修改,程序就会一直执行,不会停止!
public class VolatileDemo {
private static int num = 0;
public static void main(String[] args) {
new Thread(()->{ // 线程1
while (num == 0) {
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;
System.out.println(num);
}
}
解决方法 : volatile 关键字
什么是 volatile ?
-
volatile 是一种轻量级的同步机制,相对于 synchronized 来说
-
保证可见性 => JMM 主内存中的共享变量修改之后,会通知所有线程备份到各自的工作内存中
-
不保证原子性
-
禁止指令重排
转:
Java JUC 并发之 JMM 原理详解
--Posted from Rpc
Java 中的锁原理、锁优化、CAS、AQS 详解
阅读文本大概需要3分钟。
1、为什么要用锁?
锁-是为了解决并发操作引起的脏读、数据不一致的问题。
2、锁实现的基本原理
2.1、volatile
Java编程语言允许线程访问共享变量, 为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。
volatile在多处理器开发中保证了共享变量的“ 可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。

结论:如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。
2.2、synchronized
synchronized通过锁机制实现同步。
先来看下利用synchronized实现同步的基础:Java中的每一个对象都可以作为锁。
具体表现为以下3种形式。
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步方法块,锁是Synchonized括号里配置的对象。
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
2.2.1 synchronized实现原理
synchronized是基于Monitor来实现同步的。
Monitor从两个方面来支持线程之间的同步:
互斥执行
协作
1、Java 使用对象锁 ( 使用 synchronized 获得对象锁 ) 保证工作在共享的数据集上的线程互斥执行。
2、使用 notify/notifyAll/wait 方法来协同不同线程之间的工作。
3、Class和Object都关联了一个Monitor。

Monitor 的工作机理
线程进入同步方法中。
为了继续执行临界区代码,线程必须获取 Monitor 锁。如果获取锁成功,将成为该监视者对象的拥有者。任一时刻内,监视者对象只属于一个活动线程(The Owner)
拥有监视者对象的线程可以调用 wait() 进入等待集合(Wait Set),同时释放监视锁,进入等待状态。
其他线程调用 notify() / notifyAll() 接口唤醒等待集合中的线程,这些等待的线程需要重新获取监视锁后才能执行 wait() 之后的代码。
同步方法执行完毕了,线程退出临界区,并释放监视锁。
参考文档:https://www.ibm.com/developerworks/cn/java/j-lo-synchronized
2.2.2 synchronized具体实现
1、同步代码块采用monitorenter、monitorexit指令显式的实现。
2、同步方法则使用ACC_SYNCHRONIZED标记符隐式的实现。
通过实例来看看具体实现:
public class SynchronizedTest {
public synchronized void method1(){
System.out.println("Hello World!");
}
public void method2(){
synchronized (this){
System.out.println("Hello World!");
}
}
}
javap编译后的字节码如下:

monitorenter
每一个对象都有一个monitor,一个monitor只能被一个线程拥有。当一个线程执行到monitorenter指令时会尝试获取相应对象的
monitor,获取规则如下:
如果monitor的进入数为0,则该线程可以进入monitor,并将monitor进入数设置为1,该线程即为monitor的拥有者。
如果当前线程已经拥有该monitor,只是重新进入,则进入monitor的进入数加1,所以synchronized关键字实现的锁是可重入的锁。
如果monitor已被其他线程拥有,则当前线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor。
monitorexit
只有拥有相应对象的monitor的线程才能执行monitorexit指令。每执行一次该指令monitor进入数减1,当进入数为0时当前线程释放monitor,此时其他阻塞的线程将可以尝试获取该monitor。
2.2.3 锁存放的位置
锁标记存放在Java对象头的Mark Word中。




64位JVM Mark Word 结构
2.2.3 synchronized的锁优化
JavaSE1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。
在JavaSE1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。
锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
偏向锁:
无锁竞争的情况下为了减少锁竞争的资源开销,引入偏向锁。

轻量级锁:
轻量级锁所适应的场景是线程交替执行同步块的情况。

锁粗化(Lock Coarsening):也就是减少不必要的紧连在一起的unlock,lock操作,将多个连续的锁扩展成一个范围更大的锁。
锁消除(Lock Elimination):锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。
适应性自旋(Adaptive Spinning):自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。
另一方面,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。
2.2.4 锁的优缺点对比

2.3、CAS
CAS,在Java并发应用中通常指CompareAndSwap或CompareAndSet,即比较并交换。
1、CAS是一个原子操作,它比较一个内存位置的值并且只有相等时修改这个内存位置的值为新的值,保证了新的值总是基于最新的信息计算的,如果有其他线程在这期间修改了这个值则CAS失败。CAS返回是否成功或者内存位置原来的值用于判断是否CAS成功。
2、JVM中的CAS操作是利用了处理器提供的CMPXCHG指令实现的。
优点:
竞争不大的时候系统开销小。
缺点:
循环时间长开销大。
ABA问题。
只能保证一个共享变量的原子操作。
3、Java中的锁实现
3.1、队列同步器(AQS)
队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架。
3.1.1、它使用了一个int成员变量表示同步状态。

3.1.2、通过内置的FIFO双向队列来完成获取锁线程的排队工作。
同步器包含两个节点类型的应用,一个指向头节点,一个指向尾节点,未获取到锁的线程会创建节点线程安全(compareAndSetTail)的加入队列尾部。
同步队列遵循FIFO,首节点是获取同步状态成功的节点。
未获取到锁的线程将创建一个节点,设置到尾节点。
如下图所示:

首节点的线程在释放锁时,将会唤醒后继节点。而后继节点将会在获取锁成功时将自己设置为首节点。
如下图所示:

3.1.3、独占式/共享式锁获取
独占式:有且只有一个线程能获取到锁,如:ReentrantLock。
共享式:可以多个线程同时获取到锁,如:CountDownLatch
独占式
每个节点自旋观察自己的前一节点是不是Header节点,如果是,就去尝试获取锁。

独占式锁获取流程:

共享式:
共享式与独占式的区别:

共享锁获取流程:

4、锁的使用用例
4.1、ConcurrentHashMap的实现原理及使用


结论:ConcurrentHashMap使用的锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
☆
往期精彩
☆
01 漫谈发版哪些事,好课程推荐
02 Linux的常用最危险的命令
03 精讲Spring Boot—入门+进阶+实例
04 优秀的Java程序员必须了解的GC哪些
05 互联网支付系统整体架构详解
关注我
每天进步一点点
本文分享自微信公众号 - JAVA乐园(happyhuangjinjin88)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
Java 中的锁原理、锁优化、CAS、AQS 详解!
<divid="js_content">
<section data-role="outer" label="Powered by 135editor.com"data-mpa-powered-by="yiban.io"><p><span>来源:jianshu.com/p/e674ee68fd3f</span></p><h1><span>1、为什么要用锁?</span></h1><p><br></p><p><span>锁-是为了解决并发操作引起的脏读、数据不一致的问题。</span></p><p><br></p><h1><span>2、锁实现的基本原理</span></h1><p><br></p><h2><span>2.1、volatile</span></h2><p><br></p><p><span>Java编程语言允许线程访问共享变量, 为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。</span></p><p><br></p><p><span>volatile在多处理器开发中保证了共享变量的“ 可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。</span></p><p><br></p><figure><img data-cropselx1="0" data-cropselx2="570" data-cropsely1="0" data-cropsely2="262" data-ratio="0.4588235294117647" data-type="png" data-w="765" data-src="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEn0KE1Bu0k2NSRU6nCd4hA6CIibxHz1IkgiaQWc9Hff2QP3M2nt1OjOzjg/640?wx_fmt=png" _width="677px"src="https://img2018.cnblogs.com/blog/1112483/201911/1112483-20191112133233180-190053147.png"crossorigin="anonymous" data-fail="0"></figure><p><br></p><p><span>结论:如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。</span></p><p><br></p><h2><span>2.2、synchronized</span></h2><p><br></p><p><span>synchronized通过锁机制实现同步。</span></p><p><br></p><p><span>先来看下利用synchronized实现同步的基础:Java中的每一个对象都可以作为锁。</span></p><p><br></p><p><span>具体表现为以下3种形式。</span></p><p><br></p><p><span>对于普通同步方法,锁是当前实例对象。</span></p><p><span>对于静态同步方法,锁是当前类的Class对象。</span></p><p><span>对于同步方法块,锁是Synchonized括号里配置的对象。</span></p><p><br></p><p><span>当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。</span></p><p><br></p><h3><span>2.2.1 synchronized实现原理</span></h3><p><br></p><p><span>synchronized是基于Monitor来实现同步的。</span></p><p><span>Monitor从两个方面来支持线程之间的同步:</span></p><p><br></p><p><span>互斥执行</span></p><p><span>协作</span></p><p><br></p><p><span>1、Java 使用</span><span>对象锁 ( 使用 synchronized 获得对象锁 ) 保证工作在共享的数据集上的线程互斥执行。</span></p><p><br></p><p><span>2、使用 notify/notifyAll/wait 方法来协同不同线程之间的工作。</span></p><p><span>3、Class和Object都关联了一个Monitor。</span></p><p><br></p><figure><img data-cropselx1="0" data-cropselx2="499" data-cropsely1="0" data-cropsely2="318" data-ratio="0.638" data-type="png" data-w="500" data-src="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEn8A7zT9dIqTJ6wsa8ekVdd1NdIbqhoX2NCictftP489Z7ZToMicxPyVtA/640?wx_fmt=png" data-backw="500" data-backh="319" data-before-oversubscription-url="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEn8A7zT9dIqTJ6wsa8ekVdd1NdIbqhoX2NCictftP489Z7ZToMicxPyVtA/640?wx_fmt=png"_width="100%"src="https://img2018.cnblogs.com/blog/1112483/201911/1112483-20191112133253812-2128863132.png" crossorigin="anonymous" data-fail="0"></figure><p><br></p><p><span>Monitor 的工作机理</span></p><p><br></p><p><span>线程进入同步方法中。</span></p><p><br></p><p><span>为了继续执行临界区代码,线程必须获取 Monitor 锁。如果获取锁成功,将成为该监视者对象的拥有者。任一时刻内,监视者对象只属于一个活动线程(The Owner)</span></p><p><br></p><p><span>拥有监视者对象的线程可以调用 wait() 进入等待集合(Wait Set),同时释放监视锁,进入等待状态。</span></p><p><br></p><p><span>其他线程调用 notify() / notifyAll() 接口唤醒等待集合中的线程,这些等待的线程需要重新获取监视锁后才能执行 wait() 之后的代码。</span></p><p><br></p><p><span>同步方法执行完毕了,线程退出临界区,并释放监视锁。</span></p><p><br></p><p>参考文档:<span>https://www.ibm.com/developerworks/cn/java/j-lo-synchronized</span></p><p><br></p><h3><span>2.2.2 synchronized具体实现</span></h3><p><br></p><p><span>1、同步代码块采用monitorenter、monitorexit指令显式的实现。</span></p><p><span>2、同步方法则使用ACC_SYNCHRONIZED标记符隐式的实现。</span></p><p><br></p><p><span>通过实例来看看具体实现:</span></p><p><br></p><pre><code><span><span><span>public</span></span></span> <span><span><span><span><span><span>class</span></span></span></span></span><span><span> </span></span><span><span><span><span><span>SynchronizedTest</span></span></span></span></span><span><span> </span></span></span>{<br><br> <span><span><span><span><span><span>public</span></span></span></span></span><span><span> </span></span><span><span><span><span><span>synchronized</span></span></span></span></span><span><span> </span></span><span><span><span><span><span>void</span></span></span></span></span><span><span> </span></span><span><span><span><span><span>method1</span></span></span></span></span><span><span><span><span><span>()</span></span></span></span></span></span>{<br> System.out.println(<span><span><span>"Hello World!"</span></span></span>);<br> }<br><br> <span><span><span><span><span><span>public</span></span></span></span></span><span><span> </span></span><span><span><span><span><span>void</span></span></span></span></span><span><span> </span></span><span><span><span><span><span>method2</span></span></span></span></span><span><span><span><span><span>()</span></span></span></span></span></span>{<br> <span><span><span>synchronized</span></span></span> (<span><span><span>this</span></span></span>){<br> System.out.println(<span><span><span>"Hello World!"</span></span></span>);<br> }<br> }<br>}<br></code></pre><p><br></p><p><span>javap编译后的字节码如下:</span></p><p><br></p><figure><imgdata-cropselx1="0" data-cropselx2="570" data-cropsely1="0" data-cropsely2="784" data-ratio="1.3741666666666668" data-type="png" data-w="1200" title="" data-src="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEnGKbI0kcSdRiaTVEIeDIHzQia2LuIOHcIknEmCpdHTADKh0m7XEWmFRqg/640?wx_fmt=png"data-backw="570.025" data-backh="783.025" data-before-oversubscription-url="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEnGKbI0kcSdRiaTVEIeDIHzQia2LuIOHcIknEmCpdHTADKh0m7XEWmFRqg/640?wx_fmt=png" _width="100%" src="https://img2018.cnblogs.com/blog/1112483/201911/1112483-20191112133315873-1830567477.png" crossorigin="anonymous" data-fail="0"></figure><p><br></p><p><span>monitorenter</span></p><p><br></p><p><span>每一个对象都有一个monitor,一个monitor只能被一个线程拥有。当一个线程执行到monitorenter指令时会尝试获取相应对象的</span></p><p><br></p><p><span>monitor,获取规则如下:</span></p><p><br></p><p><span>如果monitor的进入数为0,则该线程可以进入monitor,并将monitor进入数设置为1,该线程即为monitor的拥有者。</span></p><p><br></p><p><span>如果当前线程已经拥有该monitor,只是重新进入,则进入monitor的进入数加1,所以synchronized关键字实现的锁是可重入的锁。</span></p><p><br></p><p><span>如果monitor已被其他线程拥有,则当前线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor。</span></p><p><br></p><p><span>monitorexit</span></p><p><br></p><p><span>只有拥有相应对象的monitor的线程才能执行monitorexit指令。每执行一次该指令monitor进入数减1,当进入数为0时当前线程释放monitor,此时其他阻塞的线程将可以尝试获取该monitor。</span></p><p><br></p><h3><span>2.2.3 锁存放的位置</span></h3><p><br></p><p><span>锁标记存放在Java对象头的Mark Word中。</span></p><p><br></p><figure><img data-cropselx1="0" data-cropselx2="570" data-cropsely1="0" data-cropsely2="99" data-ratio="0.175" data-type="png" data-w="1200" data-src="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEn0Y5RS19A7pTVD6ibG6vxIdkxARsDDoV71XRrlrLnRhwD2kV4bYgfnRQ/640?wx_fmt=png" data-backw="574" data-backh="101" data-before-oversubscription-url="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEn0Y5RS19A7pTVD6ibG6vxIdkxARsDDoV71XRrlrLnRhwD2kV4bYgfnRQ/640?wx_fmt=png"_width="100%"src="https://img2018.cnblogs.com/blog/1112483/201911/1112483-20191112133405678-40897258.png" crossorigin="anonymous" data-fail="0"><figcaption><span>Java对象头长度</span></figcaption></figure><figure><img data-backh="62" data-backw="574" data-before-oversubscription-url="https://mmbiz.qpic.cn/mmbiz/QCu849YTaIPksEhmXNlhegkc6ice6GYEnBA28fkQTqCR1Ju9yldmCcgNCPJ1NX0QaYFKINM78RMiats7rTwFX3Gg/640?wx_fmt=other" data-oversubscription-url="http://mmbiz.qpic.cn/mmbiz_jpg/SUicwdN39QsPJmMqSzaTSOHe08McwPRo00ibW8dXuicECprdI9fJvHkibnlto2h8EbAicAjATALJqy2GAlFYxeHl0UA/0?wx_fmt=jpeg" data-ratio="0.11" data-src="https://mmbiz.qpic.cn/mmbiz_jpg/SUicwdN39QsPJmMqSzaTSOHe08McwPRo00ibW8dXuicECprdI9fJvHkibnlto2h8EbAicAjATALJqy2GAlFYxeHl0UA/640?wx_fmt=jpeg" data-type="jpeg" data-w="1200"_width="100%"src="https://img2018.cnblogs.com/blog/1112483/201911/1112483-20191112133432358-1895366965.png" crossorigin="anonymous" data-fail="0"><figcaption><span>32位JVM Mark Word 结构</span></figcaption></figure><figure><img data-cropselx1="0" data-cropselx2="570" data-cropsely1="0" data-cropsely2="132" data-ratio="0.23166666666666666" data-type="png" data-w="1200" data-src="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEnVrmfqajSfia99uJFVViaq3iax3cC3a6HLOgu4IBI5MmOHEIbeYhbiaOfTw/640?wx_fmt=png" data-backw="574" data-backh="133" data-before-oversubscription-url="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEnVrmfqajSfia99uJFVViaq3iax3cC3a6HLOgu4IBI5MmOHEIbeYhbiaOfTw/640?wx_fmt=png"_width="100%"src="https://img2018.cnblogs.com/blog/1112483/201911/1112483-20191112133453495-1950846084.png" crossorigin="anonymous" data-fail="0"><figcaption><span>32位JVM Mark Word 状态变化</span></figcaption></figure><figure><img data-cropselx1="0" data-cropselx2="570" data-cropsely1="0" data-cropsely2="96" data-ratio="0.16916666666666666" data-type="png" data-w="1200" data-src="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEno7R9LPLGo3OSmiaLZI43120UwcUN6iaiaXwXpjBceZHC8wiaGoXSDpxgsQ/640?wx_fmt=png" data-backw="574" data-backh="97" data-before-oversubscription-url="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEno7R9LPLGo3OSmiaLZI43120UwcUN6iaiaXwXpjBceZHC8wiaGoXSDpxgsQ/640?wx_fmt=png"_width="100%"src=https://img2018.cnblogs.com/blog/1112483/201911/1112483-20191112133510746-1611249084.png" crossorigin="anonymous" data-fail="0"></figure><p><br></p><p><span>64位JVM Mark Word 结构</span></p><p><br></p><h3><span>2.2.3 synchronized的锁优化</span></h3><p><br></p><p><span>JavaSE1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。</span></p><p><br></p><p><span>在JavaSE1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。</span></p><p><br></p><p><span>锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。</span></p><p><br></p><h4><span>偏向锁:</span></h4><p><br></p><p><span>无锁竞争的情况下为了减少锁竞争的资源开销,引入偏向锁。</span></p><p><br></p><figure><img data-cropselx1="0" data-cropselx2="570" data-cropsely1="0" data-cropsely2="556" data-ratio="0.975" data-type="png" data-w="1200" data-src="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEneNic6BzhLGIN2p4uPHwrP9cBHbg8AaSgyqJCDBgJxMMeUlMAkcfOlvw/640?wx_fmt=png" data-backw="574" data-backh="559" data-before-oversubscription-url="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEneNic6BzhLGIN2p4uPHwrP9cBHbg8AaSgyqJCDBgJxMMeUlMAkcfOlvw/640?wx_fmt=png"_width="100%"src="https://img2018.cnblogs.com/blog/1112483/201911/1112483-20191112133529252-1066047276.png" crossorigin="anonymous" data-fail="0"></figure><h4></h4><h4><span><br></span></h4><h4><span>轻量级锁:</span></h4><p><br></p><p><span>轻量级锁所适应的场景是线程交替执行同步块的情况</span><span>。</span></p><p><br></p><figure><img data-cropselx1="0" data-cropselx2="570" data-cropsely1="0" data-cropsely2="556" data-ratio="0.975" data-type="png" data-w="1200" data-src="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEneNic6BzhLGIN2p4uPHwrP9cBHbg8AaSgyqJCDBgJxMMeUlMAkcfOlvw/640?wx_fmt=png" data-backw="574" data-backh="559" data-before-oversubscription-url="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEneNic6BzhLGIN2p4uPHwrP9cBHbg8AaSgyqJCDBgJxMMeUlMAkcfOlvw/640?wx_fmt=png"_width="100%"src="https://img2018.cnblogs.com/blog/1112483/201911/1112483-20191112133551972-160806677.png" crossorigin="anonymous" data-fail="0"></figure><p><br></p><p><span>锁粗化(Lock Coarsening):也就是减少不必要的紧连在一起的unlock,lock操作,将多个连续的锁扩展成一个范围更大的锁。</span></p><p><br></p><p><span>锁消除(Lock Elimination):锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。</span></p><p><br></p><p><span>适应性自旋(Adaptive Spinning):自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。</span></p><p><br></p><p><span>如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。</span></p><p><br></p><p><span>另一方面,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。</span></p><p><br></p><h3><span>2.2.4 锁的优缺点对比</span></h3><p><br></p><figure><img data-cropselx1="0" data-cropselx2="570" data-cropsely1="0" data-cropsely2="146" data-ratio="0.25583333333333336" data-type="png" data-w="1200" data-src="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEnUaFbGdC0zfEbiaicq6bWK4FZJ6RqVJOCtjica8exVZB4KZLmd1s7BcnAQ/640?wx_fmt=png" data-backw="574" data-backh="146" data-before-oversubscription-url="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEnUaFbGdC0zfEbiaicq6bWK4FZJ6RqVJOCtjica8exVZB4KZLmd1s7BcnAQ/640?wx_fmt=png"_width="100%"src="https://img2018.cnblogs.com/blog/1112483/201911/1112483-20191112133615526-2001355017.png" crossorigin="anonymous" data-fail="0"></figure><h2></h2><h2><span><br></span></h2><h2><span>2.3、CAS</span></h2><p><br></p><p><span>CAS,在Java并发应用中通常指CompareAndSwap或CompareAndSet,即比较并交换。</span></p><p><br></p><p><span>1、CAS是一个原子操作,它比较一个内存位置的值并且只有相等时修改这个内存位置的值为新的值,保证了新的值总是基于最新的信息计算的,如果有其他线程在这期间修改了这个值则CAS失败。CAS返回是否成功或者内存位置原来的值用于判断是否CAS成功。</span></p><p><br></p><p><span>2、JVM中的CAS操作是利用了处理器提供的CMPXCHG指令实现的。</span></p><p><br></p><p><span>优点:</span></p><p><br></p><ul><li><p><span>竞争不大的时候系统开销小。</span></p></li><li><p><br></p></li></ul><p><span>缺点:</span></p><p><br></p><ul><li><p><span>循环时间长开销大。</span></p></li><li><p><span>ABA问题。</span></p></li><li><p><span>只能保证一个共享变量的原子操作。</span></p></li><li><p><br></p></li></ul><h1><span>3、Java中的锁实现</span></h1><p><br></p><h2><span>3.1、队列同步器(AQS)</span></h2><p><br></p><p><span>队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架。</span></p><p><br></p><h3><span>3.1.1、它使用了一个int成员变量表示同步状态。</span></h3><p><br></p><figure><img data-cropselx1="0" data-cropselx2="570" data-cropsely1="0" data-cropsely2="152" data-ratio="0.26666666666666666" data-type="png" data-w="1200" data-src="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEn8XHoomr2cquAaEUFe2z5icicIRbJibPyYN36JLoYQTsW4SHsdo7DLPxEw/640?wx_fmt=png" data-backw="574" data-backh="154" data-before-oversubscription-url="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEn8XHoomr2cquAaEUFe2z5icicIRbJibPyYN36JLoYQTsW4SHsdo7DLPxEw/640?wx_fmt=png"_width="100%"src="https://img2018.cnblogs.com/blog/1112483/201911/1112483-20191112133632853-1001996493.png" crossorigin="anonymous" data-fail="0"></figure><h3></h3><h3><span><br></span></h3><h3><span>3.1.2、通过内置的FIFO双向队列来完成获取锁线程的排队工作。</span></h3><p><br></p><p><span>同步器包含两个节点类型的应用,一个指向头节点,一个指向尾节点,未获取到锁的线程会创建节点线程安全(compareAndSetTail)的加入队列尾部。</span></p><p><br></p><p><span>同步队列遵循FIFO,首节点是获取同步状态成功的节点。</span></p><p><span><br></span></p><ul><li><figure><img data-cropselx1="0" data-cropselx2="533" data-cropsely1="0" data-cropsely2="173" data-ratio="0.325" data-type="png" data-w="1200" data-src="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEnXAnZMvGKdXibQGBtM3S8EboqtiaqZLjicZgrGdYvBCdKGib6SVjjV5m3Gw/640?wx_fmt=png" data-backw="539" data-backh="175" data-before-oversubscription-url="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEnXAnZMvGKdXibQGBtM3S8EboqtiaqZLjicZgrGdYvBCdKGib6SVjjV5m3Gw/640?wx_fmt=png"_width="100%"src="https://img2018.cnblogs.com/blog/1112483/201911/1112483-20191112133648791-1050811559.png" crossorigin="anonymous" data-fail="0"></figure></li></ul><p><br></p><p><span>未获取到锁的线程将创建一个节点,设置到尾节点。</span></p><p><br></p><p><span>如下图所示:</span></p><figure><img data-cropselx1="0" data-cropselx2="570" data-cropsely1="0" data-cropsely2="208" data-ratio="0.365" data-type="png" data-w="1200" data-src="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEnFiahtwJRMYh0Hp4Y8Lia79AMcUmGbJInKnIicIMuiaCpkjAFLgsJ1YpI8g/640?wx_fmt=png" data-backw="574" data-backh="210" data-before-oversubscription-url="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEnFiahtwJRMYh0Hp4Y8Lia79AMcUmGbJInKnIicIMuiaCpkjAFLgsJ1YpI8g/640?wx_fmt=png"_width="100%"src="https://img2018.cnblogs.com/blog/1112483/201911/1112483-20191112133719973-431134712.png" crossorigin="anonymous" data-fail="0"></figure><p><br></p><p><span>首节点的线程在释放锁时,将会唤醒后继节点。而后继节点将会在获取锁成功时将自己设置为首节点。</span></p><p><br></p><p><span>如下图所示:</span></p><figure><img data-cropselx1="2" data-cropselx2="531" data-cropsely1="0" data-cropsely2="193" data-ratio="0.27666666666666667" data-type="png" data-w="1200" data-src="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEnUN3do5UjazdvBic02SQsmkGub6eMBMR8ssYcBaUfXjJ0tV1ribPj68LQ/640?wx_fmt=png" data-backw="574" data-backh="158" data-before-oversubscription-url="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEnUN3do5UjazdvBic02SQsmkGub6eMBMR8ssYcBaUfXjJ0tV1ribPj68LQ/640?wx_fmt=png"_width="100%"src="https://img2018.cnblogs.com/blog/1112483/201911/1112483-20191112133736561-138963095.png" crossorigin="anonymous" data-fail="0"></figure><h3></h3><h3><br></h3><h3><span>3.1.3、独占式/共享式锁获取</span></h3><p><br></p><p><span>独占式:有且只有一个线程能获取到锁,如:ReentrantLock。</span></p><p><span><br></span></p><p><span>共享式:可以多个线程同时获取到锁,如:CountDownLatch</span></p><h4><span><br></span></h4><h4><span>独占式</span></h4><p><br></p><p><span>每个节点自旋观察自己的前一节点是不是Header节点,如果是,就去尝试获取锁。</span></p><p><br></p><figure><img data-cropselx1="0" data-cropselx2="533" data-cropsely1="0" data-cropsely2="135" data-ratio="0.255" data-type="png" data-w="1200" data-src="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEnMNHticMMzBsDdSDGbp2s4N7yckcQ4x6Qq2rTJRQlh1q6UL9vncZQj6w/640?wx_fmt=png" data-backw="574" data-backh="146" data-before-oversubscription-url="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEnMNHticMMzBsDdSDGbp2s4N7yckcQ4x6Qq2rTJRQlh1q6UL9vncZQj6w/640?wx_fmt=png"_width="100%"src="https://img2018.cnblogs.com/blog/1112483/201911/1112483-20191112133754059-1054383585.png" crossorigin="anonymous" data-fail="0"></figure><p><br></p><p><span>独占式锁获取流程:</span></p><p><br></p><figure><img data-cropselx1="0" data-cropselx2="570" data-cropsely1="0" data-cropsely2="564" data-ratio="0.9883333333333333" data-type="png" data-w="1200" data-src="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEnETznz47tvRbtec3AoduyNcxASp8DMzerR794icPNkjPSYepeZn2jRkw/640?wx_fmt=png" data-backw="574" data-backh="568" data-before-oversubscription-url="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEnETznz47tvRbtec3AoduyNcxASp8DMzerR794icPNkjPSYepeZn2jRkw/640?wx_fmt=png"_width="100%"src="https://img2018.cnblogs.com/blog/1112483/201911/1112483-20191112133809834-20395484.png" crossorigin="anonymous" data-fail="0"></figure><h4></h4><h4><span><br></span></h4><h4><span>共享式:</span></h4><p><br></p><p><span>共享式与独占式的区别:</span></p><p><br></p><figure><img data-cropselx1="0" data-cropselx2="533" data-cropsely1="0" data-cropsely2="300" data-ratio="0.565" data-type="png" data-w="1200" data-src="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEn1qeTXibFkuxicOcHibkJRowMQTRa8JKX9YQDNvQcbiaDB0hGYq7AFhT0YQ/640?wx_fmt=png" data-backw="574" data-backh="324" data-before-oversubscription-url="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEn1qeTXibFkuxicOcHibkJRowMQTRa8JKX9YQDNvQcbiaDB0hGYq7AFhT0YQ/640?wx_fmt=png"_width="100%"src="https://img2018.cnblogs.com/blog/1112483/201911/1112483-20191112133833324-2045100329.png" crossorigin="anonymous" data-fail="0"></figure><p><br></p><p><span>共享锁获取流程:</span></p><p><br></p><figure><img data-cropselx1="0" data-cropselx2="570" data-cropsely1="0" data-cropsely2="403" data-ratio="0.7061923583662714" data-type="png" data-w="759" data-src="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEnNMyJGl6b1w1Obwqfl9kPwrgluJ8CTafxURicNXFGf9JLu7iaNHpZ3ruQ/640?wx_fmt=png" data-backw="574" data-backh="406" data-before-oversubscription-url="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEnNMyJGl6b1w1Obwqfl9kPwrgluJ8CTafxURicNXFGf9JLu7iaNHpZ3ruQ/640?wx_fmt=png"_width="100%"src="https://img2018.cnblogs.com/blog/1112483/201911/1112483-20191112133847909-32641824.png" crossorigin="anonymous" data-fail="0"></figure><h1></h1><h1><span><br></span></h1><h1><span>4</span>、锁的使用用例</h1><p><br></p><h2><span>4.1、ConcurrentHashMap的实现原理及使用</span></h2><p><span><br></span></p><p><br></p><figure><img data-cropselx1="0" data-cropselx2="570" data-cropsely1="0" data-cropsely2="466" data-ratio="0.8166666666666667" data-type="png" data-w="1200" data-src="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEnzHK1Qt9ibEicX0HXspWBKz5TxPqia8SFusYBdwZJTA4N04n45ngRtqQgw/640?wx_fmt=png" data-backw="574" data-backh="469" data-before-oversubscription-url="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEnzHK1Qt9ibEicX0HXspWBKz5TxPqia8SFusYBdwZJTA4N04n45ngRtqQgw/640?wx_fmt=png"_width="100%"src="https://img2018.cnblogs.com/blog/1112483/201911/1112483-20191112133903649-1059587046.png" crossorigin="anonymous" data-fail="0"><figcaption><br></figcaption><figcaption><span>ConcurrentHashMap类图</span></figcaption><figcaption><span><br></span></figcaption></figure><figure><img data-cropselx1="0" data-cropselx2="570" data-cropsely1="0" data-cropsely2="292" data-ratio="0.5108333333333334" data-type="png" data-w="1200" data-src="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEn8WYuoicLufp6B4Hu71YCdvtJRIKXfGI6T9bfAda4z7rRiauP5kR3ILXw/640?wx_fmt=png" data-backw="574" data-backh="293" data-before-oversubscription-url="https://mmbiz.qpic.cn/mmbiz_png/QCu849YTaIPksEhmXNlhegkc6ice6GYEn8WYuoicLufp6B4Hu71YCdvtJRIKXfGI6T9bfAda4z7rRiauP5kR3ILXw/640?wx_fmt=png"_width="100%"src="https://img2018.cnblogs.com/blog/1112483/201911/1112483-20191112133919142-1191235800.png" crossorigin="anonymous" data-fail="0"><figcaption><br></figcaption><figcaption><span>ConcurrentHashMap数据结构</span></figcaption></figure><p><br></p><p><span>结论:ConcurrentHashMap使用的锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。</span></p></section>
原文地址:https://mp.weixin.qq.com/s/3BITJAlZUa2AoVFtcV7xsA </div>
Java 中的队列同步器 AQS
一、AQS 概念
1、队列同步器是用来构建锁或者其他同步组件的基础框架,使用一个 int 型变量代表同步状态,通过内置的队列来完成线程的排队工作。
2、下面是 JDK8 文档中对于 AQS 的部分介绍
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements Serializable
提供一个框架,用于实现依赖先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量,事件等)。 该类被设计为大多数类型的同步器的有用依据,这些同步器依赖于单个原子int值来表示状
态。子类必须定义改变此状态的protected方法,以及根据该对象被获取或释放来定义该状态的含义。给定这些,这个类中的其他方法执行所有排队和阻塞机制。 子类可以保持其他状态字段,但只以
原子方式更新int使用方法操纵值getState() , setState(int)和compareAndSetState(int, int)被跟踪相对于同步。
此类支持默认独占模式和共享模式。 当以独占模式获取时,尝试通过其他线程获取不能成功。 多线程获取的共享模式可能(但不需要)成功。 除了在机械意义上,这个类不理解这些差异,当共享
模式获取成功时,下一个等待线程(如果存在)也必须确定它是否也可以获取。 在不同模式下等待的线程共享相同的FIFO队列。 通常,实现子类只支持这些模式之一,但是两者都可以在
ReadWriteLock中发挥作用。仅支持独占或仅共享模式的子类不需要定义支持未使用模式的方法。
总结来说就是:
①子类通过继承 AQS 并实现其抽象方法来管理同步状态,对于同步状态的更改通过提供的 getState()、setState(int state)、compareAndSetState(int expect, int update) 来进行操作,因为使用 CAS 操作保证同步状态的改变是原子的。
②子类被推荐定义为自定义同步组件的静态内部类,同步器本身并没有实现任何的同步接口,仅仅是定义了若干状态获取和释放的方法来提供自定义同步组件的使用。
③同步器既可以支持独占式的获取同步状态,也可以支持共享式的获取同步状态(ReentrantLock、ReentrantReadWriteLock、CountDownLatch 等不同类型的同步组件)
3、同步器是实现锁的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义;
二、AQS 的接口和实例
1、同步器的设计实现原理
继承同步器并且重写指定的方法,然后将同步器组合在自定义同步组件的实现中,并且调用同步器提供的模板方法(这些模板方法会调用重写的方法);而重写指定的方法的时候,需要使用 getState()、setState(int state)、compareAndSetState(int expect, int update) 来访问或者更新同步状态。下面是源码中 state 变量和三个方法的定义声明实现
1 /**
2 * .(同步状态)
3 */
4 private volatile int state;
5
6 /**
7 * (返回当前的同步状态)
8 * 此操作的内存语义为@code volatile read
9 */
10 protected final int getState() {
11 return state;
12 }
13
14 /**
15 * (设置新的同步状态)
16 * 此操作的内存语义为@code volatile read
17 */
18 protected final void setState(int newState) {
19 state = newState;
20 }
21
22 /**
23 * (如果要更新的状态和期望的状态相同,那就通过原子的方式更新状态)
24 * ( 此操作的内存语义为@code volatile read 和 write)
25 * (如果更新的状态和期望的状态不同就返回false)
26 */
27 protected final boolean compareAndSetState(int expect, int update) {
28 return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
29 }
2、下面介绍 AQS 提供可被重写的方法
1 /**
2 * 独占式的获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态
3 *
4 */
5 protected boolean tryAcquire(int arg) {
6 throw new UnsupportedOperationException();
7 }
8
9 /**
10 * 独占式的释放同步状态,等待获取同步状态的线程可以有机会获取同步状态
11 *
12 */
13 protected boolean tryRelease(int arg) {
14 throw new UnsupportedOperationException();
15 }
16
17 /**
18 * 尝试以共享模式获取。 该方法应该查询对象的状态是否允许在共享模式下获取该对象,如果是这样,就可以获取它。 该方法总是由执行获取的线程调用。
19 * 如果此方法报告失败,则获取方法可能将线程排队(如果尚未排队),直到被其他线程释放为止。 获取失败时返回负值,如果在获取成共享模式下功但没
20 * 有后续共享模式获取可以成功,则为零; 并且如果以共享模式获取成功并且随后的共享模式获取可能成功,则为正值,在这种情况下,后续等待线程必须检查可用性。
21 */
22 protected int tryAcquireShared(int arg) {
23 throw new UnsupportedOperationException(); //如果不支持共享模式 ,会抛出该异常
24 }
25
26 /**
27 * 尝试将状态设置为以共享模式释放同步状态。 该方法总是由执行释放的线程调用。
28 */
29 protected int tryReleaseShared(int arg) {
30 throw new UnsupportedOperationException(); //如果不支持共享模式 ,会抛出该异常
31 }
32
33 /**
34 * 当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占
35 */
36 protected int isHeldExclusively(int arg) {
37 throw new UnsupportedOperationException(); //如果不支持共享模式 ,会抛出该异常
38 }
3、同步器提供的模板方法
在实现自定义同步组件的时候,需要重写上面的方法,而下面的模板方法会调用上面重写的方法。下面介绍同步器提供的模板方法
1 /**
2 * 以独占模式获取,忽略中断。 通过调用至少一次tryAcquire(int)实现,成功返回。 否则线
3 * 程排队,可能会重复阻塞和解除阻塞,直到成功才调用tryAcquire(int)
4 */
5 public final void acquire(int arg) {...}
6
7 /**
8 * 以独占方式获得,如果中断,中止。 通过首先检查中断状态,然后调用至少一次
9 * tryAcquire(int) ,成功返回。 否则线程排队,可能会重复阻塞和解除阻塞,调用
10 * tryAcquire(int)直到成功或线程中断。
11 */
12 public final void acquireInterruptibly(int arg) throws InterruptedException {...}
13
14 /**
15 * 尝试以独占模式获取,如果中断则中止,如果给定的超时时间失败。 首先检查中断状态,然
16 * 后调用至少一次tryAcquire(int) ,成功返回。 否则,线程排队,可能会重复阻塞和解除阻
17 * 塞,调用tryAcquire(int)直到成功或线程中断或超时
18 */
19 public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {...}
20
21 /**
22 * 以共享模式获取,忽略中断。 通过首次调用至少一次执行 tryAcquireShared(int),成功返
23 * 回。 否则线程排队,可能会重复阻塞和解除阻塞,直到成功调用tryAcquireShared(int) 。
24 */
25 public final void acquireShared(int arg){...}
26
27 /**
28 * 以共享方式获取,如果中断,中止。 首先检查中断状态,然后调用至少一次
29 * tryAcquireShared(int) ,成功返回。 否则线程排队,可能会重复阻塞和解除阻塞,调用
30 * tryAcquireShared(int)直到成功或线程中断。
31 */
32 public final void acquireSharedInterruptibly(int arg) throws InterruptedException{...}
33
34 /**
35 * 尝试以共享模式获取,如果中断则中止,如果给定的时间超过,则失败。 通过首先检查中断
36 * 状态,然后调用至少一次tryAcquireShared(int) ,成功返回。 否则,线程排队,可能会重
37 * 复阻塞和解除阻塞,调用tryAcquireShared(int)直到成功或线程中断或超时。
38 */
39 public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException{...}
40
41 /**
42 * 独占式的释放同步状态,该方法会在释放同步状态之后,将同步队列中的第一个节点包含的线程唤醒
43 */
44 public final boolean release(int arg){...}
45
46 /**
47 * 共享式的释放同步状态
48 */
49 public final boolean releaseShared(int arg){...}
50
51 /**
52 * 获取在等待队列上的线程集合
53 */
54 public final Collection<Thread> getQueuedThreads(){...}
三、队列同步器的实现分析
1、同步队列
a) t 同步队列的实现原理
AQS 内部维护一个同步队列来完成同步状态的管理,当前线程获取同步状态失败的时候,AQS 会将当前线程以及等待状态信息构造成一个结点 Node 并将其加入同步队列中,同时阻塞当前线程,当同步状态由持有线程释放的时候,会将同步队列中的首节点唤醒使其再次尝试获取同步状态。同步队列中的结点用来保存获取同步状态失败的线程的线程引用、等待状态以及前驱结点和后继结点。下面是 Node 的属性分析
1 static final class Node {
2 /** 共享模式下构造结点 */
3 static final Node SHARED = new Node();
4 /** 独占模式下构造结点 */
5 static final Node EXCLUSIVE = null;
6
7 /** 用于指示线程已经取消的waitStatus值(由于在同步队列中等待的线程等待超时或者发生中断,需要从同步队列中取消等待,结点进入该状态将不会发生变化)*/
8 static final int CANCELLED = 1;
9 /** waitstatus值指示后续线程需要取消等待(后继结点的线程处于等待状态,而当前结点的线程如果释放了同步状态或者CANCELL,将会通知后继结点的线程以运行) */
10 static final int SIGNAL = -1;
11 /**waitStatus值表示线程正在等待条件(原本结点在等待队列中,结点线程等待在Condition上,当其他线程对Condition调用了signal()方法之后)该结点会从
等待队列中转移到同步队列中,进行同步状态的获取 */
12 static final int CONDITION = -2;
13 /**
14 * waitStatus值表示下一个共享式同步状态的获取应该无条件传播下去
15 */
16 static final int PROPAGATE = -3;
17
18 /**
19 * 不同的等到状态的int值
20 */
21 volatile int waitStatus;
22
23 /**
24 * 前驱结点,当结点加入同步队列将会被设置前驱结点信息
25 */
26 volatile Node prev;
27
28 /**
29 * 后继结点
30 */
31 volatile Node next;
32
33 /**
34 * 当前获取到同步状态的线程
35 */
36 volatile Thread thread;
37
38 /**
39 * 等待队列中的后继结点,如果当前结点是共享的,那么这个字段是一个SHARED常量;也就是说结点类型(独占和共享)和等待队列中的后继结点公用一个字段
40 */
41 Node nextWaiter;
42
43 /**
44 * 如果是共享模式下等待,那么返回true(因为上面的Node nextWaiter字段在共享模式下是一个SHARED常量)
45 */
46 final boolean isShared() {
47 return nextWaiter == SHARED;
48 }
49
50 final Node predecessor() throws NullPointerException {
51 Node p = prev;
52 if (p == null)
53 throw new NullPointerException();
54 else
55 return p;
56 }
57
58 Node() { // 用于建立初始头结点或SHARED标记
59 }
60
61 Node(Thread thread, Node mode) { // 用于添加到等待队列
62 this.nextWaiter = mode;
63 this.thread = thread;
64 }
65
66 Node(Thread thread, int waitStatus) { // Used by Condition
67 this.waitStatus = waitStatus;
68 this.thread = thread;
69 }
70 }
b)同步队列示意图和简单分析
①同步队列示意图:当一个线程获取了同步状态后,其他线程不能获取到该同步状态,就会被构造称为 Node 然后添加到同步队列之中,这个添加的过程基于 CAS 保证线程安全性。
②同步队列遵循先进先出 (FIFO),首节点是获取到同步状态的结点,首节点的线程在释放同步状态的时候将会唤醒后继结点(然后后继结点就会变成新的首节点等待获取同步状态)
2、独占式同步状态的获取和释放
①前面说过,同步器的 acquire () 方法会获取同步状态,这个方法对不会响应中断,也就是说当线程获取通同步状态失败后会被构造成结点加入到同步队列中,当线程被中断时不会从同步队列中移除。
1 /**
2 * ①首先调用tryAcquire方法尝试获取同步状态,如果获取同步状态失败,就进行下面的操作
3 * ②获取失败:按照独占式的模式构造同步结点并通过addWaiter方法将结点添加到同步队列的尾部
4 * ③通过acquireQueue方法自旋获取同步状态。
5 * ④如果获取不到同步状态,就阻塞结点中的线程,而结点中的线程唤醒主要是通过前驱结点的出队或者被中断来实现
6 */
7 public final void acquire(int arg) {
8 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
9 selfInterrupt();
10 }
②下面是 addWaiter、enq 和自旋获取同步状态 acquireQueue 方法的实现(该方法的主要作用就是将获取同步状态失败的线程构造成结点然后添加到同步队列的队尾)
1 private Node addWaiter(Node mode) {
2 Node node = new Node(Thread.currentThread(), mode);
3 //尝试直接放在队尾
4 Node pred = tail; //直接获取同步器的tail结点
5 if (pred != null) {
6 node.prev = pred;
7 if (compareAndSetTail(pred, node)) {
8 //队尾结点不为空通过原子操作将构造的结点置为队尾结点
9 pred.next = node;
10 return node;
11 }
12 }
13 //采用自旋方式保证构造的结点添加到同步队列中
14 enq(node);
15 return node;
16 }
17 private Node enq(final Node node) {
18 for (;;) { //死循环知道添加成功
19 Node t = tail;
20 if (t == null) { // Must initialize
21 if (compareAndSetHead(new Node()))
22 tail = head;
23 } else {
24 node.prev = t;
25 //通过CAS方式将结点添加到同步队列之后才会返回,否则就会不断尝试添加(这样实际上就是在并发情况下,把向同步队列添加Node变得串行化了)
26 if (compareAndSetTail(t, node)) {
27 t.next = node;
28 return t;
29 }
30 }
31 }
32 }
33 /**
34 * 通过tryAcquire()和addWaiter(),表示该线程获取同步状态已经失败,被放入同步
35 * 队列尾部了。线程阻塞等待直到其他线程(前驱结点获得同步装填或者被中断)释放同步状
36 * 态后唤醒自己,自己才能获得。
37 */
38 final boolean acquireQueued(final Node node, int arg) {
39 boolean failed = true;
40 try {
41 boolean interrupted = false;
42 //线程在死循环的方式中尝试获取同步状态
43 for (;;) {
44 final Node p = node.predecessor(); //获取前驱结点
45 //只有前驱接待是头结点的时候才能尝试获取同步状态
46 if (p == head && tryAcquire(arg)) {
47 setHead(node); //获取到同步状态之后,就将自己设置为头结点
48 p.next = null; //前驱结点已经获得同步状态去执行自己的程序了,所以需要释放掉占用的同步队列的资源,由JVM回收
49 failed = false;
50 return interrupted;
51 }
52 //如果获取同步状态失败,应该自旋等待继续获取并且校验自己的中断标志位信息
53 if (shouldParkAfterFailedAcquire(p, node) &&
54 parkAndCheckInterrupt())
55 interrupted = true; //如果被中断,就改变自己的中断标志位状态信息
56 }
57 } finally {
58 if (failed)
59 cancelAcquire(node);
60 }
61 }
③独占式获取同步状态的整个流程
④独占式同步器的释放:release 方法执行时,会唤醒头结点的后继结点线程
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;//头结点
//唤醒头结点的后继结点线程
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
3、共享式同步状态的获取和释放
①共享式获取和独占式获取最主要的区别是能否有多个线程同时获取到同步状态。如图所示简易描述二者的区别(共享式访问的时候,可以允许多个线程访问资源,但是存在独占式访问的时候,同一时刻其他的不管是共享还是独占都会被阻塞)
②关于共享式获取同步状态的方法
1 /**
2 * 此方法是共享模式下线程获取共享同步状态的顶层入口。它会尝试去获取同步状态,获取成功则直接返回,
3 * 获取失败则进入等待队列一直尝试获取(执行doAcquireShared方法体中的内容),直到获取到资源为止(条件就是tryAcquireShared方法返回值大于等于0),整个过程忽略中断
4 */
5 public final void acquireShared(int arg) {
6 if (tryAcquireShared(arg) < 0)
7 doAcquireShared(arg);
8 }
9 /**
10 * "自旋"尝试获取同步状态
11 */
12 private void doAcquireShared(int arg) {
13 //首先将该线程包括线程引用、等待状态、前驱结点和后继结点的信息封装台Node中,然后添加到等待队列里面(一共享模式添加)
14 final Node node = addWaiter(Node.SHARED);
15 boolean failed = true;
16 try {
17 boolean interrupted = false; //当前线程的中断标志
18 for (;;) {
19 final Node p = node.predecessor(); //获取前驱结点
20 if (p == head) {
21 //当前驱结点是头结点的时候就会以共享的方式去尝试获取同步状态
22 int r = tryAcquireShared(arg);
23 //判断tryAcquireShared的返回值
24 if (r >= 0) {
25 //如果返回值大于等于0,表示获取同步状态成功,就修改当前的头结点并将信息传播都后续的结点队列中
26 setHeadAndPropagate(node, r);
27 p.next = null; // 释放掉已经获取到同步状态的前驱结点的资源
28 if (interrupted)
29 selfInterrupt(); //检查中断标志
30 failed = false;
31 return;
32 }
33 }
34 if (shouldParkAfterFailedAcquire(p, node) &&
35 parkAndCheckInterrupt())
36 interrupted = true;
37 }
38 } finally {
39 if (failed)
40 cancelAcquire(node);
41 }
42 }
根据源代码我们可以了解共享式获取同步状态的整个过程
首先同步器会调用 tryAcquireShared 方法来尝试获取同步状态,然后根据这个返回值来判断是否获取到同步状态(当返回值大于等于 0 可视为获取到同步状态);如果第一次获取失败的话,就进入 '' 自旋 '' 状态(执行 doAcquireShared 方法)一直尝试去获取同步状态;在自旋获取中,如果检查到当前前驱结点是头结点的话,就会尝试获取同步状态,而一旦获取成功(tryAcquireShared 方法返回值大于等于 0)就可以从自旋状态退出。
另外,还有一点就是上面说到的一个处于等待队列的线程要想开始尝试去获取同步状态,需要满足的条件就是前驱结点是头结点,那么它本身就是整个队列中的第二个结点。当头结点释放掉所有的临界资源之后,我们考虑每个线程运行所需资源的不同数量问题,如下图所示
③共享式同步状态的释放
对于支持共享式的同步组件 (即多个线程同同时访问),它们和独占式的主要区别就是 tryReleaseShared 方法必须确保同步状态的释放是线程安全的 (CAS 的模式来释放同步状态,因为既然是多个线程能够访问,那么释放的时候也会是多个线程的,就需要保证释放时候的线程安全)
1 /**
2 * 该方法是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源。
3 */
4 public final boolean releaseShared(int arg) {
5 if (tryReleaseShared(arg)) {
6 doReleaseShared(); //
7 return true;
8 }
9 return false;
10 }
四、自定义同步组件的实现
1、共享式锁的实现
①、自定义一个同步组件,可以允许两个线程访问(共享式同步组件),超过两个线程就会被阻塞。
②、既然是共享式同步组件,按照前面所说的,组件本身需要使用 AQS 提供的共享式模板方法 acquireShared 等;组件的内部类需要实现 AQS,并且重写关于共享式获取同步状态的方法 (tryAcquireShared ()、tryReleaseShared () 等共享模式下的方法)。
③、既然是两个线程能够同时访问的话,那么状态数的取值范围就是 0、1、2 了,每当一个线程获取到同步状态的时候 state 值减 1,反之就会增加 1;当 state 值为 0 的时候就会阻塞其他想要获取同步状态的线程。对于同步状态的更改需要使用 CAS 来进行保证原子性。


1 package cn.source.concurrent;
2
3 import java.util.concurrent.TimeUnit;
4 import java.util.concurrent.locks.AbstractQueuedSynchronizer;
5 import java.util.concurrent.locks.Condition;
6 import java.util.concurrent.locks.Lock;
7
8 public class TestAQS implements Lock{
9
10 private Sync sync = new Sync(2);
11
12 private static class Sync extends AbstractQueuedSynchronizer {
13
14 Sync(int num) {
15 if(num <= 0) {
16 throw new RuntimeException("num需要大于0");
17 }
18 setState(num);
19 }
20
21 @Override
22 protected int tryAcquireShared(int arg) {
23 for(; ;) {
24 int currentState = getState();
25 int newState = currentState - arg;
26 if(newState < 0 || compareAndSetState(currentState, newState)) {
27 return newState;
28 }
29 }
30 }
31
32 @Override
33 protected boolean tryReleaseShared(int arg) {
34 for(; ;) {
35 int currentState = getState();
36 int newState = currentState + arg;
37 if(compareAndSetState(currentState, newState)) {
38 return true;
39 }
40 }
41 }
42
43
44 }
45 @Override
46 public void lock() {
47 sync.acquireShared(1);
48 }
49
50 @Override
51 public void unlock() {
52 sync.releaseShared(1);
53 }
54
55 //......
56 }


1 /**
2 * 测试结果:输出的线程名称是成对的,保证同一时刻只有两个线程能够获取到锁
3 *
4 */
5 public class TestLockShare {
6 @Test
7 public void test() {
8 Lock lock = new TestAQS();
9 class Worker extends Thread {
10
11 @Override
12 public void run() {
13 while(true) {
14 lock.lock();
15 try {
16 Thread.sleep(1000);
17 System.out.println(Thread.currentThread().getName());
18 Thread.sleep(1000);
19 } catch (Exception e) {
20 e.printStackTrace();
21 } finally {
22 lock.unlock();
23 }
24 }
25 }
26
27 }
28
29 for (int i = 0; i < 8; i++) {
30 Worker worker = new Worker();
31 worker.setDaemon(true);
32 worker.start();
33
34 }
35 for (int i = 0; i < 8; i++) {
36 try {
37 Thread.sleep(1000);
38 } catch (InterruptedException e) {
39 // TODO Auto-generated catch block
40 e.printStackTrace();
41 }
42 System.out.println();
43 }
44 }
45 }
2、独占式锁的实现


1 package cn.source.concurrent;
2
3 import java.util.concurrent.TimeUnit;
4 import java.util.concurrent.locks.AbstractQueuedSynchronizer;
5 import java.util.concurrent.locks.Condition;
6 import java.util.concurrent.locks.Lock;
7
8 public class Mutex implements Lock{
9
10 private Sync sync = new Sync();
11
12 private static class Sync extends AbstractQueuedSynchronizer {
13
14 /**
15 * 尝试获取资源,立即返回。成功则返回true,否则false。
16 */
17 @Override
18 protected boolean tryAcquire(int arg) {
19 if(compareAndSetState(0, 1)) {//state为0才设置为1,不可重入!
20 setExclusiveOwnerThread(Thread.currentThread());//设置为当前线程独占资源
21 return true;
22 }
23 return false;
24 }
25
26 /**
27 * 尝试释放资源,立即返回。成功则为true,否则false。
28 */
29 @Override
30 protected boolean tryRelease(int arg) {
31 if(getState() == 0) { //既然来释放,那肯定就是已占有状态了。只是为了保险,多层判断!
32 throw new IllegalMonitorStateException();
33 }
34 setExclusiveOwnerThread(null);
35 setState(0);
36 return true;
37 }
38
39 @Override
40 protected boolean isHeldExclusively() {
41 // 判断是否锁定状态
42 return getState() == 1;
43 }
44
45 }
46
47 @Override
48 public void lock() {
49 sync.acquire(1);
50 }
51
52 @Override
53 public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
54 return sync.tryAcquire(1);
55 }
56
57 @Override
58 public void unlock() {
59 sync.release(1);
60 }
61
62 }


1 public class TestMutex {
2 @Test
3 public void test() {
4 Lock lock = new Mutex();
5 class Worker extends Thread {
6
7 @Override
8 public void run() {
9 while(true) {
10 lock.lock();
11 try {
12 Thread.sleep(1000);
13 System.out.println(Thread.currentThread().getName());
14 Thread.sleep(1000);
15 } catch (Exception e) {
16 e.printStackTrace();
17 } finally {
18 lock.unlock();
19 }
20 }
21 }
22
23 }
24
25 for (int i = 0; i < 8; i++) {
26 Worker worker = new Worker();
27 worker.setDaemon(true);
28 worker.start();
29
30 }
31 for (int i = 0; i < 8; i++) {
32 try {
33 Thread.sleep(1000);
34 } catch (InterruptedException e) {
35 e.printStackTrace();
36 }
37 System.out.println();
38 }
39 }
40 }
今天的关于Java 并发之 AQS 详解和java中并发的分享已经结束,谢谢您的关注,如果想了解更多关于Java JUC 并发之 JMM 原理详解、Java 中的锁原理、锁优化、CAS、AQS 详解、Java 中的锁原理、锁优化、CAS、AQS 详解!、Java 中的队列同步器 AQS的相关知识,请在本站进行查询。
本文标签: