Java中的锁(Lock接口和队列同步器)
一、Lock接口
锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,而Java SE 5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。
使用synchronized关键字将会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先获取再释放。当然,这种方式简化了同步的管理,可是扩展性没有显示的锁获取和释放来的好。例如,针对一个场景,手把手进行锁获取和释放,先获得锁A,然后再获取锁B,当锁B获得后,释放锁A同时获取锁C,当锁C获得后,再释放B同时获取锁D,以此类推。这种场景下,synchronized关键字就不那么容易实现了,而使用Lock却容易许多。
Lock的使用也很简单,代码清单:
Lock lock = new ReentrantLock();
lock.lock();
try {
} finally {
//在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放。
lock.unlock();
}
Lock接口提供的synchronized关键字不具备的主要特性:
Lock的API:
二、队列同步器
队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。
同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。
同步器可重写的方法与描述如表:
实现自定义同步组件时,将会调用同步器提供的模板方法,这些(部分)模板方法与描述:
下面通过一个独占锁的示例来深入了解一下同步器的工作原理:
独占锁就是在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁
package com.wholesmart.thread4;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* 自定义同步组件---独占锁Mutex
*
* @author dyw
* @data 2020年7月15日
*/
public class Mutex implements Lock {
/**
* 仅需要将操作代理到Sync上即可
*/
private final Sync sync = new Sync();
/**
* 独占式获取同步状态
*
* <pre>
* acquire()方法首先会调用重写的tryAcquire(1)方法和acquireQueued():
*
*
* 1、
* if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
*
* !tryAcquire(arg)-->获取同步状态失败
*
* addWaiter(Node.EXCLUSIVE)-->为当前线程创建一个独占式的等待节点
*
* acquireQueued(addWaiter(Node.EXCLUSIVE), arg))-->创建一个独占等待节点进入同步队列
*
*
* 2、
* 如果当前线程没用中断,继续调用 selfInterrupt()中断线程
*
* </pre>
*/
public void lock() {
sync.acquire(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
/**
*
* 自定义同步器
*
* <pre>
* getState()-->获取当前状态
* setState()-->设置当前状态
* compareAndSetState()-->使用CAS设置当前状态,该方法能够保证状态设置的原子性
* </pre>
*
*
* @author dyw
* @data 2020年7月15日
*/
private static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 10294931901131630L;
/**
* 是否处于占用状态
*/
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
/**
* 独占式获取同步状态(0代表没有线程获取到,1代表有线程获取到)
*/
@Override
public boolean tryAcquire(int acquires) {
// 1代表获取到同步状态,所以这里要加个断言,保证获取同步状态时传过来的参数为1
assert acquires == 1;
// 如果当前状态为未获取到同步状态(0),则符合预期,通过CAS设置获取到同步状态(1)
if (compareAndSetState(0, 1)) {
// 将当前线程设置为独占线程
setExclusiveOwnerThread(Thread.currentThread());
// 成功返回true
return true;
}
// 失败返回false
return false;
}
/**
* 独占式释放同步状态
*/
@Override
protected boolean tryRelease(int releases) {
// 如果状态为未获取到同步状态(0),抛出异常
if (getState() == 0)
throw new IllegalMonitorStateException();
// 将当前所拥有的独占线程设置为null
setExclusiveOwnerThread(null);
// 将状态设置为未获取到同步状态(0)
setState(0);
// 成功返回true
return true;
}
/**
* 返回一个Condition,每个condition都包含了一个condition队列
*
* @return
*/
Condition newCondition() {
return new ConditionObject();
}
}
}
上述示例中,独占锁Mutex是一个自定义同步组件,它在同一时刻只允许一个线程占有锁。Mutex中定义了一个静态内部类,该内部类继承了同步器并实现了独占式获取和释放同步状态。在tryAcquire(int acquires)方法中,如果经过CAS设置成功(同步状态设置为1),则代表获取了同步状态,而在tryRelease(int releases)方法中只是将同步状态重置为0。用户使用Mutex时并不会直接和内部同步器的实现打交道,而是调用Mutex提供的方法,在Mutex的实现中,以获取锁的lock()方法为例,只需要在方法实现中调用同步器的模板方法acquire(int args)即可,当前线程调用该方法获取同步状态失败后会被加入到同步队列中等待,这样就大大降低了实现一个可靠自定义同步组件的门槛。
三、队列同步器的实现分析
同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
节点是构成同步队列的基础,同步器拥有首节点(head)和尾节点(tail),没有成功获取同步状态的线程将会成为节点加入该队列的尾部,同步队列的基本结构如图:
同步器包含了两个节点类型的引用,一个指向头节点,而另一个指向尾节点。试想一下,当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node
update),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。
同步器将节点加入到同步队列的过程如图:
同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点,该过程如图:
设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证,它只需要将首节点设置成为原首节点的后继节点并断开原首节点的next引用即可。