1、什么是锁,为什么需要锁?
并发环境下,会存在多个线程对同一个资源进行争抢的情况,假设线程A对资源正在进行修改,此时线程B又对同一资源进行了修改,就会导致数据不一致的问题。为了解决这个问题,很多编程语言引入了锁机制。
通过一种抽象的“锁”来对资源进行锁定,当一个线程持有“锁”时,其他线程必须等待 ------ 在临界资源上对线程进行一种串行化处理。
2、悲观锁 VS 乐观锁
乐观锁和悲观锁是一种广义上的概念,体现了看待线程并发时的角度。
- 悲观锁:认为自己在使用数据的时候一定会有别的线程修改数据,因此获取数据时会先加锁,确保不会被别的线程修改
- 乐观锁:认为自己使用数据的时候不会有别的线程修改数据,所以不会去加锁,只是在更新数据的时候去判断有没有别的线程更新了这个数据,如果没有被更新,则自己执行相关操作,如果被其他线程更新了,则执行对应的操作(重试或者报错)
3、介绍一下悲观锁。
java中,悲观锁的实现是基于object的 ------ 也就是说,每个对象都拥有一把锁,这个锁存放在对象头中,记录了当前对象被哪个线程所占用(持有)。
3.1、对象结构,对象头结构
首先说一下对象的结构:
- 对象头
- 实例数据
- 填充字节(为了满足java对象大小是8字节的整数倍,没有实际意义)
对象头结构:
- mark word --- 见下图
- class pointer --- 是一个指针,指向当前对象类型所在方法区中的class信息
相比于实例数据,对象头其实是一种额外的开销,因此设计的很小(32bit或者64bit)
从上图可以看到,“锁”的信息就存放在mark word中,根据后两个标志位,锁的状态又可以分为 无锁,偏向锁,轻量级锁,重量级锁。--- java中启用对象锁的方式就是 synchronized 关键字。下面会详细介绍一下synchronized背后的原理。
3.2 synchronized关键字
在java中,synchronized关键字可以用来加锁,而synchronized被编译后会生成两个字节码指令:monitorenter 和 monitorexit, 这两个字节码指令底层是依赖于操作系统的 mutex lock 来实现的,这个时候,java线程实际就是操作系统线程的映射,每当唤醒或者挂起一个线程的时候,都需要切换到操作系统的内核态,这个操作是重量级的(换句话说就是比较耗费时间,甚至切换的时间就超出了线程执行任务的时间。)因此,使用synchronized其实是会对程序的性能产生影响。
刚刚提到了monitor(又叫监视器、管程等),我们可以将它理解为只能容纳一个客人的饭店,客人就是想要获取对象锁的线程,一旦有一个线程进入了monitor,那么其他线程只能等待,只有当这个线程退出去,其他线程才有机会进入。
3.3 ,介绍一下对象锁的四种状态
1、无锁
顾名思义就是没有对系统资源进行锁定,1)比如某种资源不会出现在多线程环境下,或者即便在多线程环境下面也不会有竞争情况,那么就不需要加锁。2)虽然会被竞争,但是采用了其他机制来控制,而不是使用操作系统对资源锁定(CAS,后面会介绍)。
2、偏向锁
假如一个对象被加锁了,但是实际运行过程中,只有一个线程会获取这个对象锁(前提),最理想的方式是只在用户态就把锁交出去,而不是通过操作系统来切换线程状态。 ---- 如果对象能够认识这个唯一的线程,只要这个线程过来,就直接把锁交出去就行了。(对象偏爱这个线程,称为偏向锁)
偏向锁是怎么实现的呢? 回到mark word 中,当锁标志位为01的时候,判断倒数第三个bit, 如果是1,则表示当前对象锁的状态为偏向锁,此时去读mark word的前23bit,这记录的就是线程ID,可以通过线程id来判断这个线程是不是被偏爱的线程。
3、轻量级锁
某一个时间,情况发生了变化,不止一个线程想要获取这个锁了(两个线程),这个时候,偏向锁就会升级成为轻量级锁。
轻量级锁是如何实现的? --- Markword 后两位变成00的时候,就意味着当前对象锁的状态是轻量级锁了,此时不在用线程id了,前30位变成了指向虚拟机栈中锁记录的指针。
当一个线程想要获取某个对象锁,发现Markword 后两位是00,此时会在自己的虚拟机栈中开辟一块成为 lock record 的空间,这个lock record存放的是Markword 的副本以及一个owner指针
线程通过cas去尝试获取锁,一旦获得,就会复制这个对象的Markword到自身虚拟机栈的lock record中,并且将lock record中的owner指针指向该对象锁。同时,对象的mark word的前30位生成一个指针,指向持有改对象锁的线程虚拟机栈中的lock record,这样就完成了线程和对象锁的绑定,双方都知道各自的存在了。
此时,获得了对象锁的线程,就可以执行对应的任务了,那么没有获得锁的线程应该怎么办呢?--- 没有获得锁的线程会自旋等待,自旋就是一种轮询,不断判断锁有没有被释放,如果释放了,就获取锁,如果没有释放,就下一轮循环。 --- 这种自旋区别于被操作系统挂起阻塞,因为如果对象很快被释放的话,自旋获取锁就可以在用户态解决,而不用切换到内核态,效率比较高。
但是自旋其实就是CPU在空转,长时间的自旋会浪费CPU资源,于是出现了一种叫做“适应性自旋”的优化。简单来说就是,自旋时间不在固定了,而是又上一次在同一个锁上的自旋时间以及锁状态来决定,比如在同一个锁上,当前自旋等待的线程刚刚成功获得过锁,但是此时锁被其他线程持有,虚拟机就会认为下次自旋获取锁的概率很大,进而运行更长时间的自旋。
4、重量级锁
一旦自旋等待的线程超过一个,即有三个及以上的线程想获取同一个锁,这个时候就会升级成为重量级锁,这个时候就是通过monitor来对线程进行控制了,一旦进入了这个状态,就会调用内核空间,产生极大的开销。
上述描述了对象锁的四种状态,需要注意的是,锁只能升级,不能降级。
4、介绍一下乐观锁
在讲述悲观锁的时候,提到了“无锁”这个概念,其中有一种是共享资源会出现被竞争的情况,但是不是使用操作系统同步原语进行保护,而是使用CAS这种方式进行线程同步,尽量将获取锁释放锁的操作在用户空间内完成,减少用户态和内核态之间的切换次数,提升程序的性能。 --------- 可以看到,其实乐观锁并不是锁,而是一种无锁的实现。
CAS就是实现乐观锁的一种经典巧妙的算法。 compare and swap,简单的翻译为,比较然后交换。
4.1, 如何理解CAS?
举例:比如说厕所的坑位,里面没人的时候你才可以进去,有人就只能在外面等着,设定开门状态是0,关门状态是1, 某一时间,两个人都想上厕所,A先冲了过去,并把门关上,这个时候B才过来,但是发现门已经关了,但是B也不能放弃啊,就不断回来看看门打开了没。
上述例子中,AB两个人就是代表线程,坑位就是共享资源,这样就应该比较容易理解CAS了,当一个共享资源状态值为0的一瞬间,AB线程读到了,此时两个线程都认为当前这个共享资源没有被占用,于是他们会各自生成两个值:
- old value,代表之前读到的共享资源对象的状态值 -- 上述例子中都为0
- new value,代表想要将共享资源对象状态值更新的值 -- 上述例子中都为1
此时AB线程都去争抢修改对象的状态值,然后占用对象。假设A运气比较好, A将old value 和 资源对象的状态值进行compare 比较,发现是一致的,于是将对象的状态值 swap 为new value; B落后一步,compare的时候发现自己的old value 和对象的状态值不一样,只能放弃swap操作(一般是自旋操作,同时配置自旋次数防止死循环,默认值10 )。
上述是一个CAS函数,但是其实是有问题的,因为这个函数本身没有任何同步措施,还是存在不安全的问题,因此,想要通过 CAS实现乐观锁,有一个必须的前提:CAS操作本身必须是原子性的。
那么CAS是如何实现原子性的呢? --- 不同架构的CPU都提供了指令级的CAS原子操作,比如X86架构下的cmpxchg指令,ARM架构下的LL/SC指令。也就是说,CPU已经原生的支持了CAS,那么上层直接调用即可。
// 乐观锁的实现举例
private static AtomicInteger num = new AtomicInteger();
public void testCAS() {
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (num.get() < 1000) {
System.out.println(Thread.currentThread().getName()
+ ":" + num.getAndIncrement());
}
}
});
}
}
通过AtomicIteger的源码,发现是使用Unsafe类来实现CAS操作的,这个cas方法是一个native函数,说明是本地方法,和具体的平台实现相关。
5、AQS 机制是什么?
通过上述介绍CAS,我们知道了java通过unsafe 类封装了CAS方法,支持了对CAS原语的调用,但是针对上层业务开发,怎么能够无感知的调用?并且业务场景中,我们最常竞争的资源往往是通过对象进行封装的,而CAS只能原始的修改内存上的一个值。如何进一步对CAS进行抽象呢?
下面就首先介绍一下JUC中经典的同步框架AQS (AbstractQueuedSynchronizer)
5.1 、AQS的成员属性
- state是用于判断共享资源是否正在占用的标记为,volatile关键字保证了线程之间的可见性。至于为什么用int类型而不是布尔类型,是因为AQS中有独占锁和共享锁的区别,共享模式下,state可以表示占用锁的线程的数量。
- AQS中还存在一个队列,用于管理等待获取锁的线程。FIFO双向链表,head和tail定义了头和尾
// 队列的头 尾部
private transient volatile Node head;
private transient volatile Node tail;
// 判断共享资源的标志位
private volatile int state;
5.2、AQS的内部类
AQS维护了一个FIFO队列,因此定义了一个Node节点,里面存储了线程对象 thread,节点在队列中的等待状态 waitstatus,前后指针等信息。
- waitStatus:AQS工作的时候,必然伴随着Node 节点状态值的各种变化,这里的waitStatus是一个枚举值
- 0:节点初始化默认值,或者节点已经释放锁
- 1:取消获取锁
- -1:节点的后续节点需要被唤醒
- -2:条件模式相关
- -3:共享模式相关,传递共享模式下锁的释放状态
- predecessor() 方法:获取当前节点的前置Node
// 简化的,删除了很多东西
static final class Node {
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
}
5.3、AQS中的核心方法
一般业务,可以分为两种场景使用锁:
- 尝试获取锁,不管有没有获取到,立即返回
- 必须获取锁,没有获取到则进行等待
恰好,AQS中针对上述两种场景,提供了两个方法,tryAcquire 和 acquire 两个方法。
5.3.1 tryAcquire -- 尝试获取锁
这个方法是参数是一个int 类型的值,代表对 state的增加操作,返回值是boolean,代表是否成功获取锁。该方法只抛出了一个异常,目的就是为了让子类进行重写,在子类中定义相关的业务逻辑,比如没有获取到锁是等待,还是别的处理。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
5.3.2 acquire -- 必须获取锁
acquire方法被final修饰,无法重写,想要等待并获取锁,直接调用这个方法就可以。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
if条件包含了两个判断条件,tryAcquire已经说过了,现在讲一下acquireQueue方法。
【addWaiter】
首先看一下 addWaiter发方法 --- 这个方法作用就是将当前线程封装成一个node,然后加入等待队列中。
private Node addWaiter(Node mode) {
// 封装当前线程为一个NODE节点
Node node = new Node(Thread.currentThread(), mode);
// 获取当前尾节点
Node pred = tail;
if (pred != null) {
// 先将当前节点前指针指向尾节点
node.prev = pred;
// CAS判断当前尾节点是否还是尾节点,如果是,就把当前节点变为尾节点,然后返回出去
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果当前队列为空,或者CAS失败,就进入这个方法
enq(node);
return node;
}
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;
}
}
}
}
既然将node加入队列成功了,后面肯定还会从队列中取出节点,一般这种FIFO队列都会使用 “生产者-消费者”模式,但是AQS却不是这么使用的,我们接着往下看。
【acquireQueued】方法
- 首先定义了一个failed变量,默认是true,如果当前线程正常获取到了锁,这个值就改为false,finally语句块里面的方法只是为了解决异常情况下,取消当前线程获取锁的行为
- 在AQS中,头节点head是个虚节点,即队列中的第一个节点的前一个节点是头节点
- 首先只有队列中的第一个节点,才有权限尝试获取锁,如果获取到锁,进入if中,然后返回结果,如果没有获取到锁,就进入下一个判断。
- 第二个if看判断条件,从名字上来看,首先判断当前线程是否需要挂起,如果需要挂起,就执行挂起操作,如果不需要,就继续自旋获取锁。
- 【shouldParkAfterFailedAcquire】判断获取锁失败后,是否需要挂起
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 如果是队列中的第一个节点,并且获取到了锁,就进入这个if中
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 判断是否需要挂起,如果需要挂起就执行挂起操作,否则下一次for循环
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
// 取消当前线程获取锁的行为。处理try中的异常情况
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// ws为-1,当前节点也在等待拿锁,因此可以挂起休息
return true;
if (ws > 0) {
// 表示获取锁请求被取消了,就从当前队列中删除
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 将当前节点状态改为-1,然后外层重试
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
// 这一行就是执行了挂起操作。是通过unsafe的native方法操作系统原语
LockSupport.park(this);
// 被唤醒之后,返回当前线程有没有被中断。
return Thread.interrupted();
}
通过对acquireQueued 方法的分析,可以说,这个方法就是将当前队列中的线程节点都挂起,避免不必要的自旋浪费CPU资源。
既然有线程被挂起,那么就需要将这些挂起的线程唤醒,当持有锁的线程释放了锁,那么就会尝试唤醒后续的节点,AQS中提供了release方法用来唤醒挂起的线程。
5.3.3 tryRelease方法
和tryAcquire方法一样,tryRelease方法也是开放给上层实现的。
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
5.3.4 release 方法
release方法中,假设尝试释放锁成功,下一步就要唤醒等待队列中的其他节点,unparkSuccessor方法中传入的是头节点。
【unparkSuccessor】方法
- 设置头节点的状态为0,表示已经释放了锁
- 获取队列中最靠前的一个准备获取锁的节点(状态不能是canceled:取消获取锁)
- 将这个节点唤醒,去争抢锁。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
// 获取头节点的状态,如果不是0,就修改为0,表示锁已经释放了
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 获取头节点的后续节点
Node s = node.next;
// 如果为空,或者处于canceled状态,那么就从后往前搜索,找到除head外最靠前的node
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;
}
// 唤醒这个node,让他起来尝试拿锁
if (s != null)
LockSupport.unpark(s.thread);
}
【注意】 unparkSuccessor中搜索节点时是从后往前搜的,为什么这样操作呢?
我们前面介绍了addWaiter方法,后节点的pre指针先指向前节点,前节点的next指针才会指向后节点,这两个步骤不是原子性操作,因此,如果从前往后搜索,可能前节点的next还没有建立好,那么搜索可能就中断了。
6、ReentrantLock介绍
ReentrantLock被称为可重入锁,是JUC包中并发锁的实现之一,底层调用了AQS,这个锁还包含了公平锁和非公平锁的特性
6.1 ReentrantLock的属性
ReentrantLock中只有一个sync属性,sync属性被final修饰,意味着一旦ReentrantLock被初始化,sync属性就不可修改了。
ReentrantLock提供了两个构造器,通过名字可以看出来,我们可以通过传参的形式将ReentrantLock实例化为公平锁 或者是非公平锁。
private final Sync sync;
// 默认构造器,初始化为非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// 入参为true的时候是公平锁,否则是非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
6.2 ReentrantLock的内部类 - Sync
上面说了ReentrantLock存在一个Sync类型的构造器,那么一起看看这个内部类吧,这个内部类没有属性,除了lock和readObject方法,其余方法都用final修饰了,不希望被外部破坏。
- 首先Sync内部类继承了AbstractQueuedSynchronizer,说明AQS中机制Sync都可以借用了,Sync被abstrat修饰,说明需要通过子类来进行实例化,NonfairSync / FairSync 后续会做相关介绍。
- lock() 方法,加锁的抽象方法,需要子类来实现,后续介绍
- nonfairTryAcquire() 方法,从名字来看,是获取非公平锁,在父类中定义,是因为外层有调用
- tryRelease方法,返回当前锁是否完全释放
- 其余的一些方法简单了解即可
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
abstract void lock();
// 尝试获取非公平锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取AQS的state属性
int c = getState();
if (c == 0) {
// state属性为0说明锁空闲,通过cas来更改state,
// 如果成功则获取到了锁,返回true,否则在下面返回false
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// state不为0判断当前线程是否是独占线程---- 这就是可重入锁的实现
// 是独占锁,则将state+1,并赋值回去,因此AQS中的state数量其实就是当前线程重入次数
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
// state是int类型的,16位,小于0就代表溢出了 -- 可重入的最大次数
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// 释放锁 -- 注意返回值是“是否完全释放”
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
// 判断当前线程是否获取到锁
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
// 获取占用锁的线程对象
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
// 返回state的值
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
// 判断锁是否空闲
final boolean isLocked() {
return getState() != 0;
}
// 反序列化,可以不关注
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
6.3 ReentrantLock的内部类 - FairSync
- 公平锁,锁的分配会按照请求锁的顺序,比如按照AQS中的FIFO队列来排队获取锁,就是公平锁的实现。
- 公平锁能够保证只要你排队了,就一定可以拿到锁。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
// 调用AQS获取锁的逻辑
final void lock() {
acquire(1);
}
// 重写AQS的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 锁空闲,并且AQS队列中没有其他节点,那么就尝试取锁 --- 体现了公平性
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 如果获取锁的是当前线程,就state+1 -- 可重入
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
6.4 ReentrantLock的内部类 - NonFairSync
- 非公平锁:获取锁不会按照锁请求顺序,比如抢占式
- 非公平锁不会保证排队的线程一定会获取锁,某个线程可能一直处于阻塞状态,称为“饥饿”
- 为什么设计非公平锁呢? --- 某些时候,唤醒已经挂起的线程,这个线程的状态切换会产生短暂的延时,而这个延时时间可能就够进行一次业务处理,因此非公平锁可利用这段时间完成操作,提高效率。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
// 加锁,先尝试进行一次CAS,失败则进入队列排队
// 因为AQS的acquire方法中调用了tryAcquire,下面又重写了tryAcquire方法
// 结合nonfairTryAcquire方法代码,可以看到,非公平锁是先两次尝试获取锁,失败之后在排队拿锁
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// 直接调用父类的nonfairTryAcquire方法。
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
6.5 ReentrantLock的公共方法 - lock / tryLock / unlock
- lock方法 : 加锁的方法很简单,就是通过sync属性去调用,sync的lock方法在上述内部类中都已经介绍了。-- 多态
- tryLock方法:无论sync的实现是否是公平锁,tryLock的实现都是非公平的 --- nonfairTryAcquire方法写在Sync中的原因
- unlock :释放锁操作,每次对于每次执行,state-1
public void lock() {
sync.lock();
}
// 无论sync的实现是否是公平锁,tryLock的实现都是非公平的
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
// 含参的,给了个超时时间
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.release(1);
}
6.6 ReentrantLock 类的使用介绍
6.6.1 使用ReentrantLock进行普通加锁
功能类似于synchronized关键字,获取到锁的线程释放锁之后,其他线程才可以获取到锁。
public class TestReentrantLock {
private static final Lock lock = new ReentrantLock();
public void testLock() {
try {
lock.lock();
System.out.println("Thread name: " + Thread.currentThread().getName() + " lock");
Thread.sleep(2000);
lock.unlock();
System.out.println("Thread name: " + Thread.currentThread().getName() + " unlock");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
TestReentrantLock testLock = new TestReentrantLock();
// 起两个线程,第一个线程释放锁之后,第二个线程才可以获取到锁
new Thread(new Runnable() {
@Override
public void run() {
testLock.testMethod();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
testLock.testMethod();
}
}).start();
}
}
------print------------
Thread name: Thread-0 lock
Thread name: Thread-0 un lock
Thread name: Thread-1 lock
Thread name: Thread-1 un lock
6.6.2 使用ReentrantLock实现锁的可入
main方法中先加锁,,然后在解锁前调用testLock方法,因为是在一个线程中,所以不需要释放锁,也可以获取到锁。
public class TestReentrantLock {
private static final Lock lock = new ReentrantLock();
public void testLock() {
try {
lock.lock();
System.out.println("Thread name: " + Thread.currentThread().getName() + " 加锁");
Thread.sleep(2000);
lock.unlock();
System.out.println("Thread name: " + Thread.currentThread().getName() + " 解锁");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
TestReentrantLock testLock = new TestReentrantLock();
// main线程先加锁
lock.lock();
System.out.println(Thread.currentThread().getName() + " 线程获得了锁");
testLock.testLock();
lock.unlock();
System.out.println(Thread.currentThread().getName() + " 线程释放了锁");
}
}
------print-----
main 线程获得了锁
Thread name: main 加锁
Thread name: main 解锁
main 线程释放了锁
7、CountDownLatch 介绍
CountDownLatch是JUC工具包中很重要的一个同步工具。CountDownLatch基于AQS, 他的作用是,让某一个线程等待多个线程的操作完成之后,再执行自己的操作。
CountDownLatch定义了一个计数器和一个阻塞队列,当计数器的值递减为0之前,阻塞队列里面的线程,
7.1、CountDownLatch 的使用。
public class DemoCountDownLatch {
private final static Random random = new Random();
static class SearchTask implements Runnable {
private int id;
private CountDownLatch latch;
public SearchTask(int id, CountDownLatch latch) {
this.id = id;
this.latch = latch;
}
@Override
public void run() {
System.out.println("开始寻找" + id + "号龙珠");
int seconds = random.nextInt(10);
try {
Thread.sleep(seconds * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("找到" + id + "号龙珠" + "花费了" + seconds + "秒时间");
/**
* 调用countDown() 方法,计数器会减一
*/
latch.countDown();
}
}
public static void main(String[] args) throws InterruptedException {
List<Integer> idList = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
/**
* CountDownLatch初始化的时候,给定一个整数计数器,不可变
*/
CountDownLatch latch = new CountDownLatch(idList.size());
for (Integer id : idList) {
Thread thread = new Thread(new SearchTask(id, latch));
thread.start(); // 注意,start 是开启新线程多线程执行,run方法是串行的
}
/**
* 调用 await() 方法时 ,如果计数器大于0,当前线程阻塞,直到计数器被countDown方法减到0时,线程才会继续执行
*/
latch.await();
/**
* 调用 await方法时,设置超时参数
* 如果计数器大于0,当前线程阻塞,直到计数器被countDown方法减到0时,线程才会继续执行
*/
// latch.await(3, TimeUnit.SECONDS);
System.out.println("所有龙珠找到!召唤神龙!");
}
}
-----await() print ------------
----- 子线程都执行完了,主线程才开始执行 ----------
开始寻找1号龙珠
开始寻找3号龙珠
开始寻找2号龙珠
开始寻找5号龙珠
开始寻找6号龙珠
开始寻找4号龙珠
开始寻找7号龙珠
找到3号龙珠花费了3秒时间
找到1号龙珠花费了3秒时间
找到7号龙珠花费了5秒时间
找到6号龙珠花费了5秒时间
找到2号龙珠花费了6秒时间
找到4号龙珠花费了6秒时间
找到5号龙珠花费了8秒时间
所有龙珠找到!召唤神龙!
-----await(3, TimeUnit.SECONDS) print ------------
--- 设置了三秒超时,三秒之后无论子线程有没有全部结束,都执行主线程 ------
开始寻找1号龙珠
开始寻找3号龙珠
开始寻找2号龙珠
开始寻找4号龙珠
开始寻找5号龙珠
开始寻找7号龙珠
开始寻找6号龙珠
找到5号龙珠花费了3秒时间
所有龙珠找到!召唤神龙!
找到6号龙珠花费了5秒时间
找到7号龙珠花费了8秒时间
找到2号龙珠花费了8秒时间
找到1号龙珠花费了8秒时间
找到4号龙珠花费了9秒时间
找到3号龙珠花费了9秒时间
-----不执行await print ------------
--- 主线程直接执行,不等待子线程 ------
开始寻找3号龙珠
所有龙珠找到!召唤神龙!
开始寻找4号龙珠
开始寻找5号龙珠
开始寻找6号龙珠
开始寻找2号龙珠
开始寻找7号龙珠
开始寻找1号龙珠
找到1号龙珠花费了2秒时间
找到3号龙珠花费了3秒时间
找到4号龙珠花费了4秒时间
找到2号龙珠花费了5秒时间
找到5号龙珠花费了6秒时间
找到6号龙珠花费了9秒时间
找到7号龙珠花费了9秒时间
7.2、CountDownLatch设计思路?
CountDownLatch,我们就简单的描述为,主线程等待子线程处理完任务之后在继续执行自己的任务。既然主线程在等待,根据前面学习的AQS,此时主线程应该放入等待队列中,那么什么时候唤醒主线程呢?当然是子任务都执行结束之后,那么AQS中的state就可以派上用场了,state表示子线程的数量(也就是主线程需要等待的线程数目),每当一个任务完成了,state就减去1,当state值为0 的时候,就唤醒正在等待的主线程。
CountDownLatch 的设计思路大致就是上面介绍的,下面会根据源码来进行剖析。
7.2.1 sync内部类
CountDownLatch内部也定义了一个sync内部类,并继承了AQS;
【tryAcquireShared】方法,这个方法是尝试获取共享锁,是对AQS方法的一个重写。
这个方法很简单,获取state的值,如果等于0,就返回1,否则返回-1;
子类对父类方法的重写,也是要按照约定去重写的,我们在看看AQS中对tryAcquireShared方法的定义:
- 返回负值:获取锁失败
- 返回0:共享模式下获取锁成功,不唤醒后续节点
- 返回整数:获取锁成功,并唤醒后续节点
从父类方法定义来看,如果tryAcquireShared方法返回整数,是需要获取锁的,但是子类实现的方法并没有获取锁的操作,这个是为什么呢?
----
实际上,我们需要从CountDownLatch这个组件需要解决的问题来看待,当CountDownLatch被初始化时,必须传入一个count,这个值会赋给aqs的state,每当一个子任务完成,state值就会减一,一直到state为0,然后主任务继续操作。这其实就是一个子任务不断释放锁,主任务不断检查锁有没有完全释放的过程,所有的操作不涉及到加锁的情况,虽然主任务在state为0的时候也可以加锁,但是完全没有必要。
【tryReleaseShared】方法,就是一个释放锁的操作,
锁完全释放返回true,否则返回false
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
// 构造方法,需要传入一个count值
Sync(int count) {
setState(count);
}
// 获取当前count值
int getCount() {
return getState();
}
// 对AQS的重写,从名字看,方法被shared修饰,应该是用到了共享模式
// 共享模式下,state可以被多个线程同时修改,加1代表获取共享锁,减1代表释放共享锁
// 这个方法就是如果state为0,代表子任务全部完成,否则就是还有没完成的
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
// 释放锁的操作,一个自旋的过程
// 不需要释放锁或者没有完全释放锁,返回false
// 锁完全释放了,返回true
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;
}
}
}
7.2.2 内部属性和构造器
内部属性只有一个sync,只有一个有参数构造器,必须传递一个大于0的整数(主线程需要等待子线程的数量。)
private final Sync sync;
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
7.2.3 await 方法
这个方法就是主线程等待子线程执行完的逻辑。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// AQS的方法
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 锁没有完全释放的情况,也就是主线程等待子线程的场景
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 设置共享模式的node
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
// 当前节点的前置节点为head,说明当前节点可以被唤醒
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);
}
}
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
// 传播行为大于0,唤醒后续节点
// 被唤醒的节点会在doAcquireSharedInterruptibly方法中的for循环继续执行
// 不断的唤醒队列中处于等待状态的且共享模式的线程。
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
7.2.4 countDown方法
就是为了保证state的自减操作,调用此方法,state减1
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
// 前面介绍过,尝试释放锁,只有完全释放了才会返回true
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
// 释房共享锁,主要是唤醒等待队列的头节点,从后往前搜索
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;
}
}