八个经典的Java多线程编程题
八个经典的Java多线程编程题
目录
1、要求线程a执行完才开始线程b, 线程b执行完才开始下一个线程
package com.uu;
public class Thread1 {
public static class PrintThread extends Thread {
PrintThread(String name) { // 构造方法,接收线程名称参数
super(name); // 调用父类 Thread 的构造方法,设置线程名称
}
@Override // 重写注解,重写父类方法
public void run() { // 重写 Thread 类的 run() 方法,线程执行的核心逻辑
for (int i = 0; i < 100; i++) {
System.out.println(getName() + ": " + i);
}
}
}
public static void main(String[] args) {
PrintThread t1 = new PrintThread("a"); // 创建名为 "a" 的线程对象
PrintThread t2 = new PrintThread("b");
PrintThread t3 = new PrintThread("c");
try {
t1.start(); // 启动线程 t1,开始执行 run() 方法
t1.join(); // 主线程等待 t1 执行完毕后才继续
t2.start();
t2.join();
t3.start();
t3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出:
a:0 ~ 99
b:0 ~ 99
c:0 ~ 99
2、两个线程轮流打印数字,一直到100
可重入锁(ReentrantLock)是指同一个线程可以多次获取同一把锁而不会死锁。
虚假唤醒(Spurious Wakeup)是指线程在没有被通知(signal/notify)的情况下,从等待状态意外唤醒。
语法规则:对象::方法名
package com.uu;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Thread2 {
private final Lock lock = new ReentrantLock(); // 创建可重入锁对象,用于同步控制
private final Condition condition1 = lock.newCondition(); // 创建条件变量1,用于控制线程1等待/唤醒
private final Condition condition2 = lock.newCondition(); // 创建条件变量2,用于控制线程2等待/唤醒
// 交替标记:true = 轮到 printA 打印;false = 轮到 printB 打印
private boolean flag = true;
// 要打印的数字,从 0 开始
private int count = 0;
public void printA() {
for (int i = 0; i < 50; i++){ // 循环 50 次:打印 50 个数
lock.lock(); // 【加锁】:获取锁,同一时间只有一个线程能执行这里面的代码
try {
// 如果 flag 不是 true → 说明没轮到自己,进入等待
// 使用 while 防止【虚假唤醒】
while (!flag){
// 当前线程(print1)在 condition1 上等待
// 作用:释放锁 → 让别人能进 → 自己休眠
condition1.await(); //只等待自己的信号
}
// 能走到这里:说明轮到 printA 了
System.out.println("A"+ ++count);
flag = false; // 切换标记:下一轮给 print2
condition2.signal(); // 只唤醒在 condition2 上等待的线程(就是 print2)
}catch (InterruptedException e){
// 线程被中断时,恢复中断状态
Thread.currentThread().interrupt();
}finally {
// 【解锁】:finally 保证锁一定释放,防止死锁
lock.unlock();
}
}
}
public void printB() {
for (int i = 0; i < 50; i++){
lock.lock();
try {
// 如果 flag 是 true → 没轮到 print2,等待
while (flag){
condition2.await(); //只等待自己的信号
}
System.out.println("B"+ ++count);
flag = true;
condition1.signal();//唤醒线程1
}catch (InterruptedException e){
Thread.currentThread().interrupt();
}finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
Thread2 thread2 = new Thread2(); // 创建实例
new Thread(thread2::printA).start(); // 启动线程 1:执行 printA
new Thread(thread2::printB).start(); // 启动线程 2:执行 print2
}
}
核心流程
一开始 flag = true → printA 能打印,printB 必须等待。
printA 打印完 → 把 flag 改成 false → 只唤醒 printB。
printB 醒来 → 发现 flag 是 false → 打印 → 改回 true → 只唤醒 printA。
循环往复 → A、B、A、B…… 精准交替,绝不乱序。
关键
1、为什么用 Lock 而不是 synchronized?
Lock更灵活,可以手动加锁 / 解锁。finally里解锁,绝对不会死锁。
2、为什么用 两个 Condition?(最精髓)
synchronized只有一个等待队列,notifyAll()会把所有人都叫醒。- 这里 condition1 只存 print1,condition2 只存 print2。
- 唤醒时只叫醒对方,不浪费 CPU → 这就是精准交替。
3、为什么用 while 等待,不用 if?
防止虚假唤醒(操作系统底层机制,线程可能被莫名其妙唤醒)。
if:醒了直接往下跑,会出错。while:醒了再检查一遍条件,安全!
4、signal() 和 signalAll() 的区别
signal():只唤醒一个等待线程(精准)。signalAll():唤醒所有(浪费)。
3、写两个线程,一个打印152,另一个线程打印AZ,打印顺序是12A,34B,....5152Z
package com.uu;
public class Thread3 {
//线程间通信的标志。true 表示该轮到字母线程打印,false 表示轮到数字线程打印。
private boolean flag; //默认为false
private int count; //记录当前已经打印到哪个数字。
public synchronized void printNum() {
//循环26次,因为数字总共52个,每次打印两个数字,所以需要26轮
for (int i = 0; i < 26; i++){ // 如果 flag == true,就等待
while (flag){
try {
wait(); //会释放锁,并让当前线程进入等待状态,直到其他线程调用 notify() 唤醒它。
}catch (InterruptedException e){
e.printStackTrace();
}
}
flag = !flag; //改变标志
System.out.print(++count);
System.out.print(++count);
notify(); //唤醒其他线程
}
}
public synchronized void printLitter() {
//循环26次,因为字母总共52个,每次打印两个字母,所以需要26轮
for (int i = 0; i < 26; i++){
while (!flag){
try {
wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
flag = !flag;
System.out.print((char)(i + 'A'));
notify();
}
}
public static void main(String[] args) {
Thread3 t = new Thread3();
//Runnable 是一个函数式接口,里面只有一个抽象方法 void run()。
new Thread(new Runnable() {
@Override
public void run() {
t.printNum();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
t.printLitter();
}
}).start();
}
}
输出结果:

4、编写一个程序,启动三个线程,三个线程成ID分别是A,B,C;每个线程将自己的ID值在屏幕上打印5遍,打印顺序是ABCABC.....
解法一:
package com.uu;
public class Thread4 {
private int flag = 0;
public synchronized void printA(){
for (int i = 0; i < 5; i++){
while (flag != 0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 执行到这里说明flag == 0,轮到A打印
flag = 1;
System.out.print("A");
notifyAll();
}
}
public synchronized void printB(){
for (int i = 0; i < 5; i++){
while (flag != 1){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 执行到这里说明flag == 1,轮到B打印
flag = 2;
System.out.print("B");
notifyAll();
}
}
public synchronized void printC(){
for (int i = 0; i < 5; i++){
while (flag != 2){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 执行到这里说明flag == 2,轮到C打印
flag = 0;
System.out.print("C");
notifyAll();
}
}
public static void main(String[] args) {
// 创建唯一一个同步对象,三个线程共享这个对象,因此它们会竞争同一把锁,并且通过wait/notifyAll通信
Thread4 t = new Thread4();
new Thread(new Runnable() {
@Override
public void run() {
t.printA();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
t.printB();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
t.printC();
}
}).start();
}
}
输出结果:

执行流程简要说明:
- 初始
flag = 0,所以只有线程A的条件满足(flag == 0),A拿到锁后进入printA方法打印第一个A,然后将flag改为1,调用notifyAll()唤醒其他线程。 - A进入下一轮循环,因为
flag != 0而等待(释放锁)。 - 线程B、C被唤醒后竞争锁,B的条件
flag == 1满足,所以B打印B,改flag=2,notifyAll。 - B等待,C被唤醒后条件满足打印
C,改flag=0,notifyAll。 - 如此循环,直到每个线程各自打印完5次,最终输出
ABCABCABCABCABC(一共15个字母)。
关键点:
- 使用
notifyAll()而非notify():因为有三个线程,如果只用notify()可能唤醒一个等待的线程,但该线程的条件可能不满足(例如唤醒了一个不该执行的线程),再次进入等待,造成“死激活”或效率低下。notifyAll()能让所有等待线程都去检查自己的条件,确保正确的线程能够执行。 while循环检查条件:防止虚假唤醒,也是必须的。
解法二:Semaphore
import java.util.concurrent.Semaphore;
public class Thread4{
// 创建三个信号量,用于控制三个线程的执行顺序
// semA 初始有 1 个许可,表示 A 线程可以先执行
private static Semaphore semA = new Semaphore(1);
// semB 初始有 0 个许可,B 线程一开始需要等待
private static Semaphore semB = new Semaphore(0);
// semC 初始有 0 个许可,C 线程一开始需要等待
private static Semaphore semC = new Semaphore(0);
public static void main(String[] args) {
// 线程 A:负责打印字母 A
Thread a = new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
semA.acquire(); //// 从 semA 获取一个许可(如果 semA 许可数为 0,则当前线程阻塞等待)
System.out.print("A"); // 获得许可后,打印 A
semB.release(); // 释放一个 semB 的许可,让 B 线程得以继续执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread b = new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
semB.acquire();
System.out.print("B");
semC.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread c = new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
semC.acquire();
System.out.print("C");
semA.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
a.start();
b.start();
c.start();
}
}
5、编写十个线程,第一个线程从1加到10,第二个线程从11加到20....第十个线程从91加到100,最后再把10个线程结果相加
package com.uu;
public class Thread5 {
//定义了一个静态内部类 SumThread,它继承自 Thread,因此每个实例都是一个独立的线程。
public static class SumThread extends Thread {
int forch = 0; //表示线程的序号
int sum = 0; //用于存储当前线程计算出的部分和。
//构造函数,接收一个整数参数 forct,并将其赋值给实例变量 this.forct。
SumThread(int forch) {
this.forch = forch;
}
@Override
public void run() { //循环10次,计算该线程负责的10个数的和。
for (int i = 0; i <= 10; i++) {
sum += i + forch*10;
}
System.out.println(getName() + " " + sum);//输出线程名称和计算结果。
}
}
public static void main(String[] args) {
//定义一个整型变量 result,用于累加所有线程的部分和,最终输出总和。
int result = 0;
for (int i = 0; i < 10; i++) {
SumThread t = new SumThread(i);
t.start();
try {
//在主线程中调用 join() 方法,阻塞当前(主)线程,直到 sumThread 线程执行完毕才继续往下执行。
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
result += t.sum;
}
System.out.println("result = " + result);
}
}
输出结果:

6、三个窗口同时卖票
package com.uu;
class Ticket{
private int count = 0; //表示当前将要售出的票号(从第 1 张开始)。
//定义公开方法 sale(),由多个窗口线程调用,实现售票逻辑。
public void sale() {
//无限循环,只要还有未售出的票,就持续尝试售票。
while (true) {
//同步代码块,锁对象为当前 Ticket 实例
synchronized (this) {
if (count > 200) {
System.out.println("票已经卖完了");
break;
} else {
System.out.println(Thread.currentThread().getName() + "卖的第" + count++ + "张票");
}
try {
//让当前线程休眠 200 毫秒,模拟售票过程中的耗时。线程在休眠期间仍持有锁,其他窗口无法卖票。
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class SaleWindows extends Thread{
private Ticket ticket;
//构造函数:接收窗口名称(线程名)和 Ticket 对象。
public SaleWindows(String name,Ticket ticket)
{
super(name); //super(name) 调用父类 Thread 的构造方法,设置线程名。
this.ticket = ticket; //this.ticket = ticket 保存共享的票源对象。
}
@Override
public void run()
{
super.run(); //super.run() 可省略(Thread.run() 默认无操作),此处保留无实际影响。
ticket.sale(); //调用共享 Ticket 对象的 sale() 方法,开始售票。
}
}
public class Thread6 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
SaleWindows sw1 = new SaleWindows("窗口1",ticket);
SaleWindows sw2 = new SaleWindows("窗口2",ticket);
SaleWindows sw3 = new SaleWindows("窗口3",ticket);
sw1.start();
sw2.start();
sw3.start();
}
}
输出结果:

整体行为总结
- 三个窗口同时卖票,共享 200 张票。
synchronized (this)保证每次只有一个窗口能进入售票流程,避免超卖。- 每卖出一张票,线程休眠 200 毫秒(锁不释放),其他窗口只能等待。
- 当
count超过 200 时,某个窗口会打印“票已经卖完啦”并退出。由于同步块的存在,后续线程进入时也会看到count>200,可能多个窗口都会打印一次“票已经卖完啦”(这是该代码的一个小瑕疵,但不影响最终结果正确性)。 - 最终所有窗口退出,程序结束。
7、生产者消费者
生产者负责生产数据(如做包子),消费者负责处理数据(如吃包子),两者通过一个共享容器(如蒸笼)来交互。当容器满了,生产者就等待;容器空了,消费者就等待。
package com.uu;
public class Thread7 {
private final static String LOCK = "lock"; // 作为锁,用于 synchronized 同步。
private int count = 0;//盘子中的资源数量
private final static int FULL = 10; // 盘子中的最大资源数量
//定义生产者内部类,实现 Runnable。
class Producer implements Runnable{
@Override
public void run(){
//每个生产者线程反复生产 10 次。
for (int i = 0; i < 10; i++){
//使用 LOCK 对象作为同步锁。同一时刻只有一个线程能进入临界区。
synchronized (LOCK) {
//当盘子满了(count == 10)时,生产者不能继续生产,进入等待。
while(count==FULL){
try{
LOCK.wait(); //当前线程释放锁,并进入等待状态,
} catch (InterruptedException e) {
e.printStackTrace(); //若发生中断,打印堆栈
}
}
//退出 while 时,说明 count < FULL,可以生产。
System.out.println("生产者 " + Thread.currentThread().getName() + " 总共有 " + ++count + " 个资源");
//唤醒所有等待 LOCK 的线程(包括生产者和消费者)。避免只唤醒同类导致死锁
LOCK.notifyAll();
}
}
}
}
//消费者
class Consumer implements Runnable{
@Override
public void run(){
//每个消费者也循环 10 次,与总生产次数匹配。
for (int i = 0; i < 10; i++) {
synchronized (LOCK) {
//当盘子空了(count == 0)时,消费者不能继续消费,进入等待。
while (count == 0) {
try {
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费者 " + Thread.currentThread().getName() + " 总共有 " + --count + " 个资源");
LOCK.notifyAll();
}
}
}
}
public static void main(String[] args) {
Thread7 t = new Thread7();
//循环 5 次,每次创建一个生产者线程和一个消费者线程,共启动 10 个线程。
for (int i = 0; i <= 5; i++){
new Thread(t.new Producer(),"生产者-" + i).start();
new Thread(t.new Consumer(),"消费者-" + i).start();
}
}
}
输出结果:

代码执行逻辑总结:
- 共享资源:
count初始为 0,最大FULL=10。 - 同步:所有线程竞争同一个锁
LOCK。 - 生产者:当
count未满时生产(++count),否则wait();生产后唤醒所有等待线程。 - 消费者:当
count非空时消费(--count),否则wait();消费后唤醒所有等待线程。 - 循环次数:每个线程独立运行 10 次,总生产 = 总消费 = 50,程序最终能正常结束。
8、交替打印两个数组
package com.uu;
public class Thread8 {
//定义两个数组
int[] arr1 = {1 ,3 ,5 ,7 ,9 };
int[] arr2 = {2 ,4 ,6 ,8 ,10};
boolean flag ;
public synchronized void print1(){
for (int i = 0; i < arr1.length; i++){
//当 flag == true 时,print1() 应该等待,因为此时应该由 print2() 打印。
while (flag){
try {
wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
flag = !flag;
System.out.println(arr1[i]);
notifyAll();
}
}
public synchronized void print2(){
for (int i = 0; i < arr2.length; i++){
//当 flag == false 时,print2() 应该等待,因为此时应该由 print1() 打印。
while (!flag){
try {
wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
flag = !flag;
System.out.println(arr2[i]);
notifyAll();
}
}
public static void main(String[] args) {
Thread8 t = new Thread8();
new Thread(t::print1).start();
new Thread(t::print2).start();
}
}
输出结果:

提醒:
public synchronized void print1(){ }
synchronized (this) {}
上述两种有什么区别?
- 需要同步整个方法 → 用
synchronized实例方法,简洁。 - 只需要同步部分代码,或需要指定非
this锁 → 用synchronized(lock)代码块。 - 两者锁对象相同时互斥效果相同,但代码块粒度更细,更推荐用于复杂场景。
以上内容来自CSDN博主小李飞飞转:https://blog.csdn.net/shinecjj/article/details/103792151
浙公网安备 33010602011771号