[JUC] 通过ReentrantLock源码理解AQS的原理 (一)

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ......
    static final class Node {...}
    private transient volatile Node head;
    private transient volatile Node tail;
    private volatile int state;
}

一、前言

在研究java.util.concurrent包及线程问题的时候,通常会有很多疑问,例如我们本身就有线程类为什么java会在1.5以后引入JUC来做线程同步和一些新的锁的机制。他必然有其大的优势,那么优势又在哪里。他跟synchronized有什么区别,JUC中各种lock的实现又在哪里优于Object提供的wait / notify。一切的核心需要归结于本次分析的AQS。

二、什么是AQS

AQS,即AbstractQueuedSynchronizer, 队列同步器,它是Java并发用来构建锁和其他同步组件的基础框架。来看下同步组件对AQS的使用:

AQS是一个抽象类,主是是以继承的方式使用。AQS本身是没有实现任何同步接口的,它仅仅只是定义了同步状态的获取和释放的方法来供自定义的同步组件的使用。在java的同步组件中,AQS的子类(Sync等)一般是同步组件的静态内部类,即通过组合的方式使用。

抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch

三、AQS原理

1. AQS的原理:

它维护了一个volatile int state(代表共享资源)和一个FIFO(双向队列)线程等待队列(多线程争用资源被阻塞时会进入此队列),当第一个线程开始获取同步状态的时候(调用lock()方法)的 时候,维护state为1,某些锁也支持重入,所以当同一个线程重入锁的时候,state +1。但是后来线程获取同步状态失败,AQS会将该线程以及等待状态等信息构造成一个Node,将其加入同步队列的尾部,同时阻塞当前线程,当同步状态释放时,唤醒队列的头节点。

看起来原理解释就简单的几句话,但是如果我们需要了解AQS的原理,分析AQS的源码是最好的方式。接下来我们就系统的分析一下AQS的源码。

2. AQS源码结构:

2.1 AbstractQueuedSynchronizer的结构

1. AQS继承了AbstractOwnableSynchronizer类,下面是该类基本的结构:

        他包含了一个线程对象。并且有set方法将一个线程赋给这个对象。

        他还包含了一个获取当前拥有线程的get方法。

        这个线程通常就是AQS当前需要处理的线程对象。

public abstract class AbstractOwnableSynchronizer
    implements java.io.Serializable {

    protected AbstractOwnableSynchronizer() { }

    private transient Thread exclusiveOwnerThread;

    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}

2. AQS自己的结构比较复杂,但是里面最重要的应该是如下一些结构:

  •         它定义了 一个静态内部类Node,这就是上面提到过的双向队列。
  •         它本身拥有一个双向队列的头节点和一个尾节点。
  •         它有一个最重要的属性就是state。通过这属性来得知线程的状态。
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    static final class Node {...}
    private transient volatile Node head;
    private transient volatile Node tail;
    private volatile int state;
}

四、通过ReentrantLock理解AQS原理

这是本文章最重要的部分,也是对AQS理解的重要部分。因为AQS本身是一个抽象类。需要被其他类实现才能发挥其作用。我们通常知道的JUC下许多同步实现都是依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch。我们再次就通过分析ReentrantLock锁的源码来深入的理解AQS。

1. ReentrantLock源码结构:

ReentrantLock是JUC包下的locks包里面的一个实现同步的一个类。他主要结构如下:

  • 它定义了一个静态内部类Sync(该类继承了AQS类)并且持有该Sync的对象。
  • 该Sync类定义了两个子类FairSync(公平锁)和NonfairSync(非公平锁)
  • 当使用无参构造方法创建ReentrantLock,默认创建的是非公平锁。
  • 当时用有参数构造方法传入true创建ReentrantLock,创建的是公平锁。
public class ReentrantLock implements Lock, java.io.Serializable {
    private final Sync sync;
    abstract static class Sync extends AbstractQueuedSynchronizer{...}
    static final class NonfairSync extends Sync{...}
    static final class FairSync extends Sync{...}
    public ReentrantLock() {
        sync = new NonfairSync();
    }

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

2. ReentrantLock加锁机制:

我们来实现如下这段简单代码。

  1. 我们在demo中使用默认构造方法创建了一个非公平的ReentrantLock。
  2. 定义了连个线程,都是通过lock.lock();获取锁。获取前后打印一段文字表明状态。
  3. 为了让A线程先获取到锁 ,我们在start A线程后暂停了主线程2秒。之后再开启B线程。
public class ReentrantLockDemo1 {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "尝试获取锁");
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "获取锁成功,开始执行!");
        }, "Thread A");

        Thread threadB = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "尝试获取锁");
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "获取锁成功,开始执行!");
        }, "Thread B");

        threadA.start();
        TimeUnit.SECONDS.sleep(2);
        threadB.start();
    }
}

通过运行上面代码我们能够发现结果:

A线程获取到了锁开始执行 ,由于我们没有释放锁的操作。所以B线程尝试获取锁失败,线程会一直的挂起。

Thread A尝试获取锁
Thread A获取锁成功,开始执行!
Thread B尝试获取锁

我们来分析这段简单的代码。

1. 创建ReentrantLock对象

当我们执行 private static ReentrantLock lock = new ReentrantLock(); 的时候,无参构造在ReentrantLock中初始化了一个非公平锁的Sync并交给了ReentrantLock内部的sync对象。

public ReentrantLock() {
   sync = new NonfairSync();
}

2. A的初次加锁

我们看到当我们在A线程处执行lock.lock()的时候。我们调用的是ReentrantLock的lock()方法。而在该方法中他又调用了自身持有的sync对象的lock方法。

public void lock() {
    sync.lock();
}

由于已知我们使用的是非公平锁,所以这里的sync对象lock方法应该是NonfairSync的lock方法。我们来看NonfairSync这个静态内部类。

static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

它继承了Sync类,但是Sync类又继承了AQS类。所以他拥有AQS的操作方法。我们来看lock方法里面,if中判断调用了AQS的compareAndSetState(0, 1)方法:

protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

它其实就是执行了unsafe针对内存的原子操作。判断state是否为0,如果为0则改为1,结果返回为true。此处因为第一次进入,state必然为0,所以修改成功,进入判断体中,执行setExclusiveOwnerThread(Thread.currentThread()); 表示将当前正在执行的A线程赋给AQS的exclusiveOwnerThread对象。此时A线程获取同步成功。执行线程体内自己的逻辑。

3. A的第二次加锁 (可重入性)

这里我需要插入另一个实现,如果A线程已经加了一次锁。他能不能加第二次锁。因为ReentrantLock本身支持可重入。所以我们来看看下面这段代码。分析下可重入怎么实现的。

public class ReentryTest {
    private static final ReentrantLock lock  = new ReentrantLock();

    public static void main(String[] args) {
        new Thread(()  -> {
                try {
                    a();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "1号线程").start();
    }

    public static void a() throws InterruptedException {
        lock.lock();
        try {
            TimeUnit.SECONDS.sleep(1);
            System.out.println(Thread.currentThread().getName() + " 进入a方法");
            System.out.println("a方法获取锁后: " + lock.getHoldCount());
            b();
        } finally {
            lock.unlock();
            System.out.println("a方法释放锁后: " + lock.getHoldCount());
        }
    }

    public static void b() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 进入b方法");
            System.out.println("b方法获取锁后: " + lock.getHoldCount());
        } finally {
            lock.unlock();
            System.out.println("b方法释放锁后: " + lock.getHoldCount());
        }
    }
}

代码也很简单。

  1. 创建一个静态的ReentrantLock对象lock。
  2. a方法使用lock加锁并且休眠1秒后调用b方法。最终执行完毕unlock释放锁资源。
  3. b方法使用lock加锁。执行完毕释放锁志愿。
  4. main方法中创建线程执行a方法。

这里有个lock.getHoldCount()可以获取到AQS当前state的值。执行结果如下:

1号线程 进入a方法
a方法获取锁后: 1
1号线程 进入b方法
b方法获取锁后: 2
b方法释放锁后: 1
a方法释放锁后: 0

我们能够看到,当进入到a方法后,获取锁后state从0change到1。二此时执行a方法最后一段调用b方法到时候,又加了一次锁,因为是同一个线程 ,所以state从1加到2。b方法执行 完毕释放锁。state由2减为1。然后a方法也执行完毕释放锁,state由1减为0。

第一次获得同步我们上面讲解过。所以我们重点关注在b方法中再次获取同步的代码。同样我们进入到了NonfairSync非公平锁的lock方法。此时因为state不再是0,所以需要进入下面的acquire(1);方法。该方法 是AQS的方法:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

而tryAcquire这个判断方法通常是由AQS子类自行实现的,所以我们能够找到NonfairSync非公平锁对其实现的代码:

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

我们在这个方法中可以看到:

  1. 获得当前执行线程。
  2. 获得当前state
  3. 当state=0的时候表示还未有线程获取过锁,此时执行原子操作将state改为1,并且将当前线程赋给ownerThread。(因为此时state已经是1了,所以根本不会走这段)
  4. 检查所持有线程是否是当前线程,如果是,则获得的state+1并设置回state中。return true。(因为本身线程是当前锁持有线程。所以走这段逻辑返回true)

当return true后,acquire返回false则不会继续执行下去。此时b方法也获得锁,继续执行。

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值