一、什么是Lock?
lock接口是java里面的锁,锁一般是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程之间同时访问共享资源(除了读写锁)在javaSE5之前,java程序阻止多个线程之间同时访问共享线程是通过synchronized来进行实现的,而现在则在并发包中新增了Lock接口,以及相关的实现类,用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式的获取和释放锁,虽然它缺少了隐式获取/释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性
public void lockTest(){
Lock lock=new ReentrantLock();
lock.lock();
try {
}finally {
lock.unlock();
}
}
- 在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放。
- 不要将获取锁的过程写在try中,因为如果在获取锁时发生了异常,异常抛出的同时,也会导致锁的无故释放。
Lock接口提供的synchronized关键字不具备的主要特性
特性 | 描述 |
---|---|
尝试非阻塞的获取锁 | 当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁 |
能够被中断获取锁 | 与synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放 |
超时获取锁 | 在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回 |
Lock的常见API
方法名称 | 描述 |
---|---|
void lock() | 获取锁,调用该方法当前线程将会获取锁,当锁获取后,从该方法返回 |
void lockinterruptibly() throws InterruptedException | 可中断的获取锁,和lock()方法不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程 |
boolean tryLock() | 尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false |
boolean tryLock(long time,TimeUnit unit) throws InterruptedException | 超时的获取锁,当前线程在以下三种情况会返回:1.当前线程在超时时间内获得了锁。2.当前线程在超时时间内被中断。3.超时时间结束,返回false |
void unlock() | 释放锁 |
Condition new Condition() | 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的wait方法,而调用后,当前线程将释放锁 |
队列同步器
队列同步器,是用来构建锁或者其他同步组件的基础框架,他使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为他们能够保证状态的改变是安全的。子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器即可以支持独占式的获取同步状态,也可以支持共享式的获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。
同步器是实现锁的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁与同步器很快的隔离了使用者和实现者所需关注的领域
队列同步器的接口与示例
- 同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。
- 重写同步器的制定方法时,需要使用同步器提供的3个方法来访问或者修改同步状态。
-
- getState():获取当前同步状态
-
- setState(int newState): 设置当前同步状态
-
- compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性
同步器可重写的方法
方法名称 | 描述 |
---|---|
protected boolean tryAcquire(int arg) | 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态 |
protecetd boolean tryRelease(int arg) | 独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态 |
protected int tryAcquireShared(int arg) | 共享式获取同步状态,返回大于等于0的值,表示获取成功,反之获取失败 |
protected boolean tryReleaseShared(int arg) | 共享式释放同步状态 |
protected boolean isHeldExclusively() | 当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占 |
实现自定义同步组件时,将会调用同步器提供的模板方法,这些模板方法如下所示
方法名称 | 描述 |
---|---|
void acquire(int arg) | 独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用重写的tryAcquire(int arg)方法 |
void acquireInterruptibly(int arg) | 与acquire(int arg) 相同,但是该方法响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException并返回 |
boolean tryAcquireNanos(int arg,long nanos) | 在acquireInterruptibly(int arg)基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将会返回false,如果获取到了返回true |
void acquireShared(int arg) | 共享式的获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式获取的主要区别是在同一时刻可以有多个线程获取到同步状态等待,与独占式获取的主要区别是在同一时刻可以有多个线程获取到同步状态 |
void acquireSharedInterruptibly(int arg) | 与acquireShared(int arg)相同,该方法响应中断 |
boolean tryAcquireSharedNanos(int arg,long nanos) | 在acquireShaaredInterruptibly(int arg) 基础上增加了超时限制 |
boolean release(int arg) | 独占式的释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒 |
boolean releaseShared(int arg) | 共享式的释放同步状态 |
Collection《Thread》getQueuedThreads() | 获取等待在同步队列上的线程集合 |
同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态、共享式获取与释放同步状态和查询同步队列中的等待线程情况。自定义同步组件将使用同步器提供的模板方法来实现自己的同步语义。
只有掌握了同步器的工作原理才能更加深入的理解并发包中的其他并发组件,所以下面通过一个独占锁的示例来深入了解一下同步器的工作原理。
顾名思义,独占锁就是在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能用于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁,代码如下所示
package tongbuqi;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @author liusupeng
* @date 2021/7/18 12:50
*/
public class Mutex implements Lock {
//自定义同步器
private static class Sync extends AbstractQueuedSynchronizer{
//是否处于占用状态
@Override
protected boolean isHeldExclusively(){
return getState()==1;
}
//当状态为0的时候获取锁
@Override
protected boolean tryAcquire(int acquires){
if(compareAndSetState(0,1)){
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
//释放锁,将状态设置为0
@Override
protected boolean tryRelease(int releases){
if(getState()==0) {
try {
throw new IllegalAccessException();
} catch (IllegalAccessException e) {
e.printStackTrace();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
//返回一个Condition,每个condition都包含了一个condition队列
Condition newCondition(){
return new ConditionObject();
}
}
private final Sync sync=new Sync();
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1,unit.toNanos(time));
}
@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();
}
}
在上述例子中,独占锁Mutex是一个自定义同步组件,他在同一时刻只允许一个线程占有锁,Mutex中定义了一个静态内部类,该内部类继承了同步器并实现了独占式获取和释放同步状态。在tryAcquire(int acquires)方法中,如果经过CAS设置成功(同步状态设置为1),则代表获取了同步状态,而在tryRelease(int releases)方法中只是将同步状态重置为0。用于使用Mutex时并不会直接和内部同步器的实现打交道,而是调用Mutex提供的方法,在Mutex的实现中,以获取锁的lock()方法为例,只需要在方法实现中调用同步器的模板方法acquire(int args)即可,当前线程调用该方法获取同步状态失败后会被加入到同步队列中等待,这样就大大降低了实现一个可靠自定义同步组件的门槛。
队列同步器的实现分析
同步队列
同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点。
节点的属性类型与名称以及描述
- int waitStatus
等待状态。
包含如下状态。
①CANCELLED,值为1,由于在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待,节点进入该状态将不会在变化
②SIGNAL,值为-1,后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点的线程得以运行
③ CONDITION,值为-2,节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中
④PROPAGATE,值为-3,表示下一次共享式同步状态获取将会无条件的传播下去
⑤INITIAL,值为0,初始状态 - Node prev
前驱节点,当节点加入同步队列时被设置 - Node Next
后驱节点 - Node nextWaiter
等待队列中的后继节点,如果当前节点是共享的,那么这个字段将是一个SHARED常量,也就是说节点类型(独占和共享)和等待队列中的后继节点共用同一个字段 - Thread thread
获取同步状态的线程
节点解释
节点是构成同步状态,同步器拥有首节点和尾节点,没有成功获取同步状态的线程将会成为节点加入该队列的尾部。