当前位置: 首页 > news >正文

八个经典的Java多线程编程题

View Post

八个经典的Java多线程编程题

八个经典的Java多线程编程题

目录
  • 八个经典的Java多线程编程题
    • 1、要求线程a执行完才开始线程b, 线程b执行完才开始下一个线程
    • 2、两个线程轮流打印数字,一直到100
    • 3、写两个线程,一个打印152,另一个线程打印AZ,打印顺序是12A,34B,....5152Z
    • 4、编写一个程序,启动三个线程,三个线程成ID分别是A,B,C;每个线程将自己的ID值在屏幕上打印5遍,打印顺序是ABCABC.....
      • 解法一:
      • 解法二:Semaphore
    • 5、编写十个线程,第一个线程从1加到10,第二个线程从11加到20....第十个线程从91加到100,最后再把10个线程结果相加
    • 6、三个窗口同时卖票
    • 7、生产者消费者
    • 8、交替打印两个数组

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;        // 切换标记:下一轮给 print2condition2.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:执行 printAnew 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; //默认为falseprivate 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() {@Overridepublic void run() {t.printNum();}}).start();new Thread(new Runnable() {@Overridepublic void run() {t.printLitter();}}).start();}
}

输出结果:

image

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() {@Overridepublic void run() {t.printA();}}).start();new Thread(new Runnable() {@Overridepublic void run() {t.printB();}}).start();new Thread(new Runnable() {@Overridepublic void run() {t.printC();}}).start();}
}

输出结果:
image

执行流程简要说明:

  1. 初始 flag = 0,所以只有线程A的条件满足(flag == 0),A拿到锁后进入printA方法打印第一个A,然后将flag改为1,调用notifyAll()唤醒其他线程。
  2. A进入下一轮循环,因为flag != 0而等待(释放锁)。
  3. 线程B、C被唤醒后竞争锁,B的条件flag == 1满足,所以B打印B,改flag=2notifyAll
  4. B等待,C被唤醒后条件满足打印C,改flag=0notifyAll
  5. 如此循环,直到每个线程各自打印完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:负责打印字母 AThread a = new Thread(() -> {for (int i = 0; i < 5; i++) {try {semA.acquire();         //// 从 semA 获取一个许可(如果 semA 许可数为 0,则当前线程阻塞等待)System.out.print("A");  // 获得许可后,打印 AsemB.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;}@Overridepublic 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);}
}

输出结果:

image

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 保存共享的票源对象。}@Overridepublic 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();}
}

输出结果:

image

整体行为总结

  • 三个窗口同时卖票,共享 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{@Overridepublic 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{@Overridepublic 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();}}
}

输出结果:

image

代码执行逻辑总结:

  1. 共享资源count 初始为 0,最大 FULL=10
  2. 同步:所有线程竞争同一个锁 LOCK
  3. 生产者:当 count 未满时生产(++count),否则 wait();生产后唤醒所有等待线程。
  4. 消费者:当 count 非空时消费(--count),否则 wait();消费后唤醒所有等待线程。
  5. 循环次数:每个线程独立运行 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();}
}

输出结果:

image

提醒:

public synchronized void print1(){ }synchronized (this) {}
上述两种有什么区别?
  • 需要同步整个方法 → 用 synchronized 实例方法,简洁。
  • 只需要同步部分代码,或需要指定非 this → 用 synchronized(lock) 代码块。
  • 两者锁对象相同时互斥效果相同,但代码块粒度更细,更推荐用于复杂场景。

以上内容来自CSDN博主小李飞飞转:https://blog.csdn.net/shinecjj/article/details/103792151