1.多线程打印奇偶数,怎么控制打印的顺序?
面试中简短回答(一句话)
"用线程间同步/协调让两个线程轮流 执行即可,常用手段有 synchronized + wait/notify、ReentrantLock + Condition、Semaphore,要用 while 判定等待条件以防虚假唤醒,并处理结束条件和唤醒另一方。下面我举几个代码示例说明实现细节。"
要点(面试可说)
-
使用一个共享状态(当前数或轮次 flag)作为判断依据。
-
等待时用
while循环而不是if(防止虚假唤醒)。 -
wake 时建议用
notifyAll或signalAll(更稳妥),或用两个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)、直接在类加载时初始化(饿汉式)
详细解释(面试需要展开时)
-
问题的根源
在 DCL 中,代码通常会先在同步块外检查
if (instance == null),只有为空才进入synchronized。如果没有volatile,JVM/CPU 可能把对象的构造过程重排序成:-
分配内存
-
把引用赋给
instance -
执行构造函数(初始化字段)
这样,另一个线程在第 1 次检查时可能看到
instance != null,得到的是一个还没初始化完的对象(部分构造的),就会出错。 -
-
synchronized为什么不足
synchronized确保进入同步块的线程之间有内存可见性和互斥,但 如果第一次检查是在同步块外 ,那次读取没有同步保证(没有建立 happens-before 关系),因此其它线程可能看到一个"已赋值但未初始化完成"的引用。把volatile加在instance上可以保证外部的读取不会看到半成品。 -
volatile做了什么-
保证对该变量的写对其他线程可见(可见性)。
-
禁止某些重排序:在写
volatile变量之前的写操作不会被重排序到volatile写之后;在读volatile变量之后的读操作不会被重排序到volatile读之前。这就阻止了"引用先赋值、初始化后执行"的危险重排序。
-
-
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个线程等待这三个线程全部执行完在执行,怎么实现?
面试中的一句话回答(开场)
"让等待线程在开始前阻塞 ,三条工作线程各自完成后 通知它;常用手段有 CountDownLatch、Thread.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/Lock、AtomicInteger、LongAdder 等保证原子性或可见性。"
面试要点(关键词,面试时讲这几句)
-
i++= read + add + write(不是原子)。 -
会发生 race condition(竞态)→ 丢失更新(lost update)。
-
结果不确定,可能小于 100(通常会看到 90、80、甚至更低,取决于调度和时序)。
-
volatile不能保证i++的原子性(只能保证可见性/禁止重排序)。 -
解决方案:
AtomicInteger(getAndIncrement()/incrementAndGet())、synchronized/ReentrantLock、LongAdder(高并发累加时推荐),或使用线程安全的并发框架(CompletableFuture、Stream.parallel()结合归约等)。
为什么会少于 100(举个最简单的丢失更新示例)
假设初始 x = 0。两个线程 T1、T2 都要做一次 x++,可能的交错是:
-
T1 读取 x(读到 0)
-
T2 读取 x(也读到 0)
-
T1 计算 0+1,写回 x(写 1)
-
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
}
}
正确的几种改法(面试可以写出代码)
- 使用
AtomicInteger(最简洁)
java
import java.util.concurrent.atomic.AtomicInteger;
AtomicInteger x = new AtomicInteger(0);
Runnable r = () -> {
for (int i=0;i<50;i++)
x.getAndIncrement();
};
- 使用
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++ 的原子性。修复方法有 AtomicInteger、synchronized/Lock、或 LongAdder(在高并发场景下推荐),其中 AtomicInteger 最常用、语义明确。"