type
status
date
slug
summary
tags
category
icon
password
在现代计算机系统中,多线程(Multithreading)是提升程序性能和响应能力的重要手段。Java作为一种支持多线程编程的语言,提供了丰富的API和工具,使开发者能够高效地创建和管理多线程应用程序。本文将详细介绍Java中的多线程概念、实现方式、线程同步、线程通信、并发工具类以及相关的最佳实践,适合作为学习笔记。
1. 多线程的基本概念
1.1 什么是线程和进程?
- 进程(Process):是操作系统分配资源的基本单位,每个进程拥有独立的内存空间和系统资源。进程之间相互独立,彼此隔离。
- 线程(Thread):是进程中的执行单元,一个进程可以包含多个线程,线程之间共享进程的内存空间和资源。线程是程序执行的最小单位,拥有自己的栈和程序计数器。
1.2 并发与并行
- 并发(Concurrency):指在同一时间段内多个任务交替执行,给人一种同时进行的感觉。在单核处理器上,通过快速切换任务实现并发。
- 并行(Parallelism):指多个任务在同一时刻真正同时执行,通常需要多核处理器的支持。
2. Java 中多线程的实现方式
Java提供了多种方式来实现多线程编程,主要包括继承
Thread
类、实现Runnable
接口、实现Callable
与Future
接口以及使用Executor
框架。2.1 继承 Thread 类
通过继承
java.lang.Thread
类并重写其run()
方法来创建新线程。示例:
优点:
- 简单直观,适合快速创建线程。
缺点:
- 由于Java是单继承,继承
Thread
类后无法继承其他类。
- 不利于资源的共享和复用。
2.2 实现 Runnable 接口
通过实现
java.lang.Runnable
接口并实现其run()
方法,然后将Runnable
实例传递给Thread
对象。示例:
优点:
- 避免了Java的单继承限制,可以同时继承其他类。
- 更适合多个线程共享同一资源。
缺点:
- 相比继承
Thread
类,代码稍微复杂一些。
2.3 实现 Callable 与 Future 接口
Callable
接口类似于Runnable
,但可以返回结果并抛出异常。与Future
接口配合使用,可以获取线程执行的结果。示例:
优点:
- 可以获取线程执行的结果。
- 支持抛出异常。
缺点:
- 需要使用
FutureTask
或其他高级并发工具,代码相对复杂。
2.4 使用 Executor 框架
Executor
框架提供了更高级的线程管理方式,适用于处理大量线程的情况。主要通过ExecutorService
接口及其实现类来管理线程池。示例:
优点:
- 提高性能,重用线程,减少资源开销。
- 提供丰富的线程管理功能,如任务提交、调度、终止等。
- 易于扩展和维护。
缺点:
- 需要理解线程池的工作原理和配置。
3. 线程的生命周期
线程在其生命周期中会经历多个状态,Java中的线程状态由
java.lang.Thread.State
枚举表示:- NEW(新建):线程被创建,但尚未启动。
- RUNNABLE(可运行):线程正在运行或等待操作系统分配CPU资源。
- BLOCKED(阻塞):线程等待获取一个锁。
- WAITING(等待):线程无限期地等待另一个线程来执行特定操作。
- TIMED_WAITING(计时等待):线程等待另一个线程来执行特定操作,等待时间有限。
- TERMINATED(终止):线程已经完成执行或由于异常退出。
线程状态图示意:
4. 线程的优先级与调度
每个线程都有一个优先级,用于指导线程调度器决定线程执行的顺序。Java线程优先级范围为
1
到10
,Thread.MIN_PRIORITY
为1
,Thread.MAX_PRIORITY
为10
,Thread.NORM_PRIORITY
为5
。设置线程优先级:
获取线程优先级:
注意事项:
- 线程优先级只是一个建议,具体的调度行为依赖于操作系统和JVM的实现。
- 不应过度依赖线程优先级,避免引发不可预测的行为。
5. 线程同步
在多线程环境下,多个线程可能会同时访问共享资源,导致数据不一致或其他问题。线程同步机制用于控制线程对共享资源的访问,确保数据的正确性和一致性。
5.1 synchronized 关键字
synchronized
是Java中最基本的同步机制,可以用于方法或代码块,确保同一时间只有一个线程可以执行被synchronized
修饰的代码。同步实例方法:
同步代码块:
优点:
- 简单易用,内置于Java语言。
- 支持可重入锁(一个线程可以多次获取同一锁)。
缺点:
- 锁粒度粗,可能导致性能瓶颈。
- 容易引发死锁等问题。
5.2 显式锁(Lock 接口)
Java提供了
java.util.concurrent.locks.Lock
接口及其实现类(如ReentrantLock
)作为更灵活的同步机制。示例:
优点:
- 更灵活的锁机制,如可中断锁、定时锁等。
- 可以实现公平锁(线程按申请锁的顺序获取锁)。
缺点:
- 需要显式获取和释放锁,易出错。
- 相对于
synchronized
,代码更复杂。
5.3 原子变量(Atomic 包)
java.util.concurrent.atomic
包提供了一系列原子变量类(如AtomicInteger
、AtomicLong
等),通过无锁编程方式实现线程安全。示例:
优点:
- 高性能,避免了锁的开销。
- 适用于简单的计数等场景。
缺点:
- 只能用于单一变量的原子操作,复杂操作仍需使用锁。
- 代码可读性可能较低。
5.4 volatile 关键字
volatile
关键字用于声明变量的可见性,确保一个线程对变量的修改对其他线程立即可见。volatile
变量不会被线程缓存,每次访问都直接从主内存读取。示例:
优点:
- 保证变量的可见性。
- 适用于状态标志等简单场景。
缺点:
- 不能保证复合操作的原子性(如i++)。
- 使用不当可能导致不可预期的行为。
6. 线程间通信
线程间通信用于协调多个线程之间的执行顺序和资源共享。Java提供了多种机制来实现线程间的通信。
6.1 wait、notify 和 notifyAll 方法
这些方法属于
java.lang.Object
类,用于线程间的等待和通知机制。- wait():使当前线程进入等待状态,直到被其他线程唤醒。
- notify():随机唤醒一个正在等待该对象监视器的线程。
- notifyAll():唤醒所有正在等待该对象监视器的线程。
示例:生产者-消费者模型
输出示例:
注意事项:
wait
、notify
、notifyAll
必须在同步块或同步方法中调用。
- 使用
notifyAll
可以避免线程饥饿,但可能导致性能下降。
6.2 Condition 接口
java.util.concurrent.locks.Condition
接口提供了更灵活的线程间通信机制,配合显式锁使用。示例:
优点:
- 可以创建多个条件变量,提高灵活性。
- 避免了使用
Object
的监视器锁带来的局限性。
缺点:
- 需要显式管理锁和条件,代码更复杂。
7. 死锁及其避免
- *死锁(Deadlock)**是指两个或多个线程互相等待对方持有的锁,导致所有线程都无法继续执行。
产生死锁的四个必要条件:
- 互斥条件:资源被一个线程占用,其他线程无法访问。
- 占有且等待:线程持有至少一个资源,并等待获取其他被占用的资源。
- 不可剥夺:资源一旦被分配,不能被强制剥夺,只能由持有它的线程释放。
- 循环等待:形成一个等待资源的环路。
示例:
避免死锁的方法:
- 资源有序分配:为所有资源定义一个全局的顺序,线程按顺序请求资源,避免循环等待。
- 避免占有且等待:线程在请求资源前,释放已持有的资源。
- 使用超时机制:在获取锁时设置超时时间,超时则释放资源,避免永久等待。
- 检测和恢复:定期检测死锁并采取措施,如终止某些线程。
示例:资源有序分配
8. 并发工具类
Java的
java.util.concurrent
包提供了丰富的并发工具类,简化了多线程编程,提升了开发效率。8.1 CountDownLatch
CountDownLatch
用于让一个或多个线程等待,直到其他线程完成各自的任务。示例:
输出示例:
8.2 CyclicBarrier
CyclicBarrier
允许一组线程互相等待,直到所有线程都达到某个屏障点,然后继续执行。与CountDownLatch
不同,CyclicBarrier
可以循环使用。示例:
输出示例:
8.3 Semaphore
Semaphore
用于控制同时访问某个特定资源的线程数量。可以用于限流、资源池等场景。示例:
输出示例:
8.4 Exchanger
Exchanger
用于在两个线程之间交换数据,适用于需要对等交换信息的场景。示例:
输出示例:
8.5 Phaser
Phaser
是一个更灵活的同步屏障,可以动态地增加和减少参与的线程。示例:
输出示例:
9. Java 8 及以后版本的并发新特性
Java 8引入了许多新的并发特性和工具,进一步简化了多线程编程。
9.1 CompletableFuture
CompletableFuture
是Java 8引入的一个强大的异步编程工具,支持链式调用、组合、异常处理等。示例:
优点:
- 支持异步编程,非阻塞执行。
- 丰富的组合操作,如
thenApply
、thenCombine
、thenAccept
等。
- 内置的异常处理机制。
缺点:
- 需要理解异步编程模型,学习曲线较陡。
9.2 并行流(Parallel Streams)
Java 8的Streams API支持并行操作,通过并行流(
parallelStream
)可以轻松实现数据的并行处理。示例:
优点:
- 简化并行操作的实现。
- 自动利用多核处理器,提高性能。
缺点:
- 并行流的性能提升依赖于具体任务和硬件环境。
- 可能引发线程安全问题,需谨慎使用。
10. 线程安全的集合类
Java提供了多种线程安全的集合类,确保在多线程环境下的数据一致性和安全性。
10.1 同步集合
通过
Collections.synchronizedXXX
方法将非线程安全的集合包装为线程安全的集合。示例:
优点:
- 简单易用,适用于现有集合的同步需求。
缺点:
- 整个集合被同步,锁粒度粗,可能导致性能瓶颈。
- 在迭代时需要手动同步,容易出错。
10.2 并发集合
java.util.concurrent
包提供了一系列高性能的并发集合类,如ConcurrentHashMap
、CopyOnWriteArrayList
等。常用的并发集合:
- ConcurrentHashMap:线程安全的哈希映射,支持高并发的读写操作。
- CopyOnWriteArrayList:适用于读多写少的场景,写操作会复制底层数组。
- ConcurrentLinkedQueue:无界的线程安全队列,基于链接节点实现。
- BlockingQueue及其实现类:支持阻塞操作的队列,适用于生产者-消费者模型。
示例:ConcurrentHashMap
优点:
- 高性能,适合高并发场景。
- 内部采用细粒度锁或无锁算法,减少锁竞争。
缺点:
- 复杂的内部实现,理解起来较为困难。
- 某些操作(如迭代)可能反映的是弱一致性的视图。
11. 多线程的最佳实践
为了编写高效、健壮和可维护的多线程程序,遵循以下最佳实践是非常重要的。
11.1 优先使用高层次的并发工具
尽量使用
java.util.concurrent
包提供的并发工具类,如ExecutorService
、CountDownLatch
、Semaphore
等,避免手动管理线程和锁。11.2 避免使用共享可变状态
共享可变状态容易引发数据竞争和同步问题。尽量设计无状态或使用不可变对象,减少共享和同步的需求。
11.3 尽量减少同步的范围
只在必要的代码段使用同步,避免锁的持有时间过长,减少锁竞争和性能瓶颈。
11.4 使用合适的锁机制
根据具体需求选择适当的锁机制,如
ReentrantLock
提供的高级功能,或ReadWriteLock
实现读写分离。11.5 避免死锁
遵循资源有序分配、避免占有且等待等原则,设计线程间的交互,避免产生死锁。
11.6 使用线程池管理线程
使用
ExecutorService
管理线程池,避免频繁创建和销毁线程,提升性能和资源利用率。11.7 适当处理异常
在线程中捕获并处理异常,避免线程意外终止导致资源泄露或程序不稳定。
示例:
11.8 使用不可变对象
不可变对象天然线程安全,减少同步的需求,提高代码的可靠性和可维护性。
示例:
11.9 避免过度使用线程
线程的创建和上下文切换是有开销的,避免过度创建线程,合理配置线程池的大小,提升系统的整体性能。
12. 示例代码
以下示例综合展示了多线程编程的各个方面,包括线程创建、同步、线程间通信、并发工具类等。
输出示例:
解释:
- ExecutorService 管理线程:使用线程池提交任务,并通过
Future
获取任务结果。
- 线程同步:通过显式锁和原子变量保证计数器的线程安全。
- 线程间通信(生产者-消费者):使用
BlockingQueue
实现生产者放入和消费者取出数据的同步。
- CountDownLatch:等待多个任务完成后,主线程继续执行。
- CompletableFuture:异步执行任务,并在完成后处理结果。
13. 总结
多线程编程是Java中的重要特性,能够显著提升应用程序的性能和响应能力。然而,多线程编程也带来了复杂性,如线程同步、死锁、竞态条件等问题。通过合理使用Java提供的多线程工具和并发框架,遵循最佳实践,可以有效地管理多线程环境,编写高效、可靠的并发程序。
关键要点:
- 理解线程的基本概念和生命周期。
- 掌握不同的线程创建和管理方式,如继承
Thread
类、实现Runnable
和Callable
接口、使用Executor
框架等。
- 学习线程同步机制,包括
synchronized
、显式锁、原子变量和volatile
关键字。
- 掌握线程间通信的方法,如
wait
/notify
、Condition
接口。
- 了解并发工具类,如
CountDownLatch
、CyclicBarrier
、Semaphore
等。
- 避免常见的并发问题,如死锁、线程安全问题。
- 利用Java 8及以后版本的新特性,提升并发编程的效率和简洁性。
通过持续的实践和学习,可以深入理解和掌握Java中的多线程编程,为开发高性能、高可用的应用程序奠定坚实的基础。
- 作者:Maple
- 链接:https://mapleleaf.space/Coding/Java/Multi-Thread
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。