java锁和线程 参考简书
java并发(锁、AQS)
数据库锁参考博客
1. 常见的锁
锁的名词:阻塞锁,可重入锁,读写锁,互斥锁,悲观锁,乐观锁,公平锁,偏向锁,对象锁,线程锁,锁粗化,锁消除,轻量级锁,重量级锁,信号量,独享锁,共享锁,分段锁
Synchronized 和 Lock
- Synchronized,它就是一个:非公平,悲观,独享,互斥,可重入的重量级锁。原生语义上实现的锁。
- 以下两个锁都在(java.util.concurrent)JUC包下,是API层面上对Lock的实现:
(1)ReentrantLock,它是一个:默认非公平但可实现公平的,悲观,独享,互斥,可重入,重量级锁。
(2)ReentrantReadWriteLocK,它是一个,默认非公平但可实现公平的,悲观,写独享,读共享,读写,可重入,重量级锁。
1.1 锁分类
1.1.1 公平锁/非公平锁
- 公平锁,是指多个线程按照申请锁的顺序来获取锁。
- 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获得锁。有可能会造成优先级反转或者饥饿现象。
比如:
(1)ReentrantLock,通过AQS来实现线程调度,默认是非公平锁,也可以通过构造函数指定该锁为公平/非公平锁。非公平锁的优点在于吞吐量比公平锁大。
(2)Synchronized,是非公平锁,没有任何办法使其变成公平锁。
1.1.2 乐观锁/悲观锁
- 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。比如Java里面的同步原语synchronized关键字的实现也是悲观锁(synchronized通过monitor实现)。
- 乐观锁(CAS机制):很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁(而是通过自旋),但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。像在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
1.1.3 独享锁/共享锁
- 独享锁:是指该锁一次只能被一个线程所持有。
- 共享锁:是指该锁可被多个线程所持有。
注:独享锁与共享锁是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
应用:
(1)对于Java ReentrantLock而言,其是独享锁。
(2)对于Lock的另一个实现类ReentrantReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的。读写,写读 ,写写的过程是互斥的。
(3)对于Synchronized而言,当然是独享锁。
1.1.4 互斥锁/读写锁
独享锁/共享锁是一种广义的说法,互斥锁/读写锁就是具体的实现。
- 互斥锁在Java中的具体实现就是ReentrantLock(独享锁);
- 读写锁在Java中的具体实现就是ReentrantReadWriteLock(读锁是共享锁,写锁是独享锁)。
1.1.5 可重入锁
可重入锁又名递归锁,是指同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。比如,synchronized时可重入锁。
1.2 锁的现象/本质
锁的本质就是线程等待,可分为线程阻塞和线程自旋两种,区别:
1.2.1 线程阻塞
阻塞:阻塞或唤醒线程需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源。 如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间。对于只需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。
1.2.2 线程自旋
自旋:如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。(线程还是Runnable的,只是在执行空代码。缺点:一直自旋会白白消耗计算资源。)
1.3 锁优化
JDK1.6 实现各种锁优化,如适应性自旋,锁消除,锁粗化,轻量级锁,偏向锁等,这些技术都是为了在线程间更高效的共享数据,以及解决竞争问题。
1.3.1 自旋锁与自适应自旋锁(CAS)
- 自旋锁。在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得(java 线程是映射在内核之上的,线程的挂起和恢复会极大的影响开销)。如果物理机器有一个以上的处理器, 能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下",但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待。我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
- 自旋锁的缺点。自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此, 如果锁被占用的时间很短, 自旋等待的效果就会非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。因此,自旋等待的时间必须要有一定的限度, 如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是10次。
- 自适应自旋锁。在JDK 1.6中引人了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上, 自旋等待刚刚成功获得过锁,井且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。
1.3.2 锁消除
锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步, 但是被检测到不可能存在共享数据竞争的锁进行消除。
StringBuffer 的 append 方法用了 synchronized 关键词,它是线程安全的。如果在线程方法的局部变量中使用 StringBuffer,由于不同线程调用方法时都会创建不同的对象(在当前线程的虚拟机栈中创建栈帧),不会出现线程安全问题,所以 append() 没必要加锁,会进行锁消除。
1.3.3 锁粗化
如果系列的连续操作 都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的。那么即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。此时可以把多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。
1.3.4 轻量级锁
轻量级锁并不是用来代替重量级锁的, 它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。比如,基于CAS算法的类。
1.3.4.1 对象头
在代码进人同步块的时候,如果此同步对象没有被锁定(锁标志位为 “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操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。
1.3.5 偏向锁
偏向锁的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用 CAS 操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都清除掉,连CAS操作都不做了。
偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。
假设当前虚拟机启用了偏向锁,那么,当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为 “01”, 即偏向模式。同时使用 CAS 操作把获取到这个锁的线程的 ID 记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作。
当有另外一个线程去尝试获取这个锁时,偏向模式就宜告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向后恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态,后续同步操作同轻量级锁那样执行。
1.3.6 重量级锁synchronized
- 检测 Mark Word 里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁。
- 如果不是,则使用CAS将当前线程的 ID 替换 Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1
- 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
- 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
- 如果自旋成功则依然处于轻量级状态。如果自旋失败,则升级为重量级锁。
总结锁升级
2. CAS算法
2.1 CAS的定义
CAS,compare and swap,对比和交换。CAS有三个参数:内存位置V、旧的预期值A、新的预期值B。当且仅当V符合预期值A的时候,CAS用新值B原子化地更新V的值。否则他什么都不做。在任何情况下都会返回V的真实值。
- CAS的意思是——“ 我任务V的值应该是A,如果是A则将其赋值给B,若不是,则不修改,并告诉我应该为多少”。CAS是以乐观心态–它抱着成功的希望进行更新(认为总有一次别人不再对其修改,自己就可以更新该值了),并且如果,另一个线程在上次检查后更新了变量,它能够发现错误。
2.2 CAS在线程中的应用
线程自旋→乐观锁的主要实现方式→CAS。一个CAS方法包含三个参数CAS(V,E,N)。V表示要更新的变量,E表示预期的值(expected),N表示新值。只有当V的值等于E时,才会将V的值修改为N。如果V的值不等于E,说明已经被其他线程修改了,当前线程可以放弃此操作,也可以再次尝试该操作直至修改成功。
基于这样的算法,无锁的CAS也可以发现其他线程对当前线程的干扰(临界区值的修改),并进行恰当的处理。
2.3 CAS在java中的应用
以AtomicInteger CAS为例,
public class AtomicInteger extends Number implements java.io.Serializable {
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset; //value的偏移地址
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//对value的操作
private volatile int value;
public final int get() {return value;}
//返回增1后的值 设计模式:模块方法
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
//this, valueOffset, 1
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//获取当前内存中var1对象的偏移地址为valueOffset处的value值
var5 = this.getIntVolatile(var1, var2);
//判断内存中var1对象的偏移地址为valueOffset处的value值是否等于var5
//是,则把该处的值更新为var5+var4,并返回true,结束循环;
//不是,则直接返回false,继续while循环
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
public final native boolean compareAndSwapInt(Object var1,
long var2, int var4, int var5);
}
这里采用了CAS操作,每次循环中,
- 首先,通过
...
从主内存中读取数据volatile int value
, - 然后,将
...
和加1后的数据...
进行CAS操作,如果成功就返回结果,否则重试直到成功为止。
其中,compareAndSet的作用见代码注释:
do {
//获取当前内存中var1对象的偏移地址为valueOffset处的value值
var5 = this.getIntVolatile(var1, var2);
//判断内存中var1对象的偏移地址为valueOffset处的value值是否等于var5
//是,则把该处的值更新为var5+var4,并返回true,结束循环;
//不是,则直接返回false,继续while循环
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
它的实现原理:利用JNI(java native interface)来完成CPU指令的操作,即通过调用 Unsafe 的 native函数实现:unsafe.compareAndSwapInt(this, valueOffset, expect, update)。
- CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。
- JNI(java本地方法)充许java调用其它语言,比如,compareAndSet就是借助C来调用CPU底层指令实现 CPU保证原子性是通过总线锁,缓存锁实现
2.5 补充
- 原子类的底层操作都是通过 Unsafe 类完成,每个原子类内部都有一个 Unsafe 类的静态引用。Unsafe 类中大部分都是native方法。
private static final Unsafe unsafe = Unsafe.getUnsafe();
- AtomicInteger 内部由一个 int 域来保存值,其由volatile关键字修饰,用于保证可见性。类似的,AtomicLong内部是一个long型的value,AtomicBoolean 内部也是一个int,但其只会取值0或1。
private volatile int value;
- AtomicInteger中有一个 compareAndSet 方法,通过 CAS 对变量进行原子更新。它通过调用 Unsafe 的 native函数实现:
unsafe.compareAndSwapInt(this, valueOffset, expect, update)。
2.6 CAS的缺点
- ABA问题。
因为 CAS 需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。 - 循环时间长开销大。
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。 - 只能保证一个共享变量的原子操作。
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并成ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
2.7 cas通用实现模式
1、声明共享变量为volatile(比如,volatile int value);
2、使用CAS的原子条件更新来实现线程之间的同步;
3、配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信(作用)
AQS、非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用CAS模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent包的实现示意图如下:
3. AQS
3.1 AQS定义
AQS,abstractQueuedSynchronizer,
JUC当中的大多数同步器(ReentrantLock/Semaphore/CountDownLatch…各种锁机制)功能实现都是围绕共同的基础行为,比如等待队列、条件队列、独占获取,共享获取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer简称AQS,AQS是一个抽象同步框架,可以用来实现一个依赖状态的同步器。
3.2 采用的数据结构
AQS使用标志状态位 state(volatile int state)和 一个双向队列来实现。
3.2.1 state
state这个状态变量是用volatile来修饰的
- getState():获取当前同步状态。
- setState(int newState):设置当前同步状态。
- compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。
3.2.2 AQS双向同步队列CLH
同步器AQS内部的实现是依赖同步队列(CLH 队列,其实就是双向链表,java 中的 CLH 队列是原 CLH 队列的一个变种,队列里的线程由原自旋机制改为阻塞机制)来完成同步状态的管理。
同步队列主要包含节点的引用:一个指向头结点的引用(head),一个指向尾节点的引用(tail)。
3.3 AQS锁的类别:独占锁和共享锁两种
独占锁:锁在一个时间点只能被一个线程占有。根据锁的获取机制,又分为“公平锁”和“非公平锁”。等待队列中按照FIFO的原则获取锁,等待时间越长的线程越先获取到锁,这就是公平的获取锁,即公平锁。而非公平锁,线程获取的锁的时候,无视等待队列直接获取锁。ReentrantLock和ReentrantReadWriteLock.Writelock是独占锁。
共享锁:同一个时间能够被多个线程获取的锁。JUC 包中 ReentrantReadWriteLock.ReadLock,CyclicBarrier,CountDownLatch和Semaphore都是共享锁。
AQS定义了独占模式的acquire()和release()方法,共享模式的acquireShared()和releaseShared()方法.还定义了抽象方法tryAcquire()、tryAcquiredShared()、tryRelease()和tryReleaseShared()由子类实现,tryAcquire()和tryAcquiredShared()分别对应独占模式和共享模式下的锁的尝试获取,就是通过这两个方法来实现公平性和非公平性,在尝试获取中,如果新来的线程必须先入队才能获取锁就是公平的,否则就是非公平的。这里可以看出AQS定义整体的同步器框架,具体实现放手交由子类实现。
4. CountDownLatch(共享锁)
4.1 简介
CountDownLatch主要有两个方法:countDown() 和 await()
- await()方法也可以被任何线程调用多次,调用该方法的线程会处于等待/阻塞状态。如果多个线程同时执行await()方法,那么这几个线程都将处于等待状态,并且以共享模式享有同一个锁。
- countDown() 方法可以被任何线程调用多次,每次调用都会使计数器减一。其一般是执行任务的线程调用。
总结:
CountDownLatch是一个计数器闭锁,可以通过await()让一个线程或多个线程一直等待,直到其他线程调用countDown()使计数器减至0,从而全部被唤醒。CountDownLatch用一个给定的计数器来初始化,该计数器的操作是原子操作,即同时只能有一个线程去操作该计数器。
缺点:
所有线程被唤醒的现象只会出现一次,因为计数器不能被重置。如果业务上需要一个可以重置计数次数的版本,可以考虑使用CycliBarrier。
4.2 例子
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();
}
}
}
执行结果
在某些业务场景中,程序执行需要等待某个条件完成后才能继续执行后续的操作;典型的应用如并行计算,当某个处理的运算量很大时,可以将该运算任务拆分成多个子任务,等待所有的子任务都完成之后,父任务再拿到所有子任务的运算结果进行汇总。
4.3 实现原理
CountDownLatch 是基于 AbstractQueuedSynchronizer 实现的,在AbstractQueuedSynchronizer 中维护了一个 volatile 类型的整数 state,volatile 可以保证多线程环境下该变量的修改对每个线程都可见,并且由于该属性为整型,因而对该变量的修改也是原子的。创建一个 CountDownLatch 对象时,所传入的整数 n 就会赋值给 state 属性,当 countDown() 方法调用时,该线程就会尝试对 state 减一,而调用await() 方法时,当前线程就会判断 state 属性是否为 0,如果为 0,则继续往下执行,如果不为0,则使当前线程进入等待状态,直到某个线程将 state 属性置为 0,其就会唤醒在await()方法中等待的线程。
countDown()方法的源代码
public void countDown() {
sync.releaseShared(1);
}
sync 是一个继承了AbstractQueuedSynchronizer的类实例,该类是CountDownLatch的一个内部类,其声明如下:
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)方法的实现:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
调用 tryReleaseShared(int) 方法时会在无限 for 循环中设置 state 属性的值,设置成功之后其会根据设置的返回值,即当前线程是否为将 state 属性设置为 0 的线程,来判断是否执行doReleaseShared()。doReleaseShared()方法主要作用是唤醒调用了await()方法的线程。
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)方法唤醒头结点的下一个节点,使其继续往下执行。
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
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
acquireSharedInterruptibly()
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
doAcquireSharedInterruptibly(AQS) —>tryAcquireShared
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);
}
}
5. Semaphore
5.1 简介
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。比如控制用户的访问量,同一时刻只允许1000个用户同时使用系统,如果超过1000个并发,则需要等待。
Semaphore与CountDownLatch相似,不同的地方在于Semaphore的值被获取到后是可以释放的,并不像CountDownLatch那样一直减到底。它也被更多地用来限制流量,类似阀门的 功能。如果限定某些资源最多有N个线程可以访问,那么超过N个主不允许再有线程来访问,同时当现有线程结束后,就会释放,然后允许新的线程进来。有点类似于锁的lock与 unlock过程。相对来说他也有两个主要的方法:
用于获取权限的acquire(),其底层实现与CountDownLatch.countdown()类似;
用于释放权限的release(),其底层实现与acquire()是一个互逆的过程。
5.2 举例
比如模拟一个停车场停车信号,假设停车场只有两个车位,一开始两个车位都是空的。这时如果同时来了两辆车,看门人允许它们进入停车场,然后放下车拦。以后来的车必须在入口等待,直到停车场中有车辆离开。这时,如果有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,如果又离开一辆,则又可以放入一辆,如此往复。
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();
}
}
}
}
5.3 源码分析
Semaphore 通过使用内部类Sync继承AQS来实现。
支持公平锁和非公平锁。内部使用的AQS的共享锁。
Semaphore 构造器
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:可响应中断的获得信号量
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) 方法获取指定数量的信号量 。
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
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;
}
}
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()
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;
}
}