👨🏻💻 热爱摄影的程序员
👨🏻🎨 喜欢编码的设计师
🧕🏻 擅长设计的剪辑师
🧑🏻🏫 一位高冷无情的编码爱好者
大家好,我是 DevOps 工程师
欢迎分享 / 收藏 / 赞 / 在看!
多线程与并发部分全面涵盖了多线程编程的核心概念和重要内容。包括多线程的基本概念和三种实现方式(继承 Thread 类、实现 Runnable 接口、使用 Callable 和 Future 接口),以及涵盖了线程的生命周期、常用成员方法、线程安全问题和死锁现象。
此外,还提及了生产者和消费者模型,这是在多线程编程中常见的并发问题之一。了解如何通过线程间的协作来实现生产者和消费者的模式对于实现高效并发处理非常重要。
最后,还包括了线程池的内容,这是一种用于管理和复用线程的重要机制,可以帮助提高程序性能和资源利用率。
总体来说,本篇涵盖了多线程编程中的关键内容,对于理解并发编程的原理和实践有很好的指导作用。继续学习和实践多线程编程将有助于你在并发环境下开发出更稳健、高效的应用程序。
目录
1 多线程的概念
2 多线程的三种实现方式
2.1 继承 Thread 类,重写 run() 方法,调用 start() 方法
2.2 实现 Runnable 接口,重写 run() 方法,调用 start() 方法
2.3 通过 Callable 和 Future 创建线程
2.4 创建线程的三种方式的对比
3 多线程中常用的成员方法
3.1 线程的优先级
3.2 守护线程
3.3 礼让线程
3.4 插入线程
4 线程的生命周期
4.1 新建状态
4.2 就绪状态
4.3 阻塞状态
4.4 死亡状态
5 线程安全问题
5.1 概念
5.2 解决线程安全的策略
5.2.1 同步代码块
5.2.2 同步方法
5.2.3 Lock 锁
6 死锁
7 生产者和消费者
8 线程池
1 多线程的概念
多线程是指在一个程序中同时执行多个线程(也称为子任务),每个线程都是独立的执行流程。多线程使得程序可以同时执行多个任务,提高了系统的并发性和响应性。
在传统的单线程程序中,任务是按顺序依次执行的。而在多线程程序中,任务被划分为多个子任务,每个子任务由一个独立的线程执行。这些线程可以并发地执行,共享程序的资源(如内存空间和文件句柄),从而使得程序在同一时间内能够完成更多的工作。
多线程编程可以用于解决以下问题:
- 提高程序性能:多线程充分利用多核处理器,同时执行多个任务,提高了程序的整体执行速度。
- 提高系统响应性:在图形界面或网络应用中,使用多线程可以保持界面的响应性,同时处理用户输入或网络请求。
- 实现并发处理:多线程可以用于处理并发请求,例如服务器同时处理多个客户端请求。
然而,多线程编程也带来了一些挑战,例如线程安全问题、死锁等。因此,在设计和实现多线程程序时,需要仔细考虑线程间的同步和协作,以保证程序的正确性和稳定性。
2 多线程的三种实现方式
Java 提供了三种创建线程的方法:
- 继承 Thread 类
- 实现 Runnable 接口
- 实现 Callable 和 Future 创建线程
2.1 继承 Thread 类,重写 run() 方法,调用 start() 方法
创建一个线程的第一种方法是创建一个新的类,该类继承 Thread 类,然后创建一个该类的实例。
继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。
该方法尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable 接口的一个实例。
主线程和子线程交替执行操作,线程开启不一定立即执行,需要由 CPU 进行调度。
public class MT01 {public static void main(String[] args) {MyThread01 t1 = new MyThread01();MyThread01 t2 = new MyThread01();// 设置线程名称t1.setName("线程A");t2.setName("线程B");t1.start();t2.start();}public static class MyThread01 extends Thread {@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(getName() + "子线程输出" + i);}}}
}
Thread 类的一些方法
下表列出了 Thread 类的一些重要方法:
序号 | 方法描述 |
1 | public void start() |
2 | public void run() |
3 | public final void setName(String name) |
4 | public final void setPriority(int priority) |
5 | public final void setDaemon(boolean on) |
6 | public final void join(long millisec) |
7 | public void interrupt() |
8 | public final boolean isAlive() |
上述方法是被 Thread 对象调用的,下面表格的方法是 Thread 类的静态方法。
序号 | 方法描述 |
1 | public static void yield() |
2 | public static void sleep(long millisec) |
3 | public static boolean holdsLock(Object x) |
4 | public static Thread currentThread() |
5 | public static void dumpStack() |
如下的 ThreadClassDemo 程序演示了 Thread 类的一些方法:
// 通过实现 Runnable 接口创建线程
public class DisplayMessage implements Runnable {private String message;public DisplayMessage(String message) {this.message = message;}public void run() {while(true) {System.out.println(message);}}
}
// 通过继承 Thread 类创建线程
public class GuessANumber extends Thread {private int number;public GuessANumber(int number) {this.number = number;}public void run() {int counter = 0;int guess = 0;do {guess = (int) (Math.random() * 100 + 1);System.out.println(this.getName() + " guesses " + guess);counter++;} while(guess != number);System.out.println("** Correct!" + this.getName() + "in" + counter + "guesses.**");}
}
public class ThreadClassDemo {public static void main(String [] args) {Runnable hello = new DisplayMessage("Hello");Thread thread1 = new Thread(hello);thread1.setDaemon(true);thread1.setName("hello");System.out.println("Starting hello thread...");thread1.start();Runnable bye = new DisplayMessage("Goodbye");Thread thread2 = new Thread(bye);thread2.setPriority(Thread.MIN_PRIORITY);thread2.setDaemon(true);System.out.println("Starting goodbye thread...");thread2.start();System.out.println("Starting thread3...");Thread thread3 = new GuessANumber(27);thread3.start();try {thread3.join();}catch(InterruptedException e) {System.out.println("Thread interrupted.");}System.out.println("Starting thread4...");Thread thread4 = new GuessANumber(75);thread4.start();System.out.println("main() is ending...");}
}
2.2 实现 Runnable 接口,重写 run() 方法,调用 start() 方法
创建一个线程,最简单的方法是创建一个实现 Runnable 接口的类,重写 run() 方法,调用 start() 方法。
public class MT02 {public static void main(String[] args) {MyThread02 myThread02 = new MyThread02();// 两个线程共享一个 Runnable 对象Thread t1 = new Thread(myThread02);Thread t2 = new Thread(myThread02);t1.setName("线程A");t2.setName("线程B");t1.start();t2.start();}public static class MyThread02 implements Runnable {@Overridepublic void run() {for (int i = 0; i < 100; i++) {// 获取当前线程对象Thread thread = Thread.currentThread();System.out.println(thread.getName() + "子线程输出" + i);}}}
}
2.3 通过 Callable 和 Future 创建线程
- 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
- 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
- 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
- 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
public class MT03 {public static void main(String[] args) {MyThread03 myThread03 = new MyThread03();// 创建 FutureTask 对象,传入 Callable 对象,用于管理多线程运行结果FutureTask<Integer> task = new FutureTask<>(myThread03);// 创建 Thread 对象,传入 FutureTask 对象,用于启动线程new Thread(task).start();try {// 获取线程运行结果Integer sum = task.get();System.out.println("sum = " + sum);} catch (Exception e) {e.printStackTrace();}}public static class MyThread03 implements Callable<Integer> {@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 0; i < 100; i++) {sum += i;}return sum;}}
}
2.4 创建线程的三种方式的对比
- 采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。
- 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。
3 多线程中常用的成员方法
方法名称 | 说明 |
String getName() | 返回此线程的名称 |
void setName(String name) | 设置线程名称(构造方法也可以设置) |
static Thread currentThread() | 获取当前线程的对象 |
static void sleep(long time) | 让线程休眠指定时间,单位为毫秒 |
setPriority(int newPriority) | 设置线程的优先级 |
final int getPriority() | 获取线程的优先级 |
final void setDaemon(boolean on) | 设置为守护线程 |
public static void yield() | 出让线程/礼让线程 |
public static void join() | 插入线程/插队线程 |
3.1 线程的优先级
每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
设置线程优先级(1-10),数值越小优先级越高,越先执行完毕。
public class MT04 {public static void main(String[] args) {MyThread04 myThread04 = new MyThread04();Thread t1 = new Thread(myThread04, "线程A");Thread t2 = new Thread(myThread04, "线程B");// 设置线程优先级,数值越小优先级越高,越先执行完毕t1.setPriority(10);t2.setPriority(1);t1.start();t2.start();}public static class MyThread04 implements Runnable {@Overridepublic void run() {for (int i = 0; i < 100; i++) {// Thread.currentThread() 获取当前线程对象System.out.println(Thread.currentThread().getName());}}}
}
3.2 守护线程
设置为守护线程,当主线程结束时,守护线程也会陆续结束(并不会立即结束)
public class MT05 {public static void main(String[] args) {MyThread0501 t1 = new MyThread0501();MyThread0502 t2 = new MyThread0502();t1.setName("线程A");t2.setName("线程B");// 设置为守护线程,当主线程结束时,守护线程也会陆续结束t2.setDaemon(true);t1.start();t2.start();}public static class MyThread0501 extends Thread {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(getName() + " " + i);}}}public static class MyThread0502 extends Thread {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(getName() + " " + i);}}}
}
3.3 礼让线程
礼让线程,使结果尽量均匀。让出 CPU 执行权,使线程重新回到就绪状态
public class MT06 {public static void main(String[] args) {MyThread06 t1 = new MyThread06();MyThread06 t2 = new MyThread06();t1.setName("线程A");t2.setName("线程B");t1.start();t2.start();}public static class MyThread06 extends Thread {@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(getName() + " " + i);// 礼让线程,使结果尽量均匀。让出 CPU 执行权,使线程重新回到就绪状态Thread.yield();}}}
}
3.4 插入线程
主线程等待子线程执行完毕再执行
public class MT07 {public static void main(String[] args) throws InterruptedException {MyThread07 t = new MyThread07();t.setName("子线程");t.start();// 主线程等待子线程执行完毕t.join();for (int i = 0; i < 10; i++) {System.out.println("主线程" + i);}}public static class MyThread07 extends Thread {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(getName() + " " + i);}}}
}
4 线程的生命周期
线程是一个动态执行的过程,它也有一个从产生到死亡的过程。
下图显示了一个线程完整的生命周期。
4.1 新建状态
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
4.2 就绪状态
当线程对象调用了 start() 方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待 JVM 里线程调度器的调度。
运行状态:如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
4.3 阻塞状态
如果一个线程执行了 sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
- 等待阻塞
运行状态中的线程执行 wait ( ) 方法,使线程进入到等待阻塞状态。
- 同步阻塞
线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
- 其他阻塞
通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当 sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
4.4 死亡状态
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
5 线程安全问题
5.1 概念
线程安全问题是在多线程编程中出现的一类常见问题。当多个线程同时访问共享的资源(比如变量、内存、文件等),可能会导致不可预期的结果或者程序崩溃。这种情况被称为线程安全问题。
主要的线程安全问题有以下几种:
- 竞态条件(Race Condition):当多个线程竞争访问一个共享资源,并且执行的顺序不确定时,就可能出现竞态条件。这可能导致数据的错误读写,因为线程之间的交错执行可能导致意外的结果。
- 死锁(Deadlock):多个线程持有一些资源,并且在等待其他线程释放它们所持有的资源,导致所有线程都无法继续执行的情况。这会导致程序永久性地停滞。
- 活锁(Livelock):类似于死锁,但线程不会真正地阻塞,而是一直重试相同的操作,导致程序无法继续执行。
- 数据竞争(Data Race):多个线程同时访问并修改相同的共享数据,其中至少有一个线程是写入操作,可能会导致数据被破坏或者得到不正确的结果。
在 Java 中,以下代码创建三个线程,模拟三个窗口卖票,由于 ticket 是共享资源,所以产生了线程不安全问题。由运行结果可以发现,三个窗口卖了同一张票的情况。
public class MT08 {public static void main(String[] args) {// 创建三个线程,模拟三个窗口卖票MyThread08 t1 = new MyThread08();MyThread08 t2 = new MyThread08();MyThread08 t3 = new MyThread08();t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}public static class MyThread08 extends Thread {// 一共有30张票int ticket = 30;@Overridepublic void run() {// 一直卖票,直到卖完(票数<=0)while (ticket > 0) {System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket + "张票");ticket--;}}}
}
5.2 解决线程安全的策略
为了解决线程安全问题,通常采取以下策略:
- 互斥锁(Mutex):使用互斥锁来保护共享资源,确保每次只有一个线程能够访问资源。这样可以避免竞态条件和数据竞争。
- 条件变量(Condition Variable):用于线程间的通信和同步,确保线程在特定条件下才执行某些操作。
- 原子操作(Atomic Operation):对于简单的数据类型,可以使用原子操作来避免数据竞争,保证操作的原子性。
- 信号量(Semaphore):用于控制同时访问某个资源的线程数量。
- 避免共享数据:尽量避免多个线程之间共享数据,采用线程本地存储(Thread Local Storage)等方法来避免竞争。
在多线程编程中,正确处理线程安全问题非常重要,否则可能会导致程序的不稳定性和不可预测的结果。
5.2.1 同步代码块
把操作共享数据的代码锁起来。
特点:
- 锁默认打开,当有一个线程进去,锁自动关闭
- 当里面的代码全部执行完毕,线程出来,锁自动打开
synchronized(锁){操作共享数据的代码
}
在 Java 中,以下代码创建三个线程,模拟三个窗口卖票,由于 ticket 是共享资源,所以产生了线程不安全问题。
解决方法是:创建一个静态对象(一定要是唯一的),使用 synchronized 给操作共享数据 ticket 的代码块加锁。假设当 t1 获得 CPU 资源执行代码,因为加了 synchronized,同一时刻只能有一个线程进入,所以当 t1 在执行代码时,其他线程为等待状态,直至 t1 执行完成,锁释放,t1、t2、t3 再次竞争 CPU 资源,以此类推。
public class MT08 {public static void main(String[] args) {// 创建三个线程,模拟三个窗口卖票MyThread08 t1 = new MyThread08();MyThread08 t2 = new MyThread08();MyThread08 t3 = new MyThread08();t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}public static class MyThread08 extends Thread {// 一共有30张票// 为什么要用静态变量?因为静态变量是唯一的,所有线程共享static int ticket = 30;// 创建一个静态对象(一定要是唯一的),用于同步static final Object obj = new Object();@Overridepublic void run() {// 一直卖票,直到卖完(票数<=0)while (true) {// 用 synchronized 修饰的代码块,同一时刻只能有一个线程进入synchronized (obj) {if (ticket <= 0) {break;}// 打印当前窗口和票号System.out.println(getName() + "卖出了第" + ticket + "张票");ticket--;}}}}
}
5.2.2 同步方法
将执行共享数据的代码拆分成方法,加上 synchronized 关键字。
public class MT09 {public static void main(String[] args) {// 创建三个线程,模拟三个窗口卖票MyThread09 myThread09 = new MyThread09();new Thread(myThread09, "窗口1").start();new Thread(myThread09, "窗口2").start();new Thread(myThread09, "窗口3").start();}public static class MyThread09 implements Runnable {static int ticket = 100;@Overridepublic void run() {while (true) {if (!sellTicket()) break;}}private synchronized boolean sellTicket() {if (ticket <= 0) {return false;}System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket + "张票");ticket--;return true;}}
}
5.2.3 Lock 锁
创建了三个线程并启动它们,这三个线程都使用 myThread10 对象作为运行目标,模拟三个窗口卖票。这里的线程创建和启动使用了匿名类的方式。
为了确保多个线程能够安全地访问和修改 ticket 变量,代码创建了一个 Lock 对象 lock,使用 ReentrantLock 实现。ReentrantLock 是 Java 中提供的一种可重入锁,它比传统的 synchronized 关键字提供了更多灵活的锁操作。
在 run 方法中,进入一个无限循环,每次循环都会尝试获取锁 lock.lock(),如果锁没有被其他线程占用,当前线程就可以进入临界区执行代码。
首先,检查是否已经卖完所有票,即 ticket > 100,如果是,就通过 break 跳出循环,终止线程执行。
如果票还没卖完,当前线程将输出卖出的票号,并将 ticket 自增。然后释放锁 lock.unlock(),使得其他线程有机会获取锁并执行相同的操作。
需要注意的是,使用 ReentrantLock 显式加锁和解锁的方式是比较繁琐的。在实际开发中,可以考虑使用更高级的并发工具,如 java.util.concurrent 包中的 ExecutorService、ThreadPoolExecutor 等,它们可以更方便地管理多线程的执行和资源共享。
public class MT10 {public static void main(String[] args) {// 创建三个线程,模拟三个窗口卖票MyThread10 myThread10 = new MyThread10();new Thread(myThread10, "窗口1").start();new Thread(myThread10, "窗口2").start();new Thread(myThread10, "窗口3").start();}public static class MyThread10 implements Runnable {static int ticket = 1;// 创建一个 Lock 锁,用于同步static Lock lock = new ReentrantLock();@Overridepublic void run() {while (true) {lock.lock();if (ticket > 100) {break;}System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket + "张票");ticket++;lock.unlock();}}}
}
6 死锁
线程死锁是指两个或多个线程彼此持有对方所需的资源,并且由于每个线程都在等待对方释放资源而无法继续执行的情况。简单来说,线程死锁是多个线程之间形成了一个相互等待对方的局面,导致它们都无法继续执行下去。
线程死锁通常涉及两个或多个不同的资源,并且线程之间按照不同的顺序请求这些资源,从而导致死锁。为了更好地理解线程死锁,我们可以使用以下经典示例,其中涉及两个线程和两个资源:
假设有两个资源 A 和 B,以及两个线程 thread1 和 thread2。这两个线程执行以下步骤:
- thread1 锁定资源 A,并尝试访问资源 B。
- 同时,thread2 锁定资源 B,并尝试访问资源 A。
现在,由于线程1已经锁定了资源 A,并且正在等待资源 B 的释放,而线程2已经锁定了资源 B,并且正在等待资源 A 的释放。由于它们互相等待对方释放资源,所以它们都无法继续执行,形成了一个死锁状态。
简单来说,线程死锁需要满足以下四个条件:
- 互斥条件:至少有一个资源必须处于互斥状态,即一次只能由一个线程占有。
- 请求与保持条件:线程持有至少一个资源,并且在等待获取其他线程持有的资源。
- 不可剥夺条件:资源只能由持有它的线程释放,其他线程无法强行抢占。
- 环路等待条件:存在一个等待链,使得每个线程都在等待下一个线程持有的资源。
当这四个条件同时满足时,就可能发生线程死锁。
线程死锁是一个非常棘手的问题,需要仔细设计和谨慎处理多线程编程中的资源获取和释放顺序,以避免出现死锁情况。通常使用合适的同步机制、资源分配策略和避免循环等待等方法来预防和解决线程死锁问题。
7 生产者和消费者
生产者-消费者问题是一个经典的多线程编程问题,涉及到生产者线程向共享资源中放入数据(生产),而消费者线程从共享资源中取出数据(消费)。为了保证线程之间的协作和同步,我们需要使用线程唤醒机制(Thread signaling mechanism),通常结合使用 wait()、notify() 和 notifyAll() 方法来实现。
在 Java 中,可以使用 Object 类的以下方法来实现线程的等待和唤醒:
- wait(): 当一个线程调用某个对象的 wait() 方法时,它会释放该对象的锁,并且进入等待状态,直到其他线程调用了该对象的 notify() 或 notifyAll() 方法来唤醒它。
- notify(): 当一个线程调用某个对象的 notify() 方法时,它会唤醒正在等待该对象的锁的一个线程(无法确定唤醒哪个线程,由 JVM 决定)。
- notifyAll(): 当一个线程调用某个对象的 notifyAll() 方法时,它会唤醒正在等待该对象的锁的所有线程,让它们竞争锁的获取。
下面是一个简单的生产者-消费者示例代码,使用了 wait() 和 notify() 方法来实现线程的等待和唤醒:
在代码中,ProducerConsumerExample 类表示共享资源,produce() 方法用于生产数据,consume() 方法用于消费数据。dataAvailable 表示共享资源中是否有数据可供消费。
使用 synchronized 关键字锁定共享资源,并在 while 循环中使用 wait() 方法进行等待和 notify() 方法进行唤醒,确保生产者和消费者之间的协作和同步。
注意,实际中可能需要考虑更多的复杂情况和线程安全性。在实际开发中,也可以使用 BlockingQueue 等线程安全的数据结构来简化生产者-消费者模型的实现。
public class ProducerConsumerExample {private final Object lock = new Object();private int sharedResource;private boolean dataAvailable;public void produce() {synchronized (lock) {while (dataAvailable) {try {lock.wait(); // 等待消费者消费数据} catch (InterruptedException e) {e.printStackTrace();}}// 生产数据sharedResource++;System.out.println("Produced: " + sharedResource);dataAvailable = true;lock.notify(); // 唤醒消费者线程}}public void consume() {synchronized (lock) {while (!dataAvailable) {try {lock.wait(); // 等待生产者生产数据} catch (InterruptedException e) {e.printStackTrace();}}// 消费数据System.out.println("Consumed: " + sharedResource);dataAvailable = false;lock.notify(); // 唤醒生产者线程}}public static void main(String[] args) {ProducerConsumerExample example = new ProducerConsumerExample();Thread producerThread = new Thread(() -> {for (int i = 0; i < 5; i++) {example.produce();}});Thread consumerThread = new Thread(() -> {for (int i = 0; i < 5; i++) {example.consume();}});producerThread.start();consumerThread.start();}
}
阻塞队列(BlockingQueue)是在 Java 多线程中实现生产者-消费者问题的常用工具之一。阻塞队列是一种线程安全的数据结构,它具有自动阻塞等待的特性,当队列为空时,消费者线程将被阻塞,直到队列中有新的数据可供消费;当队列已满时,生产者线程将被阻塞,直到队列有空间可供新的数据存放。
Java 提供了 java.util.concurrent 包下的 BlockingQueue 接口,并提供了几种不同的实现,如 ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue 等。使用阻塞队列可以简化生产者-消费者模型的实现,无需手动使用 wait() 和 notify() 方法,线程的等待和唤醒将由阻塞队列自动处理。
以下是使用 BlockingQueue 实现生产者-消费者模型的示例代码:
在代码中,使用 ArrayBlockingQueue 作为阻塞队列的实现,它有一个固定的容量为 5。生产者线程使用 put() 方法向队列中放入数据,如果队列已满,它会被阻塞等待。消费者线程使用 take() 方法从队列中取出数据,如果队列为空,它会被阻塞等待。这样,生产者和消费者线程之间的同步将由阻塞队列自动处理,简化了代码实现,并提供了更好的可读性和线程安全性。
javaCopy code
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ArrayBlockingQueue;public class ProducerConsumerExample {private final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);public void produce() {try {for (int i = 0; i < 5; i++) {int data = i + 1;queue.put(data); // 阻塞等待,如果队列已满,则生产者线程会被阻塞System.out.println("Produced: " + data);}} catch (InterruptedException e) {e.printStackTrace();}}public void consume() {try {for (int i = 0; i < 5; i++) {int data = queue.take(); // 阻塞等待,如果队列为空,则消费者线程会被阻塞System.out.println("Consumed: " + data);}} catch (InterruptedException e) {e.printStackTrace();}}public static void main(String[] args) {ProducerConsumerExample example = new ProducerConsumerExample();Thread producerThread = new Thread(() -> {example.produce();});Thread consumerThread = new Thread(() -> {example.consume();});producerThread.start();consumerThread.start();}
}
8 线程池
- 创建一个线程池,初始状态里面是空的
- 提交任务时,线程池会创建新的线程对象,任务执行完毕,线程归还给线程池,下次再提交任务时,不需要创建新的线程,直接复用已有的线程即可
- 但是如果提交任务时,线程池中没有空闲的线程,也无法创建新的线程,任务就会排队等待
在Java中,线程池是一种用于管理和复用线程的机制。线程池通过预先创建一组线程,并维护一个任务队列,以便在需要执行任务时重用这些线程,从而避免了频繁地创建和销毁线程,提高了线程的利用率和系统性能。
Java 提供了 java.util.concurrent 包,其中包含了用于创建线程池的工具类 ExecutorService 和相关接口。ExecutorService 提供了一组方法来提交任务,并在后台管理线程的生命周期。
常见的线程池类型有以下几种:
- FixedThreadPool: 固定大小的线程池,一旦创建就会固定线程数,不会根据任务数量自动调整线程数。
- CachedThreadPool: 可缓存的线程池,线程数根据任务数量动态调整,当有新任务提交时,如果当前线程数不够,会创建新的线程,如果有空闲线程,则复用空闲线程。
- SingleThreadExecutor: 单线程的线程池,只有一个工作线程,用于顺序执行任务。
- ScheduledThreadPool: 定时任务线程池,可以定期执行任务和延迟执行任务。
以下是一个使用 ExecutorService 创建线程池的简单示例:
在代码中,使用 Executors.newFixedThreadPool(3) 创建了一个固定大小为 3 的线程池。然后,提交了 10 个任务给线程池执行。线程池会管理这些任务并自动分配线程来执行它们。最后,通过 executorService.shutdown() 关闭线程池。
使用线程池可以避免手动管理线程的创建和销毁,提高了程序的性能和可维护性。在实际开发中,根据任务类型和需求,可以选择合适的线程池类型和线程数。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class ThreadPoolExample {public static void main(String[] args) {// 创建固定大小的线程池,线程池中有3个线程ExecutorService executorService = Executors.newFixedThreadPool(3);// 提交10个任务给线程池执行for (int i = 0; i < 10; i++) {final int taskNumber = i;executorService.execute(() -> {System.out.println("Task " + taskNumber + " is being executed by " + Thread.currentThread().getName());try {Thread.sleep(1000); // 模拟任务执行时间} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Task " + taskNumber + " is completed by " + Thread.currentThread().getName());});}// 关闭线程池executorService.shutdown();}
}
自定义线程池时,可以使用 Java 中的 ThreadPoolExecutor 类来实现。ThreadPoolExecutor 是 Executor 框架的核心实现之一,它允许你自定义线程池的核心线程数、临时线程数和队列长度等参数。下面是一个示例,展示如何自定义线程池:
在代码中,我们创建了一个自定义的 ThreadPoolExecutor 实例,指定了核心线程数为5,最大线程数为 10,线程空闲时间为 60 秒,队列长度为 20。提交了 30 个任务给线程池执行,当任务数量超过核心线程数时,多余的任务会被放入队列中等待执行。如果队列已满,且线程数未达到最大线程数,新的任务会创建临时线程来执行。当临时线程空闲时间超过设定的 keepAliveTime 时,它们会被回收,从而保持线程数在核心线程数以内。
通过自定义线程池,你可以根据实际需求灵活调整线程池的参数,以满足不同场景下的性能和资源需求。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ArrayBlockingQueue;public class CustomThreadPool {public static void main(String[] args) {// 自定义线程池参数int corePoolSize = 5; // 核心线程数int maxPoolSize = 10; // 最大线程数,包括临时线程long keepAliveTime = 60; // 线程空闲时间,超过这个时间的临时线程会被回收int queueCapacity = 20; // 队列长度,用于存放等待执行的任务// 创建自定义线程池ThreadPoolExecutor customThreadPool = new ThreadPoolExecutor(corePoolSize,maxPoolSize,keepAliveTime,TimeUnit.SECONDS,new ArrayBlockingQueue<>(queueCapacity));// 提交任务给线程池执行for (int i = 0; i < 30; i++) {final int taskNumber = i;customThreadPool.execute(() -> {System.out.println("Task " + taskNumber + " is being executed by " + Thread.currentThread().getName());try {Thread.sleep(1000); // 模拟任务执行时间} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Task " + taskNumber + " is completed by " + Thread.currentThread().getName());});}// 关闭线程池customThreadPool.shutdown();}
}