java多线程详解(java多线程三种方式)
## Java多线程详解### 简介 在单核CPU时代,程序的执行是串行的,即同一时间只能执行一个任务。为了提高效率,多任务操作系统允许多个程序“同时”运行。但实际上,单个CPU核心在每个时间点只能执行一个程序,通过在程序之间快速切换,就营造了一种多任务并行的假象。随着多核CPU的出现,多线程编程应运而生。多线程允许在同一个程序内部,并发执行多个线程,每个线程都像是独立的执行路径,从而充分利用多核CPU的性能,显著提高程序的运行效率。Java作为一门天生支持多线程的语言,为开发者提供了丰富的API和机制来创建、管理和同步线程。本文将详细介绍Java多线程的各个方面,帮助你掌握并发编程的核心概念和技巧。### 一、线程基础#### 1.1 什么是线程?线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。#### 1.2 线程与进程的区别| 特性 | 进程 | 线程 | | -------- | ----------------------------------------------------------- | ----------------------------------------------------------- | | 定义 | 操作系统资源分配的基本单位 | 处理器调度和执行的基本单位 | | 资源 | 拥有独立的地址空间,资源独立 | 共享所属进程的地址空间和资源 | | 创建销毁 | 创建和销毁开销大 | 创建和销毁开销小 | | 通信 | 进程间通信需要借助IPC机制 | 线程间可以直接访问共享变量和方法进行通信 | | 其他 | 一个进程可以包含多个线程 | 线程是轻量级进程 |#### 1.3 Java线程的生命周期Java线程的生命周期包括以下五种状态:
新建(New):
线程对象被创建,但尚未启动。
就绪(Runnable):
线程已启动并等待CPU调度。
运行(Running):
线程正在执行。
阻塞(Blocked):
线程暂时停止执行,等待某个事件发生(例如,获取锁、等待IO)。
死亡(Dead):
线程执行完毕或因异常退出。线程状态之间可以相互转换,具体转换过程如下图所示:![Java线程状态转换图](https://upload-images.jianshu.io/upload_images/2251814-2906c2996e44d84b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)### 二、创建和启动线程Java中创建线程主要有两种方式:#### 2.1 继承Thread类1. 定义一个继承 `java.lang.Thread` 类的子类,并重写 `run()` 方法,该方法定义了线程的执行逻辑。 2. 创建子类实例,即创建线程对象。 3. 调用线程对象的 `start()` 方法启动线程。```java public class MyThread extends Thread {@Overridepublic void run() {System.out.println("MyThread is running.");}public static void main(String[] args) {MyThread thread = new MyThread();thread.start(); } } ```#### 2.2 实现Runnable接口1. 定义一个实现 `java.lang.Runnable` 接口的类,并实现 `run()` 方法。 2. 创建实现类实例。 3. 创建 `Thread` 对象,并将实现类实例作为参数传递给 `Thread` 构造函数。 4. 调用 `Thread` 对象的 `start()` 方法启动线程。```java public class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("MyRunnable is running.");}public static void main(String[] args) {MyRunnable runnable = new MyRunnable();Thread thread = new Thread(runnable);thread.start();} } ```#### 2.3 两种方式的比较
继承Thread类:
简单直接,但如果已经继承了其他类则无法使用。
实现Runnable接口:
更具灵活性,可以避免单继承的限制,更适合资源共享的场景。### 三、线程同步#### 3.1 为什么需要线程同步?当多个线程并发访问共享资源时,可能会导致数据不一致的问题,这就是线程安全问题。为了解决这个问题,需要使用线程同步机制来保证同一时刻只有一个线程访问共享资源。#### 3.2 同步机制Java 提供了多种线程同步机制:
synchronized 关键字:
可以修饰方法或代码块,保证同一时刻只有一个线程进入同步代码块。
修饰实例方法:
锁定的是当前实例对象。
修饰静态方法:
锁定的是当前类的 Class 对象。
修饰代码块:
需要指定锁对象,可以是 this(当前实例对象)或其他对象。
Lock 接口:
提供了更加灵活的锁机制,例如可重入锁、读写锁等。
volatile 关键字:
保证变量的可见性和禁止指令重排序,适用于简单的同步场景。
原子类(Atomic):
提供了原子操作,例如 AtomicInteger、AtomicLong 等,适用于高并发场景下的计数器、序号生成器等。#### 3.3 死锁当两个或多个线程互相持有对方所需的资源,并且都在等待对方释放资源时,就会发生死锁。
死锁的四个必要条件:
1. 互斥条件:一个资源每次只能被一个线程使用。 2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。 3. 不可剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。 4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
避免死锁的方法:
破坏“请求与保持”条件:尽量一次性获取所有需要的资源。
破坏“不可剥夺”条件:尝试获取资源失败时,释放已持有的资源。
破坏“循环等待”条件:对资源进行排序,按照顺序申请资源。### 四、线程间通信线程间通信是指线程之间进行信息交换和协作。#### 4.1 常用通信方式
共享变量:
多个线程可以通过访问同一个共享变量来进行通信,但需要注意线程安全问题。
wait()、notify()、notifyAll():
使用 Object 类中的 wait()、notify()、notifyAll() 方法可以实现线程间的等待和唤醒机制。
BlockingQueue:
阻塞队列提供了一种线程安全的队列数据结构,可以用于生产者-消费者模型的实现。
CountDownLatch:
允许一个或多个线程等待其他线程完成操作。
CyclicBarrier:
允许一组线程互相等待,直到所有线程都到达某个屏障点。### 五、线程池#### 5.1 为什么使用线程池?
降低资源消耗:重复利用已创建的线程,减少线程创建和销毁的开销。
提高响应速度:任务到达时,线程池中的线程可以立即执行,无需等待线程创建。
提高线程可管理性:线程池可以统一管理线程,例如线程数量、线程执行顺序等。#### 5.2 线程池的创建和使用Java 提供了 `java.util.concurrent.Executors` 类来创建线程池:
`newFixedThreadPool(int nThreads)`:创建固定数量线程的线程池。
`newCachedThreadPool()`: 创建一个可缓存的线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
`newScheduledThreadPool(int corePoolSize)`: 创建一个定长线程池,支持定时及周期性任务执行。
`newSingleThreadExecutor()`: 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。```java // 创建一个固定大小为 5 的线程池 ExecutorService executor = Executors.newFixedThreadPool(5);// 提交 10 个任务到线程池 for (int i = 0; i < 10; i++) {executor.execute(() -> {// 任务逻辑}); }// 关闭线程池 executor.shutdown(); ```### 六、总结Java 多线程编程是并发编程的重要组成部分,它可以帮助我们充分利用多核 CPU 的性能,提高程序的运行效率。本文详细介绍了 Java 多线程的基础知识、线程同步、线程间通信以及线程池等内容,希望能够帮助你更好地理解和应用 Java 多线程编程。在实际开发中,我们需要根据具体的需求选择合适的线程同步机制和线程间通信方式,并合理使用线程池来管理线程,以构建高效、稳定的并发程序。
Java多线程详解
简介 在单核CPU时代,程序的执行是串行的,即同一时间只能执行一个任务。为了提高效率,多任务操作系统允许多个程序“同时”运行。但实际上,单个CPU核心在每个时间点只能执行一个程序,通过在程序之间快速切换,就营造了一种多任务并行的假象。随着多核CPU的出现,多线程编程应运而生。多线程允许在同一个程序内部,并发执行多个线程,每个线程都像是独立的执行路径,从而充分利用多核CPU的性能,显著提高程序的运行效率。Java作为一门天生支持多线程的语言,为开发者提供了丰富的API和机制来创建、管理和同步线程。本文将详细介绍Java多线程的各个方面,帮助你掌握并发编程的核心概念和技巧。
一、线程基础
1.1 什么是线程?线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
1.2 线程与进程的区别| 特性 | 进程 | 线程 | | -------- | ----------------------------------------------------------- | ----------------------------------------------------------- | | 定义 | 操作系统资源分配的基本单位 | 处理器调度和执行的基本单位 | | 资源 | 拥有独立的地址空间,资源独立 | 共享所属进程的地址空间和资源 | | 创建销毁 | 创建和销毁开销大 | 创建和销毁开销小 | | 通信 | 进程间通信需要借助IPC机制 | 线程间可以直接访问共享变量和方法进行通信 | | 其他 | 一个进程可以包含多个线程 | 线程是轻量级进程 |
1.3 Java线程的生命周期Java线程的生命周期包括以下五种状态:* **新建(New):** 线程对象被创建,但尚未启动。 * **就绪(Runnable):** 线程已启动并等待CPU调度。 * **运行(Running):** 线程正在执行。 * **阻塞(Blocked):** 线程暂时停止执行,等待某个事件发生(例如,获取锁、等待IO)。 * **死亡(Dead):** 线程执行完毕或因异常退出。线程状态之间可以相互转换,具体转换过程如下图所示:![Java线程状态转换图](https://upload-images.jianshu.io/upload_images/2251814-2906c2996e44d84b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
二、创建和启动线程Java中创建线程主要有两种方式:
2.1 继承Thread类1. 定义一个继承 `java.lang.Thread` 类的子类,并重写 `run()` 方法,该方法定义了线程的执行逻辑。 2. 创建子类实例,即创建线程对象。 3. 调用线程对象的 `start()` 方法启动线程。```java public class MyThread extends Thread {@Overridepublic void run() {System.out.println("MyThread is running.");}public static void main(String[] args) {MyThread thread = new MyThread();thread.start(); } } ```
2.2 实现Runnable接口1. 定义一个实现 `java.lang.Runnable` 接口的类,并实现 `run()` 方法。 2. 创建实现类实例。 3. 创建 `Thread` 对象,并将实现类实例作为参数传递给 `Thread` 构造函数。 4. 调用 `Thread` 对象的 `start()` 方法启动线程。```java public class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("MyRunnable is running.");}public static void main(String[] args) {MyRunnable runnable = new MyRunnable();Thread thread = new Thread(runnable);thread.start();} } ```
2.3 两种方式的比较* **继承Thread类:** 简单直接,但如果已经继承了其他类则无法使用。 * **实现Runnable接口:** 更具灵活性,可以避免单继承的限制,更适合资源共享的场景。
三、线程同步
3.1 为什么需要线程同步?当多个线程并发访问共享资源时,可能会导致数据不一致的问题,这就是线程安全问题。为了解决这个问题,需要使用线程同步机制来保证同一时刻只有一个线程访问共享资源。
3.2 同步机制Java 提供了多种线程同步机制:* **synchronized 关键字:** 可以修饰方法或代码块,保证同一时刻只有一个线程进入同步代码块。* **修饰实例方法:** 锁定的是当前实例对象。* **修饰静态方法:** 锁定的是当前类的 Class 对象。* **修饰代码块:** 需要指定锁对象,可以是 this(当前实例对象)或其他对象。* **Lock 接口:** 提供了更加灵活的锁机制,例如可重入锁、读写锁等。 * **volatile 关键字:** 保证变量的可见性和禁止指令重排序,适用于简单的同步场景。 * **原子类(Atomic):** 提供了原子操作,例如 AtomicInteger、AtomicLong 等,适用于高并发场景下的计数器、序号生成器等。
3.3 死锁当两个或多个线程互相持有对方所需的资源,并且都在等待对方释放资源时,就会发生死锁。**死锁的四个必要条件:**1. 互斥条件:一个资源每次只能被一个线程使用。 2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。 3. 不可剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。 4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。**避免死锁的方法:*** 破坏“请求与保持”条件:尽量一次性获取所有需要的资源。 * 破坏“不可剥夺”条件:尝试获取资源失败时,释放已持有的资源。 * 破坏“循环等待”条件:对资源进行排序,按照顺序申请资源。
四、线程间通信线程间通信是指线程之间进行信息交换和协作。
4.1 常用通信方式* **共享变量:** 多个线程可以通过访问同一个共享变量来进行通信,但需要注意线程安全问题。 * **wait()、notify()、notifyAll():** 使用 Object 类中的 wait()、notify()、notifyAll() 方法可以实现线程间的等待和唤醒机制。 * **BlockingQueue:** 阻塞队列提供了一种线程安全的队列数据结构,可以用于生产者-消费者模型的实现。 * **CountDownLatch:** 允许一个或多个线程等待其他线程完成操作。 * **CyclicBarrier:** 允许一组线程互相等待,直到所有线程都到达某个屏障点。
五、线程池
5.1 为什么使用线程池?* 降低资源消耗:重复利用已创建的线程,减少线程创建和销毁的开销。 * 提高响应速度:任务到达时,线程池中的线程可以立即执行,无需等待线程创建。 * 提高线程可管理性:线程池可以统一管理线程,例如线程数量、线程执行顺序等。
5.2 线程池的创建和使用Java 提供了 `java.util.concurrent.Executors` 类来创建线程池:* `newFixedThreadPool(int nThreads)`:创建固定数量线程的线程池。 * `newCachedThreadPool()`: 创建一个可缓存的线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 * `newScheduledThreadPool(int corePoolSize)`: 创建一个定长线程池,支持定时及周期性任务执行。 * `newSingleThreadExecutor()`: 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。```java // 创建一个固定大小为 5 的线程池 ExecutorService executor = Executors.newFixedThreadPool(5);// 提交 10 个任务到线程池 for (int i = 0; i < 10; i++) {executor.execute(() -> {// 任务逻辑}); }// 关闭线程池 executor.shutdown(); ```
六、总结Java 多线程编程是并发编程的重要组成部分,它可以帮助我们充分利用多核 CPU 的性能,提高程序的运行效率。本文详细介绍了 Java 多线程的基础知识、线程同步、线程间通信以及线程池等内容,希望能够帮助你更好地理解和应用 Java 多线程编程。在实际开发中,我们需要根据具体的需求选择合适的线程同步机制和线程间通信方式,并合理使用线程池来管理线程,以构建高效、稳定的并发程序。