八个经典的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();
    }
}

输出结果:

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() {
            @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();

    }
}

输出结果:
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:负责打印字母 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);
    }
}

输出结果:

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

输出结果:

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{
       @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();
        }
    }
}

输出结果:

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

posted on 2026-05-05 11:16  U~U  阅读(3)  评论(0)    收藏  举报