Lock接口
synchronized关键字固化了锁的获取和释放,锁的获取和释放的过程对开发者是不可见的,不具备扩展性,对于复杂的加锁场景synchronized难以实现,Lock接口就解决了这一问题,提供了系列API用于根据需求自定义锁
Lock接口使用方式
// 以ReentrantLock为例
Lock lock = new ReentrantLock();
lock.lock();
try{
//临界区
}finally{
lock.unlock();
}
Lock接口的API
方法名称 | 描述 |
---|---|
void lock() | 获取锁,调用该方法当前线程会去阻塞式加锁,加锁成功后从该方法返回 |
void lockInterruptibly() throws InterruptedException | 可中断的获取锁,和lock的不同之处在于获取锁的过程中可以响应中断从而停止获取锁 |
boolean tryLock() | 尝试非阻塞的获取锁,调用之后立刻返回true为加锁成功,false为加锁失败 |
boolean tryLock(long time,TimeUnit unit) throws InterruptedException | 超时的获取锁,如果在超时时间内获取锁,在超时时间内被中断,超时结束这三种情况将会返回 |
void unlock() | 释放锁 |
Condition newCondition() | 获取等待通知组件,该组件和当前的锁绑定,只有获取了锁才能够调用组件的等待通知方法 |
AQS—队列同步器
AQS全称是AbstractQueuedSychronizer,是阻塞式锁和相关的同步器工具的框架(类似servlet,由开发者通过继承的方式来自定义锁),内部使用了FIFO队列来实现线程阻塞队列,是实现锁的关键
特点
- 用state属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
- 提供了基于FIFO的等待队列实现公平锁,类似于Monitor的EntryList(EntryList不是严格FIFO的)
- 条件变量来实现等待、唤醒机制;支持多个条件变量,类似于Monitor的WaitSet(某一个条件变量对应一个WaitSet)
- AQS中的阻塞和唤醒使用LockSupport中的park和unpark来实现
同步器中可重写的方法
方法名称 | 描述 |
---|---|
protected boolean tryAcquire(int arg) | 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态 |
protected boolean tryRelease | 独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态 |
protected int tryAcquireShared(int arg) | 共享式获取同步状态,返回大于等于0的值表示获取成功,反之加锁失败 |
protected int tryReleaseShared(int arg) | 共享式释放同步状态 |
proteced boolean isHeldExclusively() | 当前同步器是否在独占模式下被占用,一般该方法表示是否被当前线程所占用 |
AQS自定义锁
/***
* @author shaofan
* @Description 自定义锁,不可重入、独占锁
*/
public class MyLock implements Lock {
private MySync mySync = new MySync();
/***
* 独占锁,同步器类
*/
static class MySync extends AbstractQueuedSynchronizer{
/**
* 尝试加锁
* @param arg 定义可重入锁时使用到该参数
* @return
*/
@Override
protected boolean tryAcquire(int arg) {
// 通过cas对state进行修改,cas锁的形式实现独占锁
if(compareAndSetState(0,1)){
// 如果cas修改成功,即加锁成功,将Owner设置为当前线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
/**
* 尝试解锁
* @param arg 定义可重入锁时使用
* @return
*/
@Override
protected boolean tryRelease(int arg) {
// 保证顺序,先将当前加锁线程置空再修改state解锁;state使用volatile修饰、exclusiveOwnerThread没有,保证这个顺序来保证可见性
setExclusiveOwnerThread(null);
setState(0);
return true;
}
/**
* 是否持有独占锁
* @return
*/
@Override
protected boolean isHeldExclusively() {
return getState()==1;
}
/**
* 获取条件变量
* @return
*/
protected Condition newCondition(){
return new ConditionObject();
}
}
/**
* 加锁,加锁失败会放入队列中等待加锁
*/
@Override
public void lock() {
mySync.acquire(1);
}
/**
* 加锁,通过这个方法加锁过程中可打断,synchronized加锁不可打断,容易死锁
* @throws InterruptedException
*/
@Override
public void lockInterruptibly() throws InterruptedException {
// 底层会调用tryAcquire
mySync.acquireInterruptibly(1);
}
/**
* 尝试加锁,如果加锁不成功直接返回false
* @return
*/
@Override
public boolean tryLock() {
return mySync.tryAcquire(1);
}
/**
* 尝试加锁,带有超时时间
* @param time
* @param unit
* @return
* @throws InterruptedException
*/
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return mySync.tryAcquireSharedNanos(1,unit.toNanos(time));
}
/**
* 解锁
*/
@Override
public void unlock() {
mySync.release(0);
}
/**
* 创建一个条件变量
* @return
*/
@Override
public Condition newCondition() {
return mySync.newCondition();
}
}
ReentrantLock基本使用
ReentrantLock是JUC并发包提供的锁工具,它具备如下特点:
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
- 与synchronized的共同点:都支持可重入
可重入
可重入是指同一个线程如果首次获得了这把锁,因为它是锁的拥有者,有权利再次获取这把锁;如果是不可重入锁,那么第二次获得锁时,自己也会被锁住
@Slf4j
public final class Demo{
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args){
lock.lock();
try{
lock.lock();
try{
log.debug("可重入锁");
}finally{
lock.unlock();
}
}finally {
lock.unlock();
}
}
}
可打断
通过lockInterruptibly方法加锁的线程可以被打断,从而退出阻塞队列,而通过lock方法和synchornized加锁的线程被打断没有任何效果,仍然会等待锁
@Slf4j
public final class Demo{
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
try{
// 可打断的加锁,如果没有加锁成功会进入阻塞队列,可以被打断
lock.lockInterruptibly();
try{
}finally {
lock.unlock();
}
}catch (InterruptedException e){
e.printStackTrace();
log.debug("被打断");
return;
}
},"t");
lock.lock();
t.start();
TimeUnit.SECONDS.sleep(1);
// 打断线程
t.interrupt();
}
}
锁超时
@Slf4j
public final class Demo{
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
try {
// tryLock方法中可以传入超时时间,会在指定时间内重复尝试获取锁,超时没有获取返回
// 打断后仍然会继续获取锁,不会直接退出
if(!lock.tryLock(1,TimeUnit.SECONDS)){
log.debug("没有成功获取锁");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t");
lock.lock();
t.start();
TimeUnit.SECONDS.sleep(2);
}
}
哲学家就餐问题
几个哲学家围着一张桌子就餐,每两个哲学家之间有一只筷子,即每个哲学家的右筷子是右边哲学家的左筷子;他们要就餐需要集齐两只筷子;但当每个哲学家手里都有一只筷子时,都会等待右筷子,陷入死锁
/***
* @author shaofan
* @Description 哲学家就餐问题
*/
@Slf4j
public final class Demo{
public static void main(String[] args) throws InterruptedException {
Chopstick chopstick1 = new Chopstick("1");
Chopstick chopstick2 = new Chopstick("2");
Chopstick chopstick3 = new Chopstick("3");
Chopstick chopstick4 = new Chopstick("4");
Chopstick chopstick5 = new Chopstick("5");
new Philosopher("苏格拉底",chopstick1,chopstick2).start();
new Philosopher("亚里士多德",chopstick2,chopstick3).start();
new Philosopher("柏拉图",chopstick3,chopstick4).start();
new Philosopher("阿基米德",chopstick4,chopstick5).start();
new Philosopher("赫拉克利特",chopstick5,chopstick1).start();
}
}
/***
* 哲学家类,继承Thread,每个哲学家对应一个线程,执行就餐动作
*/
@Slf4j
class Philosopher extends Thread{
Chopstick left;
Chopstick right;
public Philosopher(String name,Chopstick left,Chopstick right){
super(name);
this.left = left;
this.right = right;
}
@SneakyThrows
@Override
public void run() {
while(true){
// 尝试获取左手筷子
synchronized (left){
// 尝试获取右手筷子
synchronized (right){
eat();
}
}
}
}
private void eat() throws InterruptedException {
log.debug(getName()+"is eating");
TimeUnit.SECONDS.sleep(1);
}
}
/***
* 筷子,每个哲学家有一个左筷子和一个右筷子,左筷子是左边哲学家的右筷子
*/
class Chopstick{
String name;
public Chopstick(String name){
this.name = name;
}
}
使用ReentrantLock超时锁解决哲学家就餐问题
指定获取筷子的时间,当获取筷子超时就不去获取筷子了,并且把已经获取到的筷子放下,这样别的哲学家就可以拿到筷子,打破死锁环境
/***
* @author shaofan
* @Description 哲学家就餐问题
*/
@Slf4j
public final class Demo{
public static void main(String[] args) throws InterruptedException {
Chopstick chopstick1 = new Chopstick("1");
Chopstick chopstick2 = new Chopstick("2");
Chopstick chopstick3 = new Chopstick("3");
Chopstick chopstick4 = new Chopstick("4");
Chopstick chopstick5 = new Chopstick("5");
new Philosopher("苏格拉底",chopstick1,chopstick2).start();
new Philosopher("亚里士多德",chopstick2,chopstick3).start();
new Philosopher("柏拉图",chopstick3,chopstick4).start();
new Philosopher("阿基米德",chopstick4,chopstick5).start();
new Philosopher("赫拉克利特",chopstick5,chopstick1).start();
}
}
/***
* 哲学家类,继承Thread,每个哲学家对应一个线程,执行就餐动作
*/
@Slf4j
class Philosopher extends Thread{
Chopstick left;
Chopstick right;
public Philosopher(String name,Chopstick left,Chopstick right){
super(name);
this.left = left;
this.right = right;
}
@SneakyThrows
@Override
public void run() {
while(true){
// 尝试获取左手筷子
if(left.tryLock(1,TimeUnit.SECONDS)){
try{
if(right.tryLock(1,TimeUnit.SECONDS)){
try{
eat();
}finally {
right.unlock();
}
}
}finally {
left.unlock();
}
}
}
}
private void eat() throws InterruptedException {
log.debug(getName()+"is eating");
TimeUnit.SECONDS.sleep(1);
}
}
/***
* 筷子,每个哲学家有一个左筷子和一个右筷子,左筷子是左边哲学家的右筷子
*/
class Chopstick extends ReentrantLock{
String name;
public Chopstick(String name){
this.name = name;
}
}
公平锁
构造ReentrantLock时,可以传入布尔值标志该锁是否是公平锁;公平锁会按照进入EntryList的顺序获取锁,每个线程之间没有优先级,可以解决饥饿问题,但是会降低并发度,一般没有必要
条件变量
- await前需要获得锁
- await执行后,会释放锁,进入conditionObject等待
- await线程被唤醒(或打断、或超时),需要重新竞争锁
- 竞争锁成功后,从await后面继续执行
@Slf4j
public final class Demo{
static ReentrantLock lock = new ReentrantLock();
static Condition condition1 = lock.newCondition();
static Condition condition2 = lock.newCondition();
static boolean flag1 = true;
static boolean flag2 = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
lock.lock();
try{
// 一个锁可以对应多个进入等待队列的条件,不同条件对应不同的waitSet
// 而synchronized只能对应对象锁的waitSet
while(flag1){
condition1.await();
}
log.debug("条件1满足");
while(flag2){
condition2.await();
}
log.debug("条件2满足");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
},"t");
t.start();
TimeUnit.SECONDS.sleep(1);
lock.lock();
try{
flag1 = false;
flag2 = false;
condition1.signalAll();
condition2.signalAll();
}finally {
lock.unlock();
}
}
}
ReentrantLock实现原理
非公平锁原理
通过默认构造器构造的ReentrantLock得到的就是非公平锁的实现
public ReentrantLock() {
sync = new NonfairSync();
}
第一个线程加锁成功:
Thread-0一直占有锁,第二个线程加锁失败:
-
cas将state从0改为1,当前state已经是1,修改失败,进入acquire逻辑
-
调用tryAcquire再次尝试加锁,仍然失败
-
进入addWaiter逻辑,构造Node阻塞队列
- 图中黄色三角表示该Node的waitStatus,其中0为默认的正常状态
- Node的创建是懒惰的
- 第一个Node称为Dummy(哑元)或哨兵,用来占位,并不关联线程
-
构造Node后当前线程进入acquireQueued逻辑:
- acquireQueued会在一个死循环中不断尝试获得锁,失败后进入park阻塞
- 如果当前结点是紧邻着head的结点,那么再次tryAcquire尝试加锁,Thread-0仍然没有解锁,加锁失败
- 进入shouldParkAfterFailedAcquire逻辑,将前驱node,即head的waitStatus改为-1,第一次返回false,不会立即让当前线程进入阻塞
- shouldParkAfterFailedAcquire执行完毕回到acquireQueued,再次tryAcquired尝试获取锁
- 当再次进入shouldparkAfterFailedAcquire时,前驱node已经是-1,这次返回true
- 进入parkAndCheckInterrupt,Thread-1被park
再有多个线程经历上述失败:
Thread-0调用unlock释放锁:
- 设置exclusiveOwnerThread为null,state为0
- 如果当前阻塞队列不为null,并且head的waitStatus=-1,进入unparkSuccessor流程,找到队列中离head最近并且没有取消的一个Node,unpark恢复其运行,本例中即为Thread-1
- 被恢复运行的Thread会回到acquireQueued流程,如果此时没有新的线程来竞争,则Thread-1会获得锁;如果此时来了一个新的线程,两个线程会开始竞争锁,可能新的线程竞争到锁,那么Thread-1又会回到阻塞队列中(非公平锁的体现)
锁重入原理
可打断原理
不可打断模式
被打断会修改打断标记,但是还是会继续循环加锁
可打断模式
当被打断时,会直接抛出异常,不再继续加锁
公平锁原理
通过带参构造器指定构造公平锁;
新线程加锁时,会先判断在阻塞队列中是否有线程在等待;没有才会加锁,优先让阻塞的线程加锁
条件变量原理
每个条件变量对应一个等待队列,其实现类是ConditionObject
await流程
开始Thread-0持有锁,调用await,进入ConditionObject的addConditionWaiter流程创建新的Node状态为-1(Node.CONDITION),关联Thread-0,加入等待队列尾部
single流程
假设Thread-1来唤醒Thread-0,取得等待队列的第一个Node,即Thread-0所在的Node,加入阻塞队列的尾部,修改前驱结点的waitStatus为-1
读写锁
读操作对数据没有修改,如果大量的读操作的情况,每个读操作都要对数据加锁,就会造成很多不必要的性能问题;当读操作远远高于写操作时,这是使用读写锁让读-读可以并发,提高性能
ReentrantRreadWriteLock
基本使用
@Slf4j
public final class Demo{
public static void main(String[] args) throws InterruptedException {
DataContainer dataContainer = new DataContainer(new Object());
Thread t1 = new Thread(()->{
dataContainer.read();
// dataContainer.write();
},"t1");
Thread t2 = new Thread(()->{
// dataContainer.read();
dataContainer.write();
},"t2");
t1.start();
t2.start();
}
}
/***
* 数据容器
* 将数据的读写操作分离,读操作和写操作使用两个不同的锁进行
*/
@Slf4j
class DataContainer{
private Object data;
/**
* 读写锁
*/
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
/**
* 读锁
*/
private ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
/**
* 写锁
*/
private ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
public DataContainer(Object data){
this.data = data;
}
/**
* 读取操作
* @return
*/
public Object read(){
log.debug("正在获取读锁");
readLock.lock();
log.debug("获取读锁成功");
try{
log.debug("正在读取数据");
TimeUnit.SECONDS.sleep(1);
return data;
} catch (InterruptedException e) {
e.printStackTrace();
return null;
} finally {
log.debug("正在释放读锁");
readLock.unlock();
}
}
/**
* 写入操作
*/
public void write(){
log.debug("正在获取写锁");
writeLock.lock();
log.debug("获取写锁成功");
try{
log.debug("正在写入数据");
TimeUnit.SECONDS.sleep(1 );
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log.debug("正在释放写锁");
writeLock.unlock();
}
}
}
读-读的情况,多个线程能同时获取读锁(非独占锁)
读-写和写-写的情况,多个线程不能同时获取锁(互斥锁)
注意事项
- 读锁不支持条件变量(非独占锁)
- 锁重入时,不支持升级;即持有读锁的情况下,不能获取写锁,会造成死锁
- 锁重入时,支持降级;即持有写锁的情况下可以获取读锁
原理
读写锁用的时同一个Sync同步器,所以阻塞队列和state是同一个
加锁
加锁流程与ReentrantLock没有特殊之处,不同点在于,写锁状态占了state的低16位,读锁的使用的是state的高16位
写锁加锁
读锁加锁
如果加锁失败,读锁回创建共享结点,连续的共享结点会被同时唤醒,保证读操作的共享性
写锁解锁
读锁解锁
StampedLock
该类自JDK8加入,是为了进一步优化读的性能,特点是在使用读锁、写锁是都必须配合戳使用
加解读锁
long stamp = lock.readLock();
lock.unlockRead(stamp);
加解写锁
long stamp = lock.writeLock();
lock.unlockWrite(stamp);
乐观读
StampLock支持tryOptimisticRead方法,读取完毕后需要做一次戳校验,如果校验通过,表示这段时间没有写操作,数据可以安全使用,如果校验没有通过,需要重新获取读锁,保证数据安全;这样可以在大量读操作的时候,没有出现写操作就不用对读加锁
long stamp = lock.tryOptimistcRead();
if(!lock.validate(stamp)){
// 锁升级
}
Condition—条件变量
synchronized中也有条件变量,体现在wait/notify的使用中,当条件不满足时线程将进入waitSet等待;而JUC并发包中的Condition条件变量就是为了实现synchornized中的这个特征,而且比synchronized功能更强,可以支持多个条件变量
- synchronized是不满足条件的线程都在一间休息室(waitSet)中休息
- 而AQS条件变量支持多间休息室,每个条件对应一个waitSet
Condition和Object监视器对比
对比项 | Object Monitor Methods | Condition |
---|---|---|
前置条件 | 获取对象的锁 | 调用Lock.lock获取锁,并调用lock.newCondition获取条件对象 |
调用方式 | 直接调用,object.wait | 直接调用,condition.await |
等待队列个数 | 一个 | 多个 |
当前线程释放锁并进入等待状态 | 支持 | 支持 |
当前线程释放锁并进入等待状态,在等待状态中不响应中断 | 不支持 | 支持 |
当前线程释放锁并进入超时等待状态 | 支持 | 支持 |
当前线程释放锁并进入等待状态至将来某个时间 | 不支持 | 支持 |
唤醒等待队列中的一个线程 | 支持 | 支持 |
唤醒等待队列中的全部线程 | 支持 | 支持 |