在当今高并发的业务场景下,如电商秒杀、金融交易、即时通讯等,Java 并发编程能力已成为衡量系统性能与稳定性的关键指标。然而,并发编程也因线程安全、死锁、性能瓶颈等问题,成为开发者面临的一大挑战。本文将从 Java 并发编程的基础原理出发,深入解析核心组件的工作机制,结合实际案例给出问题解决方案,并介绍 Java 新版本中并发相关的优化特性,助力开发者打造高并发、高可用的 Java 应用。
一、Java 并发编程基础原理
1. 线程与进程的关系
在操作系统中,进程是资源分配的基本单位,而线程是程序执行的基本单位。一个进程可以包含多个线程,这些线程共享进程的内存空间(如方法区、堆内存),但拥有各自独立的程序计数器、虚拟机栈和本地方法栈。这种内存共享特性使得线程间通信更加高效,但也带来了线程安全问题。
在 Java 中,通过java.lang.Thread类或实现Runnable接口可以创建线程。例如:
// 实现Runnable接口创建线程
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
Thread thread1 = new Thread(new MyRunnable(), "线程1");
Thread thread2 = new Thread(new MyRunnable(), "线程2");
thread1.start();
thread2.start();
}
}
上述代码创建了两个线程并启动,它们会交替执行run方法中的逻辑,输出各自的计数信息。
2. 线程的生命周期
Java 线程具有五种基本状态,分别是新建状态(New)、就绪状态(Runnable)、运行状态(Running)、阻塞状态(Blocked)和死亡状态(Terminated)。各状态之间的转换关系如下:
- 新建状态:当使用new关键字创建线程对象后,线程处于新建状态,此时线程尚未开始执行。
- 就绪状态:调用线程的start()方法后,线程进入就绪状态,它会等待 CPU 的调度。
- 运行状态:当 CPU 分配时间片给就绪状态的线程后,线程进入运行状态,开始执行run()方法中的逻辑。
- 阻塞状态:在运行过程中,线程可能因等待资源(如 I/O 操作、获取锁失败)或执行sleep()、wait()等方法而进入阻塞状态。阻塞状态的线程释放 CPU 资源,当阻塞原因消除后,线程重新进入就绪状态,等待 CPU 调度。
- 死亡状态:当线程的run()方法执行完毕,或因异常退出run()方法,线程进入死亡状态,此时线程的生命周期结束。
理解线程的生命周期,有助于开发者更好地控制线程的执行流程,避免出现线程状态异常导致的问题。
3. 线程安全的核心要素
线程安全是并发编程的核心问题,它要求多个线程在访问共享资源时,不会出现数据不一致、逻辑错误等问题。实现线程安全需关注三个核心要素:原子性、可见性和有序性。
- 原子性:指一个操作或多个操作要么全部执行成功,要么全部执行失败,不会出现部分执行的情况。例如,i++操作就不是原子性的,它包含读取i的值、将i的值加 1、将结果写回i三个步骤,在多线程环境下可能出现数据不一致。Java 中可通过synchronized关键字或java.util.concurrent.atomic包下的原子类(如AtomicInteger)保证操作的原子性。
- 可见性:指当一个线程修改了共享变量的值后,其他线程能够立即看到该修改。由于 CPU 缓存的存在,线程对共享变量的修改可能先存储在 CPU 缓存中,而未及时刷新到主内存,导致其他线程无法获取最新值。Java 中可通过volatile关键字、synchronized关键字或final关键字保证变量的可见性。
- 有序性:指程序执行的顺序按照代码的先后顺序执行。在 Java 中,为了提高程序性能,编译器和 CPU 可能会对指令进行重排序。重排序在单线程环境下不会影响程序的正确性,但在多线程环境下可能导致逻辑错误。Java 中可通过volatile关键字、synchronized关键字或java.util.concurrent.locks.Lock接口保证指令的有序性。
二、Java 并发核心组件解析
1. synchronized 关键字
synchronized是 Java 中最基本的同步机制,它可以保证被修饰的代码块或方法在同一时间只能被一个线程执行,从而实现线程安全。synchronized的实现基于对象的监视器锁(Monitor),当线程进入synchronized代码块或方法时,会获取对象的监视器锁,执行完毕后释放锁。
synchronized有三种使用方式:
- 修饰实例方法:锁对象是当前对象实例。
- 修饰静态方法:锁对象是当前类的 Class 对象。
- 修饰代码块:锁对象可以是任意对象,通过synchronized (lockObject)指定。
例如:
class SynchronizedDemo {
// 修饰实例方法
public synchronized void instanceMethod() {
// 线程安全的代码逻辑
for (int i = 0; i {
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 修饰静态方法
public static synchronized void staticMethod() {
// 线程安全的代码逻辑
for (int i = 0; i 5; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 修饰代码块
public void codeBlockMethod() {
Object lock = new Object();
synchronized (lock) {
// 线程安全的代码逻辑
for (int i = 0; i 5; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
在 Java 6 及以后版本中,synchronized进行了大量优化,引入了偏向锁、轻量级锁和重量级锁三种锁状态,根据线程竞争情况自动切换锁状态,以提高性能。
2. volatile 关键字
volatile关键字主要用于保证变量的可见性和有序性,但不能保证原子性。当一个变量被volatile修饰后,线程对该变量的修改会立即刷新到主内存,同时其他线程读取该变量时会直接从主内存获取最新值,从而保证可见性。此外,volatile还会禁止编译器和 CPU 对指令进行重排序,保证指令的有序性。
volatile常用于以下场景:
- 状态标志位:用于线程间传递状态信息,如控制线程的启动、停止。
- 单例模式的双重检查锁定:避免因指令重排序导致的单例对象初始化问题。
例如,使用volatile实现线程安全的单例模式:
class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
在上述代码中,volatile修饰instance变量,避免了instance = new Singleton()指令重排序导致的其他线程获取到未初始化完成的instance对象的问题。
3. 线程池
线程池是 Java 中管理线程的重要组件,它通过预先创建一定数量的线程,将任务提交给线程池执行,避免了频繁创建和销毁线程带来的性能开销。Java 中通过java.util.concurrent.ExecutorService接口及其实现类(如ThreadPoolExecutor)来实现线程池。
ThreadPoolExecutor的核心构造参数如下:
- corePoolSize:线程池的核心线程数,即使线程处于空闲状态,也不会被销毁(除非设置了allowCoreThreadTimeOut)。
- maximumPoolSize:线程池的最大线程数,当核心线程都在忙,且任务队列已满时,线程池会创建新线程,直到线程数达到maximumPoolSize。
- keepAliveTime:非核心线程的空闲存活时间,当非核心线程空闲时间超过keepAliveTime后,会被销毁。
- unit:keepAliveTime的时间单位。
- workQueue:任务队列,用于存储等待执行的任务,常用的队列有ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue等。
- threadFactory:线程工厂,用于创建线程,可以自定义线程的名称、优先级等属性。
- handler:拒绝策略,当线程池无法处理新任务时(线程数达到maximumPoolSize且任务队列已满),采取的处理策略,常用的拒绝策略有AbortPolicy(抛出异常)、CallerRunsPolicy(由调用者线程执行任务)、DiscardPolicy(丢弃任务)、DiscardOldestPolicy(丢弃队列中最旧的任务)。
创建线程池的示例代码如下:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
60, // 非核心线程空闲存活时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue // 任务队列
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
// 提交任务
for (int i = 0; i ; i++) {
int finalI = i;
threadPool.submit(() -> {
System.out.println(Thread.currentThread().getName() + "处理任务:" + finalI);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
threadPool.shutdown();
}
}
在实际应用中,应根据业务场景合理配置线程池参数,避免出现线程池过载或资源浪费的情况。例如,对于 CPU 密集型任务,核心线程数可设置为 CPU 核心数加 1;对于 I/O 密集型任务,核心线程数可设置为 CPU 核心数的 2 倍或更高。
4. Lock 接口与 ReentrantLock
java.util.concurrent.locks.Lock接口是 Java 中除synchronized外另一种实现同步的机制,它提供了比synchronized更灵活的功能,如可中断的锁获取、超时锁获取、公平锁等。ReentrantLock是Lock接口的常用实现类,它支持重入性,即同一线程可以多次获取同一把锁。
ReentrantLock的常用方法如下:
- lock():获取锁,如果锁已被其他线程获取,则当前线程阻塞。
- lockInterruptibly():获取锁,如果锁已被其他线程获取,当前线程会阻塞,但可以被中断。
- tryLock():尝试获取锁,如果获取成功返回true,否则返回false,不会阻塞线程。
- tryLock(long time, TimeUnit unit):在指定时间内尝试获取锁,如果获取成功返回true,否则返回false。
- unlock():释放锁,必须在finally块中调用,以确保锁的释放。
使用ReentrantLock实现线程安全的示例代码如下:
import java.util.concurrent.locks.ReentrantLock;
class LockDemo {
private final ReentrantLock lock = new ReentrantLock();
public void doTask() {
lock.lock();
try {
// 线程安全的代码逻辑
for (int i = 0; i ; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
lock.unlock();
}
}
}
public class ReentrantLockDemo {
public static void main(String[] args) {
LockDemo lockDemo = new LockDemo();
Thread thread1 = new Thread(() -> lockDemo.doTask(), "线程1");
Thread thread2 = new Thread(() -> lockDemo.doTask(), "线程2");
thread1.start();
thread2.start();
}
}
与synchronized相比,ReentrantLock的优势在于:
- 支持更灵活的锁获取方式,如可中断、超时获取。
- 可以实现公平锁,即按照线程请求锁的顺序分配锁(需在构造ReentrantLock时传入true)。
- 可以通过getHoldCount()方法获取当前线程获取锁的次数,通过getQueueLength()方法获取等待锁的线程数等,便于监控锁的状态。
但ReentrantLock也存在一些缺点,如需要手动释放锁,若忘记在finally块中调用unlock()方法,可能导致死锁;使用方式相对复杂,容易出现使用错误。
三、Java 并发问题实战解决方案
1. 死锁问题及解决
死锁是并发编程中常见的问题,它指两个或多个线程互相持有对方所需的资源,而又无法释放自己持有的资源,导致所有线程都无法继续执行的情况。
死锁产生的条件
死锁的产生需要满足四个必要条件,只要破坏其中任意一个条件,就可以避免死锁:
- 互斥条件:资源只能被一个线程占用。
- 请求与保持条件:线程在持有部分资源的同时,又请求其他资源。
- 不可剥夺条件:线程已持有的资源不能被其他线程强制剥夺。
- 循环等待条件:多个线程之间形成资源请求的循环链。
死锁示例
class DeadLockDemo {
private static final Object resource1 = new Object();
private static final Object resource2 = new Object();
public static void main(String[] args) {
// 线程1:获取resource1,再获取resource2
new Thread(() -> {
synchronized (resource1) {
System.out.println("线程1获取到resource1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) {
System.out.println("线程1获取到resource2");
}
}
}, "线程1").start();
// 线程2:获取resource2,再获取resource1
new Thread(() -> {
synchronized (resource2) {
System.out.println("线程2获取到resource2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource1) {
System.out.println("线程2获取到resource1");
}
}
}, "线程2").start();
}
}
在上述代码中,线程 1 持有resource1并等待resource2,线程 2 持有resource2并等待resource1,满足死锁的四个条件,导致死锁。
死锁解决方法
- 破坏循环等待条件:规定线程获取资源的顺序,所有线程都按照相同的顺序获取资源。例如,在上述示例中,规定线程必须先获取resource1,再获取resource2,修改后的代码如下:
class DeadLockSolution {
private static final Object resource1 = new Object();
private static final Object resource2 = new Object();
public static void main(String[] args) {
// 线程1:按顺序获取resource1、resource2
new Thread(() -> {
synchronized (resource1) {
System.out.println("线程1获取到resource1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) {
System.out.println("线程1获取到resource2");
}
}
}, "线程1").start();
// 线程2:按顺序获取resource1、resource2
new Thread(() -> {
synchronized (resource1) {
System.out.println("线程2获取到resource1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) {
System.out.println("线程2获取到resource2");
}
}
}, "线程2").start();
}
}
- 使用 tryLock () 方法:通过ReentrantLock的tryLock()方法尝试获取锁,并设置超时时间,若在超时时间内未获取到锁,则释放已持有的资源,避免死锁。
- 使用线程监控工具:如 JDK 自带的jstack命令,可用于查看线程的堆栈信息,定位死锁问题。例如,通过jps命令获取进程 ID,再通过jstack -l 进程ID命令查看线程状态,若存在死锁,会在输出信息中明确标识。
2. 线程安全的集合类选择与使用
在多线程环境下,使用非线程安全的集合类(如ArrayList、HashMap)可能导致数据不一致或ConcurrentModificationException异常。Java 提供了多种线程安全的集合类,可根据业务需求选择合适的集合类。
常用的线程安全集合类
- Vector和Hashtable:通过synchronized修饰方法实现线程安全,但性能较低,在高并发场景下不推荐使用。
- Collections.synchronizedXXX():通过Collections工具类的静态方法将非线程安全的集合类包装成线程安全的集合类,如Collections.synchronizedList(new ArrayList、Collections.synchronizedMap(new HashMap。其实现原理是在集合的方法中添加synchronized同步块,性能与Vector、Hashtable` 类似。
- java.util.concurrent包下的集合类:如ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue等,这些集合类采用了更高效的并发控制机制,性能优于Vector、Hashtable和Collections.synchronizedXXX()包装的集合类。
各线程安全集合类特性与适用场景
- ConcurrentHashMap:
-
- 特性:基于分段锁(Java 7 及以前)或 CAS(Compare And Swap)+ synchronized(Java 8 及以后)实现线程安全,支持高并发的读写操作,读取操作一般不需要加锁(除非读取正在修改的节点),写入操作仅锁定当前操作的节点,提高了并发性能。
-
- 适用场景:高并发的键值对存储场景,如缓存、配置存储等。
- CopyOnWriteArrayList:
-
- 特性:基于 “写时复制”(Copy-On-Write)机制实现线程安全,当进行添加、修改、删除等写操作时,会创建一个新的数组副本,对副本进行操作,操作完成后将原数组引用指向新数组;读取操作直接访问原数组,无需加锁。因此,CopyOnWriteArrayList适合读多写少的场景。
-
- 注意事项:由于写操作会复制数组,存在内存开销较大、数据一致性延迟(读取操作可能获取到旧数据)的问题,不适合写操作频繁或数据量较大的场景。
-
- 适用场景:读多写少的列表场景,如系统配置列表、日志列表等。
- ConcurrentLinkedQueue:
-
- 特性:基于无锁(CAS)机制实现的线程安全队列,支持高并发的入队和出队操作,具有高效的性能和良好的可扩展性。
-
- 适用场景:高并发的队列场景,如任务队列、消息队列等。
线程安全集合类使用示例
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ConcurrentLinkedQueue;
public class ThreadSafeCollectionDemo {
public static void main(String[] args) {
// ConcurrentHashMap示例
ConcurrentHashMap Integer> concurrentHashMap = new ConcurrentHashMap concurrentHashMap.put("a", 1);
concurrentHashMap.put("b", 2);
System.out.println("ConcurrentHashMap:" + concurrentHashMap.get("a"));
// CopyOnWriteArrayList示例
CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList
copyOnWriteArrayList.add("x");
copyOnWriteArrayList.add("y");
System.out.println("CopyOnWriteArrayList:" + copyOnWriteArrayList.get(0));
// ConcurrentLinkedQueue示例
ConcurrentLinkedQueue> concurrentLinkedQueue = new ConcurrentLinkedQueue concurrentLinkedQueue.offer("m");
concurrentLinkedQueue.offer("n");
System.out.println("ConcurrentLinkedQueue:" + concurrentLinkedQueue.poll());
}
}
四、Java 新版本中并发相关的优化特性
1. Java 11 中的并发优化
- var关键字在 lambda 表达式中的使用:Java 11 允许在 lambda 表达式的参数中使用var关键字,简化了代码编写,尤其在复杂的并发场景中,可提高代码的可读性。例如:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Java11LambdaVarDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit((var task) -> {
System.out.println("处理任务:" + task);
}, "任务1");
executorService.shutdown();
}
}
- HTTP/2 客户端的并发支持:Java 11 引入了新的java.net.http包,提供了支持 HTTP/2 的客户端,该客户端支持异步并发请求,可高效地处理大量的 HTTP 请求,适用于微服务间的通信等场景。
2. Java 17 中的并发优化
- 虚拟线程(Project Loom 预览特性):Java 17 中引入了虚拟线程的预览特性(在 Java 21 中正式转正),虚拟线程是轻量级的线程,由 JVM 管理,而非操作系统内核管理,创建和销毁的开销远小于传统的平台线程(操作系统线程)。一个 JVM 可以创建数百万个虚拟线程,而不会对系统资源造成过大压力,极大地提高了并发编程的效率,尤其适合处理大量的 I/O 密集型任务。
使用虚拟线程的示例代码如下(Java 17 中需启用预览特性):
public class VirtualThreadDemo {
public static void main(String[] args) throws InterruptedException {
// 创建虚拟线程并启动
Thread virtualThread = Thread.ofVirtual().start(() -> {
System.out.println("虚拟线程执行:" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
virtualThread.join();
}
}
- 结构化并发(Structured Concurrency,预览特性):Java 17 中还引入了结构化并发的预览特性,它将多个相关的任务组织成一个结构化的单元,当一个任务失败或被取消时,相关的任务也会被自动取消,避免了线程泄漏和资源浪费的问题,简化了并发编程的复杂性。
五、总结与展望
Java 并发编程是一门复杂但重要的技术,掌握它对于开发高并发、高可用的 Java 应用至关重要。本文从基础原理、核心组件、实战解决方案和新版本优化特性四个方面,全面介绍了 Java 并发编程的相关知识。在实际应用中,开发者应根据业务场景选择合适的并发控制机制和组件,关注线程安全、性能优化等问题,避免出现死锁、数据不一致等常见问题。
随着 Java 版本的不断更新,并发编程技术也在持续发展。虚拟线程、结构化并发等新特性的引入,将进一步简化并发编程的复杂性,提高并发性能。未来,Java 并发编程可能会在以下方面得到进一步发展:
- 更高效的并发控制机制:不断优化锁机制、CAS 算法等,进一步提高并发性能。
- 更好的易用性:通过引入更多的高级特性和 API,降低并发编程的门槛,使开发者更容易编写线程安全、高效的并发代码。
- 与新兴技术的融合:如与云原生、大数据、人工智能等技术结合,提供更适合这些场景的并发解决方案。
作为 Java 开发者,应持续关注 Java 并发编程技术的发展趋势,不断学习和实践新的技术和特性,以适应日益复杂的业务需求和技术环境,开发出更加优秀的 Java 应用。
19万+

被折叠的 条评论
为什么被折叠?



