技术面:Java并发(上下文切换、线程安全、并发与并行、守护线程、虚拟线程)

在多核处理器普及的今天,并发编程已成为后端开发的必备技能。Java作为企业级应用的主流语言,其并发机制经历了从线程到锁、再到虚拟线程的演进。本文将从实战角度,深入剖析Java并发的核心概念,并通过可运行的代码示例帮助你理解这些技术点。### 并发与并行:概念辨析与实战并发(Concurrency)和并行(Parallelism)常被混淆。简单来说,并发是多个任务在同一时间段内交替执行,而并行是多个任务在同一时刻同时执行。Java通过Thread类或ExecutorService来实现并发,而并行则依赖多核CPU。以下代码演示了并发与并行的区别:通过单线程模拟并发(时间片轮转),以及多线程模拟并行(实际同时运行)。javaimport java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class ConcurrencyVsParallelism { public static void main(String[] args) { // 演示并发:单线程交替执行任务 System.out.println("=== 并发演示(单线程时间片轮转) ==="); Runnable task1 = () -> { for (int i = 0; i < 3; i++) { System.out.println("任务1 - 第" + (i+1) + "次执行"); // 模拟I/O阻塞,让出CPU try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } }; Runnable task2 = () -> { for (int i = 0; i < 3; i++) { System.out.println("任务2 - 第" + (i+1) + "次执行"); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } }; // 使用两个线程实现并发(实际是交替执行) Thread t1 = new Thread(task1); Thread t2 = new Thread(task2); t1.start(); t2.start(); // 等待两个线程结束 try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } // 演示并行:使用线程池让多个任务同时运行(需多核CPU) System.out.println("\n=== 并行演示(线程池多核执行) ==="); ExecutorService executor = Executors.newFixedThreadPool(2); executor.submit(() -> { for (int i = 0; i < 3; i++) { System.out.println("并行任务A - 第" + (i+1) + "次执行"); try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } }); executor.submit(() -> { for (int i = 0; i < 3; i++) { System.out.println("并行任务B - 第" + (i+1) + "次执行"); try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } }); executor.shutdown(); }}运行结果分析:在单核CPU上,两个线程会交替输出(并发);在多核CPU上,两个线程可能同时输出(并行)。核心区别在于是否真正同时执行。### 上下文切换:代价与优化上下文切换是指CPU从一个线程切换到另一个线程时,保存当前线程状态并加载新线程状态的过程。频繁切换会带来性能开销,如缓存失效、寄存器保存等。在Java中,可以通过减少锁竞争、使用无锁数据结构来降低上下文切换。以下代码演示了上下文切换对性能的影响:高竞争锁导致频繁切换。javaimport java.util.concurrent.atomic.AtomicInteger;public class ContextSwitchDemo { private static final int THREAD_COUNT = 10; private static final int ITERATIONS = 100000; // 使用synchronized锁导致高竞争 private static int counter = 0; // 使用无锁AtomicInteger减少上下文切换 private static AtomicInteger atomicCounter = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { // 测试synchronized版本(高上下文切换) long start = System.currentTimeMillis(); Thread[] threads = new Thread[THREAD_COUNT]; for (int i = 0; i < THREAD_COUNT; i++) { threads[i] = new Thread(() -> { for (int j = 0; j < ITERATIONS; j++) { synchronized (ContextSwitchDemo.class) { counter++; // 锁竞争导致线程阻塞和切换 } } }); threads[i].start(); } for (Thread t : threads) t.join(); long syncTime = System.currentTimeMillis() - start; System.out.println("synchronized版本耗时: " + syncTime + "ms, 结果: " + counter); // 测试AtomicInteger版本(无锁,减少上下文切换) start = System.currentTimeMillis(); for (int i = 0; i < THREAD_COUNT; i++) { threads[i] = new Thread(() -> { for (int j = 0; j < ITERATIONS; j++) { atomicCounter.incrementAndGet(); // CAS操作,无锁竞争 } }); threads[i].start(); } for (Thread t : threads) t.join(); long atomicTime = System.currentTimeMillis() - start; System.out.println("AtomicInteger版本耗时: " + atomicTime + "ms, 结果: " + atomicCounter.get()); }}运行结果分析:synchronized版本因线程阻塞导致频繁上下文切换,而AtomicInteger基于CAS(比较并交换)无锁操作,显著减少了切换次数,性能提升明显。### 线程安全:从原理到实战线程安全指多个线程访问共享数据时,不会出现数据不一致或脏读。Java中通过synchronizedLockvolatile、原子类等实现。以下代码展示了一个典型的线程安全问题——竞态条件,并给出解决方案。javaimport java.util.concurrent.locks.ReentrantLock;public class ThreadSafetyDemo { private static int sharedCounter = 0; // 非线程安全 private static final ReentrantLock lock = new ReentrantLock(); // 可重入锁 public static void main(String[] args) throws InterruptedException { // 不安全版本:多线程同时修改导致数据不一致 Thread[] threads = new Thread[5]; for (int i = 0; i < 5; i++) { threads[i] = new Thread(() -> { for (int j = 0; j < 1000; j++) { sharedCounter++; // 非原子操作:读取-修改-写入 } }); threads[i].start(); } for (Thread t : threads) t.join(); System.out.println("不安全版本结果(期望5000): " + sharedCounter); // 安全版本:使用锁保护共享变量 sharedCounter = 0; for (int i = 0; i < 5; i++) { threads[i] = new Thread(() -> { for (int j = 0; j < 1000; j++) { lock.lock(); try { sharedCounter++; // 原子操作,线程安全 } finally { lock.unlock(); } } }); threads[i].start(); } for (Thread t : threads) t.join(); System.out.println("安全版本结果(期望5000): " + sharedCounter); }}关键点sharedCounter++并非原子操作,它包含读取、加1、写入三步。多线程同时执行时,可能读取到旧值,导致最终结果小于预期。使用ReentrantLocksynchronized确保同一时刻只有一个线程执行该代码块。### 守护线程:后台服务的生命周期守护线程(Daemon Thread)是Java中一种特殊的线程,当所有用户线程结束时,JVM会自动退出,无论守护线程是否运行。典型应用包括垃圾回收、日志记录等后台服务。以下代码演示了守护线程的行为。javapublic class DaemonThreadDemo { public static void main(String[] args) throws InterruptedException { // 创建守护线程:模拟后台日志服务 Thread daemonThread = new Thread(() -> { int count = 0; while (true) { System.out.println("守护线程运行中... 第" + (++count) + "次"); try { Thread.sleep(500); } catch (InterruptedException e) { break; } } }); daemonThread.setDaemon(true); // 设置为守护线程 daemonThread.start(); // 主线程(用户线程)运行3秒后结束 System.out.println("主线程开始工作,3秒后结束"); Thread.sleep(3000); System.out.println("主线程结束,JVM退出,守护线程自动终止"); }}运行结果:守护线程会在主线程结束后自动终止,不会输出“守护线程运行中… 第7次”。注意:守护线程中应避免执行I/O操作或事务,否则JVM退出时可能导致数据丢失。### 虚拟线程:Java 21的革命性特性虚拟线程(Virtual Threads)是JDK 21正式推出的轻量级线程,由JVM管理而非操作系统。它解决了传统平台线程(Platform Thread)资源昂贵、上下文切换开销大的问题。每个虚拟线程仅占用几KB内存,可创建数十万个而不会导致系统崩溃。以下代码演示了虚拟线程的高并发能力:创建10万个虚拟线程执行简单任务。javaimport java.util.concurrent.Executors;public class VirtualThreadDemo { public static void main(String[] args) throws InterruptedException { // 传统平台线程:创建10万个线程会耗尽系统资源 long start = System.currentTimeMillis(); Thread[] platformThreads = new Thread[100000]; for (int i = 0; i < 100000; i++) { platformThreads[i] = new Thread(() -> { // 模拟轻量级计算 int sum = 0; for (int j = 0; j < 10; j++) sum += j; }); platformThreads[i].start(); } for (Thread t : platformThreads) t.join(); long platformTime = System.currentTimeMillis() - start; System.out.println("平台线程耗时: " + platformTime + "ms(可能因资源不足而失败)"); // 虚拟线程:轻松承载10万个任务 start = System.currentTimeMillis(); try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0; i < 100000; i++) { executor.submit(() -> { int sum = 0; for (int j = 0; j < 10; j++) sum += j; }); } } // 自动等待所有任务完成 long virtualTime = System.currentTimeMillis() - start; System.out.println("虚拟线程耗时: " + virtualTime + "ms"); }}注意事项:运行平台线程版本时,可能会因为系统线程资源限制而抛出OutOfMemoryError。虚拟线程版本则能顺利执行,且耗时更短(尤其在I/O密集型场景下优势更明显)。虚拟线程适合高并发的网络应用、微服务等场景。### 总结Java并发编程经历了从基础线程到高级锁、再到虚拟线程的演进。上下文切换是性能瓶颈的核心,通过无锁编程和虚拟线程可以显著降低切换成本;线程安全需要谨慎处理共享数据的原子性;守护线程适合后台任务;而虚拟线程作为Java并发的新里程碑,为高并发应用提供了轻量级解决方案。在实际开发中,建议:- 优先使用虚拟线程(JDK 21+),避免手动管理线程池。- 对共享数据采用原子类或ReentrantLock,避免synchronized的过度竞争。- 利用ThreadLocal隔离线程上下文,减少锁的使用。- 合理设置线程池大小(I/O密集型可大些,CPU密集型不宜超过核心数)。掌握这些概念和实战技巧,你就能在技术面试中自信地应对Java并发相关的问题。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐