AQS(AbstractQueuedSynchronizer)源码深度解析(2)—Lock接口以及自定义锁的实现

1,083 阅读8分钟

「这是我参与2022首次更文挑战的第26天,活动详情查看:2022首次更文挑战」。

上文详细介绍了AQS的设计思想,以及总体设计结构。下面我们来介绍一下另一个和锁与AQS相关的接口,Lock接口,然后借用AQS和Lock接口快速实现一个自定义锁。

AQS相关文章:

AQS(AbstractQueuedSynchronizer)源码深度解析(1)—AQS的设计与总体结构

AQS(AbstractQueuedSynchronizer)源码深度解析(2)—Lock接口以及自定义锁的实现

AQS(AbstractQueuedSynchronizer)源码深度解析(3)—同步队列以及独占式获取锁、释放锁的原理【一万字】

AQS(AbstractQueuedSynchronizer)源码深度解析(4)—共享式获取锁、释放锁的原理【一万字】

AQS(AbstractQueuedSynchronizer)源码深度解析(5)—条件队列的等待、通知的实现以及AQS的总结【一万字】

1 Lock接口

1.1 Lock接口概述

public interface Lock

Lock接口本来和AQS没有太多关系的,但是如果想要是实现一个正规的、通用的同步组件(特别是锁),那就不得不提Lock接口。

Lock接口同样自于JDK1.5,它被描述成JUC中的锁的超级接口,所有的JUC中的锁都会实现Lock接口。

由于它是作为接口,到这里或许大家都明白了它的设计意图,接口就是一种规范。Lcok接口定义了一些抽象方法,用于获取锁、释放锁等,而所有的锁都实现Lock接口,那么它们虽然可能有不同的内部实现,但是开放给外部调用的方法却是一样的,这就是一种规范,无论你怎么实现,你给外界调用的始终是“同一个方法”!因此JUC中的锁也被常常统称为lock锁。

这种优秀的架构设计,不同的锁实现统一了锁获取、释放等常规操作的方法,方便外部人员使用和学习!类似的设计在JDBC数据库驱动上面也能看到!

1.2 Lock接口的API方法

我们来看看实现Lcok接口都需要实现哪些方法!

方法名称 描述
lock 获取锁,如果锁无法获取,那么当前的线程被挂起,直到锁被获取到,不可被中断。
lockInterruptibly 获取锁,如果获取到了锁,那么立即返回,如果获取不到,那么当前线程被挂起,直到当前线程被唤醒或者其他的线程中断了当前的线程。
tryLock 如果调用的时候能够获取锁,那么就获取锁并且返回true,如果当前的锁无法获取到,那么这个方法会立刻返回false
tryLcok(long time,TimeUnit unit) 在指定时间内尝试获取锁。如果获取了锁,那么返回true,如果当前的锁无法获取,那么当前的线程被挂起,直到当前线程获取到了锁或者当前线程被其他线程中断或者指定的等待时间到了。时间到了还没有获取到锁则返回false。
unlock 释放当前线程占用的锁
newCondition 返回一个与当前的锁关联的条件变量。在使用这个条件变量之前,当前线程必须占用锁。调用Condition的await方法,会在等待之前原子地释放锁,并在等待被唤醒后原子的获取锁

3.3 锁获取与中断

Thread类中有一个interrupt方法,可中断因为主动调用Object.wait()、Thread.join()和Thread.sleep()等方法造成的线程等待,以及使用lockInterruptibly方法和tryLock(time,timeUnit)尝试获取锁但是未获取锁而造成的阻塞,并且他们都将抛出InterruptedException异常,并且设置该线程的中断状态位为false。

但是对于因为调用lock()方法,或者因为无法获取Synchronized锁而被阻塞的线程,interrupt方法无法中断,仅会设置中断标志位为true,另外对于正常线程,同样仅会设置中断标志位为true,通知该线程应该被中断了。有一个特例是:被LockSupport.park()阻塞的线程也可以被中断,但是不会抛出异常,并且不会恢复标志位。

下面举例详细说明Lock接口的这四种方法的使用:假如线程A和线程B使用同一个锁LOCK,此时线程A首先获取到锁LOCK.lock(),并且始终持有不释放。

如果此时B要去获取锁,有四种方式:

  1. LOCK.lock(): 此方式会使得B始终处于等待中,即使调用B.interrupt()也不能中断,除非线程A调用LOCK.unlock()释放锁。
  2. LOCK.lockInterruptibly(): 此方式会使得B等待,但当调用B.interrupt()会被中断等待,并抛出InterruptedException异常,否则会与lock()一样始终处于等待中,直到线程A释放锁。
  3. LOCK.tryLock(): 调用该方法时B不会等待,一次获取不到锁就直接返回false。
  4. LOCK.tryLock(10, TimeUnit.SECONDS): 该处会在10秒时间内处于等待中,但当调用B.interrupt()会中断等待,并抛出InterruptedException。 10秒时间内如果线程A释放锁,线程B会获取到锁并返回true,否则10秒过后会直接返回false,去执行下面的逻辑。

3.4 Synchronized和Lock的区别

  1. 首先synchronized是java内置关键字,是jvm层面实现的;Lock是个java接口,可以代表JUC中的锁,通过java代码实现。
  2. synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;即lock需要显示的获得、释放锁,synchronized隐式获得、释放锁。
  3. lock在等待锁过程中可以用lockInterruptibly()方法配合interrupt方法()来中断等待,也可以使用tryLock()设置等待超时时间,而synchronized只能等待锁的释放,不能响应中断。
  4. synchronized是非公平锁,而Lock锁可以实现为公平锁或者非公平锁。
  5. Lock锁将监视器monitor方法,单独的封装到了一个Condition对象当中,更加的面向对象了,而且一个Lock锁可以拥有多个condition对象(条件队列),singal和signalAll可以选择唤醒不同的队列中的线程;而同一个synchronized块只能有一个监视器对象,一个条件队列。
  6. Lock可以提高多个线程进行读操作的效率,非常灵活。(可以通过readwritelock实现读写分离)。
  7. synchronized使用Thread.holdLock(监视器对象)检测当前线程是否持有锁,Lock可以通过lock.trylock或者isHeldExclusively方法判断是否获取到锁。

2 不可重入独占锁简单实现

经过上面的学习,我们知道了AQS的大概设计思路与方法,以及规范的锁需要实现Lock接口,现在我们尝试自己构建一个简单的独占锁。

顾名思义,独占锁就是在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁,下面是一个基于AQS的独占锁的实现。

我们需要重写tryAcquire和tryRelease(int releases)方法。将同步状态state值为1看锁被获取了,使用setExclusiveOwnerThread方法记录获取到锁的线程,state为0看作锁没被获取。另外,下面的简单实现并没有可重入的考虑,因此不具备重入性!

从实现中能够看出来,有了AQS工具,我们实现自定义同步组件还是比较简单的!

/**
 * @author lx
 */
public class ExclusiveLock implements Lock {
    /**
     * 将AQS的实现组合到锁的实现内部
     * 对于同步状态state,这里的实现将1看成同步,0看成未同步
     */
    private static class Sync extends AbstractQueuedSynchronizer {
        /**
         * 重写isHeldExclusively方法
         *
         * @return 是否处于锁占用状态
         */
        @Override
        protected boolean isHeldExclusively() {
            //state是否等于1
            return getState() == 1;
        }

        /**
         * 重写tryAcquire方法,尝试获取锁
         * 这里的实现为:当state状态为0的时候可以获取锁
         *
         * @param acquires 参数,这里我们没用到
         * @return 获取成功返回true,失败返回false
         */
        @Override
        public boolean tryAcquire(int acquires) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        /**
         * 重写tryRelease方法,释放锁
         * 这里的实现为:当state状态为1的时候,将状态设置为0
         *
         * @param releases 参数,这里我们没用到
         * @return 释放成功返回true,失败返回false
         */
        @Override
        protected boolean tryRelease(int releases) {
            //如果尝试解锁的线程不是加锁的线程,那么抛出异常
            if (Thread.currentThread() != getExclusiveOwnerThread()) {
                throw new IllegalMonitorStateException();
            }
            //设置当前拥有独占访问权限的线程为null
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        /**
         * 返回一个Condition,每个condition都包含了一个condition队列
         * 用于实现线程在指定条件队列上的主动等待和唤醒
         *
         * @return 每次调用返回一个新的ConditionObject
         */
        Condition newCondition() {
            return new ConditionObject();
        }
    }

    /**
     * 仅需要将操作代理到Sync实例上即可
     */
    private final Sync sync = new Sync();

    /**
     * lock接口的lock方法
     */
    @Override
    public void lock() {
        sync.acquire(1);
    }

    /**
     * lock接口的tryLock方法
     */
    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    /**
     * lock接口的unlock方法
     */
    @Override
    public void unlock() {
        sync.release(1);
    }

    /**
     * lock接口的newCondition方法
     */
    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }

    public boolean isLocked() {
        return sync.isHeldExclusively();
    }

    public boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }
}

如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!