Java AbstractQueuedSynchronizer(AQS)浅析之一

CSDN同步发布

本篇文章对Java中的AbstractQueuedSynchronizer(AQS)进行分析和学习。若有不正之处请多多谅解,并欢迎批评指正。

为叙述方便,下文都以AQS替代AbstractQueuedSynchronizer。

使用的Java版本

src git:(master) ✗ java -version 
java version "1.8.0_201"
Java(TM) SE Runtime Environment (build 1.8.0_201-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode)

AQS是干什么的呢?

下面是AQS类的部分介绍,咱也看不懂,只能用百度翻译一下哈哈,建议英文好的直接看源码里的类注释。

AQS提供一个框架,用于实现依赖先进先出(FIFO)等待队列的阻塞锁和相关同步器(semaphores、events等)。对于大多数依赖单个原子{@code int}值来表示状态的同步器来说,这个类是一个有用的基础。子类必须定义改变这个状态的受保护的方法,以及定义这个状态在对象被获取或释放时的含义。鉴于这些,AQS类中的其他方法执行所有排队和阻塞机制。

子类应该被定义为非公共的内部辅助类,用于实现其封闭类的同步属性。AbstractQueuedSynchronizer类不实现任何同步接口。相反,它定义了例如{@link #acquireInterruptibly}这样的方法,可以根据具体的锁和相关的同步器来适当地调用,以实现它们的公共方法。

一句话:本篇文章只需要知道AQS可以用来实现锁即可。

我们一般不会直接使用AQS,所以我们以ReentrantLock(可重入锁)来引出AQS。明白了AQS就明白了ReentrantLock是如何获取锁以及释放锁的了。

先说一下大致流程:

  • Java中的ReentrantLock的获取锁和释放锁是通过AQS来实现的。

  • AQS内部维护了一个int类型的值来表示同步状态和一个先进先出(FIFO)的等待队列

/**
 * 同步状态
 */
private volatile int state;

/**
 * 等待队列的head,延迟初始化。除了初始化之外,head只能通过setHead方法来修改。
 * 注意,如果head存在可以保证head的waitStatus不是CANCELLED.
 */
private transient volatile Node head;

/**
 * 等待队列的尾,惰性初始。只有在使用enq方法添加新的等待节点的时候修改。
 */
private transient volatile Node tail;

  • 对于非公平锁,线程总是会先尝试获取锁,如果获取成功就直接执行,如果获取失败会进入等待队列。进入等待队列中的线程会休眠,等待被唤醒。
  • 对于公平锁,如果已经有线程在等待获取锁了,那么新的线程就会直接排在等待队列后面等待获取锁。
  • 持有锁的线程执行完毕释放锁,唤醒等待队列中的线程。
  • 线程被唤醒后会尝试获取锁,如果成功获取锁那么线程就执行,否则线程会再次休眠等待被唤醒。

我们在使用ReentrantLock的过程中,既可以构建一个使用非公平策略的ReentrantLock实例,也可以构建一个使用公平策略的ReentrantLock实例。

ReentrantLock的类结构

public class ReentrantLock implements Lock, java.io.Serializable {

    //Sync成员变量
    private final Sync sync;
     
    //AQS的子类
    abstract static class Sync extends AbstractQueuedSynchronizer {
    
    }

    //非公平策略
    static final class NonfairSync extends Sync {

    }
    //公平策略
    static final class FairSync extends Sync {
    
    }

}

我们看到ReentrantLock类中有一个Sync类型的成员变量,Sync类继承了AQS,然后
NonfairSync和FairSync都继承了Sync,分别实现非公平锁和公平锁。

FairSync.png
NonfairSync.png

ReentrantLock的构造函数

public ReentrantLock() {
   //使用非公平策略
   sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    //使用公平策略
    sync = fair ? new FairSync() : new NonfairSync();
}

我们可以选择构建公平的或非公平的ReentrantLock实例,ReentrantLock中获取锁和释放锁相关的方法如下所示。我们先看非公平锁的情况。

    void lock();
    
    boolean tryLock(); 
    
    void lockInterruptibly() throws InterruptedException; 
    
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException; 
    
    void unlock();
   

非公平的ReentrantLock

ReentrantLock的lock方法

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

ReentrantLock的lock方法内调用了sync的lock方法。NonfairSync的实现如下所示。

NonfairSync的lock方法

final void lock() {
    //注释1处,
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        //注释2处
        acquire(1);
}

注释1处,首先调用AQS的compareAndSetState方法以CAS的方式修改AQS的state变量,如果修改成功,说明当前线程成功获取了锁,然后将当前线程设置为锁的持有者。注意是以独占模式持有锁的

protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}

如果修改AQS的state变量失败,说明此时有其他线程已经持有了锁,那么就调用acquire(int arg)方法获取锁,注意我们传入的参数是1。

AQS的acquire(int arg)方法

public final void acquire(int arg) {
  if (!tryAcquire(arg) &&
      //标记为独占模式
      acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
      selfInterrupt();
}

这个步骤可以分为3步(把大象放进冰箱里需要几步?)
步骤1: 调用tryAcquire(arg) 尝试获取锁,获取成功直接返回
步骤2: 尝试获取锁失败将当前线程以独占锁的方式加入等待队列
步骤3: 为已经加入队列中的线程尝试获取锁

步骤1:调用tryAcquire(arg) 尝试获取锁

AQS没有实现这个方法,需要子类来实现

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

我们看下ReentrantLock.NonfairSync类的实现

protected final boolean tryAcquire(int acquires) {
    //调用了父类ReentrantLock.Sync的nonfairTryAcquire(acquires)方法
    return nonfairTryAcquire(acquires);
}

ReentrantLock.Sync的nonfairTryAcquire(acquires)方法

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    //获取同步状态值
    int c = getState();
    //注释1处
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        //注释2处
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //注释3处
    return false;
}
 

在注释1处,如果同步状态值为0,说明没有线程持有锁,那么就以CAS的方式修改AQS的state变量,如果修改成功,说明当前线程成功获取了锁,然后将当前线程设置为锁的持有者,然后返回true,获取锁成功。

注释2处,如果有线程持有锁,并且持有锁的线程是当前线程,那么就将同步状态值加1然后重新赋值给同步状态值state,然后返回true,获取锁成功。

AQS的setState(int newState)方法

protected final void setState(int newState) {
      state = newState;
}

注意:调用这个方法的前提是当前线程就是锁的持有者,所以可以修改state值,并不需要方法同步。

注释3处,获取锁失败。

到此步骤1结束,如果步骤1中获取锁失败,就会进入步骤2。

步骤2: 获取失败将当前线程加入等待队列

在这里我们要提一下AQS的一个内部类Node。Node类是对每一个等待获取锁的线程的封装,其包含了线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。还包括指向当前节点的前驱节点的指针和后继节点的指针(双向链表)。Node类的成员变量waitStatus则表示当前Node节点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。

static final int CANCELLED =  1;

static final int SIGNAL    = -1;

static final int CONDITION = -2;

static final int PROPAGATE = -3;

//默认是0
volatile int waitStatus;
  • CANCELLED:表示当前节点由于超时或者中断而被取消。进入该状态后的节点状态将不会再变化。特别的,取消节点的线程不会被再次阻塞。

  • SIGNAL:当前节点的后继节点被阻塞了,所以当前节点在释放锁或者取消的时候必须唤醒后继节点。后继节点入队时,会将父节点的状态更新为SIGNAL。

  • CONDITION:表示节点正在一个条件队列中,本篇文章暂时忽略。

  • PROPAGATE:共享模式下,节点不仅会唤醒其后继节点,同时也可能会唤醒后继节点的后继节点。比如当前节点释放了10个资源,当前节点的后继节点只需要6个节点,那么当前节点在释放的时候就会唤醒后继节点和后继节点的后继节点。

  • 0:新节点进入等待队列时的默认状态。

注意,负值表示节点处于有效等待状态,而正值表示节点已被取消。所以源码中很多地方用>0、<0来判断节点的状态是否正常。

private Node addWaiter(Node mode) {
    //以独占模式加入等待队列
    Node node = new Node(Thread.currentThread(), mode);
    // 先尝试最快的入队方式
    Node pred = tail;
    //注释1处
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //注释2处
    enq(node);
    return node;
}

在注释1处,如果尾节点不为null,就直接将当前节点使用CAS的方式更新为尾节点,如果更新成功就返回node,这是最快的入队方式。

如果尾节点为null,或者将当前节点使用CAS的方式更新为尾节点失败,就调用注释2处的enq(final Node node)方法将node加入队列。

AQS的enq(final Node node)方法,注意,这个方法是一个无限循环,只有成功将加入到队列尾部才会返回。

private Node enq(final Node node) {
    for (;;) {//有一点疑问,这个for循环什么时候退出呢?
        Node t = tail;
        if (t == null) { // 如果队列不存在,就新建一个node然后初始化队列
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

首先如果尾节点为null,说明队列此时还不存在,就新建一个节点然后以CAS的方式将新创建的节点设置为头节点,如果成功则让尾节点也指向node。如果如果尾节点不为null,就以CAS的方式将node更新为尾节点。

注意,这个方法是一个无限循环,只有成功将node加入到队列尾部才会返回。

将node加入到等待队列成功以后会进入到AQS的acquire(int arg)方法的步骤3

步骤3: 为已经加入队列中的线程尝试获取锁

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;//标记是否成功获取锁
    try {
        boolean interrupted = false;//标记线程是否被中断
        //无限循环
        for (;;) {//注释1处
            //获取前驱节点
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                //如果前驱节点是head并且尝试获取锁成功,就将当前节点更新为head节点
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //注释2处
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                //标记线程在阻塞过程中是否被中断
                interrupted = true;
        }
    } finally {//注释3处
        if (failed)
            cancelAcquire(node);
    }
}

注释1处,如果前驱节点是head并且调用tryAcquire(int arg)方法获取锁成功,就将当前节点更新为head节点,然后返回。

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

在setHead方法中将node的thread和prev变量都置为了null,是为了帮助GC和避免不必要的唤醒和遍历。

在注释2处,如果获取锁失败后则判断是否应该阻塞当前线程

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //获取前驱节点的状态
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * node节点已经设置了状态告诉前驱节点在释放锁的时候通知自己,所以node节点可以被安全的阻塞。
         */
        return true;
    if (ws > 0) {
        /*
         * 前驱节点已经被取消了,向前寻找状态有效的前驱节点,然后将node设置为有效前驱节点的后继节点。
         * 注意:已经被取消的节点会被GC,这些节点相当于一个无引用链。
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * 以CAS的方式更新前驱节点的waitStatus为Node.SIGNAL,告诉前驱节点在释放锁的时候通知自己。
         * 可能会失败。
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

如果shouldParkAfterFailedAcquire方法返回false,那么重新循环。如果返回true则调用parkAndCheckInterrupt方法。

AQS的parkAndCheckInterrupt()方法,注意这个方法会阻塞线程,并在线程 被唤醒后,通过调用Thread.interrupted()返回在阻塞过程中线程是否被中断。

private final boolean parkAndCheckInterrupt() {
    //注释1处
    LockSupport.park(this);
    //唤醒后,返回在阻塞过程中是否被中断
    return Thread.interrupted();
}

LockSupport.park(this);

注释1处这行代码会阻塞当前线程,Thread.interrupted()这行代码就不会执行了,只有被唤醒后Thread.interrupted()这行代码才会执行。

在线程被唤醒后,返回在阻塞过程中是否被中断。注意Thread.interrupted()方法会将线程的中断状态清空。

当线程被唤醒后,也会重新循环。

到现在AQS的acquire方法就结束了。

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

这里要注意一下,因为Thread.interrupted()方法会将线程的中断状态清空,所以我们这里要判断一下,如果线程在阻塞过程中被中断了,我们在这里要调用selfInterrupt()方法来中断当前线程,也就是将当前线程的中断状态置为true。

现在总结一下AQS的acquire(int arg)方法的流程。

  1. 调用子类的tryAcquire(int acquires)方法先尝试获取锁,如果成功则直接返回;
  2. 获取失败,则调用addWaiter(Node mode)方法将该线程加入等待队列的尾部,并标记为独占模式;
  3. 将该线程加入等待队列后,调用acquireQueued(final Node node, int arg)方法来尝试获取锁,在这个过程中,线程可能会被多次阻塞、唤醒。如果成功获取锁,就将当前节点更新为head节点,然后返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取锁后才再进行自我中断selfInterrupt(),将中断补上。

到此,非公平的ReentrantLock的lock() 方法分析完毕。

boolean tryLock(); 

void lockInterruptibly() throws InterruptedException; 

boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

这三个获取锁的方法过程也是类似的,就不进行分析了,接下来看一看非公平的ReentrantLock释放锁的过程。

ReentrantLock的unlock()方法

public void unlock() {
    //调用AQS的release方法
    sync.release(1);
}

其实我们这里可以看到,一个线程可以多次获取锁(可重入锁),每获取一次锁就会将state加1,每释放一次锁,就会将state减1,当前线程将state减到0的时候,说明当前线程释放了锁。

AQS的release(int arg)方法

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            //唤醒后继节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}

首先调用tryRelease(int arg)方法,AQS没有实现这个方法,我们直接看ReentrantLock.Sync的实现

protected final boolean tryRelease(int releases) {
    //同步状态每次减1
    int c = getState() - releases;
   //如果当前线程不是锁的持有者,抛出异常,没资格释放锁,哈哈
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {//同步状态为0,表示成功释放了锁
        free = true;
        setExclusiveOwnerThread(null);
    }
    //更新同步状态的值
    setState(c);
    return free;
}

方法首先将同步状态值减去1,如果如果当前线程不是锁的持有者,抛出异常。如果同步状态值减到了0,说明表示成功释放了锁,然后我们将锁的持有者设置为null,最后更新同步状态值,然后返回。

如果tryRelease返回了false,说明没有成功释放锁,如果返回true,表示成功释放了锁,那么我们要唤醒后继节点。

private void unparkSuccessor(Node node) {
    /*
     * 首先,它会检查节点的waitStatus字段。如果waitStatus小于0
     *(表示节点的后继节点需要唤醒),那么它会尝试将waitStatus设置为0。
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * 然后,它会找到需要唤醒的节点。这个节点通常是当前节点的直接后继节点,
     * 但是如果后继节点被取消或者为null,那么它会从队列的尾部开始向前遍历,
     * 找到第一个未被取消的节点。
     */
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    //最后,如果找到了需要唤醒的节点,那么它会调用LockSupport.unpark方法来唤醒节点对应的线程。
    if (s != null)
        LockSupport.unpark(s.thread);
}

被唤醒的线程会从上面的parkAndCheckInterrupt方法中第二行代码恢复执行

private final boolean parkAndCheckInterrupt() {
    //注释1处
    LockSupport.park(this);
    //唤醒后,在这里恢复执行,返回在阻塞过程中是否被中断
    return Thread.interrupted();
}

总结一下AQS的release(int arg)方法的流程。

release方法每次释放锁就会将state值减1,如果彻底释放了(即state==0),就会唤醒等待队列里的其他线程来获取锁。

看完了非公平的ReentrantLock获取锁和释放锁的过程,接下来我们看看公平的ReentrantLock获取锁和释放锁过程。

公平的ReentrantLock

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

当我们使用上面的构造函数创建ReentrantLock实例的时候,如果传入的参数是true,那么构建的是公平的ReentrantLock

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

ReentrantLock.FairSync的lock方法

final void lock() {
    //获取锁
    acquire(1);
}

AQS的acquire(int arg)方法

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

这个步骤和非公平策略获取锁是一样的可以分为3步
步骤1: 调用tryAcquire(arg) 尝试获取锁,获取成功直接返回
步骤2: 获取失败将当前线程以独占锁的方式加入等待队列
步骤3: 为已经加入队列中的线程尝试获取锁

步骤1: 调用tryAcquire(arg) 尝试获取锁

ReentrantLock.FairSync的tryAcquire(int acquires)方法

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        //注释1处
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
       }
    }
    //注释2处
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false; 
}

在上面方法的注释1处,c==0表示没有线程持有锁,首先调用hasQueuedPredecessors方法判断等待队列里面是否有节点,如果有等待节点,返回true,那么当前线程就不去获取锁(体现了公平)。如果没有等待节点并且以CAS的方式获取锁成功则将当前线程赋值为持有锁的线程,返回true。

在注释2处,c!=0表示有线程持有锁,如果是当前线程持有锁的话,那么就将同步状态值加1,返回true。

步骤2和步骤3和非公平的ReentrantLock是一样的,就不再赘述了。

公平的ReentrantLock和非公平的ReentrantLock的release(int arg)方法也是一样的就不再赘述了。

公平的ReentrantLock和非公平的ReentrantLock的差异

  1. 公平的ReentrantLock和非公平的ReentrantLock的差异由ReentrantLock.FairSyncReentrantLock.NonfairSync体现。非公平锁总会先尝试获取锁。如下图所示:左边是公平锁,右边是非公平锁。
    lock方法
image.png

tryAcquire 方法

image.png
  1. 如果等待队列里有节点等待获取锁,公平的ReentrantLock就会直接进入等待队列排队。非公平的ReentrantLock无论等待队列里是否有节点等待获取锁,总是先尝试获取锁,如果获取失败才进入等待队列进行排队。

结尾:本篇文章通过ReentrantLock引出了AQS是如何帮助ReentrantLock实现的获取锁和释放锁的。下一篇文章打算分析一下AQS是如何帮助ReentrantLock实现Condition功能的。

参考链接:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容

  • ReentrantLock 介绍 一个可重入的互斥锁,它具有与使用{synchronized}方法和语句访问的隐式...
    tomas家的小拨浪鼓阅读 4,045评论 1 4
  • 重入锁Reentrantlock Lock接口 先大概看一看lock接口 而我们一般使用Reentrantlock...
    faunjoe阅读 2,146评论 0 11
  • AQS简介 AQS:AbstractQueuedSynchronizer,即队列同步器。它是构建锁或者其他同步组件...
    lijiaccy阅读 623评论 0 0
  • 『耕』 出品/简垃圾 蝼蚁晓天时, 春农总未迟。 一湾堤上水, 破土润田知。 2019年04月21日
    北平永胜阅读 187评论 0 0
  • 生活中有这样的一个现象:作为倾听者,对于演讲者表达的一个意思,不同的人,会有不同的理解,更有甚者会得出截然相反的观...
    丁昆朋阅读 616评论 6 2