【面试题】如何用两个线程轮流输出0-200的值

加深对并发编程的理解,如synchronized 、ReentrantLockSemaphore...

1. 使用静态变量flag进行控制

仅通过 boolean flag 控制线程切换,逻辑简单。

java 复制代码
/**
 * 静态原子整型变量,用于在多线程环境下进行原子操作。
 * 该变量初始值为0,通过AtomicInteger类提供的原子方法,可以确保在多线程环境下的线程安全操作。
 */
private static AtomicInteger atomicInteger = new AtomicInteger(0);
/**
 * 静态变量 `flag` 是一个 volatile 修饰的布尔值。
 *
 * 该变量被声明为 `volatile`,意味着它的值对所有线程都是可见的,并且对它的修改会立即反映到主内存中,
 * 从而确保多线程环境下的可见性和一致性。`volatile` 关键字通常用于标记那些可能被多个线程同时访问的变量,
 * 以避免线程间的数据不一致问题。
 *
 * 该变量通常用于控制线程的执行状态,例如作为线程循环的退出条件。
 */
private static volatile boolean flag = true;

public static void main(String[] args) {
    // 创建一个线程,执行一个Lambda表达式
    new Thread(() -> {
        // 当atomicInteger的值小于等于200时,执行循环
        while (atomicInteger.get() <= 200) {
            // 如果flag为true,则输出当前线程的名称和atomicInteger的值,并将flag设置为false
            if (flag) {
                System.out.println("Flag打印偶数---" + Thread.currentThread().getName() + ":" + atomicInteger.getAndIncrement());
                flag = false;
            }
        }
    }).start();

    // 创建另一个线程,执行另一个Lambda表达式
    new Thread(() -> {
        // 当atomicInteger的值小于等于200时,执行循环
        while (atomicInteger.get() <= 200) {
            // 如果flag为false,则输出当前线程的名称和atomicInteger的值,并将flag设置为true
            if (!flag) {
                System.out.println("Flag打印奇数---" + Thread.currentThread().getName() + ":" + atomicInteger.getAndIncrement());
                flag = true;
            }
        }
    }).start();
}

此方法参考AQS的标志位:

相同点

  1. volatile变量保证可见性
    用户代码中的 flag 和 AQS 的 state(volatile int)均通过 volatile 保证线程间变量可见性,确保状态变化能被所有线程及时感知。
  2. 线程协作控制
    两者均通过共享变量(flagstate)控制线程的执行顺序或资源访问权限,实现线程间的协作。

不同点

维度 代码实现 AQS实现
状态管理 仅通过 boolean flag 控制线程切换,逻辑简单。 通过 volatile int state 管理资源状态(如锁计数、信号量值),支持复杂状态(如 getState()setState())。
同步机制 依赖忙等待(自旋循环 while (atomicInteger.get() <= 200)),CPU消耗较高。 结合 CAS(Compare-And-Swap)操作竞争资源,失败线程直接进入阻塞队列(FIFO),通过 LockSupport.park()unpark() 实现高效线程阻塞/唤醒。
线程阻塞与唤醒 无阻塞机制,线程持续轮询 flagatomicInteger 线程竞争失败时进入阻塞队列,避免空转,降低 CPU 开销。唤醒时按队列顺序公平释放资源。
资源争用处理 通过简单条件判断(if (flag))交替执行,可能因竞争导致"漏检"或"误判"。 通过 CAS 原子操作确保资源竞争的无锁化,避免伪共享和竞态条件。
终止条件 依赖 atomicInteger 达到阈值(200)终止循环,但未处理线程安全退出(如可能超限)。 提供 tryAcquiretryRelease 等抽象方法,确保资源释放和线程终止的原子性。
通用性 针对特定场景(双线程交替打印),功能单一。 作为通用框架,支持多种同步器(如锁、信号量、栅栏),可灵活扩展。

核心差异总结:

  • 用户代码 :简单依赖 volatile 和自旋实现线程协作,适合轻量级场景,但存在性能和线程安全风险(如忙等待、可能超限)。
  • AQS:基于队列 + CAS + 阻塞机制,提供高效、可扩展的线程同步方案,适用于复杂并发场景(如锁、信号量等)。

输出结果:

2. 使用 synchronized 结合 wait()/notify()

这是最基础的线程同步方案,通过共享锁对象和内置的等待/通知机制实现交替执行。

实现步骤:

  • 定义一个共享计数器 count 和锁对象(如 Object lock)。
  • 线程A在 count 为偶数时打印并唤醒线程B;线程B在 count 为奇数时打印并唤醒线程A。
  • 每次打印后递增 count,直到达到200。

示例代码:

java 复制代码
/**
 * 静态原子整型变量,用于在多线程环境下进行原子操作。
 * 该变量初始值为0,通过AtomicInteger类提供的原子方法,可以确保在多线程环境下的线程安全操作。
 */
private static AtomicInteger atomicInteger = new AtomicInteger(0);
/*
 * 用于线程同步的静态锁对象。
 */
private static Object object = new Object();

public static void main(String[] args) {
    // 创建一个线程,执行一个Lambda表达式
    new Thread(() -> {
        // 当atomicInteger的值小于等于200时,执行循环
        while (atomicInteger.get() <= 200) {
            // 使用synchronized关键字,保证线程安全
            synchronized (object) {
                // 唤醒其他等待的线程
                object.notify();
                // 输出当前线程的名称和atomicInteger的值
                System.out.println("synchronized打印偶数---" + Thread.currentThread().getName() + ":" + atomicInteger.getAndIncrement());
                try {
                    // 当前线程等待,直到被唤醒
                    object.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }).start();

    // 创建另一个线程,执行另一个Lambda表达式
    new Thread(() -> {
        // 当atomicInteger的值小于等于200时,执行循环
        while (atomicInteger.get() <= 200) {
            // 使用synchronized关键字,保证线程安全
            synchronized (object) {
                // 唤醒其他等待的线程
                object.notify();
                // 输出当前线程的名称和atomicInteger的值
                System.out.println("synchronized打印奇数---" + Thread.currentThread().getName() + ":" + atomicInteger.getAndIncrement());
                try {
                    // 当前线程等待,直到被唤醒
                    object.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }).start();
}

关键点:

  • 使用 synchronized 确保线程安全。
  • wait() 释放锁并进入等待状态,notify() 唤醒等待线程。
  • 循环中必须用 while 而非 if,防止虚假唤醒。

输出结果:

3. 使用信号量(Semaphore

通过信号量控制线程的执行顺序,初始信号量分配决定启动顺序。

实现步骤:

  • 初始化两个信号量:Semaphore semaphoreA = new Semaphore(1)(线程A先执行),Semaphore semaphoreB = new Semaphore(0)
  • 线程A打印后释放线程B的信号量,线程B打印后释放线程A的信号量。

示例代码:

java 复制代码
/**
 * 静态原子整型变量,用于在多线程环境下进行原子操作。
 * 该变量初始值为0,通过AtomicInteger类提供的原子方法,可以确保在多线程环境下的线程安全操作。
 */
private static AtomicInteger atomicInteger = new AtomicInteger(0);
private static Semaphore semaphoreA = new Semaphore(1);
private static Semaphore semaphoreB = new Semaphore(0);
public static void main(String[] args) {
    // 创建一个线程,执行一个Lambda表达式
    new Thread(() -> {
        // 当atomicInteger的值小于等于200时,循环执行
        while (atomicInteger.get() <= 200) {
            try {
                // 获取信号量A
                semaphoreA.acquire();
                // 输出当前线程的名称和atomicInteger的值,并将atomicInteger的值加1
                System.out.println("Semaphore打印偶数---" +Thread.currentThread().getName() + ":"  + atomicInteger.getAndIncrement());
                // 释放信号量B
                semaphoreB.release();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }).start();

    // 创建另一个线程,执行另一个Lambda表达式
    new Thread(() -> {
        // 当atomicInteger的值小于200时,循环执行
        while (atomicInteger.get() < 200) {
            try {
                // 获取信号量B
                semaphoreB.acquire();
                // 输出当前线程的名称和atomicInteger的值,并将atomicInteger的值加1
                System.out.println("Semaphore打印奇数---" + Thread.currentThread().getName() + ":"  + atomicInteger.getAndIncrement());
                // 释放信号量A
                semaphoreA.release();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }).start();
}

关键点:

  • 信号量的初始许可数控制启动顺序。
  • 线程间通过 acquire()release() 交替获取执行权。

输出结果:

4. 使用 ReentrantLockCondition

利用显式锁和条件变量实现更灵活的线程协作,适合复杂场景。

实现步骤:

  • 创建 ReentrantLock 和两个 Condition 对象(如 evenConditionoddCondition)。
  • 线程A在偶数时打印并唤醒线程B,线程B在奇数时打印并唤醒线程A。

示例代码:

java 复制代码
/**
 * 静态原子整型变量,用于在多线程环境下进行原子操作。
 * 该变量初始值为0,通过AtomicInteger类提供的原子方法,可以确保在多线程环境下的线程安全操作。
 */
private static AtomicInteger atomicInteger = new AtomicInteger(0);
private static ReentrantLock lock = new ReentrantLock();
private static Condition evenCondition = lock.newCondition();
private static Condition oddCondition = lock.newCondition();

public static void main(String[] args) {
    new Thread(() -> {
        while (atomicInteger.get() <= 200) {
            lock.lock();
            try {
                if (atomicInteger.get() % 2 == 0) {
                    // 如果当前值为偶数,则打印偶数,并将原子整型变量加1
                    System.out.println("ReentrantLock打印偶数---" + Thread.currentThread().getName() + ":" + atomicInteger.getAndIncrement());
                    // 唤醒等待的奇数线程
                    oddCondition.signal();
                } else {
                    // 如果当前值为奇数,则等待
                    evenCondition.await();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }).start();

    new Thread(() -> {
        while (atomicInteger.get() <= 200) {
            lock.lock();
            try {
                if (atomicInteger.get() % 2 == 1) {
                    // 如果当前值为奇数,则打印奇数,并将原子整型变量加1
                    System.out.println("ReentrantLock打印奇数---" + Thread.currentThread().getName() + ":" + atomicInteger.getAndIncrement());
                    // 唤醒等待的偶数线程
                    evenCondition.signal();
                } else {
                    // 如果当前值为偶数,则等待
                    oddCondition.await();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }).start();
}

关键点:

  • Condition 提供更细粒度的线程等待与唤醒**。
  • 需手动释放锁(unlock())以避免死锁。

输出结果:

方法对比

方法 优点 缺点
flag 无锁设计,代码简洁 存在竞态条件,忙等待浪费资源
synchronized 代码简单,无需额外依赖 灵活性较低,无法指定唤醒线程
Semaphore 逻辑清晰,易于扩展 需处理信号量初始值设置
ReentrantLock 支持多条件变量,灵活性高 代码复杂度较高

注意事项

  1. 共享变量可见性 :需使用volatile或同步块确保变量修改对其他线程可见(示例中通过同步机制隐含保证)。
  2. 终止条件 :循环需严格判断count <= 200,防止越界。
  3. 异常处理InterruptedException需捕获并处理,避免线程意外终止。

以上方法均可实现需求,推荐根据场景选择:简单场景用synchronized,复杂同步需求用ReentrantLock,信号量适合明确许可控制的场景。

思考:可以用CountDownLatch实现吗?

可以,但不推荐

CountDownLatch属于一次性计数器:初始化时指定一个固定数值(count),线程调用 countDown()减少计数器,其他线程通过await()` 等待计数器归零。计数器归零后无法重复使用。

大致思路:

每个线程在打印后,触发对方的CountDownLatch,然后等待自己的CountDownLatch被触发。但每次循环需要重新创建CountDownLatch实例,或者使用原子操作来重置计数器。但CountDownLatch不支持重置,所以每次循环都需要新的实例,这在代码实现上可能比较复杂。

再思考,那CyclicBarrier呢?

可行,也可实践。

具体代码已上传Github:https://github.com/tyronczt/java-learn/tree/master/Interview/two_thread_print_out

再再思考,有CyclicBarrier、Semaphore、CountDownLatch的区别,适用场景?

参看:CyclicBarrier、Semaphore、CountDownLatch的区别,适用场景

相关推荐
mghio3 小时前
Dubbo 中的集群容错
java·微服务·dubbo
咖啡教室8 小时前
java日常开发笔记和开发问题记录
java
咖啡教室8 小时前
java练习项目记录笔记
java
鱼樱前端9 小时前
maven的基础安装和使用--mac/window版本
java·后端
RainbowSea9 小时前
6. RabbitMQ 死信队列的详细操作编写
java·消息队列·rabbitmq
RainbowSea10 小时前
5. RabbitMQ 消息队列中 Exchanges(交换机) 的详细说明
java·消息队列·rabbitmq
我不会编程55511 小时前
Python Cookbook-5.1 对字典排序
开发语言·数据结构·python
李少兄11 小时前
Unirest:优雅的Java HTTP客户端库
java·开发语言·http
此木|西贝11 小时前
【设计模式】原型模式
java·设计模式·原型模式