Java lock (2): Detailed explanation of AbstractQueuedSynchronizer and ReentrantLock

1. Introduction to AbstractQueuedSynchronizer

AQS (AbstractQueuedSynchronizer) is an abstract class in the locks package of the concurrent container JUC (java.util.concurrent), a synchronizer, a basic framework for building locks or other synchronization components, and maintains a member variable internally to stateindicate synchronization Status, state=0indicating that the thread has not acquired the lock, state > 0indicating that the thread has acquired the lock, state > 1indicating the number of reentrant locks, volatilemodified to ensure visibility, modified through CAS operations, and built-in maintenance of the FIFOqueue. Queue work for threads that have not acquired locks .

Two, AbstractQueuedSynchronizer source code analysis

core member variable

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    /**
     * 等待获取锁队列的头节点, 只能通过setHead方法修改
     * 如果head存在保证waitStatus状态不为CANCELLED
     */
    private transient volatile Node head;


    /**
     * 等待获取锁队列的尾节点, 只能通过enq方法添加新的等待节点
     */
    private transient volatile Node tail;
    
    /**
     * 表示锁的状态
     * state = 0 表示未锁定
     * state > 0 表示已锁定
     * state > 1 表示可重入锁, 获取锁的次数
     * volatile修饰保证了可见性
     */
    private volatile int state;
}
复制代码

AbstractQueuedSynchronizer mainly has three core member variables state, head,tail

  • state: Indicates the state of the lock, equal to 0 means unlocked, greater than 0 means locked, greater than 1 means reentrant lock, the number of times of reentry lock. Being volatilegroomed ensures visibility.
  • head: The head node of the waiting queue, except for initialization , the setHead()value can only be set by the method, if there is one, headthe state can be guaranteed waitStatusnot to be CANELLED.
  • tail: Waiting for the queue tail node, only by equadding a new waiting node.

Node node

AbstractQueuedSynchronizerThe queue is maintained internally FIFO, that is CLH, the queue. Each element of this queue is one Node, so we need to learn about other internal classes. The Nodesource code is as follows:

private static class Node {
    /**
     * 节点正在共享模式下等待的标记
     */
    static final Node SHARED = new Node();

    /**
     * 节点正在独占模式下等待的标记
     */
    static final Node EXCLUSIVE = null;

    /**
     * waitStatus变量的可选值, 取消状态, 被取消的节点不参与锁竞争, 状态也不会被改变
     */
    static final int CANCELLED = 1;

    /**
     * waitStatus变量的可选值, 下一节点处于等待状态, 如果当前节点释放锁或者被取消, 会通知下一节点去运行
     */
    static final int SIGNAL = -1;

    /**
     * waitStatus变量的可选值, 表示节点处于condition队列中, 正在等待被唤醒
     */
    static final int CONDITION = -2;

    /**
     * waitStatus变量的可选值, 下一次acquireShared应该无条件传播
     */
    static final int PROPAGATE = -3;

    /**
     * 节点的等待状态
     */
    volatile int waitStatus;

    /**
     * 上一节点
     */
    volatile Node prev;

    /**
     * 下一节点
     */
    volatile Node next;

    /**
     * 获取同步状态(锁)的线程
     */
    volatile Thread thread;

    /**
     * 下一个condition队列的等待节点
     */
    Node nextWaiter;

    /**
     * 是否是共享模式
     */
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    /**
     * 获取前一节点, 前一节点为null会抛异常
     */
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null) {
            throw new NullPointerException();
        } else {
            return p;
        }
    }

    /**
     * 无参构造方法用于初始化头部或者共享模式标记
     */
    Node () {

    }

    /**
     * 用于addWaiter方法, 设置下一个condition队列的等待节点
     */
    Node(Thread thread, Node mode) {
        this.nextWaiter = mode;
        this.thread = thread;
    }

    /**
     * 用于addConditionWaiter方法
     */
    Node (Thread thread, int waitStatus) {
        this.thread = thread;
        this.waitStatus = waitStatus;
    }
}
复制代码

core method

JUC里面的工具类基本都是基础AQS实现的,比ReentrantLockCountDownLatchCyclicBarrierSemaphore等,有的只支持独占锁,如ReentrantLock#lock(),有的支持共享锁,如Semaphore,从前文的Node类的定义也能看到

/**
 * 节点正在共享模式下等待的标记
 */
static final Node SHARED = new Node();

/**
 * 节点正在独占模式下等待的标记
 */
static final Node EXCLUSIVE = null;
复制代码

AQS实现了两套加锁解锁的方式,那就是独占锁共享锁。我们就从AQS最常用的类ReentrantLock来学习AQS的核心方法。

三、ReentrantLock

简介

ReentrantLock是基础AQS实现的一个可重入且独占式锁。内置了一个Sync同步器类实现了AQS,且支持公平锁和非公平锁,其实现类分别是FairSyncNonfairSync

ReentrantLock所有操作都是通过核心内部类Sync操作,由子类FairSyncNonfairSync实现。

private final Sync sync;
复制代码

ReentrantLock加锁过程

lock

lock()就是加锁,该方法定义如下:

public void lock() {
    sync.lock();
}
复制代码

FairSyncNonfairSync具体实现:

// FairSync实现
final void lock() {
	acquire(1);
}

// NofairSync实现 setExclusiveOwnerThread是父类AQS
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

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


复制代码

可以看到非公平锁多了一个compareAndSetState()操作,通过CAS尝试修改锁状态state的值,如果修改成功设置当前线程以独占的方式获取了锁,修改失败执行的逻辑和公平锁一样。

公平锁和非公平锁获取独占锁的核心逻辑都是acquire()方法,接下来就看看这个方法。

acquire

acquire该方法是父类AbstractQueuedSynchronizer定义的方法,源码如下:

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

该方法主要调用tryAcquire方法尝试获取锁,成功返回true表示获取到了锁,如果失败就将线程封装成节点插入队尾。

tryAcquire

tryAcquire方法在类AbstractQueuedSynchronizer没有直接实现,采用模版方法的设计模式交给子类实现,先看公平锁FairSync的实现,源码如下:

protected final boolean tryAcquire(int acquires) {
    // 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取当前锁状态 state=0表示未锁定, state>0表示已锁定
    int c = getState();
    if (c == 0) {
        // 线程没有获取到锁
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            // 没有比当前线程等待更久的线程了, 通过CAS的方式修改state
            // 如果成功则设置当前线程获取独占式锁
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        // 获取独占锁的线程就是当前线程, 表示重入
        // 重入锁的实现
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        // 修改state记录获取锁的次数
        setState(nextc);
        return true;
    }
    return false;
}
复制代码

从上面源码可以看出该方法就是独占的方式获取锁,获取成功后返回true,重入锁的逻辑也是在这里实现,主要通过修改state的值来记录获取锁的次数。

非公平锁的实现大同小异就是少了!hasQueuedPredecessors()的判断,因为是非公平锁嘛,所以不需要判断阻塞时间了。

acquire()方法除了调用tryAcquire()方法外还调用了acquireQueued(addWaiter(Node.EXCLUSIVE), arg),这里有两个方法,我们先看addWaiter()方法。

addWaiter

该方法相当于把当前线程封装成一个节点Node,并加入队列,这个方法我们在上面有写过,源码如下:

/**
 * Creates and enqueues node for current thread and given mode.
 * 为当前线程和给定模式创建并设置尾节点
 * Node.EXCLUSIVE: 独占模式
 * Node.SHARED: 共享模式
 */
private Node addWaiter(Node mode) {
    // 为当前线程创建节点
    Npde node = new Node(Thread.currentThread(), mode);
    // 获取尾节点
    Node pred = tail;
    // Try the fast path of enq; backup to full enq on failure
    // 如果队列已经创建, 尝试快速添加尾结点
    if (pred == null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 快速添加失败, 则调用enq
    enq(node);
    retur node;
}
复制代码
enq

enq方法是将节点加入队列队尾,必要时要进行初始化,通过自旋+CAS的方式保证线程安全和插入成功。源码如下:

/**
* Inserts node into queue, initializing if necessary. See picture above
* 将节点插入队列,必要时进行初始化
*/
private Node enq(final Node node) {
    // 自旋
    for(;;) {
        // 获取尾节点
        Node t = tail;
        // 尾节点为null表示队列没有初始化
        if (t == null) {
            // 设置头节点
            if (compareAndSetHead(new Node())) {
                tail = head;
            }
        } else {
            // 队列已经初始化, 设置新添加节点的前一节点是队列的尾节点
            node.prev = t;
            // 设置尾节点
            if (compareAndSetTail(t, node)) {
                // 设置队列的尾节点的下一节点是新添加的节点, 新添加的节点就插入尾节点了
                t.next = node;
                return t;
            }
        }
    }
}
复制代码

可以看出该方法就是往队列插入尾节点,通过自旋+CAS的方式,需要注意的是该方法返回的Node节点不是新插入的节点,而是新插入节点的前一节点。

enq()方法中调用的compareAndSetHead()compareAndSetTail()方法如下:

/**
 * 通过CAS设置head值, 只在enq方法调用
 */
private final boolean compareAndSetHead(Node update) {
    return unsafe.companreAndSwapObject(this, headOffset, null, update);
}

/**
 * 通过CAS函数设置tail值,仅仅在enq方法中调用
 */
private final boolean compareAndSetTail(Node expect, Node update) {
    return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}

复制代码
acquireQueued

acquireQueued()方法作用就是获取锁,如果没有获取到锁就让当前线程阻塞等待,源码如下:

/**
 * 想要获取锁的acquire方法,都会通过这个方法获取锁
 * 循环通过tryAcquire方法不断去获取锁,如果没有获取成功
 * 就有可能调用parkAndCheckInterrupt方法,让当前线程阻塞
 * 结果返回true,表示在线程等待的过程中,线程被中断了
 */
final boolean acquireQueued(final Node node, int arg) {
    // 操作是否成功
    boolean failed = true;
    try {
        // 表示线程在等待过程中是否被中断了
        boolean interrupted = false;
        // 自旋
        for (;;) {
            // 获取当前节点的前一节点
            final Node p = node.predecessor();
            // 获取前一节点是头节点, 并且尝试获取锁成功
            // 那么当前线程不就需要阻塞状态, 继续执行
            if (p == head && tryAcquire(arg)) {
                // 将当前节点设置成头节点
                setHead(node);
                p.next = null;
                // 不需要调用cancelAcquire方法
                failed = false;
                return interrupted;
            }
            // 当前节点不是头节点或者没有获取到锁
            // shouldParkAfterFailedAcquire方法用于判断当前线程是否需要被阻塞
            // 当p节点的状态是Node.SIGNAL时就会调用parkAndCheckInterrupt方法阻塞线程
            // parkAndCheckInterrupt方法用于阻塞线程并且检测线程是否被中断
            // 被阻塞的线程有两种被唤醒的方法:
            // 1. 在unparkSuccessor(Node node)方法,会唤醒被阻塞的node线程,返回false
            // 2. 当前线程被调用了interrupt方法,线程被唤醒,返回true
            // 在这里只是简单地将interrupted = true,没有跳出for的死循环,继续尝试获取锁
            if (shouldParkAfterFailedAcquire(p, node) &&
               parkAndCheckInterrupt()) {
               interrupted = true;
            }
        }
    } finally {
        // failed为true,表示发生异常非正常退出
        if (failed) 
            // 将当前节点状态设置成CANCELLED, 表示当前节点已经被取消, 不需要唤醒了
            cancelAcquire(node);
    }
}
复制代码

acquireQueued方法主要流程如下:

  1. 通过for(;;)死循环自旋,直到node(当前)节点获取到锁。
  2. 获取当前节点的前一个节点p。
  3. 如果节点p是头节点,然后调用tryAcquire()尝试获取锁,如果获取成功就将node节点设置成头节点然后返回。
  4. 如果节点p不是投节点或者获取锁失败,调用shouldParkAfterFaildAcquired()方法来决定是否要阻塞当前线程。
  5. 如果要阻塞当前线程,调用parkAndCheckInterrupt()方法阻塞当前线程。
  6. 如果当前线程发生异常,非正常退出,调用cancelAcquire()方法将当前节点的状态设置成取消。
shouldParkAfterFailedAcquire

shouldParkAfterFailedAcquire()用于判断当前线程是否需要阻塞,源码如下:

/**
 * 根绝前一个节点pred的状态来判断当前线程是否需要被阻塞
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 获取前一节点的状态
    int ws = pred.waitStatus;
    if (ws == Node.SINGAL) {
        // 如果前一节点pred的状态是Node.SINGAL, 说明当前线程需要被阻塞
        return true;
    }
    if (ws > 0) {
        // 如果前一节点状态是Node.CANCELLED(大于0就是CANCELLED)
        // 表示前一节点所在线程已经被唤醒了, 要从队列中移除CANCELLED的节点
        // 所以从pred节点一直向前查找直到找到不是CANCELLED状态的节点, 并把该节点赋值给node的prev
        // 表示node节点的前一节点已经改变
        do {
            node.prev = pred = pred.prev;
        } while(pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 此时前一节点pred的状态只能是0或者PROPAGATE, 不可能是CONDITION状态
        // CONDITION(这个是特殊状态,只在condition列表中节点中存在,CLH队列中不存在这个状态的节点)
        // 将前一个节点pred的状态设置成Node.SIGNAL, 这样子下一次循环时,就是直接阻塞当前线程
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
复制代码

这个方法是根绝前一个节点状态来判断当前线程是否需要被阻塞,前一节点的状态也是在这个方法中修改的,通过compareAndSetWaitStatus()方法。

shouldParkAfterFailedAcquire()方法主要流程如下:

  1. 如果前一节点状态是Node.SIGNAL,则直接返回true当前线程进入阻塞状态。
  2. 如果前一节点状态是Node.CANCELLED(大于0就是CANCELLED),表示前一个节点已经被唤醒了,要从队列中移动CANCELLED状态的节点,所以送pred节点一直向前查询不是CANCELLED状态的节点,并将该节点赋值成当前节点的前一节点,表示当前节点的前一节点发生变化,在acquireQueued()方法中进行下一次循环。
  3. 不是前面两种状态,只能是Node.SIGNAL状态,修改前一节点的状态为Node.SIGNAL,下一次循环时阻塞当前线程。
parkAndCheckInterrupt

该方法用于阻塞当前线程并检测线程是否被中断,源码如下:

/**
 * 阻塞当前线程,线程被唤醒后返回当前线程中断状态
 */
private final boolean parkAndCheckInterrupt() {
	// 阻塞当前线程
    LockSupport.park(this);
    // 检测当前线程是否被中断(该方法会清除中断标识位)
    return Thread.interrupted();
}
复制代码
cancelAcquire

cancelAcquire()方法在acquireQueued()方法异常的时候调用,用于将当前节点的状态设置成CANCELLED,源码如下:

// 将node节点的状态设置成CANCELLED,表示node节点所在线程已取消,不需要唤醒了。
private void cancelAcquire(Node node) {
    // 如果node为null,就直接返回
    if (node == null)
        return;

    //将获取锁节点的线程置空
    node.thread = null;

    // 跳过那些已取消的节点,在队列中找到在node节点前面的第一次状态不是已取消的节点
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    // 记录pred原来的下一个节点,用于CAS函数更新时使用
    Node predNext = pred.next;

    // Can use unconditional write instead of CAS here.
    // After this atomic step, other Nodes can skip past us.
    // Before, we are free of interference from other threads.
    // 将node节点状态设置为已取消Node.CANCELLED;
    node.waitStatus = Node.CANCELLED;

    // 如果node节点是队列尾节点,那么就将pred节点设置为新的队列尾节点
    if (node == tail && compareAndSetTail(node, pred)) {
        // 并且设置pred节点的下一个节点next为null
        compareAndSetNext(pred, predNext, null);
    } else {
        // If successor needs signal, try to set pred's next-link
        // so it will get one. Otherwise wake it up to propagate.
        int ws;
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
            unparkSuccessor(node);
        }

        node.next = node; // help GC
    }
}
复制代码
加锁过程总结
  1. 首先调用lock()方法,这个方法有两个子类FairSyncNofairSync实现,表示公平锁和非公平锁,两个类的不同就是NofairSync会直接调用compareAndSetStaus()方法修改加锁状态,如果成功当前线程获取到锁。
  2. 然后调用父类AbstractQueuedSynchronizedacquire()方法获取锁。
  3. acquire()方法调用tryAcquire()方法尝试获取锁,tryAcquire()由子类FairSyncNofairSync实现分别调用fairTryAcquire()nonfairTryAcquire()方法尝试获取锁。这两个方法里面实现了重入锁的逻辑,如果当前锁状态是未获取到锁,则调用CAS设置锁状态,如果是获取到锁状态则会判断获取锁的线程是否是当前线程,如果是则是重入锁的逻辑记录当前线程获取锁的次数。
  4. 如果tryAcquire()方法调用获取锁失败,则会调用acquireQueued()方法再获取锁或者进入阻塞状态,acquireQueued()方法首先调用了addWaiter()方法用于将当前线程封装成一个节点加入队列队尾,然后再调用acquireQueued()方法获取锁或者进入阻塞状态,acquireQueued()方法会通过自旋的方式根绝当前节点状态判断是否进入阻塞状态。当别的线程释放锁的时候,可能唤醒这个线程,再调用tryAcquire()方法获取锁。
  5. 如果发生异常,将当前节点状态设置成CANCELLED。

ReentrantLock释放锁过程

unlock

调用unlock()方法释放锁,然后调用release()方法,源码如下:

public void unlock() {
    sync.release(1);
}
复制代码
release

releaseAbstactQueuedSynchronized定义的方法用于释放锁,源码如下:

/**
 * 在独占锁模式下, 释放锁
 */
public final boolean release(int arg) {
    // 调用tryRelease方法尝试释放锁, 由子类实现
    if (tryRelease(arg)) {
        //尝试释放锁成功 获取头节点
        Node h = head;
        // 如果头节点不为null且状态不为取消状态, 调用unparkSuccessor唤醒被阻塞的线程
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
复制代码

release()释放锁流程如下:

  1. 调用tryRelease()方法尝试释放锁,返回true表示释放锁成功,返回false表示还持有锁资源。
  2. 如果释放锁成功了,且头节点不为null,就要唤醒被阻塞的线程,调用unparkSuccessor()方法唤醒一个等待的线程。
tryRelease

tryRelease尝试释放锁方法是有子类实现的,下面是ReentrantLockSynctryRelease()方法实现:

protected final boolean tryRelease(int releases) {
    // c表示新的锁状态
    int c = getState() - releases;
    // 如果当前线程不是获取独占锁的线程抛错
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 是否可以释放锁
    boolean free = false;
    // 如果新的锁状态=0表示可以释放锁
    if (c == 0) {
        free = true;
        // 获取独占锁的线程置为null
        setExclusiveOwnerThread(null);
    }
    // 锁状态设置成未锁定
    setState(c);
    return free;
}
复制代码

tryRelease()尝试释放锁流程如下:

  1. 首先获取新的锁状态
  2. 判断当前线程是否是获取独占锁的线程,如果不是抛异常。
  3. 如果新的锁状态是未锁定状态,获取独占锁的线程置为null,新的锁状态置为未锁定。
unparkSuccessor

unparkSuccessor()方法用于唤醒node节点下一节点非取消状态的节点所在线程,源码如下:

/**
 * 唤醒node节点下一节点非取消状态的节点所在线程
 */
private void unparkSuccessor(Node node) {
    // 获取node节点的状态
    int ws = node.waitStatus;
    // 如果状态小于0, 就将状态置为0, 表示这个node节点已经完成了
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    // 获取节点下一节点
    Node s = node.next;
    // 如果下一节点为null, 或者下一节点状态为取消状态, 就要寻找下一个非取消状态的节点
    if (s == null || s.waitStatus > 0) {
        // 先将s设置成null, s不是非取消状态的节点
        s = null;
        // 
        for(Node t = tail; t != null && t!= node; t = t.prev)
            //因为是从后向前遍历,所以不断覆盖找到的值,这样才能得到node节点后下一个非取消状态的节点
            if (t.waitStatus <= 0)
                s = t;
        // 如果s不为null,表示存在非取消状态的节点,那么调用LockSupport.unpark方法唤醒这个节点的线程	
        if (s != null)
            LockSupport.unpark(s.thread);
    }
}
复制代码

unparkSuccessor()方法唤醒node节点的下一个非取消状态的节点所在线程流程如下:

  1. 先将node节点的状态设置为0。
  2. 寻找下一个状态不为取消的节点s。
  3. 如果节点s不为null,调用LockSupport.unpark()方法唤醒s所在线程。
释放锁过程总结
  1. 先调用tryRelease()方法尝试释放当前持有的锁资源。
  2. 如果成功释放了锁资源,则调用unparkSuccessor()方法去唤醒一个等待锁的线程。

四、总结

到这里ReentrantLock加锁释放锁的过程已经学习完毕,ReentrantLock是基于AQS实现的独占式锁,内部维护了一个FIFO队列实现未获取到锁的线程进行排队工作, ReentrantLock内部有FairSync(公平锁)和NonfairSync(非公平锁)两种实现,通过调用lock()方法加锁,调用unlock()方法解锁。

五、自己实现一个可重入的独占锁

通过继承AbstractQueuedSynchronizer 类重写tryAcquire()tryRelease()方法实现自定义的可重入独占锁。

代码如下:

public class SyncLock extends AbstractQueuedSynchronizer {

    @Override
    protected boolean tryAcquire(int acquires) {
        // 获取当前线程
        final Thread current = Thread.currentThread();
        // 获取当前锁状态
        int c = getState();
        // 如果锁状态为0, 表示当前锁是空闲的
        if (c == 0) {
            // 调用CAS原子操作设置锁状态
            if (compareAndSetState(0, acquires)) {
                // 如果设置成功, 将当前线程设置为获取独占锁的线程
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 判断当前线程是不是获取独占锁的线程, 因为可能重入锁
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0 ) {
                throw new Error("Maximum lock count exceeded");
            }
            // 重入锁实现逻辑, 记录获取锁的次数
            setState(nextc);
            return true;
        }
        return false;
    }

    @Override
    protected boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0){
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }

}

class AQSTest {
    public static void newThread(SyncLock syncLock, String name, int time) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程" + Thread.currentThread().getName() +
                        "开始运行, 准备获取锁。");
                syncLock.acquire(1);
                try {
                    System.out.println("线程" + Thread.currentThread().getName() + ", 在run方法获取了锁。");
                    lockAgain();
                    try {
                        Thread.sleep(time);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } finally {
                    System.out.println("线程"+Thread.currentThread().getName()+" 在run方法中释放了锁。");
                    syncLock.release(1);
                }
            }
            private void lockAgain() {
                syncLock.acquire(1);
                try {
                    System.out.println("线程" + Thread.currentThread().getName() + ", 在lockAgain方法获取了锁。");
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } finally {
                    System.out.println("线程"+Thread.currentThread().getName()+" 在lockAgain方法中释放了锁。");
                    syncLock.release(1);
                }
            }
        }, name).start();
    }

    public static void main(String[] args) {
        SyncLock syncLock = new SyncLock();
        newThread(syncLock, "t1111", 1000);
        newThread(syncLock, "t2222", 1000);
        newThread(syncLock, "t3333", 1000);
        newThread(syncLock, "t4444", 1000);
    }
}
复制代码

上面代码测试结果如下:

线程t1111开始运行, 准备获取锁。
线程t2222开始运行, 准备获取锁。
线程t1111, 在run方法获取了锁。
线程t1111, 在lockAgain方法获取了锁。
线程t4444开始运行, 准备获取锁。
线程t3333开始运行, 准备获取锁。
线程t1111 在lockAgain方法中释放了锁。
线程t1111 在run方法中释放了锁。
线程t2222, 在run方法获取了锁。
线程t2222, 在lockAgain方法获取了锁。
线程t2222 在lockAgain方法中释放了锁。
线程t2222 在run方法中释放了锁。
线程t4444, 在run方法获取了锁。
线程t4444, 在lockAgain方法获取了锁。
线程t4444 在lockAgain方法中释放了锁。
线程t4444 在run方法中释放了锁。
线程t3333, 在run方法获取了锁。
线程t3333, 在lockAgain方法获取了锁。
线程t3333 在lockAgain方法中释放了锁。
线程t3333 在run方法中释放了锁。
复制代码

六、ReentrentLock和synchronized的比较

相同点:
  • 都是加锁方式同步
  • 都是重入锁。
  • 都是通过阻塞的方式实现同步。
不同点
  • 原始构成:synchronized是java语言的关键字,是原生语法层面的互斥,由JVM实现,而ReentrentLock是JDK1.5之后提供的API层面的互斥锁。
  • 实现:synchronized是通过JVM实现加锁解锁,而ReentrentLock是API层面的加锁解锁,需要手动解锁。
  • 代码编写:synchronized不需要手动释放锁,修饰方法或者代码块,而ReentrentLock必须手动释放锁,如果没有释放锁可能造成死锁现象。需要lock()unlock()方法配合try/finally语句块完成。
  • 灵活性:synchronized只能用于修饰方法或者代码块,灵活性低,而ReentrentLock是方法调用可以跨方法,灵活性高。
  • 是否等待可中断:synchronized不可中断,除非抛出异常,而ReentrentLock是可以中断的,如果持有锁的线程长期不释放锁,正在等待的线程可以选择放弃等待,通过设置超时时间方法。
  • 是否公平锁:synchronized是不公平锁,而ReentrentLock是可公平锁也可不公平锁。
  • 实现原理:synchronized是通过编译,会在同步代码块前后分别生成monitorentermonitorexit两个指令实现同步,在执行monitorenter的指令时会尝试获取锁,获取锁成功会通过计数器+1,执行完毕之后会执行monitorexit执行计数器-1,当计数器为0时释放锁,如果获取锁失败就会进入阻塞状态,而ReentrentLock是通过CAS + CLH队列实现,通过CAS原子性操作实现对锁状态state的修改,通过CLH队列实现对未获取到锁的线程进行排队工作。

Guess you like

Origin juejin.im/post/7080016641433534478