Java Condition接口使用Demo和原理分析

1.Demo

1.1 使用场景

先上一个使用场景:
多个线程之间按照顺序调用,实现ABC三个线程启动,要求如下:

  • AA打印5次,BB打印10次,CC打印15次
  • 紧接着,AA打印5次,BB打印10次,CC打印15次
  • …来10轮

1.2 代码实现

//共享资源类
class ShareResource {
    //A 1  B 2  C 3
    private int number = 1;
    private Lock lock = new ReentrantLock();
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();

    public void print5() {
        lock.lock();
        try {
            //1.判断
            while (number != 1) {
                c1.await();
            }
            //2.干活
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            //通知
            number = 2;
            c2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print10() {
        lock.lock();
        try {
            //1.判断
            while (number != 2) {
                c2.await();
            }
            //2.干活
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            //通知
            number = 3;
            c3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print15() {
        lock.lock();
        try {
            //1.判断
            while (number != 3) {
                c3.await();
            }
            //2.干活
            for (int i = 1; i <= 15; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            //通知
            number = 1;
            c1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

public class SyncAndReentrantLockDemo {
    public static void main(String[] args) {
        ShareResource shareResource = new ShareResource();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                shareResource.print5();
            }
        }, "AA").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                shareResource.print10();
            }
        }, "BB").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                shareResource.print15();
            }
        }, "CC").start();
    }
}

1.3 代码说明

场景关键是要求三个线程是按照ABC的顺序执行,循环10轮,不能乱。
demo中总共有3个线程,A B C,分别调用print5(),print10(),print15()三个方法;
在三个方法中,分别持有3个condition对象,都是先调用wait()方法,等其他线程的signal;然后干完活在使用signal()通知别人干活。
注意每个方法中都判断了number的数值,number初始值是1,所以AA线程在调用print5()方法时,不会进入wait()方法;相反,其他两个线程会进入wait()等待通知。

2.Condtion原理分析

Condtion是个接口,所有方法的实现都在实现类中实现,好在实现类并不多,只有如下两个:
在这里插入图片描述
其实就是AQS类中的ConditionObject内部类(AQS:AbstractQueuedSynchronizer,此类是比较重要的基础方法类,感兴趣可以单独了解,本文只分析Condtion相关的方法,不单独分析AQS中的其他方法);
ConditionObject类中有连个属性:firstWaiter和lastWaiter,说明ConditionObject也是维护了一个condition队列,注意和AQS队列不是同一个队列。 下文中使用condition队列和AQS队列进行区分。
在这里插入图片描述
下面看wait方法源码。

2.1 await()方法

public final void await() throws InterruptedException {
  if (Thread.interrupted())
        throw new InterruptedException();
    //初始化一个等待节点,加入等待队列尾部
    Node node = addConditionWaiter();
    //释放AQS同步队列中的节点,就是释放锁,因为调用await肯定是已经获取到锁的
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    //isOnSyncQueue(node):检查node节点是否在AQS的队列中
    //如果不在,说明还没有被唤醒,没有资格竞争锁,进入沉睡状态
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    //如果已经在AQS同步队列中,acquireQueued方法则进行抢锁
    //acquireQueued中如果抢不到,还会再次进入睡眠状态,等待下一次通知抢锁
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

步骤总结:
1.初始化一个等待节点,加入condition队列尾部,具体看下面addConditionWaiter()源码注释;
2.释放AQS同步队列中的节点,因为之前它肯定是已经拿到锁的;现在都到等待状态了,需要释放之前拿到的锁
3. while (!isOnSyncQueue(node)) 会判断当前线程是否又出现在了AQS队列中,如果没有出现,则继续睡眠等待,如果出现在等待队列中(别的线程调用了signal方法),则进行抢锁;如果抢不到,则继续睡眠。

//初始化一个等待节点,加入等待队列尾部,并返回此节点
private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    //判断当前尾部节点的状态,如果不是Node.CONDITION,说明等待被取消了,那么就从队列中删除尾部节点
    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;
}

2.2 signal()方法

继续看通知方法:

public final void signal() {
//能走到这个方法,说明应该是拿到独占锁的,不然就抛异常
   if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    //拿到condition等待队列中的头节点,不是空的话,说明有人在等,通知此人
    if (first != null)
        doSignal(first);
}
//通知队列中的第一个人
private void doSignal(Node first) {
    do {
        //让等待队列中的第二个节点变成头节点
        //如果第二个节点是null,说明没有尾节点了,赋值null
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

通知的关键方法是transferForSignal(first),继续看此方法:

final boolean transferForSignal(Node node) {
     //将此node的wait状态设置为由Node.CONDITION设置为0,
     //如果失败,说明node的状态不是 Node.CONDITION,可能已经被取消了
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    /*
     * Splice onto queue and try to set waitStatus of predecessor to
     * indicate that thread is (probably) waiting. If cancelled or
     * attempt to set waitStatus fails, wake up to resync (in which
     * case the waitStatus can be transiently and harmlessly wrong).
     */
    //加入AQS的等待队列中
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        //唤醒当前节点,正常情况下不会走到这
        LockSupport.unpark(node.thread);
    return true;
}

步骤总结:通知其实比较简单,分为两步,
1.首先获取到condition队列中的第一个节点,通知此节点
2.通知方法具体动作就是将此节点迁移到AQS队列中去抢锁

以上就是Condition接口的两个核心方法的原理,先总结到这。

发布了62 篇原创文章 · 获赞 29 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/csdn_20150804/article/details/99206924