面试题:两个线程交替打印1~100

一、通过对象锁及Wait / Notify方式实现

核心要点:

  • 定义一个对象锁lock,通过synchronized关键字来保证它的访问。
  • 交互逻辑是:
    • 拿到锁之后打印,并自增;
    • 唤醒另外一个线程来处理,自己释放锁。
  • 线程的正常退出。不能让每个线程都处在循环的逻辑中。

代码实现:

public class AlternatelyPrint {

    @SneakyThrows
    public static void main(String[] args) {
        waitNotify();
    }

    @SneakyThrows
    @SuppressWarnings("all")
    private static void waitNotify() {
        Thread wOdd = new Thread(new WaitNotifyImpl(), "奇数线程");
        Thread wEven = new Thread(new WaitNotifyImpl(), "偶数线程");

        // 只能外部操作,来控制两个线程的顺序
        wOdd.start();
        TimeUnit.MILLISECONDS.sleep(3L);
        wEven.start();
    }
}

/**
 * 对象通知模型,达到交替输出的效果
 */
class WaitNotifyImpl implements Runnable {

    private static Integer count = 1;
    /**
     * 1. 终止的条件
     * 2. 由于是不变的final对象,所以同时将它当作锁
     */
    private static final Integer NUM = 100;

    @Override
    public void run() {
        while (count <= NUM) {
            synchronized (NUM) {
                System.out.println(Thread.currentThread().getName() + " print count = " + count++);
                NUM.notify();
                // 该条件,保证线程能够正常结束
                if (count <= NUM) {
                    try {
                        NUM.wait();
                    } catch (InterruptedException ignored) {
                    }
                }
            }
        }
    }
}
  • 进入等待状态有一个判断count和NUM的逻辑。去掉后会导致两个线程都无法正常结束。

二、通过Exchanger来实现

概念解读

Exchanger是两个线程进行交互时数据传递的工具。交换数据依赖于其exchange方法:

public class Exchanger<V> {

    @SuppressWarnings("unchecked")
    public V exchange(V x) throws InterruptedException {
        // 省略具体实现
    } 
}

  • 入参是当前线程要进行交换的数据。
  • 返回值是另一个线程返回过来的数据。

从入参和返回值就可以知道,当前线程执行exhcange方法的时候,必须要等到另外一个线程也执行到这个方法,交换数据之后,各自的线程才会继续执行下去
换言之,这是一个阻塞的方法。

分析设计

1) 数据传递的过程骨架

从上面入参和返回值可以知道,exchange是一个双向交换数据的设计。

而我们这里要实现的是一个交替打印递增序列的问题。这里其实是只需要单向传递的过程,所以其中有一方传递的值始终是null。

交换次数 线程A 线程B
1 null
null 2
3 null
... ... ...
九十九 99 null
一百 null 100

到这里基本的过程骨架就出来了。

2) 自增操作由谁负责?

然后就是自增和传递两个过程的先后,我们有两种方案:

  1. 生产者负责自增。拿到数据,先消费(打印),而后自增,再传递出去。
  2. 消费者负责自增。拿到数据,先自增,再消费(打印)它,然后再传递出去。

显然这不是重点,都能做到,所以我们直接采用的是第一种方案。

3) 线程正常结束

按照这个思路,代码其实是可以运行实现了,但不能让两个线程都一直跑着占用资源吧。

结合上面的表格看,问题的本质是,执行到100次的时候,如何让两个线程都决定不再继续和对方进行数据交换:

  • 对于线程A(奇数线程)来说,它当前待传出的值是null,无法通过当前的值来判断应该结束。所以在这里,我们需要加一个变量,保存了上一次收到的值
  • 对于线程B(偶数线程)来说,它当前待传出的值是100,可以判断出应该结束 。

4) 最终代码实现

public class AlternatelyPrint {

    @SneakyThrows
    public static void main(String[] args) {
        exchanger();
    }

    @SneakyThrows
    @SuppressWarnings("all")
    private static void exchanger() {
        Exchanger<Integer> numExchanger = new Exchanger<>();
        // 通过给的值来
        Thread odd = new Thread(new ExchangerImpl(numExchanger, 1), "odd");
        Thread even = new Thread(new ExchangerImpl(numExchanger, null), "even");
        odd.start();
        TimeUnit.MILLISECONDS.sleep(3L);
        even.start();
    }
}

/**
 * Exchanger是两个线程交换空间,执行到exchange方法时候:<p>&emsp;
 * 交出数据的线程会被阻塞,直到另一个线程与它进行交换。
 */
class ExchangerImpl implements Runnable {

    private final Exchanger<Integer> numExchanger;

    /**
     * 当次待操作的值
     */
    private Integer numLocal;

    /**
     * 缓存上一次操作的数据,用于判断结束条件
     */
    private Integer lastReceived;

    private static final int NUM = 100;

    public ExchangerImpl(Exchanger<Integer> numExchanger, Integer numLocal) {
        this.numExchanger = numExchanger;
        this.numLocal = numLocal;
    }

    @Override
    @SneakyThrows
    public void run() {
        // 1. 如果一个线程本地的数据是null, 且小于,则直接递出去
        // 2. 如果本地的不是空值,则打印并自增,再递出去
        while (this.numLocal == null || this.numLocal <= NUM) {
            // 处理odd线程的退出
            boolean oddOver = this.lastReceived != null && this.lastReceived == NUM;
            if (oddOver) {
                break;
            }
            if (this.numLocal != null) {
                System.out.println(Thread.currentThread().getName() + " print count = " + numLocal++);
                // 处理even线程的退出
                if (this.numLocal == NUM + 1) {
                    break;
                }
            }
            // 最后保存时候,一定要先将上次的值做缓存
            this.lastReceived = this.numLocal;
            this.numLocal = numExchanger.exchange(this.numLocal);
        }
    }
}

可以细看一下:

  1. 增加一个变量保存上次收到的值,每次要替换当前值的时候,将当前值缓存到该字段。
  2. 对于奇数线程和偶数线程的不同判断结束的方式。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 二十一世纪旧体诗词风骚榜[https://www.jianshu.com/c/da968ae2d498] 上榜絮语...
    张成昱阅读 3,767评论 3 29
  • ↑↑ 今天起了一个非常任性俗气的标题 ↑ ↑ 昨天的情绪非常非常糟,夸张的说法,是存于 “生死边缘” ,后来因为自...
    简忞阅读 1,297评论 0 1
  • 你的孩子心气儿怎样? 无论大人孩子,各种各样的脾性。特别是孩子,呈现的状态不一,未来他(她)们也就大不相同。 有的...
    AI上善若水厚德载物JFB阅读 1,355评论 0 0
  • 人在体验犯错中成长 用人以长天下无无用之人,用人以短天下无可用之人 做一件事,就做好可以不断的培育自信,培育自己用...
    莫忘小寒阅读 1,751评论 0 2
  • 10-24# ①在做临床相关性分析时,遇到这个报错 检查输入文件,发现临床信息是字符型数据 而由于此处做了T检验,...
    生信小萌神阅读 1,517评论 0 0

友情链接更多精彩内容