看山聊并发:面试实战之多线程顺序打印

简介: 这个问题考察的是多线程协同顺序执行。也就是第一个线程最先达到执行条件,开始执行,执行完之后,第二个线程达到执行条件,开始执行,以此类推。可以想到的是,通过状态位来表示线程执行的条件,多个线程自旋等待状态位变化。

image.png

你好,我是看山。


来个面试题,让大家练练手。这个题在阿里和小米都被问过,所以放在这个抛砖引玉,期望能够得到一个更佳的答案。


实现 3 个线程 A、B、C,A 线程持续打印“A”,B 线程持续打印“B”,C 线程持续打印“C”,启动顺序是线程 C、线程 B、线程 A,打印的结果是:ABC。


解法一:状态位变量控制

这个问题考察的是多线程协同顺序执行。也就是第一个线程最先达到执行条件,开始执行,执行完之后,第二个线程达到执行条件,开始执行,以此类推。可以想到的是,通过状态位来表示线程执行的条件,多个线程自旋等待状态位变化。


线上代码:


import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class ABCThread {
    private static final Lock lock = new ReentrantLock();
    private static volatile int state = 0;
    private static final Thread threadA = new Thread(() -> {
        while (true) {
            lock.lock();
            try {
                if (state % 3 == 0) {
                    System.out.println("A");
                    state++;
                    break;
                } else {
                    System.out.println("A thread & state = " + state);
                }
            } finally {
                lock.unlock();
            }
        }
    });
    private static final Thread threadB = new Thread(() -> {
        while (true) {
            lock.lock();
            try {
                if (state % 3 == 1) {
                    System.out.println("B");
                    state++;
                    break;
                } else {
                    System.out.println("B thread & state = " + state);
                }
            } finally {
                lock.unlock();
            }
        }
    });
    private static final Thread threadC = new Thread(() -> {
        while (true) {
            lock.lock();
            try {
                if (state % 3 == 2) {
                    System.out.println("C");
                    state++;
                    break;
                } else {
                    System.out.println("C thread & state = " + state);
                }
            } finally {
                lock.unlock();
            }
        }
    });
    public static void main(String[] args) {
        threadC.start();
        threadB.start();
        threadA.start();
    }
}

可以看到,状态位state使用volatile修饰,是希望一个线程修改状态位值之后,其他线程可以读取到刚修改的数据,这个属于 Java 内存模型的范围,后续会有单独的章节描述。


这个可以解题,但是却有很多性能上的损耗。因为每个进程都在自旋检查状态值state是否符合条件,而且自旋过程中会有获取锁的过程,代码中在不符合条件时打印了一些内容,比如:System.out.println("A thread & state = " + state);,我们可以运行一下看看结果:


C thread & state = 0
...67行
C thread & state = 0
B thread & state = 0
...43行
B thread & state = 0
A
C thread & state = 1
...53行
C thread & state = 1
B
C

可以看到,在A线程获取到锁之前,C线程和B线程自旋了100多次,然后A线程才获取机会获取锁和打印。然后在B线程获取锁之前,C线程又自旋了53次。性能损耗可见一斑。


解法二:Condition实现条件判断

既然无条件自旋浪费性能,那就加上条件自旋。


代码如下:


import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class ABCThread2 {
    private static final Lock lock = new ReentrantLock();
    private static volatile int state = 0;
    private static final Condition conditionA = lock.newCondition();
    private static final Condition conditionB = lock.newCondition();
    private static final Condition conditionC = lock.newCondition();
    private static final Thread threadA = new Thread(() -> {
        while (true) {
            lock.lock();
            try {
                while(state % 3 != 0) {
                    System.out.println("A await start");
                    conditionA.await();
                    System.out.println("A await end");
                }
                System.out.println("A");
                state++;
                conditionB.signal();
                break;
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    });
    private static final Thread threadB = new Thread(() -> {
        while (true) {
            lock.lock();
            try {
                while(state % 3 != 1) {
                    System.out.println("B await start");
                    conditionB.await();
                    System.out.println("B await end");
                }
                System.out.println("B");
                state++;
                conditionC.signal();
                break;
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    });
    private static final Thread threadC = new Thread(() -> {
        while (true) {
            lock.lock();
            try {
                while(state % 3 != 2) {
                    System.out.println("C await start");
                    conditionC.await();
                    System.out.println("C await end");
                }
                System.out.println("C");
                state++;
                break;
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    });
    public static void main(String[] args) {
        threadC.start();
        threadB.start();
        threadA.start();
    }
}

通过Lock锁的Condition实现有条件自旋,运行结果如下:


C await start
B await start
A
B await end
B
C await end
C

可以从运行结果看到,C线程发现自己不符合要求,就通过conditionC.await();释放锁,然后等待条件被唤醒后重新获得锁。然后是B线程,最后是A线程开始执行,发现符合条件,直接运行,然后唤醒B线程的锁条件,依次类推。这种方式其实和信号量很类似。


解法三:信号量

先上代码:


import java.util.concurrent.Semaphore;
class ABCThread3 {
    private static Semaphore semaphoreA = new Semaphore(1);
    private static Semaphore semaphoreB = new Semaphore(1);
    private static Semaphore semaphoreC = new Semaphore(1);
    private static final Thread threadA = new Thread(() -> {
        try {
            semaphoreA.acquire();
            System.out.println("A");
            semaphoreB.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    private static final Thread threadB = new Thread(() -> {
        try {
            semaphoreB.acquire();
            System.out.println("B");
            semaphoreC.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    private static final Thread threadC = new Thread(() -> {
        try {
            semaphoreC.acquire();
            System.out.println("C");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    public static void main(String[] args) throws InterruptedException {
        semaphoreB.acquire();
        semaphoreC.acquire();
        threadC.start();
        threadB.start();
        threadA.start();
    }
}

代码中执行前先执行了semaphoreB.acquire();和semaphoreC.acquire();,是为了将B和C的信号释放,这个时候,就能够阻塞B线程、C线程中信号量的获取,直到顺序获取了信号值。


文末总结

这个题是考察大家对线程执行顺序和线程之间协同的理解,文中所实现的三种方式,都能解题,只不过代码复杂度和性能有差异。因为其中涉及很多多线程的内容,后续会单独开文说明每个知识点。


推荐阅读

Java 并发基础(一):synchronized 锁同步

Java 并发基础(二):主线程等待子线程结束

Java 并发基础(三):再谈 CountDownLatch

Java 并发基础(四):再谈 CyclicBarrier

Java 并发基础(五):面试实战之多线程顺序打印


目录
相关文章
|
14天前
|
数据采集 消息中间件 监控
Flume数据采集系统设计与配置实战:面试经验与必备知识点解析
【4月更文挑战第9天】本文深入探讨Apache Flume的数据采集系统设计,涵盖Flume Agent、Source、Channel、Sink的核心概念及其配置实战。通过实例展示了文件日志收集、网络数据接收、命令行实时数据捕获等场景。此外,还讨论了Flume与同类工具的对比、实际项目挑战及解决方案,以及未来发展趋势。提供配置示例帮助理解Flume在数据集成、日志收集中的应用,为面试准备提供扎实的理论与实践支持。
25 1
|
20天前
|
设计模式 安全 Java
Java并发编程实战:使用synchronized关键字实现线程安全
【4月更文挑战第6天】Java中的`synchronized`关键字用于处理多线程并发,确保共享资源的线程安全。它可以修饰方法或代码块,实现互斥访问。当用于方法时,锁定对象实例或类对象;用于代码块时,锁定指定对象。过度使用可能导致性能问题,应注意避免锁持有时间过长、死锁,并考虑使用`java.util.concurrent`包中的高级工具。正确理解和使用`synchronized`是编写线程安全程序的关键。
|
5天前
|
API 数据库 数据安全/隐私保护
Flask框架在Python面试中的应用与实战
【4月更文挑战第18天】Django REST framework (DRF) 是用于构建Web API的强力工具,尤其适合Django应用。本文深入讨论DRF面试常见问题,包括视图、序列化、路由、权限控制、分页过滤排序及错误处理。同时,强调了易错点如序列化器验证、权限认证配置、API版本管理、性能优化和响应格式统一,并提供实战代码示例。了解这些知识点有助于在Python面试中展现优秀的Web服务开发能力。
22 1
|
3天前
|
安全 Java 调度
Java线程:深入理解与实战应用
Java线程:深入理解与实战应用
20 0
|
1天前
|
消息中间件 缓存 NoSQL
Java多线程实战-CompletableFuture异步编程优化查询接口响应速度
Java多线程实战-CompletableFuture异步编程优化查询接口响应速度
|
1天前
|
数据采集 存储 Java
高德地图爬虫实践:Java多线程并发处理策略
高德地图爬虫实践:Java多线程并发处理策略
|
2天前
|
Java 调度
Java面试必考题之线程的生命周期,结合源码,透彻讲解!
Java面试必考题之线程的生命周期,结合源码,透彻讲解!
28 1
|
2天前
|
Java
面试官让说出8种创建线程的方式,我只说了4种,然后挂了。。。
面试官让说出8种创建线程的方式,我只说了4种,然后挂了。。。
6 1
|
5天前
|
SQL 中间件 API
Flask框架在Python面试中的应用与实战
【4月更文挑战第18天】**Flask是Python的轻量级Web框架,以其简洁API和强大扩展性受欢迎。本文深入探讨了面试中关于Flask的常见问题,包括路由、Jinja2模板、数据库操作、中间件和错误处理。同时,提到了易错点,如路由冲突、模板安全、SQL注入,以及请求上下文管理。通过实例代码展示了如何创建和管理数据库、使用表单以及处理请求。掌握这些知识将有助于在面试中展现Flask技能。**
12 1
Flask框架在Python面试中的应用与实战
|
6天前
|
SQL 关系型数据库 MySQL
Python与MySQL数据库交互:面试实战
【4月更文挑战第16天】本文介绍了Python与MySQL交互的面试重点,包括使用`mysql-connector-python`或`pymysql`连接数据库、执行SQL查询、异常处理、防止SQL注入、事务管理和ORM框架。易错点包括忘记关闭连接、忽视异常处理、硬编码SQL、忽略事务及过度依赖低效查询。通过理解这些问题和提供策略,可提升面试表现。
26 6

热门文章

最新文章

相关实验场景

更多