队列同步器主要包括:
1. 同步队列
2. 首节点(head)
3. 尾节点(tail)
4 独占式获取与释放同步状态
5. 共享式获取与释放同步状态
6. 超时获取与释放同步状态
1.同步队列:
1.1 一个FIFO双向队列
1.2 当前线程获取同步状态失败时,会向队列中添加节点(Node)
节点的组成:
-
-
- 线程的引用(获取同步状态失败的线程) -- Thread thread
- 等待状态 -- int waitStatus
-
- CANCELLED 值为1.
- SIGNAL 值为-1
- CONDITION 值为-2
- PROPAGATE 值为-3
- INITIAL 值为0
-
- 前驱结点 -- Node prev 前驱节点 当节点加入同步队列时被设置
- 后继节点 -- Node next
- -- Node nextWaiter
-
2. 首节点和尾节点的操作逻辑
2.1 当一个线程成功地获取了同步状态(或者锁),其他的线程将无法获取到同步状态,转而被构造称为节点并加入到同步队列的尾部。同时有多个尾节点插入同步队列是线程不安全的。同步器提供了一个基于CAS的设置尾节点的方法来保证线程安全:compareAndSetTail(Node except, Node update)---它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。
2.2 首节点是获取同步状态成功的节点,首节点的线程在释放同步状态(或者解锁)时,将会唤醒后继节点,而后继节点会在获取同步状态成功时将自己设置为首节点(head)
3 独占式同步状态获取与释放
通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对该线程进行中断操作时,线程不会从同步队列中移出
1 //同步器的acquire方法 2 public final void acquire(int arg) { 3 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 4 selfInterrupt(); 5 }
主要逻辑是:首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现
1 //同步器的addWaiter和enq方法 2 private Node addWaiter(Node mode) { 3 Node node = new Node(Thread.currentThread(), mode); 4 // 快速尝试在尾部添加 5 Node pred = tail; 6 if (pred != null) { 7 node.prev = pred; 8 if (compareAndSetTail(pred, node)) { 9 pred.next = node; 10 return node; 11 } 12 } 13 enq(node); 14 return node; 15 } 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 if (compareAndSetTail(t, node)) { 26 t.next = node; 27 return t; 28 } 29 } 30 } 31 }
在enq(final Node node)方法中,同步器通过“死循环”来保证节点的正确添加,在“死循环”中只有通过CAS将节点设置成为尾节点之后,当前线程才能从该方法返回,否则,当前线程不断地尝试设置。可以看出,enq(final Node node)方法将并发添加节点的请求通过CAS变得“串行化”了。
节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞节点的线程),代码如下:
1 final boolean acquireQueued(final Node node, int arg) { 2 boolean failed = true; 3 try { 4 boolean interrupted = false; 5 for (;;) { 6 final Node p = node.predecessor(); 7 if (p == head && tryAcquire(arg)) { 8 setHead(node); 9 p.next = null; // help GC 10 failed = false; 11 return interrupted; 12 } 13 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) 14 interrupted = true; 15 } 16 } finally { 17 if (failed) 18 cancelAcquire(node); 19 } 20 }
在acquireQueued(final Node node,int arg)方法中,当前线程在“死循环”中尝试获取同步状态,只有前驱节点是头节点才能够尝试获取同步状态,如图:
独占式同步状态获取流程,也就是acquire(int arg)方法调用流程,如图5-5所示。
当前线程获取同步状态并执行了相应逻辑之后,就会调用同步器的release(int arg)方法释放同步状态
1 //同步器的release方法 2 public final boolean release(int arg) { 3 if (tryRelease(arg)) { 4 Node h = head; 5 if (h != null && h.waitStatus != 0) 6 unparkSuccessor(h); 7 return true; 8 } 9 return false; 10 }
该方法执行时,会唤醒头节点的后继节点线程,unparkSuccessor(Node node)方法使用LockSupport(在后面的章节会专门介绍)来唤醒处于等待状态的线程
总结:在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点
4.共享式同步状态获取与释放
同一时刻可以有多个线程同时获取到同步状态。
通过调用同步器的acquireShared(int arg)方法可以共享式地获取同步状态
1 //同步器的acquireShared和doAcquireShared方法 2 public final void acquireShared(int arg) { 3 if (tryAcquireShared(arg) < 0)//tryAcquireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取到同
步状态 4 doAcquireShared(arg); 5 } 6 private void doAcquireShared(int arg) { 7 final Node node = addWaiter(Node.SHARED); 8 boolean failed = true; 9 try { 10 boolean interrupted = false; 11 for (;;) { 12 final Node p = node.predecessor(); 13 if (p == head) { 14 int r = tryAcquireShared(arg); 15 if (r >= 0) { 16 setHeadAndPropagate(node, r); 17 p.next = null; 18 if (interrupted) 19 selfInterrupt(); 20 failed = false; 21 return; 22 } 23 } 24 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) 25 interrupted = true; 26 } 27 } finally { 28 if (failed) 29 cancelAcquire(node); 30 } 31 }
与独占式一样,共享式获取也需要释放同步状态,通过调用releaseShared(int arg)方法可以释放同步状态
1 //释放同步状态 2 public final boolean releaseShared(int arg) { 3 if (tryReleaseShared(arg)) { 4 doReleaseShared(); 5 return true; 6 } 7 return false; 8 }
tryReleaseShared(int arg)方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和CAS来保证的,因为释放同步状态的操作会同时来自多个线程
5.独占式超时获取同步状态
通过调用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以响应中断和超时获取同步状态,即在指定的时间段内获取同步状态,如果获取到同步状态则返回true,否则,返回false。
1 private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { 2 long lastTime = System.nanoTime();final Node node = addWaiter(Node.EXCLUSIVE); 3 boolean failed = true; 4 try { 5 for (;;) { 6 final Node p = node.predecessor(); 7 if (p == head && tryAcquire(arg)) { 8 setHead(node); 9 p.next = null; // help GC 10 failed = false; 11 return true; 12 } 13 if (nanosTimeout <= 0) 14 return false; 15 if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) 16 LockSupport.parkNanos(this, nanosTimeout); 17 long now = System.nanoTime(); 18 //计算时间,当前时间now减去睡眠之前的时间lastTime得到已经睡眠 19 //的时间delta,然后被原有超时时间nanosTimeout减去,得到了 20 //还应该睡眠的时间 21 nanosTimeout -= now - lastTime; 22 lastTime = now; 23 if (Thread.interrupted()) 24 throw new InterruptedException(); 25 } 26 } finally { 27 if (failed) 28 cancelAcquire(node); 29 } 30 }
如果当前线程获取同步状态失败,则判断是否超时(nanosTimeout小于等于0表示已经超时),如果没有超时,重新计算超时间隔nanosTimeout,然后使当前线程等待nanosTimeout纳秒(当已到设置的超时时间,该线程会从LockSupport.parkNanos(Objectblocker,long nanos)方法返回)。
如果nanosTimeout小于等于spinForTimeoutThreshold(1000纳秒)时,将不会使该线程进行超时等待,而是进入快速的自旋过程。
6.自定义同步组件——TwinsLock (该工具在同一时刻,只允许至多两个线程同时访问,超过两个线程的访问将被阻塞 )
步骤:
- 重写tryAcquireShared(int args)方法和tryReleaseShared(int args)方法
-
定义资源数。TwinsLock在同一时刻允许至多两个线程的同时访问,表明同步资源数为2,这样可以设置初始状态status为2,当一个线程进行获取,status减1,该线程释放,则status加1,状态的合法范围为0、1和2,其中0表示当前已经有两个线程获取了同步资源。
1 public class TwinsLock implements Lock { 2 private final Sync sync = new Sync(2);//定义资源数为2 3 private static final class Sync extends AbstractQueuedSynchronizer { 4 Sync(int count) { 5 if (count <= 0) { 6 throw new IllegalArgumentException("count must large than zero."); 7 } 8 setState(count);//初始化同步状态--同步状态的初始值为同步资源数 9 } 10 public int tryAcquireShared(int reduceCount) { 11 for (;;) { 12 int current = getState(); 13 int newCount = current - reduceCount; 14 if (newCount < 0 || compareAndSetState(current, newCount)) { 15 return newCount; 16 } 17 } 18 } 19 public boolean tryReleaseShared(int returnCount) { 20 for (;;) { 21 int current = getState(); 22 int newCount = current + returnCount; 23 if (compareAndSetState(current, newCount)) { 24 return true; 25 } 26 } 27 } 28 } 29 public void lock() { 30 sync.acquireShared(1); 31 } 32 public void unlock() { 33 sync.releaseShared(1); 34 } 35 // 其他接口方法略 36 }
同步器会先计算出获取后的同步状态,然后通过CAS确保状态的正确设置,当tryAcquireShared(int reduceCount)方法返回值大于等于0时,当前线程才获取同步状态(锁)。
测试TwinsLock:
在测试用例中,定义了工作者线程Worker,该线程在执行过程中获取锁,当获取锁之后使当前线程睡眠1秒(并不释放锁),随后打印当前线程名称,最后再次睡眠1秒并释放锁
1 //测试TwinsLock 2 public class TwinsLockTest { 3 @Test 4 public void test() { 5 final Lock lock = new TwinsLock(); 6 class Worker extends Thread { 7 public void run() { 8 while (true) { 9 lock.lock(); 10 try { 11 SleepUtils.second(1); 12 System.out.println(Thread.currentThread().getName()); 13 SleepUtils.second(1); 14 } finally { 15 lock.unlock(); 16 } 17 } 18 } 19 } 20 // 启动10个线程 21 for (int i = 0; i < 10; i++) { 22 Worker w = new Worker(); 23 w.setDaemon(true); 24 w.start(); 25 } 26 // 每隔1秒换行 27 for (int i = 0; i < 10; i++) { 28 SleepUtils.second(1); 29 System.out.println(); 30 } 31 } 32 }