Condition无法指定唤醒线程,Condition本身是无法控制线程执行流程的

Condition 可以指定唤醒线程?

先说说问题的起源,最近在网上看到一个网友提问,说是看到了一个教学视频,讲到了线程的执行顺序问题,有一个疑惑,先将代码贴出来。

/**
 * @describe: 测试
 * @author: sunlight
 * @date: 2021/7/31 11:24
 */
public class Test {
    public static void main(String[] args) {
        Loop loop = new Loop();
        new Thread(() -> {
            loop.loopA();
        }, "a").start();

        new Thread(() -> {
            loop.loopB();
        }, "b").start();

        new Thread(() -> {
            loop.loopC();
        }, "c").start();
    }
}

/**
 * 定义资源类
 */
class Loop {
    /**
     * 线程执行顺序标记,1:表示loopA执行,2:表示loopB执行,3:表示loopC执行
     */
    private volatile int number = 1;

    /**
     * 获得lock锁
     */
    private Lock lock = new ReentrantLock();

    /**
     * 创建condition对象用来await(阻塞)和signal(唤醒)线程A
     */
    private Condition c1 = lock.newCondition();

    /**
     * 创建condition对象用来await(阻塞)和signal(唤醒)线程B
     */
    private Condition c2 = lock.newCondition();

    /**
     * 创建condition对象用来await(阻塞)和signal(唤醒)线程C
     */
    private Condition c3 = lock.newCondition();

    protected void loopA() {
        //上锁
        lock.lock();
        try {
            //如果不是第一个标志位,就阻塞,为了解决虚假唤醒问题,使用while关键字
            while (number != 1) {
                try {
                    //阻塞类似wait()
                    c1.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ":A");
            //将标记改成2
            number = 2;
            //唤醒第二个线程,类似notify()方法
            c2.signal();
        } finally {
            lock.unlock();//解锁
        }
    }

    protected void loopB() {
        //上锁
        lock.lock();
        try {
            //如果不是第二个标志位,就阻塞,为了解决虚假唤醒问题,使用while关键字
            while (number != 2) {
                try {
                    //阻塞类似wait()
                    c2.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ":B");
            //将标记改成3
            number = 3;
            //唤醒第三个线程,类似notify()方法
            c3.signal();
        } finally {
            lock.unlock();//解锁
        }
    }

    protected void loopC() {
        //上锁
        lock.lock();
        try {
            //如果不是第二个标志位,就阻塞,为了解决虚假唤醒问题,使用while关键字
            while (number != 3) {
                try {
                    //阻塞类似wait()
                    c3.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ":C");
            //将标记改成1
            number = 1;
            //唤醒第一个线程,类似notify()方法
            c1.signal();
        } finally {
            lock.unlock();//解锁
        }
    }
}

毫无疑问,程序的执行结果肯定是,不管重复多少次也是一样的。
a:A
b:B
c:C

网友的疑惑点在于以下代码:

//唤醒第二个线程,类似notify()方法
c2.signal();

为什么 c2.signal();可以唤醒第二个线程呢?线程和Condition是如何绑定的呢?

问题分析

要回答这个问题,首先要确定两个条件:

  • condition是否有控制指定线程的能力?
  • condition是什么时候和指定线程绑定的?

首先看第一个问题,condition有控制指定线程的能力吗?答案显然是否定的,因为线程的执行是由操作系统来调度的,程序无法指定线程的执行。再看第二个问题,condition有和线程绑定吗?答案也是否定的,从整个程序来看,condition对象和线程之间唯一的关系就是condition的数字和线程的字母具有连贯性,我们假设condition有唤醒指定线程的能力,那c2为什么是唤醒线程B,就因为一个是数字2,一个是字母2?从java序号都是从0开始来看,不应该是c0唤醒线程a,c1唤醒线程b吗,在我们没有定义c0的情况下,岂不是要空指针?

问题验证

为了验证上面的猜想,我们先对程序做一个修改,就是将condition条件改成一个,其他地方不变,如下所示:

/**
 * @describe: 测试只有一个条件
 * @author: sunlight
 * @date: 2021/7/31 11:24
 */
public class Test2 {
    public static void main(String[] args) {
        Loop2 loop = new Loop2();
        new Thread(() -> {
            loop.loopA();
        }, "a").start();

        new Thread(() -> {
            loop.loopB();
        }, "b").start();

        new Thread(() -> {
            loop.loopC();
        }, "c").start();

    }
}

/**
 * 定义资源类
 */
class Loop2 {
    /**
     * 线程执行顺序标记,1:表示loopA执行,2:表示loopB执行,3:表示loopC执行
     */
    private volatile int number = 1;

    /**
     * 获得lock锁
     */
    private Lock lock = new ReentrantLock();

    /**
     * 创建condition对象用来await(阻塞)和signal(唤醒)线程A
     */
    private Condition c = lock.newCondition();


    protected void loopA() {
        //上锁
        lock.lock();
        try {
            //如果不是第一个标志位,就阻塞,为了解决虚假唤醒问题,使用while关键字
            while (number != 1) {
                try {
                    //阻塞类似wait()
                    c.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ":A");
            //将标记改成2
            number = 2;
            //唤醒第二个线程,类似notify()方法
            c.signal();
        } finally {
            lock.unlock();//解锁
        }
    }

    protected void loopB() {
        //上锁
        lock.lock();
        try {
            //如果不是第二个标志位,就阻塞,为了解决虚假唤醒问题,使用while关键字
            while (number != 2) {
                try {
                    //阻塞类似wait()
                    c.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ":B");
            //将标记改成3
            number = 3;
            //唤醒第三个线程,类似notify()方法
            c.signal();
        } finally {
            lock.unlock();//解锁
        }
    }

    protected void loopC() {
        //上锁
        lock.lock();
        try {
            //如果不是第二个标志位,就阻塞,为了解决虚假唤醒问题,使用while关键字
            while (number != 3) {
                try {
                    //阻塞类似wait()
                    c.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ":C");
            //将标记改成1
            number = 1;
            //唤醒第一个线程,类似notify()方法
            c.signal();
        } finally {
            lock.unlock();//解锁
        }
    }
}

显然,执行结果依然是正确的。

然后我们再来做第二个验证,我们还是拿第一次的代码,我们调整一下线程的启动顺序,且做一个打印,代码如下:


/**
 * @describe:  测试
 * @author: sunlight
 * @date: 2021/7/31 11:24
 */
public class Test {
    public static void main(String[] args) {
        Loop loop = new Loop();
        new Thread(() -> {
            loop.loopB();
        }, "b").start();
        new Thread(() -> {
            loop.loopA();
        }, "a").start();
        new Thread(() -> {
            loop.loopC();
        }, "c").start();
    }
}

/**
 * 定义资源类
 */
class Loop {
    /**
     * 线程执行顺序标记,1:表示loopA执行,2:表示loopB执行,3:表示loopC执行
     */
    private volatile int number = 1;

    /**
     * 获得lock锁
     */
    private Lock lock = new ReentrantLock();

    /**
     * 创建condition对象用来await(阻塞)和signal(唤醒)线程A
     */
    private Condition c1 = lock.newCondition();

    /**
     * 创建condition对象用来await(阻塞)和signal(唤醒)线程B
     */
    private Condition c2 = lock.newCondition();

    /**
     * 创建condition对象用来await(阻塞)和signal(唤醒)线程C
     */
    private Condition c3 = lock.newCondition();

    protected void loopA() {
        //上锁
        lock.lock();
        try {
            System.out.println("我是线程a,当前标记为:" + number);
            //如果不是第一个标志位,就阻塞,为了解决虚假唤醒问题,使用while关键字
            while (number != 1) {
                try {
                    //阻塞类似wait()
                    c1.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ":A");
            //将标记改成2
            number = 2;
            //唤醒第二个线程,类似notify()方法
            c2.signal();
        } finally {
            lock.unlock();//解锁
        }
    }

    protected void loopB() {
        //上锁
        lock.lock();
        try {
            System.out.println("我是线程b,当前标记为:" + number);
            //如果不是第二个标志位,就阻塞,为了解决虚假唤醒问题,使用while关键字
            while (number != 2) {
                try {
                    //阻塞类似wait()
                    c2.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ":B");
            //将标记改成3
            number = 3;
            //唤醒第三个线程,类似notify()方法
            c3.signal();
        } finally {
            lock.unlock();//解锁
        }
    }

    protected void loopC() {
        //上锁
        lock.lock();
        try {
            System.out.println("我是线程c,当前标记为:" + number);
            //如果不是第二个标志位,就阻塞,为了解决虚假唤醒问题,使用while关键字
            while (number != 3) {
                try {
                    //阻塞类似wait()
                    c3.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ":C");
            //将标记改成1
            number = 1;
            //唤醒第一个线程,类似notify()方法
            c1.signal();
        } finally {
            lock.unlock();//解锁
        }
    }
}

查看执行结果:

我是线程b,当前标记为:1
我是线程a,当前标记为:1
a:A
我是线程c,当前标记为:2
b:B
c:C

显然,先是执行了线程b,然后被等待,然后唤醒了a且执行完毕,然后被唤醒的是线程c,又被等待,然后是线程b被唤醒继续执行,执行完后唤醒其他线程,再然后c被唤醒再次执行,程序结束。由此可见,condition唤醒的线程完全具有随机性,也就是说,整个程序中,控制顺序的其实是标记number,condition只是起到了阻塞和唤醒线程的作用,没有控制顺序的作用。

网上或者教学资料为什么这么写?

个人觉得,两个原因:

  • 真正懂condition作用的,之所以要设置三个变量c1c2和c3,是为了让程序看起来更直观,更便于阅读,第二个测试代码显然没有第一个源代码阅读性强。
  • 没有真正懂condition的作用,只是看jdk文档中demo用了两个condition,就错误的以为condition可以指定线程,没有深入的思考,其实问题分析中的第二个点很容易就想到condition不可能和线程顺序有关系的。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值