我是靠谱客的博主 曾经茉莉,这篇文章主要介绍java 并发(锁、AQS),现在分享给大家,希望可以做个参考。

锁大概有以下名词:
阻塞锁,可重入锁,读写锁,互斥锁,悲观锁,乐观锁,公平锁,偏向锁,对象锁,线程锁,锁粗化,锁消除,轻量级锁,重量级锁,信号量,独享锁,共享锁,分段锁

1. 常见的锁

Synchronized 和 Lock

Synchronized,它就是一个:非公平,悲观,独享,互斥,可重入的重量级锁。原生语义上实现的锁。
以下两个锁都在JUC包下,是API层面上的实现:
ReentrantLock,它是一个:默认非公平但可实现公平的,悲观,独享,互斥,可重入,重量级锁。
ReentrantReadWriteLocK,它是一个,默认非公平但可实现公平的,悲观,写独享,读共享,读写,可重入,重量级锁。

1.1 公平锁/非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁。非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获得锁。有可能会造成优先级反转或者饥饿现象。对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

1.2 乐观锁/悲观锁

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。比如Java里面的同步原语synchronized关键字的实现也是悲观锁。

乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

1.3 独享锁/共享锁

独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReentrantReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。对于Synchronized而言,当然是独享锁。

1.4 互斥锁/读写锁

独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。互斥锁在Java中的具体实现就是ReentrantLock,读写锁在Java中的具体实现就是ReentrantReadWriteLock

1.5 可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

2. synchronized

2.1 Synchronized的作用主要有3个:

  • 原子性:让线程互斥的访问同步代码
  • 可见性:保证共享变量的修改能够及时可见
  • 有序性:解决重排序问题

2.2 Synchronized 使用:

  • 修饰普通方法【监视器锁(monitor)是对象实例(this)】
  • 修饰静态方法【监视器锁(monitor)是对象的Class实例,因为Class数据存在于永久代,因此静态方法锁相当于该类的一个全局锁】
  • 修改代码块【监视器锁(monitor)便是括号括起来的对象实例】
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test { public synchronized void test(){ } public synchronized static void test1(){ } public void test2(){ synchronized (Test.class) { } } }

2.3 Synchronized 实现

同步方法:JVM 使用 ACC_SYNCHRONIZED 标识来实现。同步代码块,JVM使用 monitorenter 和 monitorexit 指令来实现同步。
同步代码块:每个对象都会与一个monitor相关联,当 monitor 被占用时,就会处于锁定状态,当线程执行到 monitorenter 指令时,就会去尝试获得对应的monitor。步骤如下:

  • 每个 monitor 维护着一个记录着拥有次数的计数器。未被拥有的 monitor 的该计数器为0,当一个线程获得 monitor 后,该计数器自增变为 1 。
  • 当同一个线程再次获得该 monitor 的时候,计数器再次自增;
  • 如果其他线程已经占有了 monitor ,则该线程进入阻塞状态,直到 monitor 计数器为0,再重新尝试获取 monitor
  • 当一个线程释放 monitor(执行monitorexit指令)的时候,计数器自减。当计数器为 0 的时候,monitor将被释放,其他线程便可以获得monitor。

同步方法:同步方法是隐式的。一个同步方法会在运行时常量池中的 method_info 结构体中存放 ACC_SYNCHRONIZED 标识符。当一个线程访问方法时,会去检查是否存在 ACC_SYNCHRONIZED 标识,如果存在,则先要获得对应的 monitor 锁,然后执行方法。当方法执行结束(不管是正常return还是抛出异常)都会释放对应的 monitor 锁。如果此时有其他线程也想要访问这个方法时,会因得不到monitor锁而阻塞。

3. 锁优化

JDK1.6 实现各种锁优化,如适应性自旋,锁消除,锁粗化,轻量级锁,偏向锁等,这些技术都是为了在线程间更高效的共享数据,以及解决竞争问题。

3.1 自旋锁与自适应自旋

在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得(java 线程是映射在内核之上的,线程的挂起和恢复会极大的影响开销)。如果物理机器有一个以上的处理器, 能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下",但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待。我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁(1.4.2)。
自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此, 如果锁被占用的时间很短, 自旋等待的效果就会非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。因此,自旋等待的时间必须要有一定的限度, 如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是10次。
在JDK 1.6中引人了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上, 自旋等待刚刚成功获得过锁,井且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。

3.2 锁消除

锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步, 但是被检测到不可能存在共享数据竞争的锁进行消除。
StringBuffer 的 append 方法用了 synchronized 关键词,它是线程安全的。如果在线程方法的局部变量中使用 StringBuffer,由于不同线程调用方法时都会创建不同的对象(在当前线程的虚拟机栈中创建栈帧),不会出现线程安全问题,所以 append() 没必要加锁,会进行锁消除。

3.3 锁粗化

如果系列的连续操作 都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的:那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。此时可以把多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。

3.4 轻量级锁

轻量级锁并不是用来代替重量级锁的, 它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

对象头

HotSpot 虚拟机的对象头分为两部分信息,第一部分用于存储对象自身的运行时数据,如哈看码(HashCode)、 GC分代年龄(Generational GC Age)等,官方称它为"Mark Word",它是实现轻量级锁和偏向锁的关键。另外一部分用于存储指向方法区对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分用于存储数组长度。
在这里插入图片描述
轻量级锁:在代码进人同步块的时候,如果此同步对象没有被锁定(锁标志位为 “01” 状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的 Mark Word 的拷贝。
在这里插入图片描述

然后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark Word 的锁标志位将转变为 “00” 。即表示此对象处于轻量级锁定状态。
在这里插入图片描述

如果这个更新操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧。如果是说明当前线程已经拥有该对象的锁,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程抢占了。如果有两条以上的线程争用同一个锁,(自旋失败后)那轻量级锁就不再有效,要膨胀为重量级镇,锁标志的状态值变为 ”10“ ,Mark Word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻寒状态。
它的解锁过程也是通过 CAS 操作来进行的,如果对象的 Mak Word 仍然指向着线程的锁记录,那就用 CAS 操作把对象当前的 Mark Word 和线程中复制的Displaced Mark Word 替换回来,如果替换成功整个同步过程就完成了。如果替换失败,说明有其他线程尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程。
如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。

3.5 偏向锁

偏向锁的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用 CAS 操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都清除掉,连CAS操作都不做了。
偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。
假设当前虚拟机启用了偏向锁,那么,当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为 “01”, 即偏向模式。同时使用 CAS 操作把获取到这个锁的线程的 ID 记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作。
当有另外一个线程去尝试获取这个锁时,偏向模式就宜告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向后恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态,后续同步操作同轻量级锁那样执行。

3.6 synchronized锁流程如下:

  • 检测 Mark Word 里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁。
  • 如果不是,则使用CAS将当前线程的 ID 替换 Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1
  • 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
  • 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
  • 如果自旋成功则依然处于轻量级状态。如果自旋失败,则升级为重量级锁。

在这里插入图片描述

3.7 几种锁的对比

在这里插入图片描述

4. Lock 锁

4.1 Lock 和 synchronized 的不同点

  • Lock是一个接口,而 synchronized 是 Java 中的关键字, synchronized是内置的语言实现(虚拟机级), lock是通过代码实现的.(API级)
  • synchronized 在发生异常时,会自动释放线程占有的锁;而 Lock 在发生异常时,如果没有主动通过 unLock() 去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
  • Lock 可以让等待锁的线程响应中断,线程可以中断去干别的事务,而 synchronized 却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
  • 通过 Lock 可以知道有没有成功获取锁,而synchronized却无法办到。
  • Lock 可以提高多个线程进行读操作的效率。
    在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

4.2 主要的实现类

ReentrantLock

此类中有3个内部类,分别是Sync抽象同步器、NonfairSync非公平锁同步器、FairSync公平锁同步器。

复制代码
1
2
3
4
abstract static class Sync extends AbstractQueuedSynchronizer {...} static final class NonfairSync extends Sync{...} static final class FairSync extends Sync {...}
Reentrant.lock()方法的调用过程

默认非公平
在这里插入图片描述

公平锁加锁过程

首先公平锁对应的是 ReentrantLock 内部静态类 FairSync

  1. 加锁时会先从 lock 方法中去获取锁,调用 AQS 中 的 acquire() 方法。
复制代码
1
2
3
4
final void lock() { acquire(1); }
  1. acquire() 方法去调用了 tryAcquire() (FairSync 实现)方法。
复制代码
1
2
3
4
5
6
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { 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; }
  1. tryAcquire() 方法通过 getState() 获取当前同步状态,如果 state 为 0,则通过 CAS 设置该状态值,state 初始值为1,设置锁的拥有者为当前线程,tryAcquire返回true,否则返回false。如果同一个线程在获取了锁之后,再次去获取了同一个锁,状态值就加 1,释放一次锁状态值就减一,这就是可重入锁。只有线程 A 把此锁全部释放了,状态值减到0,其他线程才有机会获取锁。
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; }
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
  1. 如果获取锁失败,也就是 tryAcquire 返回 false,则调用的 addWaiter(Node mode) 方法把该线程包装成一个 node 节点入同步队列(FIFO),即尝试通过 CAS 把该节点追加到队尾,如果修改失败,意味着有并发,同步器通过进入 enq 以死循环的方式来保证节点的正确添加,只有通过 CAS 将节点设置成为尾节点之后,当前线程才能从该方法中返回,否则当前线程不断的尝试设置。加入队列时,先去判断这个队列是不是已经初始化了,没有初始化,则先初始化,生成一个空的头节点,然后才是线程节点。
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
  1. 加入了同步队列的线程,通过acquireQueued方法把已经追加到队列的线程节点进行阻塞,但阻塞前又通过 tryAccquire 重试是否能获得锁,如果重试成功能则无需阻塞)。
复制代码
1
2
3
4
public void unlock() { sync.release(1); }
复制代码
1
2
3
4
5
6
7
8
9
10
11
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ 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 <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); }
  1. 头节点在释放同步状态的时候,会调用unlock(),而unlock会调用release(),release() 会调用 tryRelease 方法尝试释放当前线程持有的锁(同步状态),成功的话调用unparkSuccessor() 唤醒后继线程,并返回true,否则直接返回false,
    注意:队列中的节点在被唤醒之前都处于阻塞状态。当一个线程节点被唤醒然后取得了锁,对应节点会从队列中删除。

非公平锁加锁过程

首先非公平锁对应的是 ReentrantLock 内部静态类 NonfairSync

复制代码
1
2
3
4
5
6
7
final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
  1. 加锁时会先从 lock 方法中去获取锁,不同的是,它的 lock 方法是先直接 CAS 设置 state 变量,如果设置成功,表明加锁成功。设置失败,再调用 acquire 方法将线程置于队列尾部排队。也是去获取锁调用 acquire() 方法,acquire 方法内部同样调用了 tryAcquire() 方法,nonfairTryAcquire() 方法比公平锁的 tryAcquire 的if判断少了一个 !hasQueuedPredecessors()。
复制代码
1
2
3
4
protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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; }

hasQueuedPredecessors():判断是否有其他线程比当前线程在同步队列中等待的时间更长。有的话,返回 true,否则返回 false,进入队列中会有一个队列可能会有多个正在等待的获取锁的线程节点,可能有Head(头结点)、Node1、Node2、Node3、Tail(尾节点),如果此时Node2节点想要去获取锁,在公平锁中他就会先去判断整个队列中是不是还有比我等待时间更长的节点,如果有,就让他先获取锁,如果没有,那我就获取锁(这里就体会到了公平性)

其他步骤和公平锁一致

非公平锁的机制:如果新来了一个线程,试图访问一个同步资源,只需要确认当前没有其他线程持有这个同步状态,即可获取到。
公平锁的机制:既需要确认当前没有其他线程持有这个同步状态,而且要确认同步器的FIFO队列为空,或者队列不为空但是自己是队列中头结点指向的下一个节点。

这个区别很重要,因为线程在阻塞和非阻塞之间切换时需要比较长的时间,如果刚好线程A释放了资源,A会去唤醒下一个排着队的Node节点,当这个唤醒操作还没完成的时候,这时又来了一个线程B,线程B发现当前没人持有这个资源,于是自己就迅速拿到了这个资源,充分利用了线程A去唤醒B的这一段时间,这就是公平锁和非公平锁之间的差异,这里也体现了非公平锁性能较高的地方。

ReentrantReadWriteLock

ReentrantReadWriteLock 是 Lock 的另一种实现方式,我们已经知道了 ReentrantLock 是一个排他锁,同一时间只允许一个线程访问,而 ReentrantReadWriteLock 允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。相对于排他锁,提高了并发性。在实际应用中,大部分情况下对共享数据(如缓存)的访问都是读操作远多于写操作,这时ReentrantReadWriteLock能够提供比排他锁更好的并发性和吞吐量。
另外:

  1. ReentrantReadWriteLock支持锁的降级。
  2. 读锁不支持Condition,会抛出UnsupportedOperationException异常,写锁支持Condition。

锁降级/升级
同一个线程中,在没有释放读锁的情况下,就去申请写锁,这属于锁升级,ReentrantReadWriteLock是不支持的。
同一个线程中,在没有释放写锁的情况下,就去申请读锁,这属于锁降级,ReentrantReadWriteLock是支持的。
锁降级中读锁获取的意义:
主要是为了保证数据的可见性,如果当前线程不获取读锁而直接释放写锁,假设此刻另一个线程(T)获取了写锁并修改了数据,那么当前线程是无法感知线程 T 的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程 T 才能获取写锁进行数据更新。

ReentrantReadWriteLock 包含五个内部类:

复制代码
1
2
3
4
5
6
abstract static class Sync extends AbstractQueuedSynchronizer{...} static final class NonfairSync extends Sync{...} static final class FairSync extends Sync {...} public static class ReadLock implements Lock, java.io.Serializable{...} public static class WriteLock implements Lock, java.io.Serializable{...}

ReadLock 和 WriteLock 方法部分是通过调用 Sync 的方法实现的,Sync 源码分析:

AQS 的状态 state 是 32 位(int 类型)的,高16位表示持有读锁的线程数(sharedCount),低16位表示写锁的重入次数 (exclusiveCount)。状态值为 0 表示锁空闲,sharedCount 不为 0 表示分配了读锁,exclusiveCount 不为 0 表示分配了写锁,sharedCount和exclusiveCount 一般不会同时不为 0,只有当线程占用了写锁,该线程可以重入获取读锁,反之不成立。

读锁获取锁过程
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
abstract static class Sync extends AbstractQueuedSynchronizer { static final int SHARED_SHIFT = 16; // 由于读锁用高位部分,所以读锁个数加1,其实是状态值加 2^16 static final int SHARED_UNIT = (1 << SHARED_SHIFT); // 写锁的可重入的最大次数、读锁允许的最大数量 static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; // 写锁的掩码,用于状态的低16位有效值 static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; // 读锁计数,当前持有读锁的线程数 static int sharedCount(int c) { return c >>> SHARED_SHIFT; } // 写锁的计数,也就是它的重入次数 static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } }

调用 lock() 方法

复制代码
1
2
3
4
public void lock() { sync.acquireShared(1); }
复制代码
1
2
3
4
5
public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }

返回值大于0表示获取到资源,小于0没有获取到资源

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread(); int c = getState(); //体现锁降级的思想,如果写锁被占用,并且占用写锁的线程不是当前线程,返回。 if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; int r = sharedCount(c); if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) { if (r == 0) { //保存第一个获取到读锁的线程 firstReader = current; //保存第一个获取读锁的线程的重入的次数 firstReaderHoldCount = 1; } else if (firstReader == current) { firstReaderHoldCount++; } else { //保存最近获取读锁的线程 HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; } return 1; } return fullTryAcquireShared(current); }

读锁是否需要阻塞,在公平锁中,如果同步队列中有阻塞的节点就阻塞,在非公平锁中,如果队列中有写线程节点就阻塞,目的是防止写线程饥饿。

复制代码
1
2
3
4
5
6
7
8
9
10
11
//公平锁 final boolean readerShouldBlock() { return hasQueuedPredecessors(); } // 非公平锁 final boolean readerShouldBlock() { return apparentlyFirstQueuedIsExclusive(); }

在以下几种情况,获取读锁会失败:

(1)有线程持有写锁,且该线程不是当前线程,获取锁失败。

(2)写锁空闲且公平策略决定读线程应当被阻塞,除了重入获取,其他获取锁失败。

(3)读锁数量达到最多,抛出异常。

除了以上三种情况,该线程会循环尝试获取读锁直到成功 fullTryAcquireShared()。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void doAcquireShared(int arg){ final Node node = addWaiter(Node.SHARED); // 1. 将当前的线程封装成 Node 加入到 Sync Queue 里面 boolean failed = true; try { boolean interrupted = false; for(;;){ final Node p = node.predecessor(); // 2. 获取当前节点的前继节点 (当一个n在 Sync Queue 里面, 并且没有获取 lock 的 node 的前继节点不可能是 null) if(p == head){ int r = tryAcquireShared(arg); // 3. 判断前继节点是否是head节点(前继节点是head, 存在两种情况 (1) 前继节点现在占用 lock (2)前继节点是个空节点, 已经释放 lock, node 现在有机会获取 lock); 则再次调用 tryAcquireShared 尝试获取一下 if(r >= 0){ setHeadAndPropagate(node, r); // 4. 获取 lock 成功, 设置新的 head, 并唤醒后继获取 readLock 的节点 p.next = null; // help GC if(interrupted){ // 5. 在获取 lock 时, 被中断过, 则自己再自我中断一下(外面的函数可能需要这个参数) selfInterrupt(); } failed = false; return; } } if(shouldParkAfterFailedAcquire(p, node) && // 6. 调用 shouldParkAfterFailedAcquire 判断是否需要中断(这里可能会一开始 返回 false, 但在此进去后直接返回 true(主要和前继节点的状态是否是 signal)) parkAndCheckInterrupt()){ // 7. 现在lock还是被其他线程占用 那就睡一会, 返回值判断是否这次线程的唤醒是被中断唤醒 interrupted = true; } } }finally { if(failed){ // 8. 在整个获取中出错(比如线程中断/超时) cancelAcquire(node); // 9. 清除 node 节点(清除的过程是先给 node 打上 CANCELLED标志, 然后再删除) } } }

调用 lock() 方法进行加锁,会调用 acquireShared(),而 acquireShared() 会调用tryAcquireShared(),
tryAcquireShared() 先判断是否写锁被占用,如果写锁被占用,并且占用写锁的线程不是当前线程,直接返回 -1; 再通过得到 state 值,进而得到读锁的线程占有数,判断是否超出最大线程持有数,如果没有超出,通过 CAS 修改state。获取线程持有数 int r = sharedCount©

  1. 如果此时 r= 0,说明当前线程是第一个获取读锁的线程,则保存当前线程到 第一个获取读锁的线程 firstReader 以及保存当前线程的重入次数 firstReaderHoldCount 为1。
  2. 如果此时得到的值不为 0,且判断当前线程不是目前持有读锁的线程,如果是 firstReaderHoldCount 加 1,否则用一个继承自 threadLocal 的 HoldCounter 类,保存当前线程的的id,以及之后重入锁的个数
读锁释放锁流程:

调用 unlock() 方法进行解锁,会调用release(),而release会调用tryRelease()。

复制代码
1
2
3
4
public void unlock() { sync.releaseShared(1); }
复制代码
1
2
3
4
5
6
7
8
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
protected final boolean tryReleaseShared(int unused) { Thread current = Thread.currentThread(); if (firstReader == current) { // assert firstReaderHoldCount > 0; if (firstReaderHoldCount == 1) firstReader = null; else firstReaderHoldCount--; } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) rh = readHolds.get(); int count = rh.count; if (count <= 1) { readHolds.remove(); if (count <= 0) throw unmatchedUnlockException(); } --rh.count; } for (;;) { int c = getState(); int nextc = c - SHARED_UNIT; if (compareAndSetState(c, nextc)) // Releasing the read lock has no effect on readers, // but it may allow waiting writers to proceed if // both read and write locks are now free. return nextc == 0; } }

这个方法先判断当前线程是不是第一个获取到读锁的线程 firstReader:

  • 如果是,判断第一个获取读锁的线程重入数是不是为 1(firstReaderHoldCount) ,如果是将 firstReader 置为null,如果大于 1 ,则进行减 1。
  • 如果不是,则将对应该线程的 HoldCounter 类,如果重入数小于等于 1 ,将该线程的 readHolds 移除,否则进行减 1。
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void doReleaseShared() { for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases unparkSuccessor(h); } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } }
写锁进行加锁流程:

1.调用 lock() 方法进行加锁,会调用acquire(),而acquire会调用tryAcquire()
2.先获取state状态值

  • 如果不为 0 ,如果不是该线程拥有的写锁, 则直接返回false。否则先判断是否超过最大重入数,如果没有则用 CAS 状态值。
  • 如果为 0 ,判断是否需要阻塞,如果不需要则用 CAS 修改状态值。
复制代码
1
2
3
4
public void lock() { sync.acquire(1); }
复制代码
1
2
3
4
5
6
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }

tryAcquire:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); int c = getState(); int w = exclusiveCount(c); if (c != 0) { // (Note: if c != 0 and w == 0 then shared count != 0) if (w == 0 || current != getExclusiveOwnerThread()) return false; if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); // Reentrant acquire setState(c + acquires); return true; } if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; setExclusiveOwnerThread(current); return true; }
写锁进行释放锁流程:

调用 unlock() 方法进行解锁,会调用release(),而 release() 会调用tryRelease(),
先得到当前线程已有的读锁重入个数,如果此时得到的值为1,进行减一操作,再将读锁置为null(如果当前线程多次获得过再读锁,则进行Count–); 减之后如果阻塞队列不为空,则唤醒阻塞的线程。

复制代码
1
2
3
4
public void unlock() { sync.release(1); }
复制代码
1
2
3
4
5
6
7
8
9
10
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
复制代码
1
2
3
4
5
6
7
8
9
10
11
protected final boolean tryRelease(int releases) { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); int nextc = getState() - releases; boolean free = exclusiveCount(nextc) == 0; if (free) setExclusiveOwnerThread(null); setState(nextc); return free; }

5. 同步器框架AQS

5.1 定义

JUC当中的大多数同步器(ReentrantLock/Semaphore/CountDownLatch…各种锁机制)功能实现都是围绕共同的基础行为,比如等待队列、条件队列、独占获取,共享获取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer简称AQS,AQS是一个抽象同步框架,可以用来实现一个依赖状态的同步器。

5.2 采用的数据结构

使用标志状态位 state(volatile int state)和 一个双向队列来实现。
state这个状态变量是用volatile来修饰的

  • getState():获取当前同步状态。
  • setState(int newState):设置当前同步状态。
  • compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。

AQS同步队列
同步器AQS内部的实现是依赖同步队列(CLH 队列,其实就是双向链表,java 中的 CLH 队列是原 CLH 队列的一个变种,队列里的线程由原自旋机制改为阻塞机制)来完成同步状态的管理。
同步队列主要包含节点的引用:一个指向头结点的引用(head),一个指向尾节点的引用(tail)。

5.3 AQS锁的类别:独占锁和共享锁两种。

独占锁:锁在一个时间点只能被一个线程占有。根据锁的获取机制,又分为“公平锁”和“非公平锁”。等待队列中按照FIFO的原则获取锁,等待时间越长的线程越先获取到锁,这就是公平的获取锁,即公平锁。而非公平锁,线程获取的锁的时候,无视等待队列直接获取锁。ReentrantLock和ReentrantReadWriteLock.Writelock是独占锁。
共享锁:同一个时间能够被多个线程获取的锁。JUC 包中 ReentrantReadWriteLock.ReadLock,CyclicBarrier,CountDownLatch和Semaphore都是共享锁。

AQS定义了独占模式的acquire()和release()方法,共享模式的acquireShared()和releaseShared()方法.还定义了抽象方法tryAcquire()、tryAcquiredShared()、tryRelease()和tryReleaseShared()由子类实现,tryAcquire()和tryAcquiredShared()分别对应独占模式和共享模式下的锁的尝试获取,就是通过这两个方法来实现公平性和非公平性,在尝试获取中,如果新来的线程必须先入队才能获取锁就是公平的,否则就是非公平的。这里可以看出AQS定义整体的同步器框架,具体实现放手交由子类实现。

6 CountDownLatch(共享锁)

6.1 简介

CountDownLatch是一个计数器闭锁,通过它可以完成类似于阻塞当前线程的功能,即:一个线程或多个线程一直等待,直到其他线程执行的操作完成。CountDownLatch用一个给定的计数器来初始化,该计数器的操作是原子操作,即同时只能有一个线程去操作该计数器。调用该类 await 方法的线程会一直处于阻塞状态,直到其他线程调用countDown方法使当前计数器的值变为零,每次调用countDown计数器的值减1。当计数器值减至零时,所有因调用await()方法而处于等待状态的线程就会继续往下执行。这种现象只会出现一次,因为计数器不能被重置,如果业务上需要一个可以重置计数次数的版本,可以考虑使用CycliBarrier。

CountDownLatch主要有两个方法:countDown() 和 await()。countDown() 方法用于使计数器减一,其一般是执行任务的线程调用,await()方法则使调用该方法的线程处于等待状态,其一般是主线程调用。这里需要注意的是,countDown()方法并没有规定一个线程只能调用一次,当同一个线程调用多次countDown()方法时,每次都会使计数器减一;另外,await()方法也并没有规定只能有一个线程执行该方法,如果多个线程同时执行await()方法,那么这几个线程都将处于等待状态,并且以共享模式享有同一个锁。

6.2使用示例

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class CountDownLatchExample { public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(5); Service service = new Service(latch); Runnable task = () -> service.exec(); for (int i = 0; i < 5; i++) { Thread thread = new Thread(task); thread.start(); } System.out.println("main thread await. "); latch.await(); System.out.println("main thread finishes await. "); } } public class Service { private CountDownLatch latch; public Service(CountDownLatch latch) { this.latch = latch; } public void exec() { try { System.out.println(Thread.currentThread().getName() + " execute task. "); sleep(2); System.out.println(Thread.currentThread().getName() + " finished task. "); } finally { latch.countDown(); } } private void sleep(int seconds) { try { TimeUnit.SECONDS.sleep(seconds); } catch (InterruptedException e) { e.printStackTrace(); } } }

执行结果:
在这里插入图片描述
在某些业务场景中,程序执行需要等待某个条件完成后才能继续执行后续的操作;典型的应用如并行计算,当某个处理的运算量很大时,可以将该运算任务拆分成多个子任务,等待所有的子任务都完成之后,父任务再拿到所有子任务的运算结果进行汇总。

6.3 实现原理

CountDownLatch 是基于 AbstractQueuedSynchronizer 实现的,在AbstractQueuedSynchronizer 中维护了一个 volatile 类型的整数 state,volatile 可以保证多线程环境下该变量的修改对每个线程都可见,并且由于该属性为整型,因而对该变量的修改也是原子的。创建一个 CountDownLatch 对象时,所传入的整数 n 就会赋值给 state 属性,当 countDown() 方法调用时,该线程就会尝试对 state 减一,而调用await() 方法时,当前线程就会判断 state 属性是否为 0,如果为 0,则继续往下执行,如果不为0,则使当前线程进入等待状态,直到某个线程将 state 属性置为 0,其就会唤醒在await()方法中等待的线程。

countDown()方法的源代码

复制代码
1
2
3
4
public void countDown() { sync.releaseShared(1); }

sync 是一个继承了AbstractQueuedSynchronizer的类实例,该类是CountDownLatch的一个内部类,其声明如下:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static final class Sync extends AbstractQueuedSynchronizer { Sync(int count) { setState(count); } int getCount() { return getState(); } protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; } protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero for (;;) { int c = getState(); if (c == 0) return false; int nextc = c-1; if (compareAndSetState(c, nextc)) return nextc == 0; } } }

releaseShared(int)方法的实现:

复制代码
1
2
3
4
5
6
7
8
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }

调用 tryReleaseShared(int) 方法时会在无限 for 循环中设置 state 属性的值,设置成功之后其会根据设置的返回值,即当前线程是否为将 state 属性设置为 0 的线程,来判断是否执行doReleaseShared()。doReleaseShared()方法主要作用是唤醒调用了await()方法的线程。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void doReleaseShared() { for (;;) { Node h = head; // 记录等待队列中的头结点的线程 if (h != null && h != tail) { // 头结点不为空,且头结点不等于尾节点 int ws = h.waitStatus; if (ws == Node.SIGNAL) { // SIGNAL状态表示当前节点正在等待被唤醒 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) // 清除当前节点的等待状态 continue; unparkSuccessor(h); // 唤醒当前节点的下一个节点 } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; } if (h == head) // 如果h还是指向头结点,说明前面这段代码执行过程中没有其他线程对头结点进行过处理 break; } }

首先判断头结点不为空,且不为尾节点,说明等待队列中有等待唤醒的线程,这里需要说明的是,在等待队列中,头节点中并没有保存正在等待的线程,其只是一个空的Node对象,真正等待的线程是从头节点的下一个节点开始存放的,因而会有对头结点是否等于尾节点的判断。在判断等待队列中有正在等待的线程之后,其会清除头结点的状态信息,并且调用unparkSuccessor(Node)方法唤醒头结点的下一个节点,使其继续往下执行。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); }

await():

await()---->acquireSharedInterruptibly()---->doAcquireSharedInterruptibly(AQS) —>tryAcquireShared

复制代码
1
2
3
4
public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); }

acquireSharedInterruptibly()

复制代码
1
2
3
4
5
6
7
8
public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); }

doAcquireSharedInterruptibly(AQS) —>tryAcquireShared

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.SHARED); // 使用当前线程创建一个共享模式的节点 boolean failed = true; try { for (;;) { final Node p = node.predecessor(); // 获取当前节点的前一个节点 if (p == head) { // 判断前一个节点是否为头结点 int r = tryAcquireShared(arg); // 查看当前线程是否获取到了执行权限 if (r >= 0) { // 大于0表示获取了执行权限 setHeadAndPropagate(node, r); // 将当前节点设置为头结点,并且唤醒后面处于等待状态的节点 p.next = null; // help GC failed = false; return; } } // 走到这一步说明没有获取到执行权限,就使当前线程进入“搁置”状态 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }

7. CyclicBarrier

7.1 简介

CyclicBarrier 也是一个同步辅助类,它允许一组线程相互等待,直到到达某个公共屏障点(common barrier point)。通过它可以完成多个线程之间相互等待,只有当每个线程都准备就绪后,才能各自继续往下执行后面的操作。

CountDownLatch主要是实现了1个或N个线程需要等待其他线程完成某项操作之后才能继续往下执行操作,描述的是1个线程或N个线程等待其他线程的关系。CyclicBarrier主要是实现了多个线程之间相互等待,直到所有的线程都满足了条件之后各自才能继续执行后续的操作,描述的多个线程内部相互等待的关系。
CountDownLatch是一次性的,而CyclicBarrier则可以被重置而重复使用。

7.2 示例

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class CyclicBarrierDemo { static class TaskThread extends Thread { CyclicBarrier barrier; public TaskThread(CyclicBarrier barrier) { this.barrier = barrier; } @Override public void run() { try { Thread.sleep(1000); System.out.println(getName() + " 到达栅栏 A"); barrier.await(); System.out.println(getName() + " 冲破栅栏 A"); Thread.sleep(2000); System.out.println(getName() + " 到达栅栏 B"); barrier.await(); System.out.println(getName() + " 冲破栅栏 B"); } catch (Exception e) { e.printStackTrace(); } } } public static void main(String[] args) { int threadNum = 5; CyclicBarrier barrier = new CyclicBarrier(threadNum, new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + " 完成最后任务"); } }); for(int i = 0; i < threadNum; i++) { new TaskThread(barrier).start(); } } }

执行结果:
在这里插入图片描述

7.3 CyclicBarrier 源码分析

1. 数据结构

复制代码
1
2
3
4
5
6
7
private final ReentrantLock lock = new ReentrantLock(); private final Condition trip = lock.newCondition(); private final int parties; private final Runnable barrierCommand; private Generation generation = new Generation(); private int count;

(1)lock 用于保护屏障入口的锁;
(2)trip 线程等待条件;
(3)parties 参与等待的线程数;
(4)barrierCommand 当所有线程到达屏障点之后,首先执行的命令;
(5)count 实际中仍在等待的线程数,每当有一个线程到达屏障点,count 值就会减一;当一次新的运算开始后,count 的值被重置为 parties。

2. 构造方法

复制代码
1
2
3
4
5
6
7
8
9
10
11
public CyclicBarrier(int parties, Runnable barrierAction) { if (parties <= 0) throw new IllegalArgumentException(); this.parties = parties; this.count = parties; this.barrierCommand = barrierAction; } public CyclicBarrier(int parties) { this(parties, null); }

3. 执行流程

await
await:–>dowait–> ReentrantLock lock.lock()

复制代码
1
2
3
4
5
6
7
8
public int await() throws InterruptedException, BrokenBarrierException { try { return dowait(false, 0L); } catch (TimeoutException toe) { throw new Error(toe); // cannot happen } }

dowait: ReentrantLock lock.lock()

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException { final ReentrantLock lock = this.lock; // 锁住 lock.lock(); try { // 当前代 final Generation g = generation; // 如果这代损坏了,抛出异常 if (g.broken) throw new BrokenBarrierException(); // 如果线程中断了,抛出异常 if (Thread.interrupted()) { // 将损坏状态设置为 true // 并通知其他阻塞在此栅栏上的线程 breakBarrier(); throw new InterruptedException(); } // 获取下标 int index = --count; // 如果是 0 ,说明到头了 if (index == 0) { // tripped boolean ranAction = false; try { final Runnable command = barrierCommand; // 执行栅栏任务 if (command != null) command.run(); ranAction = true; // 更新一代,将 count 重置,将 generation 重置. // 唤醒之前等待的线程 nextGeneration(); // 结束 return 0; } finally { // 如果执行栅栏任务的时候失败了,就将栅栏失效 if (!ranAction) breakBarrier(); } } for (;;) { try { // 如果没有时间限制,则直接等待,直到被唤醒 if (!timed) trip.await(); // 如果有时间限制,则等待指定时间 else if (nanos > 0L) nanos = trip.awaitNanos(nanos); } catch (InterruptedException ie) { // g == generation >> 当前代 // ! g.broken >>> 没有损坏 if (g == generation && ! g.broken) { // 让栅栏失效 breakBarrier(); throw ie; } else { // 上面条件不满足,说明这个线程不是这代的. // 就不会影响当前这代栅栏执行逻辑.所以,就打个标记就好了 Thread.currentThread().interrupt(); } } // 当有任何一个线程中断了,会调用 breakBarrier 方法. // 就会唤醒其他的线程,其他线程醒来后,也要抛出异常 if (g.broken) throw new BrokenBarrierException(); // g != generation >>> 正常换代了 // 一切正常,返回当前线程所在栅栏的下标 // 如果 g == generation,说明还没有换代,那为什么会醒了? // 因为一个线程可以使用多个栅栏,当别的栅栏唤醒了这个线程,就会走到这里,所以需要判断是否是当前代。 // 正是因为这个原因,才需要 generation 来保证正确。 if (g != generation) return index; // 如果有时间限制,且时间小于等于0,销毁栅栏,并抛出异常 if (timed && nanos <= 0L) { breakBarrier(); throw new TimeoutException(); } } } finally { lock.unlock(); } }

8. Semaphore

8.1 简介

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。比如控制用户的访问量,同一时刻只允许1000个用户同时使用系统,如果超过1000个并发,则需要等待。
Semaphore与CountDownLatch相似,不同的地方在于Semaphore的值被获取到后是可以释放的,并不像CountDownLatch那样一直减到底。它也被更多地用来限制流量,类似阀门的 功能。如果限定某些资源最多有N个线程可以访问,那么超过N个主不允许再有线程来访问,同时当现有线程结束后,就会释放,然后允许新的线程进来。有点类似于锁的lock与 unlock过程。相对来说他也有两个主要的方法:
用于获取权限的acquire(),其底层实现与CountDownLatch.countdown()类似;
用于释放权限的release(),其底层实现与acquire()是一个互逆的过程。

8.2 示例

比如模拟一个停车场停车信号,假设停车场只有两个车位,一开始两个车位都是空的。这时如果同时来了两辆车,看门人允许它们进入停车场,然后放下车拦。以后来的车必须在入口等待,直到停车场中有车辆离开。这时,如果有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,如果又离开一辆,则又可以放入一辆,如此往复。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class SemaphoreDemo { private static Semaphore s = new Semaphore(2); public static void main(String[] args) { ExecutorService pool = Executors.newCachedThreadPool(); pool.submit(new ParkTask("1")); pool.submit(new ParkTask("2")); pool.submit(new ParkTask("3")); pool.submit(new ParkTask("4")); pool.submit(new ParkTask("5")); pool.submit(new ParkTask("6")); pool.shutdown(); } static class ParkTask implements Runnable { private String name; public ParkTask(String name) { this.name = name; } @Override public void run() { try { s.acquire(); System.out.println("Thread "+this.name+" start..."); TimeUnit.SECONDS.sleep(new Random().nextInt(10)); } catch (InterruptedException e) { e.printStackTrace(); } finally { s.release(); } } } }

8.3 源码分析

Semaphore 通过使用内部类Sync继承AQS来实现。
支持公平锁和非公平锁。内部使用的AQS的共享锁。

Semaphore 构造器

复制代码
1
2
3
4
5
6
7
8
9
public Semaphore(int permits) { sync = new NonfairSync(permits); } public Semaphore(int permits, boolean fair) { sync = fair ? new FairSync(permits) : new NonfairSync(permits); }

构造方法指定信号量的许可数量,默认采用的是非公平锁,也只可以指定为公平锁。
permits 赋值给 AQS 中的 state 变量。

acquire:---->acquireSharedInterruptibly—>tryAcquireShared
acquire:可响应中断的获得信号量

复制代码
1
2
3
4
5
6
7
8
9
public void acquire() throws InterruptedException { sync.acquireSharedInterruptibly(1); } public void acquire(int permits) throws InterruptedException { if (permits < 0) throw new IllegalArgumentException(); sync.acquireSharedInterruptibly(permits); }

获得信号量方法,这两个方法支持 Interrupt中断机制,可使用acquire() 方法每次获取一个信号量,也可以使用acquire(int permits) 方法获取指定数量的信号量 。

复制代码
1
2
3
4
5
6
7
8
public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); }
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected int tryAcquireShared(int acquires) { return nonfairTryAcquireShared(acquires); } final int nonfairTryAcquireShared(int acquires) { for (;;) { int available = getState(); int remaining = available - acquires; if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; } }
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }

release()

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public void release() { sync.releaseShared(1); } public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; } protected final boolean tryReleaseShared(int releases) { for (;;) { int current = getState(); int next = current + releases; if (next < current) // overflow throw new Error("Maximum permit count exceeded"); if (compareAndSetState(current, next)) return true; } } private void doReleaseShared() { for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases unparkSuccessor(h); } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } }

最后

以上就是曾经茉莉最近收集整理的关于java 并发(锁、AQS)的全部内容,更多相关java内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(72)

评论列表共有 0 条评论

立即
投稿
返回
顶部