一.Lock与synchronized对比分析
锁 | 实现方式 | 优点 | 缺点 |
---|---|---|---|
synchronized | monitor锁 | 隐式获取和释放锁,比较便捷 | 扩展性没有Lock好 |
Lock | 队列器同步AQS | 非阻塞地获取锁;能被中断地获取锁;超时获取锁 | 显示获取和释放锁比较复杂 |
二.Lock的使用
//创建一个可重入锁
Lock lock = new ReentrantLock();
//获取锁
lock.lock();
try {
//同步代码
//……
} finally {
//释放锁
lock.unlock();
}
Lock接口:
- void lock():获取锁,调用该方法当前线程将会获取锁,当锁获得后从该方法中返回
- void lockInterruptibly() throws InterruptedException:可中断获取锁,和lock()方法不同的是该方法会响应中断,即在锁获取过程中如果线程被设置为中断,会抛出中断异常
- boolean tryLock():尝试非阻塞地获取锁,该方法会立刻返回,能获取就返回true,否则返回false
- boolean tryLock(long, java.util.concurrent.TimeUnit)
throws InterruptedException:超时获取锁,在以下3种情况下会返回:当前线程在超时时间内获取到了锁;当前线程在超时时间内被中断;超时时间结束,返回false - void unlock():释放锁
- Condition newCondition():获取等待通知组件,该组件和当前线程绑定,当前线程只有获取到了锁,才能调用该组件的wait()方法,调用后,当前线程释放锁
Lock接口常用实现类:
- ReentrantLock:可重入锁(后文会详细介绍具体实现原理)
- ReentrantReadWriteLock:可重入读写锁(后文会详细介绍具体实现原理)
三.队列同步器(AQS)
队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来
供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、
ReentrantReadWriteLock和CountDownLatch等)。
同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。
1.AbstractQueuedSynchronizer常用方法
修改同步状态:
- getState():获取当前同步状态
- setState(int newState):设置当前同步状态
- compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性
可重写的方法:
- boolean tryAcquire(int
arg):独占式地获取同步状态,实现该方法需要判断当前状态是否符合预期,然后通过CAS设置同步状态 - boolean tryRelease(int arg):独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态
- int tryAcquireShared(int arg):共享式获取同步状态,返回大于等于0的值表示获取成功,反之获取失败
- int tryReleaseShared(int arg):共享式释放同步状态
2.AQS的实现原理
同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
获取同步状态失败后,通过CAS操作设置尾节点:
首节点释放同步状态时,唤醒后继节点,将其设置为首节点:
3.独占式同步状态获取与释放
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
(1)首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法
保证线程安全的获取同步状态
(2)如果同步状态获取失败,则构造同步节点(独占式
Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部
(3)调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态
4.共享式同步状态获取与释放
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。以文件的读写为例,如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以是共享式访问,两种不同的访问模式在同一时刻对文件或资源的访问情况。