CAS
无锁,乐观锁,自旋锁,轻量级锁
定义
Compare and Swap,是基于硬件级别的指令实现的同步原语,Java并发包java.utile.concurrent许多同步类基于CAS构建
cas中的value 为volatile,最终执行lock cmpxchgq 2个汇编指定
lock :硬件指令集,保证多次内存操作的原子性。 通过lock去对要操作的内存的缓存行加一把锁,缓存行超过64字节就上一把总线锁。 硬件级别加锁只能针对一个缓存行加锁,如果跨多个缓存行就在总线上帮你加一把锁,总线锁的粒度就很大了。
也就是说在比较交换期间其它别的线程是拿不到锁了,这里相当于是串行化,配合while使用进行自旋
缺陷
- 效率问题
若存在多个线程竞争,可能导致CAS失败,此时可能需要循环(自旋)执行CAS,影响性能
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
- ABA问题
ABA问题是无锁结构实现中常见的一种问题
进程P1读取了一个数值A P1被挂起(时间片耗尽、中断等),
进程P2开始执行 P2修改数值A为数值B,然后又修改回A
P1被唤醒,比较后发现数值A没有变化,程序继续执行。
与设计原意不符,atomic包引入AtomicStampedReference类解决ABA问题,增加了标记版本字段
synchronized
Synchronized 互斥锁(排它锁/独占锁) 悲观锁 同步锁 重量级锁 可重入
是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 称之为重量级锁。
- synchronized 作用于实例方法锁的是当前实例对象,在该方法中使用 wait()或则 this.wait()可释放锁和线程阻塞暂停
- synchronized 作用于静态方法锁的是当前类class对象,在该方法中需要使用 类名.class.wait()的方式来释放锁进行线程暂停。
- synchronized 作用于代码块锁的是当前传入的对象,使用 对象.wait()来释放锁并暂停线程。
代码块的同步是利用monitorenter和monitorexit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。
当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;
当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。
synchronized 用的锁是存在哪里?
Java对象头
以Hotspot 虚拟机为例,对象头主要包括两部分数据:
Mark Word(标记字段)
默认存储对象的HashCode,分代年龄和锁标志位信息。
这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化
64位的虚拟机Mark Word结构:
32位的虚拟机Mark Word结构:
现在虚拟机一般都是64位,64位的对象头有点浪费空间,JVM默认会开启指针压缩(XX:+useCompressedDops),按32位形式记录对象头,
epoch为偏向的时间戳,
hashCode、分代年龄不是消失,而是被拷贝到其它地方(一般是获取锁的线程里面存储),恢复无锁(即释放锁)后拷贝回来
Klass Pointer(类型指针)
对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
锁四种状态
锁状态 | 存储内容 | 标志位 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|---|
无锁 | 对象的hashCode、对象分代年龄、是否是偏向锁(0) | 01 | 没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功 | ||
偏向锁 | 偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) | 01 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 指向栈中锁记录的指针 | 00 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到索竞争的线程,使用自旋会消耗CPU | 追求响应速度,同步块执行速度非常快 |
重量级锁 | 指向互斥量的指针 | 10 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较慢 |
synchronized优化-锁升级
jvm 默认延时4秒开启偏向锁,可通过-XX:BiasedLockingStartupDelay=0取消延时
不启用-XX:-UseBiaseLocking=false
有2种方式可以使一把锁直接升级为重量级状态
- 调用wait方法
wait()、notify、notifyAll()只有在同步代码块中才能使用,确保程序执行顺序,否则java.lang.IllegalMonitorStateException,
- 在同步代码块中调用对象的hashcode方法
R大回答:
这是一个针对HotSpot VM的锁实现的问题。
简单答案是:
当一个对象已经计算过identity hash code,它就无法进入偏向锁状态;
当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为重量锁;
重量锁的实现中,ObjectMonitor类里有字段可以记录非加锁状态下的mark word,其中可以存储identity hash code的值。或者简单说就是重量锁可以存下identity hash code。
请一定要注意,这里讨论的hash code都只针对identity hash code。用户自定义的hashCode()方法所返回的值跟这里讨论的不是一回事。Identity hash code是未被覆写的 java.lang.Object.hashCode() 或者 java.lang.System.identityHashCode(Object) 所返回的值。
消费生产模型
private List<Date> quence;
//生产者
public synchronized void put() {
while (quence.size == maxSize) {
wait();
}
dosome();
notifyAll();
}
public synchronized void take() {
while (quence.size == 0) {
wait();
}
dosome2();
notifyAll();
}
LongAdder
相当于分段CAS,维护一个cell数组,在多线程情况下是对多个资源竞争,也就是减少了竞争压力,减少CPU空转自旋时间,取值时累计base和cell数组值
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
AbstractQueuedSynchronizer
组成
private volatile int state; // aqs状态
private transient volatile Node head;
private transient volatile Node tail;
private transient Thread exclusiveOwnerThread; 父类中的,当前持有独占锁的线程
两个核心设计:
资源状态state
在独占锁的场景下, 可以理解为资源占有的标识位; 在共享锁的情况下, 可以作为资源数量理解,AQS在获得资源状态state使用了CAS操作
抽象同步队列
抽象同步队列,是一个FIFO的CLH双向队列
CLH队列全称是(Craig, Landin, andHagersten) lock queuee, 是一个虚拟的双向队列(即不存在队列实例,仅存在节点之间的关联关系),其内部通过节点head和tail记录队首和队尾元素,队列元素类型为Node,用来存储被阻塞的线程信息。添加阻塞线程到CLH队尾时都是使用CAS操作
两种工作模式:
-
独占(exclusive)模式
同一时间只有一个线程能拿到锁执行,当锁被某个线程成功获取时,其他线程无法获取到该锁。锁的状态只有0和1两种情况。实现了acquire方法,
eg:ReentrantReadWriteLock中的WriteLock -
共享(shared)模式
同一时间有多个线程可以拿到锁协同工作,当锁被某个线程成功获取时,其他线程仍然可能获取到该锁。锁的状态大于或等于0。实现了acquireShared方法,
eg:ReentrantReadWriteLock中的ReadLock
CLH入队
当线程获取同步状态失败后,会将当前线程构造成一个Node节点插入链表(如果第一次插入会初始化new head()节点为虚拟节点),插入链表都是尾部插入并且setTail为当前节点,同时会阻塞当前线程(调用LockSupport.park方法)。
//获取锁
public final void acquire(int arg) {
//tryAcquire尝试获取锁,钩子方法,成功status=1退出;反之
//addWaiter加入同步等待队列
//acquireQueued:调用tryAcquire,成功setHead(当前节点)退出,失败则阻塞当前线程
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
selfInterrupt();
}
}
//加入等待队列
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 非首次插入,可直接setTail
// 设置老的tail为当天tail的pre节点
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//首次插入,需要创建虚拟的head节点
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 如果 tail 是 null,就创建一个虚拟节点,同时指向 head 和 tail,称为 初始化。
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {// 如果不是 null
// 和 上个方法逻辑一样,将新节点追加到 tail 节点后面,并更新队列的 tail 为新节点。
// 只不过这里是死循环的,失败了还可以再来 。
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
//调用tryAcquire,成功setHead(当前节点)退出,失败则阻塞当前线程
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 新创建的节点的上一个节点,若是head,尝试获取锁
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
//获取锁成功,setHead(当前节点),之前的head废弃,注意 head 节点是永远不会唤醒的
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 检查上一个节点的状态,如果是 SIGNAL 就阻塞,否则就改成 SIGNAL
if (shouldParkAfterFailedAcquire(p, node) &&
//阻塞当前线程 LockSupport.park(this);
// 等待被LockSupport.unpark 唤起后,for循环,重新进入当前自旋操作,可重新获取锁
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
CLH出队
public final boolean release(int arg) {
//修改AQS的status setState(getState() - arg)
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
//释放锁 注意head节点是永远不会唤醒的 唤醒的是当前节点的next节点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
// 将 head 节点的 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;
// 如果 next 是 null,或者 next 被取消了。就从 tail 开始向上找节点。
if (s == null || s.waitStatus > 0) {
s = null;
// 从尾部开始寻找,直到不是 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);
}
AQS独占模式流程
ReentrantReadWriteLock
内部类ReadLock、WriterLock,共享锁、独占锁的分别实现
public static class ReadLock implements Lock, java.io.Serializable {
public void lock() {
sync.acquireShared(1);
}
public void unlock() {
sync.releaseShared(1);
}
}
public static class WriteLock implements Lock, java.io.Serializable {
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
}
Sync
独占锁、写锁
abstract static class Sync extends AbstractQueuedSynchronizer {
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
//判断AQS中的state==0?,0表示目前没有其他线程获得锁,当前线程就可以尝试获取锁。
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
//判断持有锁的线程是否为当前线程 支持重入 state+1
//从这里看出 若一个线程持有了写锁c!=0 and w != 0,该线程可以接着获取写锁,其他线程不行
// 若一个线程持有了读锁 c!= 0 and w == 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;
}
}
共享锁、读锁
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
Thread current = Thread.currentThread();
int c = getState();
// 判断是否持有写锁 && 持有写锁的不是当前线程 ==> 失败
// 那 持有写锁 && 持有写锁的是当前线程,即持有写锁,后获取读锁 不会返回-1
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
// 读锁是否需要被阻塞
if (!readerShouldBlock() &&
r < MAX_COUNT &&
//state高16位+1 cas成功表示获取到了读锁
compareAndSetState(c, c + SHARED_UNIT)) {
// 能进这个方法 表示获取到了读锁
if (r == 0) {//目前是第一个拿到了读锁,没人拿或者之前拿的释放了
// firstReader作为缓存 第一个获取读锁的线程
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、FairSync下阻塞队列中有其他元素排在前面等待锁,不能直接获取锁
// 2、NonFairSync非公平模式下
// a、阻塞队列中head的第一个后继节点是来获取写锁的
// b、阻塞队列中head的第一个后继节点是来获取读锁的,然后cas竞争失败了
//3、持有写锁,当前线程再获取读锁
final int fullTryAcquireShared(Thread current) {
/*
* This code is in part redundant with that in
* tryAcquireShared but is simpler overall by not
* complicating tryAcquireShared with interactions between
* retries and lazily reading hold counts.
*/
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
} else if (readerShouldBlock()) {
// Make sure we're not acquiring read lock reentrantly
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
firstReader、firstReaderHoldCount 作为缓存 第一个获取读锁的线程和重入次数
cachedHoldCounter 作为缓存 上一个获取读锁的线程id和重入次数 readHolds 所有的读锁线程缓存ThreadLocal
目的是提高性能,统计重入次数。
基于的原理是:通常读锁的获取很快就会伴随着释放,显然,在 获取->释放 读锁这段时间,如果没有其他线程获取读锁的话,此缓存就能帮助提高性能,因为这样就不用到 readHolds中ThreadLocal 查询 map
FairSync公平锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
//判断 AQS 的队列中是否有其他线程,如果有则不会尝试获取锁
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
NonfairSync 非公平锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
//当一个线程想要获得写锁成功时,只有当前的写锁没有被其他线程持有,且读锁没有被任意线程持有(注意这里是任意线程,包括自己线程持有读锁)
//直接返回false这是一种完全的非公平实现
//表示我谁都不拦,写锁尽管去抢吧,不管你是先来的还是后来的谁抢到算谁的
return false; // writers can always barge
}
// 此处 不是完全的非公平实现,完全非公平实现应返还false
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* block if there is a waiting writer behind other enabled
* readers that have not yet drained from the queue.
*/
return apparentlyFirstQueuedIsExclusive();
}
}
//判断最前面的节点是不是独占
// 写锁定义了更高的优先级,
// 如果 head.next 即head下一个是排队来获取写锁的,返回true, 那么读锁的线程不应该和它抢
// 如果 head.next 即head下一个是排队来获取读锁的,返回false,那么其他的读锁的线程也可以来抢
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
总结
- 若一个线程持有了写锁,tryAcquire - w == 0 || current != getExclusiveOwnerThread() return false 分析出本线程可以接着获取写锁,支持可重入,其他线程读锁、写锁都阻塞
- 若一个线程持有了写锁,tryAcquireShared - exclusiveCount© != 0 && getExclusiveOwnerThread() != current return -1分析出本线程可以接着获取读锁,这种称之为锁降级
锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级,就是普通的加锁释放锁流程。
锁降级是指当前拥有了写锁,再去获取读锁,随后释放(先前拥有的)写锁的过程。
- 若一个线程持有了读锁,tryAcquire - (if c != 0 and w == 0 then shared count != 0)是不可以获取到写锁
- 若一个线程持有了读锁,tryAcquireShared - readerShouldBlock,其他线程包括本线程是可以继续来抢读锁
Condition
Condition是Java提供了来实现等待/通知的类,它可以为多个线程间建立不同的Condition,
使用synchronized/wait()只有一个阻塞队列,notifyAll会唤起所有阻塞队列下的线程,
而使用lock/condition,可以实现多个阻塞队列,signalAll只会唤起某个阻塞队列下的阻塞线程,适用于1对1的等待
Condition内部维护了单向链表,当有线程调用了Condition的await(),就会将该线程封装成Node结点,并放入到单向链表尾部。
当有线程调用了Condition的signal(),便会将Condition中的链表头部的节点,放入到AQS的双向链表中通过自旋(即不断循环)获取锁。signalAll()会将Condition链表中所有结点都放到AQS中自旋
Condition单向链表队列
await
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 将该线程封装成Node结点,并放入到单向链表尾部
Node node = addConditionWaiter();
//释放当前的锁,得到锁的状态,并唤醒 AQS 队列中的一个线程
int savedState = fullyRelease(node);
int interruptMode = 0;
// 判断这个节点是否在aqs队列上
// 首次一定是false,因前面已释放锁了,当前线程会被LockSupport.park(this)挂起
// 当调用了signal唤醒方法时去唤醒
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 到这步 表示线程被唤醒了,重新去抢锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
signal
public final void signal() {
//判断当前线程是否持有锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
//Transfers a node from a condition queue onto sync queue.
// 将 node 从 condition队列中转换到 AQS 队列中,修改 AQS 队列中原先尾节点的状态,唤起线程
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 唤醒节点上的线程.
LockSupport.unpark(node.thread);
return true;
}
消费生产模型
private List<Date> quence;
Lock lock=new ReentrantLock();
Condition full = lock.newCondition();
Condition empty = lock.newCondition();
public void put(){
lock.lock();
while(quence.size() == maxSize){
full.await();//阻塞生产线程
}
dosome();
empty.signalAll();//唤醒消费线程
lock.unlock();
}
public void take(){
lock.lock();
while(quence.size() == 0){
empty.await();//阻塞消费线程
}
dosome2();
full.signalAll();//唤醒生产线程
lock.unlock();
}
LockSupport
- park
等待一个许可,将当前调用Thread阻塞,
线程恢复的条件为:- 线程调用了unpark
- 其它线程中断了线程
- 发生了不可预料的事情
- 过期时间到/指定的deadLine到了
- unpark
为指定线程提供一个许可,将指定线程Thread唤醒
其实JVM在实现LockSupport的时候,内部会给每一个线程维护一个计数器变量_counter,这个变量是表示的含义是“许可证的数量”,只有当有许可证的时候线程才可以执行,同时许可证最大的数量只能为1。当调用一次park的时候许可证的数量会减一。当调用一次unpark的时候计数器就会加一,但是计数器的值不能超过1。
park的时候是要检查内部的一个线程permit状态的,如果permit=true,那么park就不阻塞,跳过这次park继续执行,如果permit=false则阻塞。unpark相当于修改thread对应的permit为true
所以先unpark()再park()是允许这么操作的,但先unpark()、再unpark()、再park()、再park()第二次的park仍被阻塞,“许可”是一次性的
public static void park(Object blocker) {
Thread t = Thread.currentThread();
//记录当前线程阻塞的原因,底层就是unsafe.putObject,就是把对象存储起来
setBlocker(t, blocker);
//执行 unsafe.park
UNSAFE.park(false, 0L);
//线程恢复后,去掉阻塞原因
setBlocker(t, null);
}
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
Thread.sleep() | Object.wait() | Condition.await() | LockSupport.park() |
不会释放占有的锁,必须传入时间到期自动唤醒,会抛出InterruptedException异常 | 会释放占有的锁,需要在synchronized块中执行,可传时间,也可不传时间表示一直阻塞,需要另外线程notify/notifyAll唤醒,在wait()之前执行了notify()会抛出IllegalMonitorStateException异常 | 释放锁,调用LockSupport.park()阻塞当前线程 | 不会释放占有的锁,需要另一个线程执行unpark()来唤醒,或者传入时间到期唤醒,在park()之前执行了unpark()线程不会被阻塞,直接跳过这次park()继续执行 |
Semaphore信号量
Semaphore内部也是集成了AQS实现线程管理,持公平锁和非公平锁,通过构造函数参数permits设置许可数,它最后传递给了AQS的state值,state设置为1时是排他锁,大于1时是共享锁。
当state>0时线程acquire()能立即获取许可继续执行,否则线程阻塞等待其他线程release()释放许可
由于Semaphore锁的实现原理采用计数方式的state记录许可个数并没有某个特定线程所绑定,适用于做流量控制,特别是公共资源有限的应用场景,比如数据库连接
调用release函数之前并没有要求一定要先调用acquire函数,但是注意防止出现由于release导致的semaphore 的许可大于在构造函数中设置的值
CountDownLatch减数计数器
CountDownLatch是AQS的共享模式的实现,是一个倒数的计数器阀门,其内部也有一个静态内部类Sync继承了AQS。
通过构造函数参数指定了AQS的同步状态state的值,表示计数器个数,初始化时阀门关闭,当数量倒数countDown()减到0时阀门打开,被阻塞线程(调用await()阻塞)被唤醒。LockSupport.park unpark实现
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
//有参构造器,指定state的值
Sync(int count) {
setState(count);
}
//获取state状态值
int getCount() {
return getState();
}
//实现了AQS的共享模式的加锁方法
protected int tryAcquireShared(int acquires) {
//如果state为0,则返回大于0的数值,不等于0则返回小于0的数值
return (getState() == 0) ? 1 : -1;
}
//通过死循环的方式释放锁
protected boolean tryReleaseShared(int releases) {
for (;;) {
//获取状态值
int c = getState();
//如果此时状态值已经为0了,说明计数器已经减到0了不能再减了
if (c == 0)
return false;
//否则就将计数器减1,并通过CAS的方式赋值给state,因为此时还会有其他线程修改状态
//这里与ReentrantLock的独占解锁方式不同,独占是直接setState,因为它不会有其他线程竞争
int nextc = c-1;
if (compareAndSetState(c, nextc))
//如果计数器为0,则返回true,唤醒同步队列中的等待线程
//不等于0则返回false
return nextc == 0;
}
}
}
CyclicBarrier循环栅栏
CyclicBarrier是一个可循环的屏障,它允许多个线程在执行完相应的操作后彼此等待共同到达一个point,等所有线程都到达后再继续执行
CyclicBarrier也可以像CountDownLatch一样适用于多个子任务并发执行,当所有子任务都执行完后再继续接下来的工作
CyclicBarrier之所以被称为循环栅栏,是因为它还有一个主要的特点,它内部的计数器在为0之后又被重置了还可以再循环利用。
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
//阻塞方法
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
//获取独占锁
final ReentrantLock lock = this.lock;
//加锁,之后的代码都是属于同步代码
lock.lock();
try {
final Generation g = generation;
//broken默认false,已经broken的barrier不能再次使用了
if (g.broken)
throw new BrokenBarrierException();
//如果线程被打断了,那么将唤醒所有的阻塞线程
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
//count值减1
int index = --count;
//如果index值为0,则表示所有的线程都到达了障点
if (index == 0) { // tripped
boolean ranAction = false;
try {
//获取Runnable执行单元,如果不为空则执行逻辑
//此处就可以明白为什么Runnable逻辑优先执行了吧
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
//唤醒阻塞的所有线程,重置count
//此处可以明白CyclicBarrier为什么可以循环利用了吧
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
// 如果index不为0,则表示还有线程没有达到障点
//死循环一直等待唤醒
for (;;) {
try {
//如果没有设置超时时间,则调用Condition的await()方法
//await方法线程释放锁并加入等待队列
//是不是又到了AQS了
if (!timed)
trip.await();
else if (nanos > 0L)
//如果设置了超时时间,则调用Condition的awaitNanos()方法
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException();
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
//释放锁
lock.unlock();
}
}
CyclicBarrier内部维护了独占锁ReentrantLock,并且关联了一个Condition。
CountDownLatch | CyclicBarrier |
---|---|
执行await()线程会等待计数器减为0 | 执行await()方法会使线程进入阻塞等待其他线程到达障点 |
计数器不能重置,减为0之后就不能再使用 | 内部的计数器在为0后可被重置再循环利用 |
基于AQS的共享模式实现 | 基于ReentrantLock和Condition实现 |
不会让子线程进入阻塞 | 会使所有子线程进入阻塞等待到达障点 |
参考:
http://t.zoukankan.com/housh-p-13047396.html
https://zhuanlan.zhihu.com/p/137797509