ReentrantLock原理

ReentrantLock 原理

在这里插入图片描述

ReentrabtLock实现了lock接口,里面维护了一个sync同步器,Sync是一个抽象类,有两种实现FairSync和NonfairSync分别对应着公平锁和非公平锁两种实现。

1. 非公平锁实现原理

加锁解锁流程

加锁流程

先从构造器开始看,默认为非公平锁实现

//ReentrantLock
private final Sync sync;//同步器
public ReentrantLock() {
    sync = new NonfairSync();
}

Sync是ReentrantLock的一个内部类,继承自AQS,Sync有两个子类NonfairSync和fairSync,分别对应非公平锁和公平锁

//ReentrantLock
abstract static class Sync extends AbstractQueuedSynchronizer {
}
//ReentrantLock
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;
    
    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }
    
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

ReentrantLock的lock方法里面,调用的sync是lock方法

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

在lock方法里,首先会调用compareAndSetState方法,compareAndSetState方法是一个CAS操作,CAS是一种无锁算法,有3个操作数:内存值V、旧的预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

这里尝试修改state状态(state为0表示没加锁,为1表示加锁)。“非公平锁”的体现就在这里,因为每个线程调用lock方式时,都会去尝试CAS操作获取锁,而不会让已经阻塞的线程优先获取锁。

假设当前没有竞争,CAS操作修改成功,表示当前线程获得锁,就调用setExclusiveOwnerThread(Thread.currentThread())将exclusiveOwnerThread设置为当前线程。

//NonfairSync
final void lock() {
    // 首先用 cas 尝试(仅尝试一次)将 state 从 0 改为 1, 如果成功表示获得了独占锁
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // 如果尝试失败,进入 ㈠
        acquire(1);
}

假设当前有三个线程去竞争锁,假设线程A的CAS操作成功了,获得了锁,那么线程B和C则设置state失败,走到了else里面,调用acquire方法。

// ㈠ AQS 继承过来的方法, 方便阅读, 放在此处
public final void acquire(int arg) {
    // ㈡ tryAcquire
    if (
            !tryAcquire(arg) &&
                    // 当 tryAcquire 返回为 false 时, 先调用 addWaiter ㈣, 接着 acquireQueued ㈤
                    acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
            ) {
        selfInterrupt();
    }
}

第一步:调用 tryAcquire方法尝试获取锁

//NonfairSync ㈡ 进入 ㈢
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

// ㈢ Sync 继承过来的方法, 方便阅读, 放在此处
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 如果还没有获得锁
    if (c == 0) {
        // 尝试用 cas 获得, 这里体现了非公平性: 不去检查 AQS 队列
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入
    else if (current == getExclusiveOwnerThread()) {
        // state++
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 获取失败, 回到调用处
    return false;
}

tryAcquire方法的流程是:先检查state状态是否为0,如果为0,表示锁未被占用,就CAS操作尝试修改state状态,如果成功,当前线程获取到锁,返回true;如果不为0,检查锁程是否被当前线程自己占用,如果是被自己占用,更新state字段,表示重入锁的次数。如果以上两点都没有成功,则获取锁失败,返回false。

由于上文中提到线程A已经占用了锁,所以B和C执行tryAcquire失败,会进行入队操作。

第二步,入队。由于上文中提到线程A已经占用了锁,所以B和C执行tryAcquire失败,并且入等待队列。如果线程A拿着锁死死不放,那么B和C就会被挂起。

首先,调用addWaiter(Node.EXCLUSIVE),创建节点(独占模式)

/**
 * 将新节点和当前线程关联并且入队列
 * @param mode 独占/共享
 * @return 新节点
 */
/// ㈣ AQS 继承过来的方法, 方便阅读, 放在此处
private Node addWaiter(Node mode) {
    //初始化节点,设置关联线程和模式(独占 or 共享)
    Node node = new Node(Thread.currentThread(), mode);
    // 获取尾节点引用
    Node pred = tail;
    // 尾节点不为空,说明队列已经初始化过
    if (pred != null) {
        node.prev = pred;
        // 设置新节点为尾节点
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 尾节点为空,说明队列还未初始化,需要初始化head节点并入队新节点
    enq(node);
    return node;
}

B、C线程同时尝试入队列,由于队列尚未初始化,tail==null,故至少会有一个线程会走到enq(node)。我们假设B、C同时走到了enq(node)里。

/**
 * 将节点加入队列,如果有需要初始化队列
 */
// ㈥ AQS 继承过来的方法, 方便阅读, 放在此处
private Node enq(final Node node) {
    //开始自旋
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            // 如果tail为空,则新建一个head节点(哨兵节点),并且tail指向head
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            // tail不为空,将新节点入队
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

这里compareAndSetHead的实现使用了unsafe类提供的CAS操作,保证只有一个线程会创建head节点成功,head节点是哨兵,用来占位,并不关联线程。

假设线程B新建head节点成功,线程C失败。两个线程都进入下一轮循环,这一次tail不为空,进入else里面。假设是线程B的compareAndSetTail成功,则线程B返回,线程C还要进入下一轮循环。最终所有线程都入队。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b6vHASiD-1611156521938)(2F249DFA546C475EBFD1EE3231DEA7FF)]

3. 第三步,挂起。B和C相继执行acquireQueued(final Node node, int arg)。这个方法让已经入队的线程尝试获取锁,若失败则会被挂起。

/**
 * 已经入队的线程尝试获取锁
 */
// ㈤ AQS 继承过来的方法, 方便阅读, 放在此处
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true; //标记是否成功获取锁
    try {
        boolean interrupted = false; //标记线程是否被中断过
        for (;;) {
            final Node p = node.predecessor(); //获取前驱节点
            //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取锁
            if (p == head && tryAcquire(arg)) {
                setHead(node); // 获取成功,将当前节点设置为head节点
                p.next = null; // 原head节点出队,在某个时间点被GC回收
                failed = false; //获取成功
                return interrupted; //返回是否被中断过
            }
            // 判断获取失败后是否可以挂起,若可以则挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                // 线程若被中断,设置interrupted为true
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

若当前线程的节点的前驱节点是head,就有资格尝试获取锁。
假设B和C在竞争锁的过程中A一直持有锁,那么它们的tryAcquire操作都会失败,因此会走到第2个if语句中。看shouldParkAfterFailedAcquire和parkAndCheckInterrupt都做了哪些事吧

// ㈦ AQS 继承过来的方法, 方便阅读, 放在此处
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 获取上一个节点的状态
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL) {
        // 上一个节点都在阻塞, 那么自己也阻塞好了
        return true;
    }
    // > 0 表示取消状态
    if (ws > 0) {
        // 上一个节点取消, 那么重构删除前面所有取消的节点, 返回到外层循环重试
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 这次还没有阻塞
        // 但下次如果重试不成功, 则需要阻塞,这时需要设置上一个节点状态为 Node.SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
/**
 * 挂起当前线程,返回线程中断状态并重置
 */
// ㈧ 阻塞当前线程,AQS源码
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

线程入队后能够挂起的前提是,它的前驱节点的状态为SIGNAL。所以shouldParkAfterFailedAcquire会先判断当前节点的前驱是否状态符合要求,若符合则返回true,然后调用parkAndCheckInterrupt,将自己挂起。如果不符合,再看前驱节点是否>0(CANCELLED),若是那么向前遍历直到找到第一个符合要求的前驱,若不是则将前驱节点的状态设置为SIGNAL。

整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心挂起,需要去找个安心的挂起点,同时可以再尝试下看有没有机会去尝试竞争锁。

节点的等待状态有下面几种

//由于超时或中断,此节点被取消
static final int CANCELLED =  1;
//用于指示后续节点可以被阻塞,锁释放时,当前节点可以竞争锁
static final int SIGNAL    = -1;
//处于条件变量的队列
static final int CONDITION = -2;
static final int PROPAGATE = -3;

最终队列可能会如下图所示
在这里插入图片描述

解锁流程

//ReentrantLock
public void unlock() {
    sync.release(1);
}
//AQS
public final boolean release(int arg) {
    // 尝试释放锁, 进入 ㈠
    if (tryRelease(arg)) {
        // 队列头节点 unpark
        Node h = head;
        if (
            // 队列不为 null
                h != null &&
                        // waitStatus == Node.SIGNAL 才需要 unpark
                        h.waitStatus != 0
                ) {
            // unpark AQS 中等待的线程, 进入 ㈡
            unparkSuccessor(h);
        }
        return true;
    }
    return false;
}

解锁流程大致为先尝试释放锁,若释放成功,那么查看头结点的状态是否为SIGNAL,如果是则唤醒头结点的下个节点关联的线程,如果释放失败那么返回false表示解锁失败。这里我们也发现了,每次都只唤起头结点的下一个节点关联的线程。

tryRelease的执行过程

/**
 * 释放当前线程占用的锁
 * @param releases
 * @return 是否释放成功
 */
 // ㈠ Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryRelease(int releases) {
    // 计算释放后state值
    int c = getState() - releases;
    // 如果不是当前线程占用锁,那么抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 支持锁重入, 只有 state 减为 0, 才释放成功
    if (c == 0) {
        free = true;
        // 清空独占线程
        setExclusiveOwnerThread(null);
    }
    // 更新state值
    setState(c);
    return free;
}

这里入参为1。tryRelease的过程为:当前释放锁的线程若不持有锁,则抛出异常。若持有锁,计算释放后的state值是否为0,若为0表示锁已经被成功释放,并且则清空独占线程,最后更新state值,返回free。

如果tryRelease成功,且队列有阻塞的线程,就调用unparkSuccessor方法。

// ㈡ AQS 继承过来的方法, 方便阅读, 放在此处
    private void unparkSuccessor(Node node) {
        // 如果状态为 Node.SIGNAL 尝试重置状态为 0
        // 不成功也可以
        int ws = node.waitStatus;
        if (ws < 0) {
            compareAndSetWaitStatus(node, ws, 0);
        }
        // 找到需要 unpark 的节点, 但本节点从 AQS 队列中脱离, 是由唤醒节点完成的
        Node s = node.next;
        // 不考虑已取消的节点, 从 AQS 队列从后至前找到队列最前面需要 unpark 的节点
        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;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

unparkSuccessor方法里面会调用LockSupport.unpark唤醒线程。被唤醒的线程在acquireQueued在方法里面interrupted = true;开始执行,进入下一轮循环。下一轮循环又会重新尝试获取锁。这也体现了非公平锁。

/**
 * 已经入队的线程尝试获取锁
 */
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true; //标记是否成功获取锁
    try {
        boolean interrupted = false; //标记线程是否被中断过
        for (;;) {
            final Node p = node.predecessor(); //获取前驱节点
            //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取锁
            if (p == head && tryAcquire(arg)) {
                setHead(node); // 获取成功,将当前节点设置为head节点
                p.next = null; // 原head节点出队,在某个时间点被GC回收
                failed = false; //获取成功
                return interrupted; //返回是否被中断过
            }
            // 判断获取失败后是否可以挂起,若可以则挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                // 线程若被中断,设置interrupted为true
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

2、可重入原理

可重入原理在上面有讲到,

static final class NonfairSync extends Sync {
    // ...
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
    // Sync 继承过来的方法, 方便阅读, 放在此处
    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()) {
            // state++
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

    // Sync 继承过来的方法, 方便阅读, 放在此处
    protected final boolean tryRelease(int releases) {
        // state--
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        // 支持锁重入, 只有 state 减为 0, 才释放成功
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }

        setState(c);
        return free;
    }
}

非公平锁在尝试加锁失败时,调用tryAcquire方法,里面会调用Sync的nonfairTryAcquire。在nonfairTryAcquire里面可以看出,就果state状态不为0且占用的线程是当前线程,则state+1。在释放锁时,state-1,然后判断state是否为0,不为0,则当前线程还是占有锁,锁释放没有成功。

3. 可打断原理

不可打断模式

在此模式下,即使它被打断,仍会驻留在AQS队列中,一直要等到获得锁后方能得知自己被打断了。调用lock方法,就是此模式。

// Sync 继承自 AQS
static final class NonfairSync extends Sync {
    // ...

    private final boolean parkAndCheckInterrupt() {
        // 如果打断标记已经是 true, 则 park 会失效
        LockSupport.park(this);
        // interrupted 会清除打断标记
        return Thread.interrupted();
    }

    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;
                    failed = false;
                    // 还是需要获得锁后, 才能返回打断状态
                    return interrupted;
                }
                if (
                        shouldParkAfterFailedAcquire(p, node) &&
                                parkAndCheckInterrupt()
                        ) {
                    // 如果是因为 interrupt 被唤醒, 返回打断状态为 true
                    interrupted = true;
                }
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

    public final void acquire(int arg) {
        if (
                !tryAcquire(arg) &&
                        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
                ) {
            // 如果打断状态为 true
            selfInterrupt();
        }
    }

    static void selfInterrupt() {
        // 重新产生一次中断
        Thread.currentThread().interrupt();
    }
}

可以看到,线程被打断,在parkAndCheckInterrupt方法里面,会调用Thread.interrupted()方法,也就是返回true,并清除了打断标志;然后返回到acquireQueued方法里面,设置interrupted为ture,并进入下一轮循环;直到获取到锁后,返回interrupted值,然后在acquire方法里面调用selfInterrupt()方法,再次打断线程(打断线程,线程并不会抛出异常,需要自行处理)。总的就是,即使打断线程,也要获取到锁后,才会得知自己被打断。

测试

@Slf4j(topic = "c.testDemo4")
public class Demo4 {

	public static void main(String[] args) throws InterruptedException {
		ReentrantLock lock=new ReentrantLock();
		Thread t1,t2;
		t1=new Thread(()->{
			lock.lock();
			log.debug("t1 lock");
			try {
				Thread.sleep(5000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			log.debug("t1 unlock");
			lock.unlock();
		},"t1");
		t2=new Thread(()->{
			lock.lock();
			log.debug("t2 lock");
			if(Thread.currentThread().isInterrupted()){
				log.debug("被打断了");
			}
			log.debug("t2 unlock");
			lock.unlock();
		},"t2");
		t1.start();
		Thread.sleep(1000);
		t2.start();
		log.debug("主线程打断t2线程");
		t2.interrupt();

	}
}

在这里插入图片描述

可打断模式

调用lockInterruptibly方法

public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}
static final class NonfairSync extends Sync {
	public final void acquireInterruptibly(int arg) throws InterruptedException {
		if (Thread.interrupted())
			throw new InterruptedException();
		// 如果没有获得到锁, 进入 ㈠
		if (!tryAcquire(arg))
			doAcquireInterruptibly(arg);
	}

	// ㈠ 可打断的获取锁流程
	private void doAcquireInterruptibly(int arg) throws InterruptedException {
		final Node node = addWaiter(Node.EXCLUSIVE);
		boolean failed = true;
		try {
			for (;;) {
				final Node p = node.predecessor();
				if (p == head && tryAcquire(arg)) {
					setHead(node);
					p.next = null; // help GC
					failed = false;
					return;
				}
				if (shouldParkAfterFailedAcquire(p, node) &&
						parkAndCheckInterrupt()) {
					// 在 park 过程中如果被 interrupt 会进入此
					// 这时候抛出异常, 而不会再次进入 for (;;)
					throw new InterruptedException();
				}
			}
		} finally {
			if (failed)
				cancelAcquire(node);
		}
	}
}

在park过程中,就果被打断,返回true回到doAcquireInterruptibly方法里面,直接抛出异常,不会等到获取锁后,才知道打断状态。

测试

@Slf4j(topic = "c.testDemo4")
public class Demo4 {

	public static void main(String[] args) throws InterruptedException {
		ReentrantLock lock=new ReentrantLock();
		Thread t1,t2;
		t1=new Thread(()->{
			lock.lock();
			System.out.println("t1 lock");
			try {
				Thread.sleep(5000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			lock.unlock();
			System.out.println("t1 unlock");
		});
		t2=new Thread(()->{
			try {
				lock.lockInterruptibly();
			} catch (InterruptedException e) {
				System.out.println("被打断了");
				e.printStackTrace();
			}
			System.out.println("t2 lock");
			lock.unlock();
			System.out.println("t2 unlock");
		});
		t1.start();
		Thread.sleep(1000);
		t2.start();
		t2.interrupt();

	}
}

在这里插入图片描述

4、公平锁的原理

static final class FairSync extends Sync {
	private static final long serialVersionUID = -3000897897090466540L;
	final void lock() {
		acquire(1);
	}

	// AQS 继承过来的方法, 方便阅读, 放在此处
	public final void acquire(int arg) {
		if (
				!tryAcquire(arg) &&
						acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
				) {
			selfInterrupt();
		}
	}
	// 与非公平锁主要区别在于 tryAcquire 方法的实现
	protected final boolean tryAcquire(int acquires) {
		final Thread current = Thread.currentThread();
		int c = getState();
		if (c == 0) {
			// 先检查 AQS 队列中是否有前驱节点, 没有才去竞争
			if (!hasQueuedPredecessors() &&
					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;
	}

	// ㈠ AQS 继承过来的方法, 方便阅读, 放在此处
	public final boolean hasQueuedPredecessors() {
		Node t = tail;
		Node h = head;
		Node s;
		// h != t 时表示队列中有 Node
		return h != t &&
				(
					// (s = h.next) == null 表示队列中还有没有老二
					(s = h.next) == null ||
					// 或者队列中老二线程不是此线程
					s.thread != Thread.currentThread()
				 );
	}
}

可以看到,在加锁时,没有像非公平锁那些先尝试CAS获取锁,而是直接进入tryAcquire方法。在tryAcquire方法里面,要等待队列中没有前驱节点,才会CAS尝试获取锁。这样就保证了被阻塞的线程,会优先获取到锁。

5、条件变量

在ReentrantLock对象上的newCondition()可以得到一个Condition对象,可以通过在Condition上调用await()方法来挂起一个任务(线程),通过在Condition上调用signal()来通知任务,从而唤醒一个任务,或者调用signalAll()来唤醒所有在这个Condition上被其自身挂起的任务。

调用ReentrantLock的newCondition方法,实例上是返回AQS的内部类 ConditionObject

//ReentrantLock
public Condition newCondition() {
    return sync.newCondition();
}
//Sync
final ConditionObject newCondition() {
    return new ConditionObject();
}
//AbstractQueuedSynchronizer
public class ConditionObject implements Condition, java.io.Serializable {
        private static final long serialVersionUID = 1173984872572414699L;
        /** First node of condition queue. */
        private transient Node firstWaiter;
        /** Last node of condition queue. */
        private transient Node lastWaiter;

        /**
         * Creates a new {@code ConditionObject} instance.
         */
        public ConditionObject() { }
}

await 流程

先看ConditionObject的await方法

// 等待 - 直到被唤醒或打断
	public final void await() throws InterruptedException {
		if (Thread.interrupted()) {
			throw new InterruptedException();
		}
		// 添加一个 Node 至等待队列, 见 ㈠
		Node node = addConditionWaiter();
		// 释放节点持有的锁
		int savedState = fullyRelease(node);
		int interruptMode = 0;
		// 如果该节点还没有转移至 AQS 队列, 阻塞
		while (!isOnSyncQueue(node)) {
			// park 阻塞
			LockSupport.park(this);
			// 如果被打断, 退出等待队列
			if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
				break;
		}
		// 退出等待队列后, 还需要获得 AQS 队列的锁
		if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
			interruptMode = REINTERRUPT;
		// 所有已取消的 Node 从队列链表删除, 见 ㈡
		if (node.nextWaiter != null)
			unlinkCancelledWaiters();
		// 应用打断模式, 见 ㈤
		if (interruptMode != 0)
			reportInterruptAfterWait(interruptMode);
	}

await方法里面,首先是调用addConditionWaiter方法,这个方式是添加一个 Node 至等待队列,并返回该节点。

// ㈠ 添加一个 Node 至等待队列
	private Node addConditionWaiter() {
		Node t = lastWaiter;
		// 所有已取消的 Node 从队列链表删除, 见 ㈡
		if (t != null && t.waitStatus != Node.CONDITION) {
			unlinkCancelledWaiters();
			t = lastWaiter;
		}
		// 创建一个关联当前线程的新 Node, 添加至队列尾部
		Node node = new Node(Thread.currentThread(), Node.CONDITION);
		if (t == null)
			firstWaiter = node;
		else
			t.nextWaiter = node;
		lastWaiter = node;
		return node;
	}

addConditionWaiter流程是:先从等待队列中清除已经取消的节点(状态不为CONDITION),然后创建一个新的节点关联当前线程状态为CONDITION添加到等待队列中。

addConditionWaiter方法执行完毕后,调用fullyRelease释放该线程占用的锁

// 外部类方法, 方便阅读, 放在此处
	// ㈣ 因为某线程可能重入,需要将 state 全部释放
	final int fullyRelease(Node node) {
		boolean failed = true;
		try {
			int savedState = getState();
			if (release(savedState)) {
				failed = false;
				return savedState;
			} else {
				throw new IllegalMonitorStateException();
			}
		} finally {
			if (failed)
				node.waitStatus = Node.CANCELLED;
		}
}

然后调用isOnSyncQueue方法,判断节点是否在AQS队列中,若不在AQS队列中,阻塞线程,直到被唤醒。

signal流程

// 唤醒 - 必须持有锁才能唤醒, 因此 doSignal 内无需考虑加锁
	public final void signal() {
		if (!isHeldExclusively())
			throw new IllegalMonitorStateException();
		Node first = firstWaiter;
		if (first != null)
			doSignal(first);
}
// 唤醒 - 将没取消的第一个节点转移至 AQS 队列
	private void doSignal(Node first) {
		do {
			// 已经是尾节点了
			if ( (firstWaiter = first.nextWaiter) == null) {
				lastWaiter = null;
			}
			first.nextWaiter = null;
		} while (
			// 将等待队列中的 Node 转移至 AQS 队列, 不成功且还有节点则继续循环 ㈢
				!transferForSignal(first) &&
						// 队列还有节点
						(first = firstWaiter) != null
				);
}

doSignal方法,从第一个节点开始,将节点转移到AQS队列中,若转移不成功,继续下一个节点。

transferForSignal转移节点

// ㈢ 如果节点状态是取消, 返回 false 表示转移失败, 否则转移成功
	final boolean transferForSignal(Node node) {
		// 如果状态已经不是 Node.CONDITION, 说明被取消了
		if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
			return false;
		// 加入 AQS 队列尾部
		Node p = enq(node);
		int ws = p.waitStatus;
		if (
			// 上一个节点被取消
				ws > 0 ||
						// 上一个节点不能设置状态为 Node.SIGNAL
						!compareAndSetWaitStatus(p, ws, Node.SIGNAL)
				) {
			// unpark 取消阻塞, 让线程重新同步状态
			LockSupport.unpark(node.thread);
		}
		return true;

transferForSignal方法里面,首先CAS操作将节点状态设置为0,若失败,说明已经被取消了,直接返回false。然后调用enq方法,将节点加入AQS队列队尾,并返回上一个节点。如果上一个节点被取消或者设置状态为Signal失败,唤醒节点线程。不管换不唤醒节点线程,最后都返回true。

上面调用enq方法返回的上一个节点p,有可能节点状态为0,这时,CAS操作设置节点p状态为Signal成功,所以并没有直接唤醒当前节点node的线程,而是进入了AQS队列。等待锁释放时,且刚好唤醒了当前节点,才继续向下运行。也有可能节点状态大于0,也就是节点被取消,或者CAS操作设置失败,上一个节点状态发生改变,那么直接唤醒当前节点的线程,继续向下运行。

向下运行,是从await方法代码里面继续运行,调用acquireQueuedc尝试获取锁,如果获取锁失败,可能还要继续阻塞。

// 等待 - 直到被唤醒或打断
	public final void await() throws InterruptedException {
		if (Thread.interrupted()) {
			throw new InterruptedException();
		}
		// 添加一个 Node 至等待队列, 见 ㈠
		Node node = addConditionWaiter();
		// 释放节点持有的锁
		int savedState = fullyRelease(node);
		int interruptMode = 0;
		// 如果该节点还没有转移至 AQS 队列, 阻塞
		while (!isOnSyncQueue(node)) {
			// park 阻塞
			LockSupport.park(this);
			// 如果被打断, 退出等待队列
			if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
				break;
		}
		// 退出等待队列后, 还需要获得 AQS 队列的锁
		if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
			interruptMode = REINTERRUPT;
		// 所有已取消的 Node 从队列链表删除, 见 ㈡
		if (node.nextWaiter != null)
			unlinkCancelledWaiters();
		// 应用打断模式, 见 ㈤
		if (interruptMode != 0)
			reportInterruptAfterWait(interruptMode);
	}

测试

@Slf4j(topic = "c.test")
public class Test{
	public static void main(String[] args)
	{
		ReentrantLock lock=new ReentrantLock();
		Condition condition = lock.newCondition();
		Thread t1,t2,t3;
		t1=new Thread(()->{
			log.debug("t1 start");
			lock.lock();
			log.debug("t1 lock");
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			try {
				log.debug("t1 等待");
				condition.await();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			log.debug("t1 继续运行");
		},"t1");
		t2=new Thread(()->{

			log.debug("t2 start");

			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			lock.lock();
			log.debug("t2 lock");
			condition.signal();
			log.debug("t2 唤醒t1");
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			log.debug("t2 unlock");
			lock.unlock();
		},"t2");

		t1.start();
		t2.start();
	}
}

在这里插入图片描述

调用await()和signal()方法前,都要先获取到锁,不然会抛出异常。从结果可以看出,t2唤醒t1后,由于t2占有锁,所以t1线程没有立即获取到锁,所以还处于阻塞,直到t2释放了锁,t1获取到锁,才继续向下运行。

参考:https://www.bilibili.com/video/BV16J411h7Rd?p=246

https://blog.csdn.net/fuyuwei2015/article/details/83719444

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: ReentrantLockJava中的一个锁类,它是一个可重入锁,允许同一个线程多次获得同一个锁。在使用ReentrantLock时,我们需要显式地获取锁和释放锁,可以通过lock()和unlock()方法来完成这些操作。 ReentrantLock采用了一种非公平的获取锁的方式,这意味着当多个线程同时请求锁时,ReentrantLock并不保证锁的获取顺序与请求锁的顺序相同。这种方式的好处是可以减少线程竞争,从而提高系统的并发性能。 另外,ReentrantLock还支持Condition条件变量,可以使用它来实现线程的等待和通知机制,以及更加灵活的线程同步和通信。 总之,ReentrantLockJava中一个非常强大的锁类,可以帮助我们实现高效的线程同步和并发控制。但是,使用ReentrantLock也需要注意一些问题,比如需要正确地使用try-finally块来释放锁,避免死锁等问题。 ### 回答2: ReentrantLockJava中的一种可重入锁,它提供了与synchronized关键字相似的功能,但具有更强大的扩展性和灵活性。 ReentrantLock内部使用一个同步器Sync来实现锁机制。Sync是ReentrantLock的核心组件,它有两个实现版本,分别是NonfairSync和FairSync。 NonfairSync是默认的实现版本,它采用非公平方式进行线程获取锁的竞争,即线程请求锁的时候,如果锁可用,则直接将锁分配给请求的线程,而不管其他线程是否在等待。 FairSync是公平版本,它按照线程请求锁的顺序来分配锁,当锁释放时,会优先分配给等待时间最长的线程。 ReentrantLock在实现上使用了Java的锁机制和条件变量来管理线程的等待与唤醒。当一个线程调用lock方法获取锁时,如果锁可用,线程会立即获得锁;如果锁被其他线程占用,调用线程就会被阻塞,进入等待队列。 当一个线程占用了锁之后,可以多次重复地调用lock方法,而不会引起死锁。这就是ReentrantLock的可重入性。每次重复调用lock都需要记住重入次数,每次成功释放锁时,重入次数减1,直到次数为0,锁才会被完全释放。 与synchronized相比,ReentrantLock提供了更多的高级功能。例如,可以选择公平或非公平版本的锁,可以实现tryLock方法来尝试获取锁而不会阻塞线程,可以使用lockInterruptibly方法允许线程在等待时可以被中断等等。 总之,ReentrantLock通过灵活的接口和可重入特性,提供了一种强大的同步机制,使多个线程可以安全地访问共享资源,并且具有更大的灵活性和扩展性。它在并发编程中的应用非常广泛。 ### 回答3: ReentrantLock是一种与synchronized关键字相似的线程同步工具。与synchronized相比,ReentrantLock提供了更灵活的锁操作,在并发环境中能更好地控制线程的互斥访问。 ReentrantLock原理主要包含以下几个方面: 1. 线程控制:ReentrantLock内部维护了一个线程的等待队列,每个线程通过调用lock()方法来竞争锁资源。当一个线程成功获取到锁资源时,其他线程会被阻塞在等待队列中,直到锁被释放。 2. 重入性:ReentrantLock允许同一个线程多次获取锁资源,而不会发生死锁。这种机制称为重入性。在线程第一次获取到锁资源后,锁的计数器会加1,当该线程再次获取锁时,计数器会再次加1。而在释放锁时,计数器会递减。只有当计数器减为0时,表示锁已完全释放。 3. 公平性和非公平性:ReentrantLock可以根据需要选择公平锁或非公平锁。在公平锁模式下,等待时间最久的线程会优先获取到锁资源。而在非公平锁模式下,锁资源会被直接分配给新到来的竞争线程,不考虑等待时间。 4. 条件变量:ReentrantLock提供了Condition接口,可以创建多个条件变量,用于对线程的等待和唤醒进行管理。与传统的wait()和notify()方法相比,Condition提供了更加灵活的等待和通知机制,可以更加精确地控制线程的流程。 总的来说,ReentrantLock是通过使用等待队列、重入性、公平性和非公平性、条件变量等机制,来实现线程的互斥访问和同步。它的灵活性和粒度更高,可以更好地适应各种复杂的并发场景。但由于使用ReentrantLock需要手动进行锁的获取和释放,使用不当可能会产生死锁等问题,因此在使用时需要仔细思考和设计。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值