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

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

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 并发基础(五):面试实战之多线程顺序打印


目录
相关文章
|
7天前
|
监控 Kubernetes Java
阿里面试:5000qps访问一个500ms的接口,如何设计线程池的核心线程数、最大线程数? 需要多少台机器?
本文由40岁老架构师尼恩撰写,针对一线互联网企业的高频面试题“如何确定系统的最佳线程数”进行系统化梳理。文章详细介绍了线程池设计的三个核心步骤:理论预估、压测验证和监控调整,并结合实际案例(5000qps、500ms响应时间、4核8G机器)给出具体参数设置建议。此外,还提供了《尼恩Java面试宝典PDF》等资源,帮助读者提升技术能力,顺利通过大厂面试。关注【技术自由圈】公众号,回复“领电子书”获取更多学习资料。
|
2天前
|
算法 安全 Java
Java线程调度揭秘:从算法到策略,让你面试稳赢!
在社招面试中,关于线程调度和同步的相关问题常常让人感到棘手。今天,我们将深入解析Java中的线程调度算法、调度策略,探讨线程调度器、时间分片的工作原理,并带你了解常见的线程同步方法。让我们一起破解这些面试难题,提升你的Java并发编程技能!
43 16
|
8天前
|
缓存 架构师 Java
Maven实战进阶(01)面试官:Maven怎么解决依赖冲突?| 有几种解决方式
本文介绍了Maven的核心功能和依赖管理技巧。Maven是基于项目对象模型(POM)的构建工具,具备跨平台、标准化、自动化等特性。其三大核心功能为依赖管理、仓库管理和项目构建。依赖管理通过pom.xml文件引入第三方组件并自动下载;仓库管理涉及中央仓库、私服和本地仓库;项目构建则通过生命周期管理编译、测试、打包等流程。文章还详细讲解了依赖冲突的解决方法,包括默认规则、手工排除和版本指定等策略。
|
11天前
|
安全 Java 程序员
面试直击:并发编程三要素+线程安全全攻略!
并发编程三要素为原子性、可见性和有序性,确保多线程操作的一致性和安全性。Java 中通过 `synchronized`、`Lock`、`volatile`、原子类和线程安全集合等机制保障线程安全。掌握这些概念和工具,能有效解决并发问题,编写高效稳定的多线程程序。
51 11
|
10天前
|
Java Linux 调度
硬核揭秘:线程与进程的底层原理,面试高分必备!
嘿,大家好!我是小米,29岁的技术爱好者。今天来聊聊线程和进程的区别。进程是操作系统中运行的程序实例,有独立内存空间;线程是进程内的最小执行单元,共享内存。创建进程开销大但更安全,线程轻量高效但易引发数据竞争。面试时可强调:进程是资源分配单位,线程是CPU调度单位。根据不同场景选择合适的并发模型,如高并发用线程池。希望这篇文章能帮你更好地理解并回答面试中的相关问题,祝你早日拿下心仪的offer!
29 6
|
15天前
|
缓存 安全 算法
Java 多线程 面试题
Java 多线程 相关基础面试题
|
1月前
|
并行计算 算法 安全
面试必问的多线程优化技巧与实战
多线程编程是现代软件开发中不可或缺的一部分,特别是在处理高并发场景和优化程序性能时。作为Java开发者,掌握多线程优化技巧不仅能够提升程序的执行效率,还能在面试中脱颖而出。本文将从多线程基础、线程与进程的区别、多线程的优势出发,深入探讨如何避免死锁与竞态条件、线程间的通信机制、线程池的使用优势、线程优化算法与数据结构的选择,以及硬件加速技术。通过多个Java示例,我们将揭示这些技术的底层原理与实现方法。
90 3
|
30天前
|
缓存 安全 Java
【JavaEE】——单例模式引起的多线程安全问题:“饿汉/懒汉”模式,及解决思路和方法(面试高频)
单例模式下,“饿汉模式”,“懒汉模式”,单例模式下引起的线程安全问题,解锁思路和解决方法
|
30天前
|
Java 调度
|
2月前
|
安全 Java
线程安全的艺术:确保并发程序的正确性
在多线程环境中,确保线程安全是编程中的一个核心挑战。线程安全问题可能导致数据不一致、程序崩溃甚至安全漏洞。本文将分享如何确保线程安全,探讨不同的技术策略和最佳实践。
60 6

热门文章

最新文章

相关实验场景

更多