目录:
- 共享锁和独占锁的区别
- 共享锁实现原理
- 共享锁和独占锁在源码上有何区别
共享锁和独占锁的区别
共享锁和独占锁(排它锁)最大的区别就是,在同一时刻能否有多个线程获取同步状态。
- 独占模式,获取资源后,只有一个线程获取同步状态并执行。
- 共享模式,在获取资源后,多个线程共同执行。
共享锁实现原理
1、加锁:
共享锁和排它锁的实现原理类似,我这次就不具体说明了,直接上源码。
1 /** 2 * 共享模式,获取资源 3 */ 4 public final void acquireShared(int arg) { 5 // 成功获取资源则结束,失败则进入等待队列 6 // 小于0的都任务是获取资源失败,反之大于等于0则是成功 7 if (tryAcquireShared(arg) < 0) 8 doAcquireShared(arg); 9 }
同样的tryAcquireShared()也是交给子类实现的加锁函数,可参照CountDownLatch。
1 protected int tryAcquireShared(int arg) { 2 throw new UnsupportedOperationException(); 3 }
1 /** 2 * CountDownLatch:当状态为0的时候则可以获取锁(共享资源的状态),反之则不能 3 */ 4 protected int tryAcquireShared(int acquires) { 5 return (getState() == 0) ? 1 : -1; 6 }
——————————————————————————————————————————————————————————————————————
接下来我们来看下获取资源失败,进入队列等待执行了什么逻辑。
1 private void doAcquireShared(int arg) { 2 // 自旋添加共享模式待队尾,与独占模式一致,只是入参不同 3 final Node node = addWaiter(Node.SHARED); 4 boolean failed = true; 5 try { 6 boolean interrupted = false; 7 for (;;) { 8 final Node p = node.predecessor(); 9 if (p == head) { 10 int r = tryAcquireShared(arg); 11 // r >= 0标识共享资源获取成功 12 if (r >= 0) { 13 // 将当前结点设置为头结点,并检查后继节点是否在共享模式下等待 14 setHeadAndPropagate(node, r); 15 p.next = null; // help GC 16 if (interrupted) 17 selfInterrupt(); 18 failed = false; 19 return; 20 } 21 } 22 // 与独占模式类似,校验是否需要阻塞线程,判断中断状态 23 if (shouldParkAfterFailedAcquire(p, node) && 24 parkAndCheckInterrupt()) 25 interrupted = true; 26 } 27 } finally { 28 if (failed) 29 cancelAcquire(node); 30 } 31 }
哈哈,是不是和独占锁很像,的确!
但也有些不同,共享锁是只有线程是head.next时,也就是线程为头节点的后继节点时才会去尝试获取资源,如果还有其它节点还会唤醒之后的线程。
就这样说你可能会有些疑惑,我来解释下;首先会自旋,也就是会一直轮询第8行那块,直到满足第9行才会去执行第10行;也就是说第8行的p一定是head,而p的值是当前节点的前驱结点,所以p肯定为head的后继节点。
1 final Node predecessor() throws NullPointerException { 2 Node p = prev; 3 if (p == null) 4 throw new NullPointerException(); 5 else 6 return p; 7 }
——————————————————————————————————————————————————————————————————————
上面说到如果还有其它节点还会唤醒之后的线程,那么问题来了。
假如老大用完后释放了5个资源,而老二需要6个,老三需要1个,老四需要2个。 老大先唤醒老二,老二发现自己的资源都不够,老二是否把资源让给老三呢?
答案是否定的,老二会继续park()等待其它线程释放资源,更不会唤醒老三和老四。
——————————————————————————————————————————————————————————————————————
接下来就是我们共享锁的重点部分了,setHeadAndPropagate()。
1 /** 2 * 成为头节点,并在满足条件时唤醒后继节点 3 */ 4 private void setHeadAndPropagate(Node node, int propagate) { 5 Node h = head; // Record old head for check below 6 // 把当前节点设为头节点 7 setHead(node); 8 /* 9 * 满足以下三种情况执行唤醒操作: 10 * 1、propagate > 0,标识后继节点需要被唤醒(AQS默认只有一个是大于0的,就是线程取消,CANCELLED) 11 * 2、原头节点为null或ws < 0 12 * 3、新的头节点(也就是当前节点)为null或ws < 0 13 */ 14 if (propagate > 0 || h == null || h.waitStatus < 0 || 15 (h = head) == null || h.waitStatus < 0) { 16 Node s = node.next; 17 // 若s == null或s是共享模式,则唤醒后继线程 18 if (s == null || s.isShared()) 19 doReleaseShared(); 20 } 21 }
这部分也不是很难,你应该能很快的理解它。
——————————————————————————————————————————————————————————————————————
在上述setHeadAndPropagate()中说道满足特定条件且是共享模式时会唤醒后继线程,也就是调用doReleaseShared(),这其实就是一个解锁的过程,我们来看看具体的实现。
1 private void doReleaseShared() { 2 /* 3 * 自旋后释放后继节点 4 */ 5 for (;;) { 6 Node h = head; 7 // 表示队列中至少有2个节点 8 if (h != null && h != tail) { 9 int ws = h.waitStatus; 10 // ws == SIGNAL(后继节点需要被唤醒) 11 if (ws == Node.SIGNAL) { 12 // 通过CAS设置h的waitStatus字段为0 13 if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0)) 14 // CAS失败则继续自旋 15 continue; // loop to recheck cases 16 // CAS成功,则唤醒后继节点,执行过程同独占模式下的唤醒流程 17 unparkSuccessor(h); 18 } 19 // 同上,若h是初始状态0,则CAS为PROPAGATE(表示锁的下一次获取可以“无条件传播”) 20 else if (ws == 0 && 21 !h.compareAndSetWaitStatus(0, Node.PROPAGATE)) 22 continue; // loop on failed CAS 23 } 24 // 自旋的跳出条件,head不变则跳出,变化则一直自旋 25 // head不变表示设置完成,可以退出循环 26 // head变化,可能被唤醒的其它节点重新设置了头节点,这样头节点发生了变化,需要进行重试,保证可以传播唤醒型号 27 if (h == head) // loop if head changed 28 break; 29 } 30 }
至此acquireShared()已经解析完成了,我们来总结下:
1、首先tryAcquireShared()尝试获取资源,成功则直接返回;失败则通过doAcquireShared()进入等待队列park(), 直到被unpark()/intemupt()并成功获取到资源才返回。整个等待过程也是忽略中断的。
2、其实跟acquire()的流程大同小异,只不过多了个自己拿到资源后,还会去唤醒后继队友的操作(这才是共享嘛)。
——————————————————————————————————————————————————————————————————————
2、解锁:
解锁这个东西啊你只要把上面弄懂了,就是小事情了,我不再赘述了,哈哈。
1 public final boolean releaseShared(int arg) { 2 if (tryReleaseShared(arg)) { 3 doReleaseShared(); 4 return true; 5 } 6 return false; 7 }
共享锁和独占锁在源码上有何区别
1、获取资源
- 共享锁(doAcquireShared()):
- 是只有线程是head.next时,也就是线程为头节点的后继节点时才会去尝试获取资源。
- 如果还存在其它节点,还会唤醒之后的线程,也就是自己执行完之后还会叫队友来获取资源。
- 独占锁(acquireQueued()):
- 同共享锁,但不仅要线程为头节点的后继节点,还需要获取到锁。
- 独占锁执行完后就结束了,不会唤醒队友(脏线男枪,哈哈哈哈哈哈)。
2、释放资源:同样的共享锁会唤醒队友,而独占锁不会。