目录
2.AQS同步器 (AbstractQueuedSynchronized)
1.lock与synchronized的区别
它虽然失去了synchronized隐式获取锁和释放锁的便捷性,但lock却拥有了释放锁和获取 所得可操作性,可中断的获取锁和超时获取锁等多种synchronnized所不具备的同步特性
以下是lock和synchronized的区别表:
类别 | synchronized | Lock |
---|---|---|
存在层次 | Java的关键字,在jvm层面上 | 是一个类 |
锁的释放 | 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 | 在finally中必须释放锁,不然容易造成线程死锁 |
锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 | 分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待 |
锁状态 | 无法判断 | 可以判断 |
锁类型 | 可重入 不可中断 非公平 | 可重入 可判断 可公平(两者皆可) |
性能 | 少量同步 | 大量同步 |
1.1 lock补充
从Lock接口中我们可以看到主要有个方法,这些方法的功能可以看出:
-
lock():获取锁,如果锁被暂用则一直等待
-
unlock():释放锁
-
tryLock(): 注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true
-
tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间
-
lockInterruptibly():用该锁的获得方式,如果线程在获取锁的阶段进入了等待,那么可以中断此线程,先去做别的事
通过 以上的解释,大致可以解释在上个部分中“锁类型(lockInterruptibly())”,“锁状态(tryLock())”等问题,还有就是前面子所获取的过程我所写的“大致就是可以尝试获得锁,线程可以不会一直等待”用了“可以”的原因。
2.AQS同步器 (AbstractQueuedSynchronized)
什么是同步器?
多线程并发的执行,之间通过某种 共享 状态来同步,只有当状态满足 xxxx 条件,才能触发线程执行 xxxx 。
这个共同的语义可以称之为同步器。可以认为以上所有的锁机制都可以基于同步器定制来实现的。
我们来看下java.util.concurrent.locks大致结构:
上图中,LOCK的实现类其实都是构建在AbstractQueuedSynchronizer上,为何图中没有用UML线表示呢,这是每个Lock实现类都持有自己内部类Sync的实例,而这个Sync就是继承AbstractQueuedSynchronizer(AQS)。为何要实现不同的Sync呢?这和每种Lock用途相关。另外还有AQS的State机制。
2.1 设计意图
AQS提供给同步组件实现者,为其屏蔽了同步状态的管理,线程排队等 底层操作实现者只需要通过AQS提供的模板方法实现同步组件的语义,lock(同步组件)是面向使用者的,定义了接口,隐藏了实现细节。
2.2 如何使用AQS实现自定义同步组件
- 重写protected方法,告诉AQS如何判断 当前同步状态获取是否成功或者失败
- 同步组件调用AQS的模板方法,实现同步语义。而提供的模板方法又会调用被重写的方法
- 实现自定义同步组件时,推荐采用继承AQS的静态内存类
2.3 可重写的方法
AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
- tryAcquire:独占方式。尝试获取资源,成功则返回true,失败则返回false。不会阻塞
- tryRelease:独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared:共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared:共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
- isHeldExclusively:该线程是否正在独占资源。只有用到condition才需要去实现它。
2.4 AQS提供的模板方法
1.独占式获取与释放同步状态
2.共享式获取与释放同步状态
3.查询同步队列中等待线程情况
AQS提供了独占锁和共享锁必须实现的方法,具有独占锁功能的子类,它必须实现tryAcquire、tryRelease、isHeldExclusively等;共享锁功能的子类,必须实现tryAcquireShared和tryReleaseShared等方法,带有Shared后缀的方法都是支持共享锁加锁的语义。Semaphore是一种共享锁,ReentrantLock是一种独占锁。
独占锁获取锁时,设置节点模式为Node.EXCLUSIVE
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
共享锁获取锁,节点模式则为Node.SHARED
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
.....
}
3.AQS源码解析
3.1 AQS同步队列的数据结构
带有结点的双向链表实现的队列
3.2 独占与共享
java并发包提供的加锁模式分为独占锁和共享锁,独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock就是以独占方式实现的互斥锁。共享锁,则允许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock。AQS的内部类Node定义了两个常量SHARED和EXCLUSIVE,他们分别标识 AQS队列中等待线程的锁获取模式。
很显然,独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。 java的并发包中提供了ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,或者被一个 写操作访问,但两者不能同时进行。
独占锁
ReentrantLock是AQS独占功能的一个实现,通常的使用方式如下:
reentrantLock.lock();
// do something
reentrantLock.unlock();
ReentrantLock会保证执行do something
在同一时间有且只有一个线程获取到锁,其余线程全部挂起,直到该拥有锁的线程释放锁,被挂起的线程被唤醒重新开始竞争锁。
ReentrantLock的加锁全部委托给内部代理类完成,ReentrantLock只是封装了统一的一套API而已,而ReentrantLock又分为公平锁和非公平锁。
abstract static class Sync extends AbstractQueuedSynchronizer {}
static final class NonfairSync extends Sync {}
static final class FairSync extends Sync {}
- 公平锁:每个线程抢占锁的顺序为先后调用lock方法的顺序,并依此顺序获得锁,类似于排队吃饭;
- 非公平锁:每个线程抢占锁的顺序不变,谁运气好,谁就获得锁,和调用lock方法的先后顺序无关,类似后插入。
换句话说,公平锁和非公平锁的唯一的区别是在获取锁的时候是直接去获取锁,还是进入队列排队的问题。
共享锁
获取锁的过程:
- 当线程调用acquireShared()申请获取锁资源时,如果成功,则进入临界区。
- 当获取锁失败时,则创建一个共享类型的节点并进入一个FIFO等待队列,然后被挂起等待唤醒。
- 当队列中的等待线程被唤醒以后就重新尝试获取锁资源,如果成功则唤醒后面还在等待的共享节点并把该唤醒事件传递下去,即会依次唤醒在该节点后面的所有共享节点,然后进入临界区,否则继续挂起等待。
释放锁过程:
- 当线程调用releaseShared()进行锁资源释放时,如果释放成功,则唤醒队列中等待的节点,如果有的话。