java面试集锦

Lock中的AQS、独占锁、重入锁、读锁、写锁、Conditio

2019-12-26  本文已影响0人  代码小当家

前言

除去使用 synchronized 隐式加锁的方式外,我们可以使用 Lock 更加灵活的控制加锁、解锁、等待和唤醒等操作

Java 中的 Lock 有如下几种实现

不论是重入锁还是读写锁,他们都是通过 AQS(AbstractQueuedSynchronizer)来实现的,并发包作者(Doug Lea)期望 AQS 作为一个构建所或者实现其它自定义同步组件的基础框架

理解 AQS 对理解锁来说至关重要,我们先来利用 AQS 来实现一个自定义的同步组件独占锁

独占锁 Mutex

public class Mutex implements Lock {    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();    }    private static class Sync extends AbstractQueuedSynchronizer {        @Override        protected boolean tryAcquire(int arg) {            if (compareAndSetState(0, 1)) {                setExclusiveOwnerThread(Thread.currentThread());                return true;            }            return false;        }        @Override        protected boolean tryRelease(int arg) {            if (getState() == 0) {                throw new IllegalMonitorStateException();            }            setExclusiveOwnerThread(null);            setState(0);            return true;        }        @Override        protected boolean isHeldExclusively() {            return getState() == 1;        }        Condition newCondition() {            return new ConditionObject();        }    }}

整体调用逻辑如下

Lock中的AQS、独占锁、重入锁、读锁、写锁、Condition源码分析 Lock中的AQS、独占锁、重入锁、读锁、写锁、Condition源码分析

加锁

调用 lock() 的时候,首先会去调用 acquire() 方法

Lock中的AQS、独占锁、重入锁、读锁、写锁、Condition源码分析

如果 tryAcquire() 获取锁成功这里使用的是 CAS 获取锁并且直接返回成功或者失败,如果获取成功了方法返回,线程持有锁成功,否则就会调用 addWaiter() 将当前线程构造成功 Waiter 结点,放入同步队列中,同步队列是一个 FIFO 的队列,所以说新来的结点要放入尾部

Lock中的AQS、独占锁、重入锁、读锁、写锁、Condition源码分析

然后调用 acquireQueued() 方法尝试将该节点挂起或者从队列中取出首结点然后再尝试调用 tryAcquire() 获取同步状态,如果获取成功了那么从同步队列中移除,如下图 2,如果不是头结点这线程挂起进入等待状态,直到线程被中断或者被唤醒为止再次进行头结点和获取锁的逻辑,需要注意的是每次判断的是当前节点的前一个节点,后续方便 setHead() 设置当前节点

Lock中的AQS、独占锁、重入锁、读锁、写锁、Condition源码分析 Lock中的AQS、独占锁、重入锁、读锁、写锁、Condition源码分析 Lock中的AQS、独占锁、重入锁、读锁、写锁、Condition源码分析

解锁 unlock()

解锁过程

Lock中的AQS、独占锁、重入锁、读锁、写锁、Condition源码分析

调用 tryRelease(),这个方法将 state 设置为 0 并且将锁所属于的线程置位 null,这里没有使用 CAS 因为释放锁的时候,线程是获取到锁的状态不会存在竞争

以上对 AQS 的部分功能接口做了讲解,后面结合其它锁会逐步展开剩下的点

重入锁 ReentrantLock

ReentrantLock 有两种实现分别是

公平锁

在创建 ReentrantLock(true) 就可以创建公平锁的实现,在其内部指定了内部的 AQS 使用 FairSync

    public ReentrantLock(boolean fair) {        sync = fair ? new FairSync() : new NonfairSync();    }

获取锁逻辑

Lock中的AQS、独占锁、重入锁、读锁、写锁、Condition源码分析 Lock中的AQS、独占锁、重入锁、读锁、写锁、Condition源码分析

上述逻辑都是整理源码罗列出来的逻辑和我们之前的独占锁的区别主要在于,同一个线程可以多次获取锁 state 会依次增加代表了重入的次数。

释放锁逻辑

Lock中的AQS、独占锁、重入锁、读锁、写锁、Condition源码分析

释放锁的时候必须是当前线程,减少锁持有的次数为 0 的话就完全释放,将锁所属于的线程设置为 null

非公平锁

非公平锁比公平锁的实现简单了许多而且性能也好了很多,区别主要在于

其它基本都是相似的

我们知道 AQS 是一个 FIFO 队列,从中唤醒的都是有先后顺序的,同时在这种情况下又多了操作就是由于线程是可以重入的,那么在这种情况下,同一个线程可能会连续的持有锁,导致处于等待中的队列等待更久,比如如下代码每个线程获取锁 2 次,启动了多个任务

Lock中的AQS、独占锁、重入锁、读锁、写锁、Condition源码分析 Lock中的AQS、独占锁、重入锁、读锁、写锁、Condition源码分析

读写锁

读写锁用一个值维护读和写锁,用高 16 位表示读状态,用低 16 位表示写状态

Lock中的AQS、独占锁、重入锁、读锁、写锁、Condition源码分析

读锁

Lock中的AQS、独占锁、重入锁、读锁、写锁、Condition源码分析

写锁

Lock中的AQS、独占锁、重入锁、读锁、写锁、Condition源码分析

总结一下:

Condition

一个锁可以持有一个同步队列,多个 Condition,每个 Condition 上存在多个等待结点

Lock中的AQS、独占锁、重入锁、读锁、写锁、Condition源码分析

当调用 await() 的时候就会构造对应的结点,首先从同步队列头部移除结点,然后释放锁,然后放入 Condition 列表尾部

Lock中的AQS、独占锁、重入锁、读锁、写锁、Condition源码分析 Lock中的AQS、独占锁、重入锁、读锁、写锁、Condition源码分析

当调用 singal() 的时候就会从 Condition 等待队列中移除放入同步队列的尾部

Lock中的AQS、独占锁、重入锁、读锁、写锁、Condition源码分析 Lock中的AQS、独占锁、重入锁、读锁、写锁、Condition源码分析

参考:

作者:在江湖中coding
链接:https://juejin.im/post/5e01878ff265da33e82bca06

上一篇下一篇

猜你喜欢

热点阅读