news 2026/8/25 22:24:14

高并发基石:多线程、守护线程、线程安全、线程同步、互斥锁,一文扫尽!

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
高并发基石:多线程、守护线程、线程安全、线程同步、互斥锁,一文扫尽!

引言

在互联网飞速发展的今天,高并发已经成为系统设计的核心挑战之一。从电商秒杀、社交平台的消息推送,到金融系统的交易处理,无一不考验着系统的并发处理能力。而多线程编程,正是应对高并发场景的基石技术。它允许程序同时执行多个任务,充分利用多核CPU的计算能力,大幅提升系统吞吐量和响应速度。

然而,多线程编程并非易事。线程的创建与销毁、资源共享的冲突、死锁的规避、性能的优化……每一个环节都充满了陷阱。稍有不慎,就可能导致数据不一致、程序崩溃甚至系统雪崩。因此,深入理解多线程、守护线程、线程安全、线程同步、互斥锁等核心概念,是每一位后端开发者、架构师的必修课。

本文将从最基础的线程概念讲起,逐步深入到线程安全机制、同步工具、锁优化,再到实际案例和最佳实践,力求用2万余字的篇幅,将高并发编程的基石知识一网打尽。无论你是初学者还是有一定经验的开发者,都能从中获得启发和收获。


第一章:多线程基础

1.1 进程与线程

在操作系统中,进程是资源分配的最小单位,线程是CPU调度的最小单位。一个进程可以包含多个线程,它们共享进程的资源(如内存、文件句柄等),但每个线程拥有独立的程序计数器、栈和局部变量。

进程的特点:

  • 独立性:进程之间相互隔离,一个进程崩溃通常不影响其他进程。

  • 资源开销大:创建、销毁进程需要分配独立的地址空间,成本较高。

  • 通信复杂:进程间通信(IPC)需要借助管道、消息队列、共享内存等机制。

线程的特点:

  • 轻量性:线程创建、切换的开销远小于进程。

  • 资源共享:同一进程内的线程共享堆内存和方法区,通信方便(只需通过共享变量)。

  • 风险:由于资源共享,多线程同时访问共享数据时容易引发线程安全问题。

1.2 线程的生命周期

在Java中,线程的生命周期由Thread.State枚举定义,共包含6种状态:

  1. NEW(新建):线程对象已创建,但尚未调用start()方法。

  2. RUNNABLE(可运行):线程正在Java虚拟机中执行,但它可能正在等待操作系统分配CPU时间片(就绪状态),或者正在执行(运行状态)。这两种状态在Java中统一为RUNNABLE。

  3. BLOCKED(阻塞):线程试图获取一个锁,而该锁被其他线程持有,进入阻塞状态,直到获得锁。

  4. WAITING(等待):线程调用了Object.wait()、Thread.join()或LockSupport.park()方法,等待另一个线程执行特定操作(如通知或中断)。

  5. TIMED_WAITING(限时等待):线程调用了带超时参数的方法,如Thread.sleep(long)、Object.wait(long)、Thread.join(long)等。

  6. TERMINATED(终止):线程执行完毕或因异常退出。

状态流转图:

text

NEW -> RUNNABLE -> BLOCKED/WAITING/TIMED_WAITING -> RUNNABLE -> TERMINATED

1.3 创建线程的四种方式

1.3.1 继承Thread类

java

public class MyThread extends Thread { @Override public void run() { System.out.println("线程运行中..."); } public static void main(String[] args) { MyThread t = new MyThread(); t.start(); // 启动线程 } }

缺点:Java单继承,如果已经继承了其他类,则无法再继承Thread。

1.3.2 实现Runnable接口

java

public class MyRunnable implements Runnable { @Override public void run() { System.out.println("线程运行中..."); } public static void main(String[] args) { Thread t = new Thread(new MyRunnable()); t.start(); } }

优点:解耦了任务定义与线程执行,且可以继续继承其他类,推荐使用。

1.3.3 实现Callable接口 + FutureTask

Runnable的run()方法没有返回值,也不能抛出受检异常。如果需要线程执行后返回结果,可以使用Callable。

java

public class MyCallable implements Callable<String> { @Override public String call() throws Exception { return "任务执行结果"; } public static void main(String[] args) throws Exception { FutureTask<String> futureTask = new FutureTask<>(new MyCallable()); Thread t = new Thread(futureTask); t.start(); String result = futureTask.get(); // 阻塞获取结果 System.out.println(result); } }
1.3.4 线程池

频繁创建和销毁线程会带来较大开销,线程池通过复用线程来提升性能。

java

ExecutorService executor = Executors.newFixedThreadPool(5); executor.submit(() -> { System.out.println("线程池任务执行"); }); executor.shutdown();

1.4 线程常用方法详解

  • start():启动线程,使其进入RUNNABLE状态,由JVM自动调用run()方法。注意:直接调用run()只是普通方法调用,不会启动新线程。

  • run():线程执行的任务逻辑,需要重写。

  • sleep(long millis):让当前线程进入TIMED_WAITING状态,释放CPU,但不释放锁。

  • yield():提示线程调度器当前线程愿意让出CPU,但调度器可能忽略该提示。线程从运行状态转为就绪状态。

  • join():等待调用join的线程终止。例如,在线程A中调用线程B的join(),则A进入WAITING状态,直到B执行完毕。

  • interrupt():中断线程,设置中断标志。被中断的线程可以在适当时候响应中断(如抛出InterruptedException或检查isInterrupted())。

  • isAlive():判断线程是否存活(已启动且未终止)。

  • setPriority(int priority):设置线程优先级(1-10),但优先级依赖操作系统实现,不一定有效。

1.5 线程优先级

Java线程优先级范围是1~10,默认优先级为5(NORM_PRIORITY)。高优先级线程获得CPU时间片的概率更高,但不能保证绝对顺序。不同操作系统的线程优先级映射可能不同,因此依赖优先级实现业务逻辑是不可靠的。

1.6 守护线程的概念

线程分为两类:用户线程和守护线程(Daemon Thread)。守护线程是一种特殊线程,它在后台运行,为其他线程提供服务。当JVM中只剩下守护线程时,JVM就会退出。典型的守护线程如垃圾回收线程(GC)。

设置守护线程需在线程启动前调用setDaemon(true)

java

Thread daemonThread = new Thread(() -> { while (true) { System.out.println("守护线程运行..."); try { Thread.sleep(1000); } catch (InterruptedException e) {} } }); daemonThread.setDaemon(true); daemonThread.start();

注意:守护线程中打开的资源(如文件、网络连接)可能不会正确关闭,因为JVM退出时不会等待守护线程执行finally块。因此,守护线程不适合执行需要清理资源的任务。


第二章:守护线程深入

2.1 定义与特点

守护线程(Daemon Thread)是一种服务于用户线程的线程,它的生命周期依赖于用户线程。当所有用户线程结束时,守护线程会随JVM一起终止,无论它是否正在执行。守护线程的典型特征是“默默奉献”,比如JVM的垃圾回收线程、内存监控线程等。

特点

  • 当只剩下守护线程时,JVM自动退出。

  • 守护线程的finally块不一定执行(因为JVM可能直接退出)。

  • 守护线程创建的子线程默认也是守护线程。

  • 必须在start()之前调用setDaemon(true),否则会抛出IllegalThreadStateException。

2.2 设置守护线程的方法

java

Thread thread = new Thread(task); thread.setDaemon(true); thread.start();

可以通过thread.isDaemon()检查线程是否为守护线程。

2.3 守护线程的典型应用

  1. 垃圾回收线程:JVM中最著名的守护线程,负责回收不再使用的对象内存。

  2. 心跳检测:在分布式系统中,守护线程可以定期发送心跳包,检测连接是否存活。

  3. 日志清理:定期清理过期日志文件。

  4. 缓存刷新:定时刷新本地缓存。

2.4 注意事项:finally块不一定执行

考虑以下代码:

java

Thread daemon = new Thread(() -> { try { System.out.println("守护线程开始工作"); // 模拟长时间任务 Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println("守护线程finally执行"); } }); daemon.setDaemon(true); daemon.start(); // 主线程很快结束 Thread.sleep(1000); System.out.println("主线程结束");

输出结果可能只有“守护线程开始工作”和“主线程结束”,而“守护线程finally执行”可能不会出现。因为主线程结束后,JVM检测到只剩守护线程,立即退出,不会等待守护线程的finally块执行。

结论:不要在守护线程中执行重要的资源清理操作,如关闭文件、释放数据库连接等。这些操作应该在用户线程中完成,或者通过Runtime.addShutdownHook添加钩子函数。


第三章:线程安全

3.1 什么是线程安全?

当多个线程同时访问一个共享可变变量时,如果没有采取正确的同步措施,就可能导致数据不一致、程序执行错误。如果一个类在单线程环境下表现正确,在多线程环境下也表现正确,那么这个类就是线程安全的。

线程安全的核心问题是:多个线程对共享变量的读写操作相互干扰。这种干扰通常表现为三种情况:

  • 原子性问题:一个操作(或一组操作)被中途打断,导致执行结果不符合预期。例如,i++操作看似一步,但实际包含“读取-修改-写入”三步,可能被其他线程打断。

  • 可见性问题:一个线程对共享变量的修改,另一个线程不能立即看到。这是因为每个线程有自己的工作内存(CPU缓存),修改后可能没有及时刷新到主内存。

  • 有序性问题:编译器和处理器为了提高性能,可能会对指令进行重排序。在多线程环境下,重排序可能导致意想不到的结果。

3.2 线程不安全示例:计数器的错误

java

public class Counter { private int count = 0; public void increment() { count++; // 非原子操作 } public int getCount() { return count; } } // 多线程测试 public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Runnable task = () -> { for (int i = 0; i < 10000; i++) { counter.increment(); } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.getCount()); // 可能小于20000 }

运行多次,结果通常小于20000,因为count++不是原子操作,两个线程可能同时读取到相同的count值,然后各自加1,导致少加一次。

3.3 Java内存模型(JMM)

Java内存模型定义了多线程程序中共享变量的访问规则,旨在屏蔽不同硬件和操作系统的内存访问差异。JMM规定:

  • 所有变量存储在主内存中。

  • 每个线程拥有自己的工作内存,保存了该线程使用的变量的副本。

  • 线程对变量的操作(读取、赋值)必须在工作内存中进行,不能直接读写主内存。

  • 不同线程之间无法直接访问对方工作内存中的变量,需要通过主内存来传递。

三大特性

  1. 原子性:一个或多个操作要么全部执行且不被中断,要么全部不执行。Java中,对基本类型的读写(除long/double)是原子操作,但像i++这种复合操作不是原子的。可以通过synchronized、Lock、Atomic类保证原子性。

  2. 可见性:当一个线程修改了共享变量,其他线程能立即看到这个修改。volatile关键字保证可见性,synchronized和Lock也能保证(通过释放锁前刷新到主内存)。

  3. 有序性:程序执行的顺序按照代码的先后顺序执行。Java允许编译器和处理器对指令重排序,但遵循as-if-serial语义(单线程下不能改变执行结果)。volatile可以禁止指令重排序,synchronized和Lock也能保证其代码块内部有序(但代码块之间依然可能重排)。

3.4 volatile关键字

volatile是轻量级的同步机制,保证:

  • 可见性:写volatile变量后,会强制刷新到主内存;读volatile变量前,会从主内存重新加载。

  • 有序性:禁止指令重排序(通过内存屏障实现)。

适用场景:一个线程写,多个线程读;或者用作状态标志。

java

public class FlagExample { private volatile boolean running = true; public void stop() { running = false; // 写volatile,对其他线程立即可见 } public void run() { while (running) { // 执行任务 } } }

不适用场景:复合操作(如count++),因为volatile不保证原子性。

3.5 实现线程安全的策略

  1. 无状态:如果一个类不包含任何共享变量,那么它是线程安全的。例如,工具方法类(没有成员变量)天生线程安全。

  2. 不可变:对象一旦创建,其状态就不能改变。所有成员变量用final修饰,且不提供修改方法。不可变对象是线程安全的,如String、Integer。

  3. 线程封闭:将共享变量限制在单个线程内访问,就不会发生竞争。常见方式:

    • 栈封闭:局部变量(存在于虚拟机栈,线程私有)。

    • ThreadLocal:每个线程保存自己的副本。

  4. 同步机制:通过锁或原子类保护共享状态。


第四章:线程同步机制

线程同步是为了协调多个线程对共享资源的访问,确保线程安全。Java提供了多种同步机制,从基础的synchronized到丰富的并发工具类。

4.1 synchronized关键字

synchronized是Java内置的锁机制,可以修饰方法或代码块。它保证:

  • 原子性:同一时刻只有一个线程执行同步代码。

  • 可见性:进入同步块前会清空工作内存,从主内存加载最新变量;退出时会将修改刷新到主内存。

  • 有序性:同步块内部的代码不会被重排序到块外。

4.1.1 三种用法
  1. 修饰实例方法:锁对象是当前实例(this)。

    java

    public synchronized void method() { // 同步代码 }
  2. 修饰静态方法:锁对象是当前类的Class对象。

    java

    public static synchronized void staticMethod() { // 同步代码 }
  3. 修饰代码块:可以指定任意对象作为锁。

    java

    public void block() { synchronized (lockObject) { // 同步代码 } }
4.1.2 底层原理

synchronized在JVM中是通过monitorenter和monitorexit指令实现的。每个对象都有一个监视器锁(monitor)。当线程进入同步代码时,尝试获取对象的monitor所有权;如果获取失败,则进入阻塞队列等待。

在Java 6之后,JVM对synchronized进行了大量优化,引入了锁升级机制:

  • 偏向锁:无竞争时,偏向第一个获取锁的线程,避免CAS操作。

  • 轻量级锁:当有另一个线程竞争时,偏向锁升级为轻量级锁,通过自旋尝试获取锁,避免线程阻塞。

  • 重量级锁:如果自旋超过一定次数或线程数较多,则升级为重量级锁,线程真正阻塞,由操作系统调度。

锁升级是单向的:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。

4.1.3 可重入性

synchronized是可重入锁,即一个线程已经获得某个对象的锁,可以再次进入该对象的其他同步方法/代码块。实现原理:每个锁关联一个计数器,当线程第一次获得锁时计数器为1,每次重入增加1,退出同步块时递减,直到0才释放锁。

4.2 ReentrantLock

ReentrantLock是java.util.concurrent.locks包提供的显式锁,实现了Lock接口,具备比synchronized更丰富的功能。

4.2.1 基本用法

java

Lock lock = new ReentrantLock(); lock.lock(); try { // 同步代码 } finally { lock.unlock(); // 必须在finally中释放,避免死锁 }
4.2.2 主要特性
  • 可重入:与synchronized相同。

  • 可中断lock.lockInterruptibly()允许在等待锁时响应中断。

  • 可定时tryLock(long time, TimeUnit unit)尝试获取锁,超时返回false。

  • 公平性:构造函数可指定公平锁(new ReentrantLock(true)),按照线程请求锁的顺序分配,但公平锁会降低吞吐量。

  • 多个条件队列:通过newCondition()可以创建多个条件对象,实现精细的线程等待/唤醒。

4.2.3 Condition示例

java

class BoundedBuffer { final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); final Object[] items = new Object[100]; int putptr, takeptr, count; public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); // 等待不满 items[putptr] = x; if (++putptr == items.length) putptr = 0; count++; notEmpty.signal(); // 通知非空 } finally { lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (count == 0) notEmpty.await(); // 等待不空 Object x = items[takeptr]; if (++takeptr == items.length) takeptr = 0; count--; notFull.signal(); // 通知不满 return x; } finally { lock.unlock(); } } }
4.2.4 ReentrantLock vs synchronized
特性synchronizedReentrantLock
用法隐式,自动释放显式,需手动释放
可中断不支持支持
超时不支持支持
公平性非公平可指定公平
条件等待一个条件(wait/notify)多个Condition
性能优化后与Lock接近略高(特定场景)

选择建议:如果不需要高级功能,优先使用synchronized(简洁、不易出错);如果需要可中断、多条件等,则使用ReentrantLock。

4.3 读写锁 ReentrantReadWriteLock

读写锁维护一对锁:读锁(共享锁)和写锁(独占锁)。它允许多个线程同时读,但写操作必须独占。在读多写少的场景下,可以大幅提升并发性能。

java

ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); Lock readLock = rwLock.readLock(); Lock writeLock = rwLock.writeLock(); // 读操作 readLock.lock(); try { // 读取共享数据 } finally { readLock.unlock(); } // 写操作 writeLock.lock(); try { // 修改共享数据 } finally { writeLock.unlock(); }

锁降级:写锁可以降级为读锁,即持有写锁的线程可以再获取读锁,然后释放写锁,实现数据的可见性保护。但读锁不能升级为写锁,否则可能导致死锁。

4.4 StampedLock(Java 8)

StampedLock是读写锁的改进,支持三种模式:写锁、悲观读锁、乐观读。乐观读不阻塞写操作,通过版本戳判断数据是否被修改。

java

StampedLock lock = new StampedLock(); // 乐观读 long stamp = lock.tryOptimisticRead(); int data = sharedData; if (!lock.validate(stamp)) { // 乐观读失败,升级为悲观读锁 stamp = lock.readLock(); try { data = sharedData; } finally { lock.unlockRead(stamp); } } // 写锁 long stamp = lock.writeLock(); try { sharedData = newValue; } finally { lock.unlockWrite(stamp); }

注意:StampedLock是不可重入的,且不支持条件等待。

4.5 其他同步工具类

4.5.1 CountDownLatch(倒计时门闩)

允许一个或多个线程等待其他线程完成操作。初始化一个计数器,线程完成任务后调用countDown()递减,等待线程调用await()阻塞,直到计数器归零。

java

CountDownLatch latch = new CountDownLatch(5); for (int i = 0; i < 5; i++) { new Thread(() -> { // 执行任务 latch.countDown(); }).start(); } latch.await(); // 等待所有任务完成 System.out.println("所有任务完成");
4.5.2 CyclicBarrier(循环屏障)

让一组线程互相等待,直到所有线程都到达某个公共屏障点,然后继续执行。可以重用。

java

CyclicBarrier barrier = new CyclicBarrier(5, () -> { System.out.println("所有线程到达屏障,执行额外操作"); }); for (int i = 0; i < 5; i++) { new Thread(() -> { System.out.println("线程" + Thread.currentThread().getName() + "开始工作"); try { Thread.sleep((long)(Math.random()*1000)); barrier.await(); // 等待其他线程 System.out.println("线程" + Thread.currentThread().getName() + "继续执行"); } catch (Exception e) {} }).start(); }
4.5.3 Semaphore(信号量)

控制同时访问特定资源的线程数量。通过acquire()获取许可,release()释放许可。

java

Semaphore semaphore = new Semaphore(3); // 最多允许3个线程同时访问 for (int i = 0; i < 10; i++) { new Thread(() -> { try { semaphore.acquire(); System.out.println(Thread.currentThread().getName() + "获取许可,开始工作"); Thread.sleep(1000); } catch (InterruptedException e) {} finally { semaphore.release(); } }).start(); }
4.5.4 Exchanger(交换者)

用于两个线程交换数据。每个线程调用exchange()方法,当两个线程都到达交换点时,交换数据。

java

Exchanger<String> exchanger = new Exchanger<>(); new Thread(() -> { try { String data = "来自线程A的数据"; String received = exchanger.exchange(data); System.out.println("线程A收到: " + received); } catch (InterruptedException e) {} }).start(); new Thread(() -> { try { String data = "来自线程B的数据"; String received = exchanger.exchange(data); System.out.println("线程B收到: " + received); } catch (InterruptedException e) {} }).start();

第五章:互斥锁

互斥锁(Mutex)是最基本的同步机制,保证同一时刻只有一个线程访问共享资源。从广义上讲,synchronized、ReentrantLock都是互斥锁的实现。本章深入探讨锁的实现原理、分类及优化策略。

5.1 锁的实现机制

5.1.1 自旋锁

自旋锁是指线程在尝试获取锁时,如果锁已被占用,线程不会立即阻塞,而是循环等待(自旋),不断检查锁是否释放。自旋的好处是避免了线程上下文切换的开销,适用于锁持有时间很短的场景。缺点:如果锁持有时间长,自旋会浪费CPU。

Java中的自旋锁实现:java.util.concurrent.atomic包下的AtomicReference、AbstractQueuedSynchronizer(AQS)中使用了自旋。

5.1.2 阻塞锁

阻塞锁是指线程在获取锁失败时,会被挂起(进入阻塞状态),由操作系统调度唤醒。线程上下文切换开销较大,但CPU利用率更高。synchronized在升级为重量级锁后,就是阻塞锁。

现代JVM会根据锁的竞争情况自适应选择自旋或阻塞。

5.2 公平锁与非公平锁

  • 公平锁:按照线程请求锁的顺序分配锁,先到先得。优点是避免线程饥饿,缺点是吞吐量较低(因为需要维护队列和唤醒)。

  • 非公平锁:线程在请求锁时,可以插队,直接尝试获取锁,获取不到再进入队列。优点是减少线程唤醒开销,吞吐量高,但可能导致某些线程一直得不到锁(饥饿)。

synchronized是非公平锁。ReentrantLock默认非公平,但可通过构造参数指定公平。

5.3 锁优化技术

5.3.1 锁粒度细化

将一个大锁拆分成多个小锁,减少锁竞争。经典案例:ConcurrentHashMap在Java 7之前使用分段锁(Segment数组),每个Segment独立加锁;Java 8之后使用CAS + synchronized对每个桶加锁,粒度更细。

5.3.2 锁粗化

JVM自动将多个连续的加锁、解锁操作合并成一个范围更大的锁,以减少锁操作次数。例如:

java

for (int i = 0; i < 100; i++) { synchronized (lock) { // 少量操作 } } // JVM可能会优化为: synchronized (lock) { for (int i = 0; i < 100; i++) { // 少量操作 } }
5.3.3 锁消除

JIT编译器通过逃逸分析,发现某个锁对象不可能被其他线程访问,就会消除这个锁。例如,在局部变量上加锁,而该变量不会逃逸出方法。

5.3.4 锁升级

如前所述,synchronized从偏向锁->轻量级锁->重量级锁的升级过程,就是为了在不同竞争程度下获得最佳性能。

5.4 死锁、活锁与饥饿

5.4.1 死锁

两个或多个线程互相持有对方需要的锁,且都不释放,导致所有线程都无法继续执行。

死锁产生的四个必要条件

  1. 互斥:资源一次只能被一个线程占用。

  2. 请求与保持:线程已持有至少一个资源,又请求其他资源,且不释放已有资源。

  3. 不可剥夺:资源只能由持有者主动释放。

  4. 循环等待:存在一个线程等待链,形成环路。

死锁示例

java

Object lockA = new Object(); Object lockB = new Object(); // 线程1 new Thread(() -> { synchronized (lockA) { try { Thread.sleep(100); } catch (Exception e) {} synchronized (lockB) { System.out.println("线程1获得两个锁"); } } }).start(); // 线程2 new Thread(() -> { synchronized (lockB) { try { Thread.sleep(100); } catch (Exception e) {} synchronized (lockA) { System.out.println("线程2获得两个锁"); } } }).start();

避免死锁的方法

  • 按顺序获取锁(破坏循环等待)。

  • 使用定时锁(tryLock),超时后释放已持有的锁。

  • 检测死锁并恢复(如通过jstack分析)。

5.4.2 活锁

线程虽然没有阻塞,但始终无法取得进展,因为一直在重复相同的操作。例如,两个线程互相谦让,都释放自己的锁给对方,结果谁也无法继续。活锁可以通过引入随机等待时间来避免。

5.4.3 饥饿

线程因为无法获得所需资源而一直无法执行。例如,低优先级的线程总是被高优先级线程抢占;或者非公平锁导致某个线程永远抢不到锁。可以通过公平锁来避免饥饿。


第六章:实践案例与常见问题

6.1 单例模式与双重检查锁定(DCL)

单例模式中,懒汉式需要保证线程安全。双重检查锁定是一种常见实现,但需要正确使用volatile。

java

public class Singleton { private static volatile Singleton instance; // volatile防止指令重排序 private Singleton() {} public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 第二次检查 instance = new Singleton(); } } } return instance; } }

为什么需要volatile?
instance = new Singleton()不是原子操作,可分解为:

  1. 分配内存

  2. 初始化对象

  3. 将内存地址赋值给instance

由于指令重排序,可能先执行3再执行2,导致另一个线程在第一次检查时看到instance不为空,却访问到一个未初始化的对象。volatile禁止了重排序,保证对象初始化完成后再赋值。

6.2 生产者消费者模式

生产者消费者模式是多线程协作的经典案例。可以使用wait/notify或BlockingQueue实现。

6.2.1 使用wait/notify

java

class Storage { private final int MAX_SIZE = 10; private LinkedList<Object> list = new LinkedList<>(); public void produce() throws InterruptedException { synchronized (list) { while (list.size() == MAX_SIZE) { list.wait(); // 仓库满,生产者等待 } list.add(new Object()); System.out.println("生产一个,当前库存:" + list.size()); list.notifyAll(); // 唤醒等待的消费者 } } public void consume() throws InterruptedException { synchronized (list) { while (list.isEmpty()) { list.wait(); // 仓库空,消费者等待 } list.remove(); System.out.println("消费一个,当前库存:" + list.size()); list.notifyAll(); // 唤醒等待的生产者 } } }
6.2.2 使用BlockingQueue

BlockingQueue内部已经处理了同步,实现更简洁。

java

BlockingQueue<Object> queue = new LinkedBlockingQueue<>(10); // 生产者 new Thread(() -> { try { queue.put(new Object()); System.out.println("生产一个"); } catch (InterruptedException e) {} }).start(); // 消费者 new Thread(() -> { try { queue.take(); System.out.println("消费一个"); } catch (InterruptedException e) {} }).start();

6.3 线程池详解

线程池通过复用线程,减少创建销毁开销,并有效控制并发数。

6.3.1 ThreadPoolExecutor 参数

java

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
  • corePoolSize:核心线程数,即使空闲也保留。

  • maximumPoolSize:最大线程数。

  • keepAliveTime:非核心线程空闲存活时间。

  • unit:时间单位。

  • workQueue:任务队列,用于存放等待执行的任务。

  • threadFactory:线程工厂,用于创建线程(可自定义命名)。

  • handler:拒绝策略,当队列满且线程数达到最大值时执行。

拒绝策略

  • AbortPolicy:抛出RejectedExecutionException(默认)。

  • CallerRunsPolicy:调用者线程自己执行任务。

  • DiscardPolicy:直接丢弃任务。

  • DiscardOldestPolicy:丢弃队列中最旧的任务,然后重试提交。

6.3.2 线程池工作流程
  1. 提交任务时,如果当前线程数 < corePoolSize,创建新线程执行。

  2. 如果当前线程数 >= corePoolSize,将任务放入workQueue。

  3. 如果workQueue已满,且当前线程数 < maximumPoolSize,创建新线程执行。

  4. 如果workQueue已满且当前线程数 == maximumPoolSize,执行拒绝策略。

  5. 当线程空闲时间超过keepAliveTime,非核心线程会被终止;若允许核心线程超时,可通过allowCoreThreadTimeOut(true)设置。

6.3.3 常用线程池(Executors工具类)
  • newFixedThreadPool(int n):固定大小线程池,队列无限大(LinkedBlockingQueue)。

  • newCachedThreadPool():可缓存线程池,空闲线程60秒回收,队列SynchronousQueue(直接交付)。

  • newSingleThreadExecutor():单线程线程池,保证任务顺序执行。

  • newScheduledThreadPool(int core):定时任务线程池。

警告:生产环境不建议使用Executors创建线程池,因为可能使用无界队列(如FixedThreadPool的队列Integer.MAX_VALUE)或允许创建大量线程(CachedThreadPool最大线程数Integer.MAX_VALUE),容易导致OOM。推荐直接使用ThreadPoolExecutor自定义参数。

6.4 ThreadLocal原理与内存泄漏

ThreadLocal提供了线程局部变量,每个线程拥有自己的副本,互不干扰。

6.4.1 原理

每个Thread内部有一个ThreadLocalMap,键为ThreadLocal实例(弱引用),值为存储的对象。当调用set(value)时,以当前ThreadLocal为键,将值放入当前线程的map中;get()则从map中取出。

6.4.2 内存泄漏问题

ThreadLocalMap的key是弱引用(WeakReference<ThreadLocal>),但value是强引用。如果ThreadLocal对象被回收(因为弱引用),而value还存在,并且线程一直存活(如线程池中的线程),那么value将无法被回收,造成内存泄漏。

解决方法:使用完ThreadLocal后,手动调用remove()方法清除。

java

ThreadLocal<String> tl = new ThreadLocal<>(); try { tl.set("value"); // 使用... } finally { tl.remove(); // 防止内存泄漏 }

6.5 并发容器

6.5.1 ConcurrentHashMap
  • Java 7:采用分段锁(Segment继承ReentrantLock),默认16个Segment,每个Segment独立加锁,支持16个线程并发写。

  • Java 8:放弃分段锁,使用CAS + synchronized对每个桶加锁。当链表长度超过8时,转为红黑树,提高查询效率。size()等方法通过累加计数器实现,无需全局锁。

6.5.2 CopyOnWriteArrayList

适用于读多写少的场景。写操作时,复制一份新数组,在新数组上进行修改,然后将原数组引用指向新数组。读操作不加锁,直接在原数组上读。缺点:写操作开销大,且数据可能有一致性问题(最终一致性)。

6.5.3 BlockingQueue 实现类
  • ArrayBlockingQueue:有界数组队列,公平性可配置。

  • LinkedBlockingQueue:链表实现,可有界或无界。

  • PriorityBlockingQueue:支持优先级排序。

  • DelayQueue:元素只有到达延迟时间才能被取出。

6.6 常见问题模拟

6.6.1 线程不安全示例:i++

前面已经演示过,解决方案:

  • 使用synchronized修饰increment()。

  • 使用AtomicInteger。

  • 使用ReentrantLock。

6.6.2 死锁模拟与诊断

死锁示例代码如5.4.1。诊断死锁:使用jstack pid查看线程堆栈,会发现“Found one Java-level deadlock”提示。

6.6.3 活锁模拟

java

public class LiveLock { static class Spoon { private Diner owner; public Spoon(Diner owner) { this.owner = owner; } public synchronized void use() { System.out.println(owner.name + " 使用勺子"); } public void setOwner(Diner diner) { this.owner = diner; } } static class Diner { private String name; private boolean isHungry; public Diner(String name) { this.name = name; isHungry = true; } public void eatWith(Spoon spoon, Diner spouse) { while (isHungry) { if (spoon.owner != this) { try { Thread.sleep(1); } catch (Exception e) {} continue; } if (spouse.isHungry) { System.out.println(name + " 让给 " + spouse.name); spoon.setOwner(spouse); continue; } spoon.use(); isHungry = false; System.out.println(name + " 吃饱了"); spoon.setOwner(spouse); } } } public static void main(String[] args) { Diner husband = new Diner("丈夫"); Diner wife = new Diner("妻子"); Spoon spoon = new Spoon(husband); new Thread(() -> husband.eatWith(spoon, wife)).start(); new Thread(() -> wife.eatWith(spoon, husband)).start(); } }

两个线程互相谦让,导致谁也无法吃饭。可通过随机等待或优先级解决。


第七章:性能考量与最佳实践

7.1 避免过度同步

同步会带来性能开销(锁竞争、上下文切换)。应尽量减少同步的范围,只同步必要的代码块。例如:

java

// 不好的实践:同步整个方法 public synchronized void process() { // 读取配置(无需同步) // 修改共享变量(需同步) // 记录日志(无需同步) } // 好的实践:只同步必要的部分 public void process() { // 读取配置 synchronized (this) { // 修改共享变量 } // 记录日志 }

7.2 减少锁的持有时间

尽量缩短锁内代码的执行时间,将不需要同步的操作移到锁外。例如,不要在锁内执行耗时操作(如IO、网络请求)。

7.3 选择合适的并发工具

  • 读多写少:使用读写锁、CopyOnWriteArrayList。

  • 计数等待:CountDownLatch、CyclicBarrier。

  • 流量控制:Semaphore。

  • 生产者消费者:BlockingQueue。

  • 原子操作:AtomicInteger、LongAdder(高并发下性能更好)。

7.4 线程池大小设置

  • CPU密集型任务:线程数 = CPU核心数 + 1(避免缺页中断导致CPU空闲)。

  • IO密集型任务:线程数 = CPU核心数 * (1 + 平均等待时间/平均计算时间)。或者使用公式:线程数 = CPU核心数 * (1 + IO耗时/CPU耗时)。

一般经验值:IO密集型可设置为2倍CPU核心数以上。但最佳实践是通过压测逐步调整。

7.5 监控与调试

  • jstack:打印线程堆栈,分析死锁、线程状态。

  • jvisualvm:可视化监控线程数、CPU、内存。

  • Arthas:阿里巴巴开源的Java诊断工具,可以实时查看线程状态、方法调用等。

  • 日志:记录线程池活跃度、任务队列大小等关键指标。

7.6 常见误区

  1. 使用volatile保证原子性:volatile不能保证复合操作原子性,如count++。

  2. 忘记在finally中释放锁:ReentrantLock必须手动释放,否则导致死锁。

  3. 线程池用完不关闭:导致JVM无法退出。

  4. 使用ThreadLocal后不remove:内存泄漏。

  5. 认为单例模式简单:DCL需要volatile,枚举单例更安全。

  6. 滥用线程优先级:依赖操作系统,不可靠。


第八章:总结与展望

8.1 核心回顾

本文从多线程基础出发,系统梳理了以下核心概念:

  • 多线程:进程与线程、生命周期、创建方式、常用方法。

  • 守护线程:定义、设置、应用场景及注意事项。

  • 线程安全:原子性、可见性、有序性,Java内存模型,volatile,线程安全策略。

  • 线程同步:synchronized、ReentrantLock、读写锁、StampedLock,以及CountDownLatch等同步工具。

  • 互斥锁:自旋锁、阻塞锁、公平性、锁优化、死锁等。

  • 实践案例:单例模式、生产者消费者、线程池、ThreadLocal、并发容器。

高并发编程的核心思想是:在保证线程安全的前提下,最大限度地提高并发性能。这需要开发者深入理解底层原理,并结合实际场景灵活运用。

8.2 未来展望

随着硬件和编程语言的发展,并发编程模型也在不断演进:

  • 协程(Coroutine):比线程更轻量,由用户态调度。Go语言的goroutine已经大放异彩。Java正在通过Project Loom引入虚拟线程(Virtual Threads),预计在Java 21及以后版本中成熟。虚拟线程将极大地简化并发编程,开发者可以像编写同步代码一样编写高并发程序,而无需担心线程开销。

  • 响应式编程:通过异步数据流和背压机制,处理高并发场景。如Reactor、RxJava等框架。

  • 无锁编程:基于CAS和硬件支持的原子操作,设计无锁数据结构,避免锁竞争带来的开销。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/14 16:55:30

如何使用php-image-resize实现高质量图片压缩与格式转换

如何使用php-image-resize实现高质量图片压缩与格式转换 【免费下载链接】php-image-resize PHP library to resize, scale and crop images. Cloud solution available at: 项目地址: https://gitcode.com/gh_mirrors/ph/php-image-resize php-image-resize是一款功能强…

作者头像 李华
网站建设 2026/7/14 16:55:31

IPED日志分析工具集成:与Splunk和ELK Stack协同工作

IPED日志分析工具集成&#xff1a;与Splunk和ELK Stack协同工作 【免费下载链接】IPED IPED Digital Forensic Tool. It is an open source software that can be used to process and analyze digital evidence, often seized at crime scenes by law enforcement or in a cor…

作者头像 李华
网站建设 2026/7/14 16:55:29

TIS与大数据分析平台集成:Spark与Flink批流处理协同的终极指南

TIS与大数据分析平台集成&#xff1a;Spark与Flink批流处理协同的终极指南 【免费下载链接】tis Support agile DataOps Based on Flink, DataX and Flink-CDC, Chunjun with Web-UI 项目地址: https://gitcode.com/GitHub_Trending/ti/tis TIS是一个企业级数据集成服务…

作者头像 李华