JUC锁与AQS技术【我的Android开发技术】
AQS原理
AQS就是一个同步器,要做的事情就相当于一个锁,所以就会有两个动作:一个是获取,一个是释放。获取释放的时候该有一个东西来记住他是被用还是没被用,这个东西就是一个状态。如果锁被获取了,也就是被用了,还有很多其他的要来获取锁,总不能给全部拒绝了,这时候就需要他们排队,这里就需要一个队列。这大概就清楚了AQS的主要构成了:
- 获取和释放两个动作
- 同步状态(原子操作)
- 阻塞队列
Lock特性
锁机制用于保证操作的原子性、可见性、顺序性。 JDK1.5的concurrent并发包 中新增了 Lock接口以及相关实现类来实现锁功能 ,最明显的特性就是需要显式的申请锁和释放锁。比synchronized更加灵活。
显示锁的释放锁的操作一定要放到finally块中,否则可能会因为异常导致锁永远无法释放!这是显式锁最明显的缺点。
1.1.显示加锁、解锁
- synchronized 关键字是自动进行加锁、解锁的,而Lock的具体实现需要 lock() 和 unlock()方法配合 try/finally 语句块来完成,来手动加锁、解锁。
1.1.可重入
像 synchronized 和 ReentrantLock 都是可 重入锁 ,可重入性表明 了锁的分配机制是 基于线程的分配 ,而 不是基于方法调用 的分配。
- 可重入锁:又名 递归锁 ,即一个线程得到一个对象锁后再次请求该对象锁,是永远可以拿到锁的。在 Java 中线程获得对象锁的操作是 以线程为单位的 ,而不是以方法调用为单位的。但 获取锁和释放锁必须要成对出现 。
- 如 ReentrantLock ,它是基于 AQS(AbstractQueueSyncronized) 实现的, AQS 是基于 volitale 和 CAS 实现的 ,其中 AQS 中维护一个 valitale 类型的变量 state 来做一个可重入锁的重入次数,加锁和释放锁也是围绕这个变量来进行的 。
1.2.可响应中断
- 当线程因为获取锁而进入 阻塞状态 , 外部是可以中断该线程的 , 调用方通过捕获nterruptedException可以捕获中断
- 如 ReentrantLock 中的 lockInterruptibly() 方法 可以使线程在被 阻塞 时响应中断
假设: 线程1 通过 lockInterruptibly() 方法获取到一个可重入锁,并执行一个长时间的任务,另一个 线程2 通过 interrupt() 方法 就可以立刻 打断 线程1的执行 ,来获取 线程1 持有的那个可重入锁。而通过 ReentrantLock 的 lock() 方法或者 Synchronized 持有锁的线程是不会响应其他线程的 interrupt() 方法的,直到该方法主动释放锁之后才会响应 interrupt() 方法。
1.3.可设置等待超时时间
- synchronized 关键字无法设置锁的超时时间,如果一个获得锁的线程内部发生死锁,那么其他线程就会一直进入阻塞状态, 而 Lock的具体实现,可以 设置线程获取锁的等待超时时间 ,通过 方法返回值 判断是否成功获取锁,来 避免死锁
1.4.锁的公平性
提供公平锁和非公平锁2种选择。
-
公平锁( 默认 ) :线程将按照他们发出 发出申请锁的顺序 来获取锁,先进先出, 不允许插队
-
非公平锁: 允许插队 ,当一个线程请求获取锁时,如果这个锁是 可用 的,那这个线程将 跳过所在队列里等待线程并获得锁。
- 如: synchronized 关键字是一 种非公平锁 ,先抢到锁的线程先执行。而 ReentrantLock的构造方法中允许设置 true/false 来实现公平、非公平锁 ,如果设置为 true ,则线程获取锁要遵循 "先来后到" 的规则,每 次都会构造一个 线程 Node ,然后到双向链表的 "尾巴"后面排队,等待前面的 Node 释放锁资源。
- 考虑这么一种情况:A线程持有锁,B线程请求这个锁,因此B线程被挂起;A线程释放这个锁时,B线程将被唤醒,因此再次尝试获取锁; 与此同时,C线程也请求获取这个锁,那么C线程很可能在B线程被完全唤醒之前 获得、使用以及释放 这个锁。这是种双赢的局面,B获取锁的时刻(B被唤醒后才能获取锁)并没有推迟,C更早地获取了锁,并且吞吐量也获得了提高。 在大多数情况下,非公平锁的性能要高于公平锁的性能。
另外,这个公平性是针对 线程 而言的,不能依赖此来实现业务上的公平性,应该由开发者自己控制,比如通过 FIFO队列 来保证公平。
1.5.读写锁
-
允许读锁和写锁分离,读锁与写锁互斥,但是多个读锁可以共存, 即一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行 。适用于 读远大于写 的场景
-
即:读读共享、写写互斥、读写互斥
关于读写锁的一些知识:
-
1.重入方面其内部的写锁可以获取读锁,但是反过来读锁想要获得写锁则永远都不要想。
-
2.写锁可以降级为读锁,顺序是: 先获得写锁再获得读锁,然后释放写锁 ,这时候线程将保持读锁的持有。反过来读锁想要升级为写锁则不行
-
3. 读锁被线程持有时排斥任何的写锁,而线程持有写锁则是完全的互斥.这一特性最为重要, ,对于 读多写少 的场景使用此类才可以提高并发量。
-
4.不管是读锁还是写锁都支持 响应中断
-
5.写锁支持Condition且用于与ReentrantLock一样, 而读锁则不能使用Condition ,否则抛出UnsupportedOperationException异常。
1.6.丰富的API
提供了多个方法来获取锁相关的信息,可以帮助开发者监控和排查问题
- isFair() :判断锁是否是公平锁
- isLocked() :判断锁是否被任何线程获取了
- isHeldByCurrentThread() :判断锁是否被当前线程获取了
- hasQueuedThreads() :判断是否有线程在等待该锁
- etHoldCount() :查询当前线程占有lock锁的次数
- getQueueLength() :获取正在等待此锁的线程数
1.7.常用方法
void lock() :在线程获取锁时如果锁已被其他线程获取,则进行 等待
Lock lock = new ReentrantLock();//获取锁
lock.lock();
try{
//获取到了被本锁保护的资源,处理任务
//捕获异常
}finally{
lock.unlock(); //释放锁
}
- boolean tryLock() : 尝试获取锁 ,如果当前锁没有被其他线程占用,则获取成功,返回 true,否则返回 false
我们可以根据是否能获取到锁来决定后续程序的行为。该方法会立即返回,在拿不到锁时也不会一直等待,通常我们用 if 语句 判断 tryLock() 的返回结果 , 根据是否获取到锁来执行不同的业务逻辑 ,使用方法如下。
Lock lock = new ReentrantLock();//获取锁
//如果能获取到锁
if(lock.tryLock()) {
try{
//处理任务
}finally{
lock.unlock(); //释放锁
}
}
//如果不能获取锁,则做其他事情
else {
}
利用 tryLock() 方法我们还可以解决死锁问题
public void tryLock(Lock lock1, Lock lock2) throws InterruptedException {
//自旋
while (true) {
//如果能获取锁1
if (lock1.tryLock()) {
try {
//如果能获取锁2
if (lock2.tryLock()) {
try {
System.out.println("获取到了两把锁,完成业务逻辑");
return;
} finally {
//释放锁2
lock2.unlock();
}
}
} finally {
//释放锁1
lock1.unlock();
}
}
//如果不能能获取锁,线程休眠若干秒
else {
Thread.sleep(new Random().nextInt(1000));
}
}
}
- boolean tryLock(long time, TimeUnit unit): 可响应中断并且有超时时间的尝试获取锁 ,在拿不到锁时会等待一定的时间,如果在时间结束后,还获取不到锁,就会返回 false;如果一开始就获取锁或者等待期间内获取到锁,则返回 true。
这个方法解决了 lock() 方法容易发生死锁的问题,使用 tryLock(long time, TimeUnit unit) 时 ,在等待了一段指定的超时时间后, 线程会主动放弃获取这把锁 ,避免永久等待。 等待获取锁的期间,也可以 随时中断线程 ,这就避免了死锁的发生。
- void lockInterruptibly(): 可响应中断的去获取锁 ,如果这个锁当前是可以获得的,那么这个方法会立刻返回,但是如果这个锁当前是不能获得的(被其他线程占用),那么当前线程便会开始 等待 ,除非它 等到了这把锁或者是在等待的过程中被中断了 ,否则这个线程便会一直在这里执行这行代码。一句话总结就是, 除非当前线程在获取锁期间被 中断 ,否则便会 一直尝试获取 直到获取到为止。
顾名思义, lockInterruptibly() 是可以 响应中断 的。相比于不能响应中断的 synchronized 锁 , lockInterruptibly() 可以让程序更灵活,可以在获取锁的同时, 保持对中断的响应 。我们可以把这个方法理解为 超时时间是无穷长的 tryLock(long time, TimeUnit unit) ,因为 tryLock(long time, TimeUnit unit) 和 lockInterruptibly() 都能响应中断 ,只不过 lockInterruptibly() 永远不会超时。
public void lockInterruptibly() {
try {
lock.lockInterruptibly();
try {
System.out.println("操作资源");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放锁
lock.unlock();
}
}
- void unlock() :释放当前线程占用的锁, 必须使用finally块保证发生异常时锁一定被释放
- Condition newCondition() :创建绑定到此 Lock 实例的 Condition实例 ,用于替代wait()/notify()/notifyAll()方法, 实现线程的等待通知机制
Condition对象的await()/signal()/signalAll() 的功能和wait()/notify()/notifyAll()一样
1. Lock接口
在Java5之前,要想使用锁来保护共享资源大多数情况是使用synchronized关键字。在Java5之后,并发包里增加了Lock接口及其实现类来提供锁的功能。
Lock接口与synchronized有许多区别。synchronized修饰在方法体或者代码块上可以隐式地获取和释放锁,并且锁地获取和释放操作被固化。但是Lock接口可以显示地获取和释放锁,提高了锁的可操作性。并且Lock接口支持可中断锁以及超时锁等synchronized没有的特性。
Lock接口的使用范式如下:
1 Lock lock = new ReentrantLock();
2 lock.lock();
3 try{
4 //访问临界区
5 }finally{
6 lock.unlock();
7 }
Lock接口的实现类能实现锁的功能,靠的是聚合AQS的子类作为同步器,将提供给用户的锁的操作委托给这个同步器执行。
2. 队列同步器(AQS)
AQS是并发包里同步组件的核心基础,可以用来构建锁和其他同步器件。
AQS维护一个int型的同步状态属性,利用这个同步状态属性可以实现独占锁和共享锁,以及如信号量等同步组件。比如要实现独占锁,则同步状态往往初始化为1。另外AQS内部还定义了一个静态内部类Node。Node组成了AQS的同步队列和等待队列,Node里保存了线程信息,等待状态等。AQS维护同步队列的头结点head和尾结点tail,并负责同步队列的操作。
AQS采用模板方法的设计模式,提供了独占和共享式获取释放同步状态的方法,以及可中断和超时获取同步状态的方法。这些模板方法调用了一些抽象方法。AQS里的抽象方法由继承它的子类根据需要实现。自定义同步组件往往会声明一个继承AQS的静态内部类,称为同步器。并将Lock接口提供给用户调用的方法委托给同步器处理。这样做的好处有两个方面,第一个是对同步组件的使用者隐藏了实现细节,用户只需要调用Lock接口提供的方法就可以获得锁的功能,而不知道具体的执行是由同步器来完成。第二个是向同步组件的开发者提供了便捷的开发接口,隐藏了底层操作系统的线程管理等细节。
2.1 AQS的同步状态操作
利用AQS设计同步组件的关键在于同步状态。AQS提供了三个方法保证线程安全的修改同步状态
1 private volatile int state;
2 protected final void setState(int newState) {
3 state = newState;
4 }
5 protected final int getState() {
6 return state;
7 }
8 protected final boolean compareAndSetState(int expect, int update) {
9 // See below for intrinsics setup to support this
10 return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
11 }
可以看到同步状态的线程安全是基于volatile的可见性和禁止重排序,以及利用CAS原子性替换。
2.2 AQS的实现分析
2.2.1 同步队列
AQS里的同步队列(FIFO队列,双向链表)用来完成同步状态的管理。当线程获取同步状态失败时,其线程引用,等待状态,会被封装成Node,并被加入到同步队列,同时AQS会阻塞该线程,直到被同步队列首节点的线程唤醒,并尝试获取同步状态。
AQS维护同步队列的头结点head和尾结点tail,当线程获取同步状态失败时会被加入到同步队列的尾部。
当线程获取同步状态失败时会被构造成Node并插入同步队列尾部,由于同一时刻可能与多个线程要被同时插入到尾部,为了避免出现类似HashMap在多线程环境下链表拉链可能拉成环的情况,AQS使用CAS的方式确保插入尾结点线程安全。
AQS通过addWaiter方法构造Node结点,并将其插入到同步队列尾部,代码如下。首先尝试进行一次快速的插入,利用CAS检查实际尾结点和线程认为的尾结点是否一致,相同则修改结点指针完成插入。如果快速插入失败,则进入enq方法自旋加CAS不断尝试插入的同步队列尾部直到成功。
1 private Node addWaiter(Node mode) {
2 Node node = new Node(Thread.currentThread(), mode);
3 // Try the fast path of enq; backup to full enq on failure
4 Node pred = tail;
5 if (pred != null) {
6 node.prev = pred;
7 if (compareAndSetTail(pred, node)) {
8 pred.next = node;
9 return node;
10 }
11 }
12 enq(node);
13 return node;
14 }
AQS的头结点通过unpartSuccessor方法释放同步状态,并利用LockSupport的方法唤醒其后继结点,这一过程没有使用CAS。头结点将其引用修改尾其后继结点,并断开与后继结点的连接,完成释放。
2.2.2 独占式同步状态获取与释放
调用AQS的acquire(int)方法可以独占式的获取同步状态,该方法的逻辑简单来说是这样的:首先调用tryAcquire(int)方法,线程安全地获取同步状态,如果获取同步状态失败,则构造Node结点,并将其加入等待队列中。在等待队列里的线程不断地自旋,检查其前驱是否是首结点并且能否获取同步状态,如果前驱不是首结点或者获取同步状态失败,则修改结点的等待状态,并且阻塞结点保存的线程,直到首结点释放同步状态,并唤醒后继结点,让等待的线程成功获取到同步状态,这个自旋地过程是不响应中断的。
1 public final void acquire(int arg) {
2 if (!tryAcquire(arg) &&
3 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
4 selfInterrupt();
5 }
6 final boolean acquireQueued(final Node node, int arg) {
7 boolean failed = true;
8 try {
9 boolean interrupted = false;
10 for (;;) {
11 final Node p = node.predecessor();
12 //如果前驱不是首结点或者前驱是首结点但是获取同步状态失败
13 //则被阻塞
14 if (p == head && tryAcquire(arg)) {
15 setHead(node);
16 p.next = null; // help GC
17 failed = false;
18 return interrupted;
19 }
20 if (shouldParkAfterFailedAcquire(p, node) &&
21 parkAndCheckInterrupt())
22 interrupted = true;
23 }
24 } finally {
25 if (failed)
26 cancelAcquire(node);
27 }
28 }
调用AQS的release(int)方法可以释放独占式同步状态。该方法的逻辑简单如下:首先调用treRelease释放同步状态,如果释放成功,则唤醒其后继结点,并且将头结点引用修改成其后继。由于获取到同步状态只能由一个线程,故这一操作不需要CAS来保证线程安全。
1 public final boolean release(int arg) {
2 if (tryRelease(arg)) {
3 Node h = head;
4 if (h != null && h.waitStatus != 0)
5 unparkSuccessor(h);
6 return true;
7 }
8 return false;
9 }
2.2.3 共享式同步状态获取与释放
共享式访问同步状态与独占式访问最大的区别在于,共享式访问不会阻塞其他共享式访问同步状态,但是独占式只能有一个线程进入访问,其他线程无论是共享还是独占都会被阻塞。
调用AQS的acquireShared(int)方法来共享式获取同步状态,首先调用tryAcquireShared(int)方法尝试获取共享同步状态,当该方法返回值大于等于0表示获取成功,小于0获取失败,则调用doAcquireShared(int)方法。doAcquireShared方法简单来说,就是构造Node结点,加入到同步队列尾部,然后进入自旋过程。在自选过程里,当前驱是首结点时,再次尝试获取同步状态,获取成功则退出自旋,否则被阻塞。
1 public final void acquireShared(int arg) {
2 if (tryAcquireShared(arg) < 0)
3 doAcquireShared(arg);
4 }
5 private void doAcquireShared(int arg) {
6 //构造Node结点,构造状态为共享
7 final Node node = addWaiter(Node.SHARED);
8 boolean failed = true;
9 try {
10 boolean interrupted = false;
11 //自旋尝试获取共享式同步状态
12 for (;;) {
13 final Node p = node.predecessor();
14 //只有当前驱是首结点时才会尝试获取同步状态
15 if (p == head) {
16 int r = tryAcquireShared(arg);
17 if (r >= 0) {
18 setHeadAndPropagate(node, r);
19 p.next = null; // help GC
20 if (interrupted)
21 selfInterrupt();
22 failed = false;
23 return;
24 }
25 }
26 //前驱不是首结点或者获取同步状态失败,被阻塞,直到首结点释放同步状态将其唤醒
27 if (shouldParkAfterFailedAcquire(p, node) &&
28 parkAndCheckInterrupt())
29 interrupted = true;
30 }
31 } finally {
32 if (failed)
33 cancelAcquire(node);
34 }
35 }[![复制代码](https://common.cnblogs.com/images/copycode.gif)
调用AQS的releaseShared(int)方法释放共享式同步状态,该方法首先调用tryReleaseShared(int),尝试释放同步状态,如果释放成功,则调用doReleaseShared()方法,唤醒后续处于等待的结点,这过程是采用CAS来保证线程安全,因为共享式同步状态往往有多个线程同时持有同步状态。
2.2.4 等待队列
在总结AQS等待队列实现之前,首先要总结一下Condition接口相关的知识。
任意一个Java对象都有wait,notify,notifyAll方法,利用这些方法可以在线程不满足某些执行条件时进入等待状态,直到其他线程将其唤醒。Java对象自带的这些监视器方法配合synchronized锁可以实现等待通知范式,但是相比之下,利用Condition接口提供的方法也能做到一样的功能,并且更加灵活。且Condition接口与对象监视器方法的不同点有:Condition支持中断屏蔽等待,特定时间等待,以及最重要的,对象监视器只有一个等待队列,但是利用Condition可以支持多个条件,多个等待队列。
Condition的使用也非常简单,下面是一个简单的示例:
1 Lock lock = new ReentrantLock();
2 Condition c1 = lock.newCondition();
3
4 public void sample() throw InterruptedException{
5 //获取锁
6 lock.lock();
7 try{
8 //只有获取锁成功才能调用条件对象的相关方法,调用之后,线程释放锁,并被构造成Node结点
9 //进入Condition的等待队列
10 condition.await();
11 }finally{
12 lock.unlock();
13 }
14 }[![复制代码](https://common.cnblogs.com/images/copycode.gif)
获取到锁的线程调用await方法会释放锁,然后进入等待队列,直到其他线程调用signal方法将其唤醒。Condition对象的创建依赖于Lock对象。
ConditionObject是AQS的内部类,实现了Condition接口。ConditionObject对象维护等待队列的头结点和尾结点。等待队列的结点类型与同步队列结点一样,都是Node。AQS里可以被多个ConditionObject依赖,故相比于synchronized的对象监视器只能有一个等待队列,基于AQS的同步组件可以有多个等待队列。
当获取到锁的线程调用await方法时将会释放锁并进入等待状态,具体过程简单来说如下:首先检查线程中断状态,如果被中断抛出中断异常。之后调用addConditionWaiter()方法将线程引用以及等待状态构造成Node结点,并释放锁,这部分过程没有CAS来确保线程安全,因为这时候线程还持有锁。最后进入自旋过程,不断检查引用了自己的结点是否被加入到同步队列中参与同步状态的获取,如果没有被加入同步队列,则被阻塞。最后当发现自己被加入到同步队列时,退出自旋过程,调用aquiredQueued方法自旋获取同步状态。
1 public final void await() throws InterruptedException {
2 if (Thread.interrupted())
3 throw new InterruptedException();
4 //将线程引用以及等待状态构造成新的Node结点,添加到等待队列尾部,这个过程由锁确保线程安全
5 Node node = addConditionWaiter();
6 //释放锁,并唤醒同步队列的后继结点
7 int savedState = fullyRelease(node);
8 int interruptMode = 0;
9 //自旋检查自己是否被加入到同步队列,没有则被阻塞
10 while (!isOnSyncQueue(node)) {
11 LockSupport.park(this);
12 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
13 break;
14 }
15 //当发现自己被加入到同步队列,退出自旋,并参与到锁的获取
16 if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
17 interruptMode = REINTERRUPT;
18 if (node.nextWaiter != null) // clean up if cancelled
19 unlinkCancelledWaiters();
20 if (interruptMode != 0)
21 reportInterruptAfterWait(interruptMode);
22 }[![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);
当调用ConditionObject对象的signal方法时,过程简单描述如下:首先检查并确保调用siganl方法的线程是获取锁的线程。之后调用doSignal方法,循环尝试修改等待队列头结点指向其后继,并断开原本首结点与其后继的指针,再进入自旋加CAS过程,尝试将其从等待队列转移到同步队列尾部,这个过程没有构造新的Node。当被加入到同步队列成功时,当前线程唤醒刚被插入到同步队列尾部的结点的线程,被唤醒的线程退出wait方法里的检查是否在同步队列循环,参与到自旋获取锁的过程。
1 public final void signal() {
2 if (!isHeldExclusively())
3 throw new IllegalMonitorStateException();
4 Node first = firstWaiter;
5 if (first != null)
6 doSignal(first);
7 }
8 private void doSignal(Node first) {
9 do {
10 if ( (firstWaiter = first.nextWaiter) == null)
11 lastWaiter = null;
12 first.nextWaiter = null;
13 } while (!transferForSignal(first) &&
14 (first = firstWaiter) != null);
15 }
image
以上就是Android技术中的JUC锁与AQS技术;这些东西可以帮助你在 APP启动优化 上面更好的铺垫;有关更多Android技术或者性能优化方面的学习;大家可以点击《Android核心技术类目》查看一些技术资料;里面内容概括了Android大部分的高工进阶知识点。
最后
AQS究竟是做什么的?
你该如何保证多个线程访问该对象时,正确地进行阻塞等待,正确地被唤醒?
关于这个问题,java的设计者认为应该是一套通用的机制,因此将一套线程阻塞等待以及被唤醒时锁分配的机制称之为AQS
全称 AbstractQuenedSynchronizer
中文名即抽象的队列式同步器。基于AQS,实现了例如ReentenLock之类的经典JUC类。
网友评论