JUC-场景题

1.多线程打印奇偶数,怎么控制打印的顺序?

面试中简短回答(一句话)

"用线程间同步/协调让两个线程轮流 执行即可,常用手段有 synchronized + wait/notifyReentrantLock + ConditionSemaphore,要用 while 判定等待条件以防虚假唤醒,并处理结束条件和唤醒另一方。下面我举几个代码示例说明实现细节。"

要点(面试可说)

  • 使用一个共享状态(当前数或轮次 flag)作为判断依据。

  • 等待时用 while 循环而不是 if(防止虚假唤醒)。

  • wake 时建议用 notifyAllsignalAll(更稳妥),或用两个 Semaphore 做严格交替。

  • 避免忙等(busy-wait),优先用阻塞同步原语。

  • 如果需要公平性可用公平 Semaphore/Lock 或更复杂的调度。

实现1:synchronized + wait/notifyAll

java 复制代码
public class OddEvenSynchronized {
    private final Object lock = new Object();
    private int cur = 1;
    private final int n;

    public OddEvenSynchronized(int n) { this.n = n; }

    public void printOdd() {
        try {
            while (true) {
                synchronized (lock) {
                    while (cur % 2 == 0 && cur <= n) { // 等待轮到奇数
                        lock.wait();
                    }
                    if (cur > n) { 
                        lock.notifyAll(); 
                        break; 
}
                    System.out.println(Thread.currentThread().getName() + " -> " + cur);
                    cur++;
                    lock.notifyAll(); // 唤醒另一线程
                }
            }
        } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }

    public void printEven() {
        try {
            while (true) {
                synchronized (lock) {
                    while (cur % 2 == 1 && cur <= n) { // 等待轮到偶数
                        lock.wait();
                    }
                    if (cur > n) { 
                    lock.notifyAll(); 
                    break; 
   }
                    System.out.println(Thread.currentThread().getName() + " -> " + cur);
                    cur++;
                    lock.notifyAll();
                }
            }
        } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }

    public static void main(String[] args) {
        OddEvenSynchronized p = new OddEvenSynchronized(10);
        Thread t1 = new Thread(p::printOdd, "OddThread");
        Thread t2 = new Thread(p::printEven, "EvenThread");
        t1.start(); 
        t2.start();
    }
}

说明 :用 while 判定等待条件,结束时**notifyAll()**唤醒防止对方永远阻塞。

实现2:ReentrantLock + Condition

java 复制代码
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class OddEvenLock{
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private int cur = 1;
    private final int n;
    private boolean oddTurn = true;
public OddEvenLock(int n) { 
this.n = n; 
}
  public void printOdd() {
        while (true) {
            lock.lock();
            try {
                while (!oddTurn && cur <= n) condition.await();
                if (cur > n) { condition.signalAll(); break; }
                System.out.println(Thread.currentThread().getName() + " -> " + cur);
                cur++;
                oddTurn = false;
                condition.signalAll();
            } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
            finally { lock.unlock(); }
        }
    }
  public void printEven() {
        while (true) {
            lock.lock();
            try {
                while (oddTurn && cur <= n) condition.await();
                if (cur > n) { condition.signalAll(); break; }
                System.out.println(Thread.currentThread().getName() + " -> " + cur);
                cur++;
                oddTurn = true;
                condition.signalAll();
            } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
            finally { lock.unlock(); }
        }
    }
 public static void main(String[] args) {
        OddEvenLock p = new OddEvenLock(10);
        new Thread(p::printOdd, "OddThread").start();
        new Thread(p::printEven, "EvenThread").start();
    }
}

说明 :比 synchronized 更灵活,Condition 可替代 wait/notify

实现3:Semaphore(逻辑最直观、可靠)

java 复制代码
import java.util.concurrent.Semaphore;

public class OddEvenSemaphore {
    private final Semaphore oddSem = new Semaphore(1);  // 先允许奇数打印
    private final Semaphore evenSem = new Semaphore(0);
    private final int n;

    public OddEvenSemaphore(int n) { this.n = n; }

    public void printOdd() {
        for (int i = 1; i <= n; i += 2) {
            try {
                oddSem.acquire();
                if (i > n) { evenSem.release(); break; }
                System.out.println(Thread.currentThread().getName() + " -> " + i);
                evenSem.release();
            } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        }
    }

    public void printEven() {
        for (int i = 2; i <= n; i += 2) {
            try {
                evenSem.acquire();
                if (i > n) { oddSem.release(); break; }
                System.out.println(Thread.currentThread().getName() + " -> " + i);
                oddSem.release();
            } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        }
    }

    public static void main(String[] args) {
        OddEvenSemaphore p = new OddEvenSemaphore(10);
        new Thread(p::printOdd, "OddThread").start();
        new Thread(p::printEven, "EvenThread").start();
    }
}

说明 :用两个 Semaphore 控制严格交替,代码直观且不会出现 wait/notify 的陷阱。

2.单例模型既然已经用了synchroinzed,为什么还要在加volatile?

简短一句话(面试开场可用)

"因为 volatile 不仅保证可见性,还禁止重排序;在双重检查锁(DCL)里,volatile 防止另一个线程看到一个尚未完全构造 的实例引用,所以 synchronized + DCL 要配合 volatile 使用才能安全且高效。"

面试回答要点(要说的关键词)

  • 可见性(visibility)

  • 防止指令重排序(prevents reordering)

  • 双重检查锁(Double-Checked Locking, DCL)

  • Java 内存模型(JMM),自 Java 5 起 volatile 语义被修正

  • 备选方案:静态内部类(Initialization-On-Demand Holder)、枚举单例(enum)、直接在类加载时初始化(饿汉式)

详细解释(面试需要展开时)

  1. 问题的根源

    在 DCL 中,代码通常会先在同步块外检查 if (instance == null),只有为空才进入 synchronized。如果没有 volatile,JVM/CPU 可能把对象的构造过程重排序成:

    • 分配内存

    • 把引用赋给 instance

    • 执行构造函数(初始化字段)

    这样,另一个线程在第 1 次检查时可能看到 instance != null,得到的是一个还没初始化完的对象(部分构造的),就会出错。

  2. synchronized 为什么不足
    synchronized 确保进入同步块的线程之间有内存可见性和互斥,但 如果第一次检查是在同步块外 ,那次读取没有同步保证(没有建立 happens-before 关系),因此其它线程可能看到一个"已赋值但未初始化完成"的引用。把 volatile 加在 instance 上可以保证外部的读取不会看到半成品。

  3. volatile 做了什么

    • 保证对该变量的写对其他线程可见(可见性)。

    • 禁止某些重排序:在写 volatile 变量之前的写操作不会被重排序到 volatile 写之后;在读 volatile 变量之后的读操作不会被重排序到 volatile 读之前。这就阻止了"引用先赋值、初始化后执行"的危险重排序。

  4. Java 版本注意

    在 Java 5 之前,volatile 的语义不够强(可能无法可靠防止重排序)。从 Java 5(JSR-133)起,volatile 语义被加强,DCL + volatile 成为正确且高效的实现方式。现在大多数面试默认 Java 8/11/17 环境,可直接说明 "在 Java 5+ 中有效"。

推荐演示代码(面试中可以写出)

java 复制代码
public class Singleton {
    private static volatile Singleton instance; // volatile 必须的

    private Singleton() { /* init */ }

    public static Singleton getInstance() {
        if (instance == null) {                 // 第一次检查(无锁)
            synchronized (Singleton.class) {
                if (instance == null) {         // 第二次检查(有锁)
                    instance = new Singleton(); // 需要 volatile 防止重排序问题
                }
            }
        }
        return instance;
    }
}

面试回答示例(整合一句话 + 解释)

"volatile 是为了防止在双重检查锁 里出现指令重排序与可见性问题------没有它,另一个线程可能看到 instance 已被赋值但构造未完成的半初始化对象。synchronized 本身不能解决 外层(无锁)读取带来的可见性/重排序问题;在 Java 5+ 中,volatile + DCL 是正确且高效的做法。实际工程中,若想更简单安全,可以用静态内部类或 enum 单例。"

3.3个线程并发执行,1个线程等待这三个线程全部执行完在执行,怎么实现?

面试中的一句话回答(开场)

"让等待线程在开始前阻塞 ,三条工作线程各自完成后 通知它;常用手段有 CountDownLatchThread.join()CompletableFuture.allOf()ExecutorService + Future 等,CountDownLatch 是最直观的单次等待工具,CyclicBarrier/Phaser 适合可复用或更复杂的阶段同步。"

要点(面试时要说)

  • 这是"等待多个任务完成再继续"的经典问题 ------ 要用线程协作原语阻塞等待并在任务完成时解除阻塞。

  • CountDownLatch:一次性(one-shot),简单直观,可调用 await()(可带超时),并且 countDown() 的动作与 await() 返回之间有 happens-before 保证。

  • Thread.join():最简单、直接,但需要有线程引用并且不适合线程池场景。

  • CompletableFuture.allOf() / Future.get():适合异步任务/线程池,便于异常处理与组合。

  • 注意:处理 InterruptedException、考虑超时、避免忙等(busy-wait)。

实现1:CountDownLatch(推荐,面试常写)

java 复制代码
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class WaitThreeWithCountDownLatch {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);
        ExecutorService es = Executors.newFixedThreadPool(3);

        for (int i = 1; i <= 3; i++) {
            final int id = i;
            es.submit(() -> {
                try {
                    // 模拟工作
                    System.out.println("Worker " + id + " start");
                    Thread.sleep(500L * id);
                    System.out.println("Worker " + id + " done");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    latch.countDown(); // 通知完成
                }
            });
        }

        // 等待三个工作线程全部完成
        System.out.println("Waiting thread waiting...");
        latch.await(); // 可用 await(long, TimeUnit) 加超时
        System.out.println("All workers finished --- continue waiting thread.");

        es.shutdown();
    }
}

优点:简单、直观,适合"一次性等待多个任务完成"。注意处理中断和可能的超时。

实现2:直接使用 Thread.join()

java 复制代码
public class WaitThreeWithJoin {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> { /* work */ });
        Thread t2 = new Thread(() -> { /* work */ });
        Thread t3 = new Thread(() -> { /* work */ });

        t1.start(); 
        t2.start(); 
        t3.start();
        // 等待
        t1.join();
        t2.join();
        t3.join();
        // 三个线程都结束后继续
        System.out.println("All three threads finished.");
    }
}

优点:最原始、简单;

缺点:需要持有 Thread 对象引用,不方便与线程池/异步 API 配合。

实现3:CompletableFuture(现代、易组合)

java 复制代码
import java.util.concurrent.CompletableFuture;

public class WaitThreeWithCF {
    public static void main(String[] args) {
        CompletableFuture<Void> f1 = CompletableFuture.runAsync(() -> doWork(1));
        CompletableFuture<Void> f2 = CompletableFuture.runAsync(() -> doWork(2));
        CompletableFuture<Void> f3 = CompletableFuture.runAsync(() -> doWork(3));

        // 等待所有完成
        CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2, f3);
        all.join(); // join() 在抛出 unchecked exception 时会包装异常

        System.out.println("All tasks finished (CompletableFuture).");
    }

    static void doWork(int id) {
        try {
            System.out.println("task " + id + " start");
            Thread.sleep(300L * id);
            System.out.println("task " + id + " done");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

优点:适合异步组合、异常传播和链式处理;

缺点:稍微复杂些但功能强大。

实现4:ExecutorService + Future.get()

java 复制代码
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class WaitThreeWithFutures {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService es = Executors.newFixedThreadPool(3);
        List<Future<?>> futures = new ArrayList<>();
        for (int i = 1; i <= 3; i++) {
            final int id = i;
            futures.add(es.submit(() -> {
                try {
                    Thread.sleep(200L * id);
                    System.out.println("worker " + id + " done");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }));
        }

        // 等待所有 Future 完成
        for (Future<?> f : futures) {
            f.get(); // 可使用 get(timeout, unit)
        }
        System.out.println("All futures done.");
        es.shutdown();
    }
}

}

优点:与线程池自然配合,get() 可获取返回值/异常;

缺点:需要处理 checked 异常。

面试回答示例(整合)

"可以用 CountDownLatch:主/等待线程调用 latch.await() 阻塞,三个工作线程各自完成后调用 latch.countDown()CountDownLatch 是 one-shot,很适合这种场景。简单实现也可以用 Thread.join(),如果是在异步/线程池环境下更推荐 CompletableFuture.allOf()ExecutorService + Future.get()。注意处理中断、可能的超时和异常传播。"

4.假设两个线程并发读写同一个整型变量,初始值为零,每个线程加 50 次,结果可能是什么?

简短一句话(面试开场)

"因为 i++ 不是原子操作 ------ 是读-改-写三个步骤,在并发下会产生丢失更新 ,所以两个线程各做 50 次的最终结果是非确定性的 ,有可能不到 100(通常介于 1 到 100 之间);正确的做法是用 synchronized/LockAtomicIntegerLongAdder 等保证原子性或可见性。"

面试要点(关键词,面试时讲这几句)

  • i++ = read + add + write(不是原子)。

  • 会发生 race condition(竞态)→ 丢失更新(lost update)。

  • 结果不确定,可能小于 100(通常会看到 90、80、甚至更低,取决于调度和时序)。

  • volatile 不能保证 i++ 的原子性(只能保证可见性/禁止重排序)。

  • 解决方案:AtomicIntegergetAndIncrement()/incrementAndGet())、synchronized/ReentrantLockLongAdder(高并发累加时推荐),或使用线程安全的并发框架(CompletableFutureStream.parallel() 结合归约等)。

为什么会少于 100(举个最简单的丢失更新示例)

假设初始 x = 0。两个线程 T1、T2 都要做一次 x++,可能的交错是:

  1. T1 读取 x(读到 0)

  2. T2 读取 x(也读到 0)

  3. T1 计算 0+1,写回 x(写 1)

  4. T2 计算 0+1,写回 x(写 1,覆盖了 T1 的写)

结果 x = 1,而不是 2。把这个模式扩展到 50 次,就会"丢失"很多次,最终小于 100。

代码示例(有竞态的版本)

java 复制代码
public class RaceIncrement {
    static int x = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> { 
        for (int i=0;i<50;i++) x++; 
        });
        Thread t2 = new Thread(() -> { 
        for (int i=0;i<50;i++) x++; 
        });
        t1.start(); 
        t2.start();
        t1.join(); 
        t2.join();
        System.out.println("result = " + x); // 很多时候 < 100
    }
}

正确的几种改法(面试可以写出代码)

  1. 使用 AtomicInteger(最简洁)
java 复制代码
import java.util.concurrent.atomic.AtomicInteger;

AtomicInteger x = new AtomicInteger(0);
Runnable r = () -> { 
for (int i=0;i<50;i++) 
x.getAndIncrement(); 
};
  1. 使用 synchronized
java 复制代码
int x = 0;
Object lock = new Object();
Runnable r = () -> { 
for (int i=0;i<50;i++) {
    synchronized(lock) { 
x++;
}
 }
  };

面试示例回答(整合版,100% 可直接念)

"因为**i++** 不是原子操作,它分为读-改-写三步,两个线程并发会产生竞态,导致丢失更新,所以两个线程各自加 50 次的结果是非确定性的 ------ 很可能小于 100(理论上介于 1 到 100 之间,实际常见值如 80、90 等)。volatile 只是保证可见性/禁止重排,不能保证 i++ 的原子性。修复方法有 AtomicIntegersynchronized/Lock、或 LongAdder(在高并发场景下推荐),其中 AtomicInteger 最常用、语义明确。"

相关推荐
Wyy_9527*2 小时前
行为型设计模式——状态模式
java·spring boot·后端
a程序小傲2 小时前
京东Java面试被问:基于Gossip协议的最终一致性实现和收敛时间
java·开发语言·前端·数据库·python·面试·状态模式
tqs_123452 小时前
Spring Boot 的自动装配机制和 Starter 的实现原理
开发语言·python
组合缺一2 小时前
MCP 进化:让静态 Tool 进化为具备“上下文感知”的远程 Skills
java·ai·llm·agent·mcp·skills
程序员小白条2 小时前
面试 Java 基础八股文十问十答第二十二期
java·开发语言·数据库·面试·职场和发展·毕设
编程大师哥2 小时前
JavaScript 和 Python 哪个更适合初学者?
开发语言·javascript·python
taihexuelang2 小时前
jenkins 部署java项目
java·servlet·jenkins
建军啊2 小时前
php伪协议、代码审计工具和实战
开发语言·php
手握风云-2 小时前
JavaEE 进阶第十二期:Spring Ioc & DI,从会用容器到成为容器(上)
java·spring·java-ee