细看一下AQS中Condition的源码

单刀直入,今天就是想说一下ConditionObject源码,闲话不多说。ConditionObject这个单词看着可能有点陌生,不过Condition你总熟悉了吧?在ReentrantLock中,一般都是使用condition.await(),condition.signal()来实现锁的等待唤醒机制。

先看一个简单的例子吧,直接看源码可能有点头痛的。

public class WaitTest {

    private static Lock lock = new ReentrantLock();

    private static Condition condition = lock.newCondition();

    public static void main(String[] args) {

        // 线程一
        Thread thread1 = new Thread(() -> {
            try {
                lock.lock();
                //to do sth
                System.out.println("线程一开始");
                condition.await();
                System.out.println("线程一等待后继续运行");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "Thread1");
        //线程二
        Thread thread2 = new Thread(() -> {
            try {
                //线程二等待下,让线程一先开始
                Thread.sleep(20);
                lock.lock();
                //获取锁,再等待,线程一已经进入等待队列,只能等线程二
                Thread.sleep(200);
                //to do sth
                System.out.println("线程二开始");
                //唤醒了线程一,但是没有释放锁,即使释放锁了,线程一抢占时间片也需要时间,未必有线程二先执行完
                condition.signal();
                System.out.println("线程二等待后继续运行");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "Thread2");

        thread1.start();
        thread2.start();
    }
}

看一下执行结果:

线程一开始
线程二开始
线程二等待后继续运行
线程一等待后继续运行

我们来简单的看下这个程序,一个主函数,创建了两个异步线程,使用独占锁ReentrantLock,同时使用了ReentrantLock中的condition等待唤醒机制。

首先,是线程一、线程二抢占锁,当然,线程二睡眠了一小会,相当于让出了锁的使用权,只能在同步队列中阻塞了。

接着,线程一等待,进入了等待队列,让出了lock的所有权。

线程二抢占到锁,开始向下执行。

线程二接着调用了condition.signal(),唤醒了等待队列中的线程一,线程一从等待队列进入了同步队列,然并卵,锁还让线程二占着呢!线程一只能继续等待。

当然,哪怕线程二释放了锁的控制权,就是在condition.signal()之后,执行 lock.unlock(),线程二也会比线程一先执行完,毕竟线程一移动到同步队列,抢占锁都需要时间的,而线程二直接就运行over了。

另外需要提一下的就是,调用condition.signal()之后,并不是说唤醒的线程就能执行了!只是将线程放入到了同步队列,只是相当与就绪而已。这个时候,线程还需要获取到锁的控制权,也就是要最终分配到时间片才能执行,这个需要大家注意下,java中的线程没有唤醒之后就立即能执行的哦!

关于condition,我们要弄明白的有2步:

1、释放锁,进入等待队列

2、唤醒锁,移出等待队列,重新将线程移动到同步队列

下面来看一下源码:

 /* lock 锁等待方法 */ 
 public final void await() throws InterruptedException {
			//线程是中断的,没法等待,抛异常
            if (Thread.interrupted())
                throw new InterruptedException();
			// 新建一个节点,并添加到等待队列中,这个时候是占有lock锁的
            Node node = addConditionWaiter();
			// 释放当前线程占有的lock
            int savedState = fullyRelease(node);
            int interruptMode = 0;
			//是否在同步队列中
            while (!isOnSyncQueue(node)) {
				//阻塞当前
                LockSupport.park(this);
				//checkInterruptWhileWaiting(node)) 不等于0,跳出,这个是唤醒之后的操作。
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
			// 自旋等待获取到同步状态(即获取到lock)
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }
		

上面这段代码是await等待的主要逻辑,下面来说一下lock中的等待唤醒的主要机制。首先,我们要知道,把线程节点移动到等待队列,那么这个时候,这个线程肯定是持有锁的。

我们先来看第一步,代码都有解释,我来大概说下,首先,当前线程是持有锁的,当前线程新建一个node节点,添加到等待队列的尾部,这个等待队列是FIFO的等待队列,加入队列完成后,就释放掉锁,结束对资源的占用。但是这个时候,这个线程并没有直接阻塞,而是继续运行,但是已经释放锁住的资源了哦。 我们可以看到 fullyRelease(node);下面仍然有代码在执行。

接着就是执行了一个while循环,既然是while循环,那肯定会跳出去,总不可能无限循环吧?首先,while条件如果是false的话,那么自然不会再执行了,还有就是这个循环体中有个标志 ‘break’,如果执行了break,自然也会跳出去!

第一次判断,是否在同步队列中(!isOnSyncQueue(node)),肯定不在啊,刚移出来,在啥子哟?然后就进入循环体, 执行LockSupport.park(this); ,然后就卡这了, 这个时候,第一步算是彻底完成~

我们来瞅瞅里面的部分源码。

the first!添加一个新的节点到等待队列。

  private Node addConditionWaiter() {
			//将lastWaiter赋值给t
            Node t = lastWaiter;
            // 节点在等待队列上,但是状态又不是Node.CONDITION,那状态就是CANCELLED(1)了,
			// 已经取消调度,直接可以清除了
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
			//根据当前进程,新建一个节点。
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }
		

这个没啥难度,就是在等待队列上加了一个节点,我相信大伙都看得懂,(*^▽^*)。里面的一个调用我也贴下吧

	 /* 取消已取消的服务程序节点 */
   private void unlinkCancelledWaiters() {
            Node t = firstWaiter;
            Node trail = null;
			//首节点不为空,进入循环,就是为了干掉所有滴
            while (t != null) {
                Node next = t.nextWaiter;
                if (t.waitStatus != Node.CONDITION) {
                    t.nextWaiter = null;
                    if (trail == null)
                        firstWaiter = next;
                    else
                        trail.nextWaiter = next;
                    if (next == null)
                        lastWaiter = trail;
                }
                else
                    trail = t;
                t = next;
            }
        }

下面就是比较关键的一段代码了,释放锁

 final int fullyRelease(Node node) {
		 //失败标志
        boolean failed = true;
        try {
			// 获取当前线程的state
            int savedState = getState();
			//释放锁,调用的lock.release
            if (release(savedState)) {
				// 释放成功,返回state
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
			// 释放失败,那就直接让这个线程节点跪了呗。
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

这段代码就是让出了锁的控制权,和lock中释放锁的代码有点类似。首先是获取线程state,然后是release,在执行完release之后,其实,锁的控制权就已经让出去了,后面的代码都是当前线程的一些处理罢了,返回之后,最终也是会执行到LockSupport.park(this);阻塞当前线程。

这样我们基本能知道await的一个流程:建立等待节点---》进入等待队列---》释放锁---》阻塞等待

下面我们来说第2步,signal——唤醒。

要唤醒线程,我们要明确两点:第一,当前线程必须拿到锁,然后才可以唤醒。第二,唤醒不能指定,等待队列是FIFO,只能唤醒最先进入等待队列的那个线程。

好了,我们类瞅一瞅代码。

	// 唤醒最早进入等待队列的线程
	 public final void signal() {
			// 是否拥有锁,没有就抛出异常
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
			// 获取第一个节点
            Node first = firstWaiter;
            if (first != null)
				// 不为空就唤醒
                doSignal(first);
        }
	
	   // 是否独占锁,公平锁的实现方式
	   protected final boolean isHeldExclusively() {
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

这上面的源码没啥绕的,唤醒的操作都在 doSignal(first); 这个操作里,我们进去看下。

	 // 唤醒等待线程
	  private void doSignal(Node first) {
            do {
				// 等待队列首节点干掉
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
				
            }
			//成功就加入到同步队列,结束循环,否则就继续找下一个节点
			while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }
		
 
    final boolean transferForSignal(Node node) {
        // 重新设置waitStatus,设置失败,说明已经cancel了,会在循环中干掉
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        //下面的代码是不是很熟悉?嘿嘿,没错,就是加入到同步队列中去的操作
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

我们来看下doSignal这段代码,首先,拿到首节点firstWaiter,把这个节点从等待队列中移除掉。然后进行transferForSignal操作,如果状态更新成功,就移动到同步队列中去,结束循环,如果这个节点是已经cancel的,那么继续找下一个节点,直到唤醒等待队列中还存活的第一个节点。

那唤醒所有呢?其实很简单上面不是唤醒一个就结束跳出循环么?我不跳出循环了,一直唤醒不就行了?\(^o^)/~,show me~

 // 唤醒所有的等待队列
	  private void doSignalAll(Node first) {
            lastWaiter = firstWaiter = null;
            do {
                Node next = first.nextWaiter;
                first.nextWaiter = null;
				//移出等待队列,添加到同步队列
                transferForSignal(first);
                first = next;
            } 
			//首节点不为空就一直循环
			while (first != null);
        }

核心代码就这几行,是不是so easy?

总结来说,就是线程持有锁----》拿到等待队列首节点----》移出等待队列----》添加到同步队列

思路并不是很难理解,代码实现的话,我都贴出来了,如果哪里有错误,大家可以在评论中指出哈~

另外,如果直接看源码比较难受的话,大家可以试着debug下我上面的那个测试main方法,主要的逻辑都能走到哦~

No sacrifice,no victory~

猜你喜欢

转载自blog.csdn.net/zsah2011/article/details/111665592