Lazy loaded image
八股盛宴
JUC
字数 10570阅读时长 27 分钟
2025-5-18
2026-1-30
type
Post
status
Published
date
Jan 30, 2026 12:22 PM
slug
summary
Java 并发编程
tags
Java
category
八股盛宴
icon
password

进程、线程

进程

进程是程序运行的执行单位,系统运行程序即是进程创建、运行到消亡的过程。
Java 程序的运行可以被描述为 JVM 进程运行。

线程

线程是一个比进程更小的执行单位,进程在执行过程中可以产生多个线程。 线程和进程最大的区别在于进程是相互独立的,而线程则不一定,同一进程中的线程可能互相影响。 多个线程共享公共资源(进程的堆和方法区),各自持有私有资源(程序计数器、虚拟机栈和本地方法栈等),线程切换仍然产生上下文切换,只不过代价小于进程上下文切换。但是这种方式不利于资源的管理和保护
Java 程序也可以描述为 main 线程和其他若干线程同时运行。
为什么要使用多线程?
  • 提升并发处理能力
  • 改变程序编写方式
 

内存模型

Java Memory Module
Java Thread & System Thread
当 JVM 创建 Java Thread 时,实际上是请求操作系统创建内核线程,这个内核线程由操作系统管理和调度,负责执行 Java 线程任务,Java Thread 通过 JVM 和内核线程交互。
可以说 Java 线程存在于用户空间,并且每个 Java 线程都有一个对应的内核空间线程映射。
所以尽管 Java 支持设置线程优先级,但是其不能作为程序正确性保证,因为操作系统不必理会 Java 对于线程优先级的设定。
Windows 和 Linux 中 Java Thread 和 System Thread 1:1 映射。
重排序
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
JMM 保证:
重排序遵循 as-if-serial 语义保证单线程重排序结果一致,不会对存在数据依赖关系的操作重排序。
数据依赖:若干操作访问同一变量且其中有写操作则称这些操作存在数据依赖。
  • 单线程程序。 单线程程序不会出现内存可见性问题。编译器、 runtime和处理器会共同确保单线程程序的执行结果与该程序在顺序一致 性模型中的执行结果相同。
  • 正确同步的多线程程序。 正确同步的多线程程序的执行将具有顺序一致性(程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同)。这是JMM关注的重点,JMM通过限制编译器和处理器的重排 序来为程序员提供内存可见性保证。
  • 未同步/未正确同步的多线程程序。 JMM 为它们提供了最小安全性保障:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0、null、false)。
所有处理器内存模型都允许写读重排序,处理器性能越强,内存模型的束缚越弱,优化越多,JMM 作为语言内存模型会限制处理器的重排序,主要通过内存屏障实现。
  • LoadLoad 确保前面的读操作(Load1)在后续的读操作(Load2)之前完成。
  • StoreStore 确保前面的写操作(Store1)在后续的写操作(Store2)之前完成并可见。
  • LoadStore 确保前面的读操作(Load1)在后续的写操作(Store2)之前完成。
  • StoreLoad 最强屏障,确保前面的所有读/写操作在后续的所有读/写操作之前完成并可见。
缓存交互
JMM 通过控制主内存(L3 + RAM)本地内存(L1 + L2)之间的交互,来为 Java 程序员提供内存可见性保证。
硬件缓存交互(内存屏障的底层实现)
从Java视角理解CPU缓存和伪共享
CPU Cache 分为 L1、L2、L3 缓存,级别越小越接近缓存,存储单位是 Cache Line(64 byte)。
不同内核之间不同级别的缓存使用 MESI 协议保证缓存和内存的相干性。
Modified
RAM 不一致
Exclusive
RAM 一致,其他处理器没有缓存
Shared
RAM 一致,其他处理器缓存
Invalid
Cache Line 失效,不可使用
  • Local Write(Invalid to Modified)
  • Local Read(other to Exclusive & Shared)
  • Remote Read(Core 1 需要 Core 2 的 Cache Line,Core 2 通过 Memory Controller 将信息发送到 Core 1 ,此过程中内存中也会得到该数据并保存)
  • Remote Write(Core 2 得到数据后发送 Request For Owner 获取该 Cache Line 权限,其他 Core 将该数据设置为 Invalid)
上述操作知道写操作的代价很高,特别是发送 RFO 请求:
  • 线程工作在 Core 之间转移
  • 不同 Core 之间需要操作相同的 Cache Line
这会导致伪共享问题,Core 1 线程想要更新 X 值,Core 2 想要更新 Y 值,但是 X 与 Y 位于相同 Cache Line,两个线程会轮番发送 RFO 信息,L1 和 L2 上数据都是 Invalid,其他线程需要读取只能从 L3 或者内存上读取。
  • volatile 内存语义
    • 写 volatile 变量时 线程将修改后的值写入本地内存 L1 或 L2,根据 MESI 协议将 Cache Line 标记为 Modified,并通过总线通知其他 Core,L3 缓存会同步该 Cache Line 值,物理 RAM 可能异步更新
    • 读 volatile 变量时 线程将 L1 或 L2 中对应 Cache Line 标记为 Invalid ,然后从 L3 或者物理 RAM 中加载 Cache Line
  • 锁的内存语义
    • 当线程释放锁时 JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
    • 当线程获取锁时 JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

线程状态

notion image
Damon 线程
Daemon 线程是一种支持型线程,主要被用作程序中后台调度以及支持性工作。
可以通过调用 Thread.setDaemon(true) 将线程设置为 Daemon 线程。Daemon 属性需要在启动线程之前设置,不能在启动线程之后设置。
当一个 JVM 中不存在非 Daemon 线程的时候,Java 虚拟机将会退出。 JVM 退出时 Daemon 线程中的 finally 块并不一定会执行,所以在构建 Daemon 线程时,不能依靠 finally 块中的内容来确保执行关闭或清理资源的逻辑。
创建、启动和终止
  • Thread thread = new Thread(Runnable runnable) 当前线程为被构建线程的 parent,parent 负责 child 的空间分配,child 同时继承 parent 是否为 Daemon、优先级和加载资源的 contextClassLoader 以及可继承的 ThreadLocal,同时分配唯一 ID 标识 child 线程。
  • thread.start() parent 通知 JVM,只要线程规划器空闲,应该立即启动调用 start() 方法线程。
  • thread.interrupt() 标识位属性,标识运行中线程是否被其他线程进行中断操作。其他线程通过 interrupt() 方法对其进行中断操作。 方法不会强制终止线程,而是设置线程的中断标志,由线程自己决定如何响应这个中断信号,这样可以避免一些因直接终止线程而可能引发的问题。
  • thread.suspend、thread.resume、thread.stop 已经弃用
线程间通信
共享内存并发模型:线程之间共享程序的公共状态,通过读写内存中的公共状态进行隐式通信。 消息传递并发模型:线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信。
  • ThreadLocal ThreadLocal 主要解决的就是让每个线程绑定自己的值。 每个 Thread 中都具备一个 ThreadLocalMap,而 ThreadLocalMap 可以存储以 ThreadLocal 为 key ,Object 对象为 value 的键值对。 static final ThreadLocal<?> holder = ThreadLocal.withInitial(ThreadLocalTest::)
    • 潜在的内存泄露 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。 ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。但是使用完 ThreadLocal 方法后最好手动调用remove() 方法
  • volatile 和 synchronized
    • volatile 修饰成员变量,使得程序对该变量的任何访问都需要从共享内存获取,而对其修改必须同步刷新回共享内存,确保所有线程对变量访问的可见性。
    • synchronized 修饰方法或同步块,确保多个线程在同一时刻只能一个线程处于方法或者同步块中,确保线程对变量访问的可见性和排他性。 本质上是获取某个对象的监视器,而这个获取过程是排他的。
      • 监视器:任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入 BLOCKED 状态。
  • 通知和等待机制
    • Object.notify() & Object.notifyAll() notify 通知一个在对象上等待的线程,使其从 wait() 方法返回。 notifyAll 通知所有等待在该对象上的线程。
    • Object.wait() & Thread.sleep() wait() 释放线程已经获取的对象锁,将当前线程放置到对象的等待队列,需要等待其他线程唤醒。sleep() 则并不释放对象锁,常用于暂停执行,线程自动苏醒。
    • thread.join() 同步机制,让当前线程等待调用方法线程终止后才返回。

虚拟线程

线程并行

Fork/Join Framework

线程池

池化技术减少每次获取资源时的消耗,提高对资源的利用率。
线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。

Executor Framework

Executor Framework 将线程分为执行单元工作机制
  • 任务
    • interface Runnable,不返回结果
    • interface Callable,返回结果。 callable(Runnable task, T result) 可以实现转换。
  • 执行
    • interface ExecutorService
      • ThreadPoolExecutor
        • FixedThreadPool
        • SingleThreadExecutor
        • CachedThreadPool
      • ScheduledThreadPoolExecutor
  • 结果
    • interface Future
      • FutureTask
      • CompletableFuture

ThreadPoolExecutor

使用构造函数创建。
任务处理
  1. 核心线程池是否已满 否则创建核心线程执行任务,该线程会被封装为 Worker 执行当前任务, 然后 Worker 会循环获取 BlockingQueue 中任务来执行。
  1. 工作队列是否已满 否则将任务存储在工作队列里。 为什么要如此设置,而不是直接调用非核心线程执行任务? 削峰,为了在性能和消耗之间取得平衡。
  1. 线程池是否已满 否则创建非核心线程执行任务。
  1. 最后按照拒绝策略处理无法执行任务。
创建使用
  • 创建并配置线程池 new ThreadPoolExecutor()
    • corePoolSize
      核心线程数,即可以复用的线程数量。
      • allowCoreThreadTimeOut(true) 设置核心线程可被过期销毁。
      • prestartAllCoreThreads() & prestartCoreThread() 启动所有核心线程 & 启动单个核心线程
      确定核心线程数。 从经验出发,CPU 密集型任务一般设置为 N+1,IO 密集型任务一般设置为 2N。 但依然可以通过公式计算准确值,不过公式过于理论,实践意义不大。
      maximumPoolSize
      最大线程数 任务队列满并且已经创建线程数小于最大线程,则线程池会创建新的线程执行任务,线程自有存活时间,到期销毁。
      workQueue
      任务队列,存放待执行任务的队列。 当提交的任务数超过核心线程数大小后,再提交的任务就存放在这里。 它仅仅用来存放被 execute 方法提交的 Runnable 任务
      • ArrayBlockingQueue 有界队列
      • LinkedBlockingQueue 无界队列 & 有界队列
      • SynchronousQueue 不存储元素,所有插入操作会阻塞,直到另一个线程进行移出操作,反之亦然。
      • PriorityBlockingQueue 支持优先级排序的无界阻塞队列,堆排序。
        • PriorityBlockingQueue 队列元素要求 implements interface Comparable ,重写 compareTo() 比较优先级,创建时需要传入 Comparator 指定任务之间排序规则。 可能发生任务大量堆积导致的内存溢出饥饿问题
      • DelayedQueue 按照延迟时间长短排序的无界阻塞队列堆排序
        • DelayQueue 队列元素要求 implements interface Delayed ,该接口要求实现 getDelay() 返回延迟时间和 compareTo() 比较延迟任务。
      keepAliveTime & TimeUnit
      非核心线程存活时间。 当线程池中的线程数量超过 corePoolSize 时,这些额外的线程将不会永久存活;它们将在 keepAliveTime 指定的时间内保持空闲状态,等待新任务的到来。如果在这段时间内没有新的任务到来,线程将会被终止以释放资源。 当核心线程被设置为可以超时,其存活时间应用此数值。
      ThreadFactory
      设置创建线程工厂
      RejectedExecutionHandler
      拒绝策略
      • AbortPolicy 抛出 RejectedExecutionException 来拒绝新任务的处理。
      • CallerRunsPolicy 调用执行自己的线程运行任务,如果执行程序已关闭,则会丢弃该任务。
      • DiscardOldestPolicy 丢弃最早的未处理的任务请求。
      • DiscardPolicy 不处理新任务,直接丢弃。
  • 向线程池提交任务
    • thread.execute(Runnable) 提交不需要返回值的任务。
    • thread.submit(Callable) 提交需要返回值的任务。
  • 关闭线程池
    • thread.shutdownNow() 尝试停止所有线程。
    • thread.shutdown() 中断所有空闲线程。
监控修改
  • 更新线程池 ThreadPoolExecutor 提供部分 setter 方法以及部分调整内部状态的方法,均为线程安全 volatile 修饰:
    • threadPoolExecutor.setCorePoolSize()
      • 线程池会直接覆盖原来的 corePoolSize 值,并且基于当前值和原始值的比较结果采取不同的处理策略。
      • 对于当前值小于当前工作线程数的情况,说明有多余的 worker 线程,此时会向当前idle的 worker 线程发起中断请求以实现回收,多余的 worker 在下次idel的时候也会被回收;
      • 对于当前值大于原始值且当前队列中有待执行任务,则线程池会创建新的 worke r线程来执行队列任务
    • threadPoolExecutor.setMaximumPoolSize() 线程池覆盖原始值,判断当前值是否大于原始值,如果大于则对空闲线程发起中断请求。
    • 只允许修改基本参数,如何动态修改 BlockingQueue
    • 自定义缓存队列实现缓存队列长度调整。(静态代理)
    • 成熟框架
      • Hippo4j 异步线程池框架,支持线程池动态变更&监控&报警,无需修改代码轻松引入。| 支持多种使用模式,轻松引入,致力于提高系统运行保障能力。
      • Dynamic TPopen in new window 轻量级动态线程池,内置监控告警功能,集成三方中间件线程池管理,基于主流配置中心(已支持 Nacos、Apollo,Zookeeper、Consul、Etcd,可通过 SPI 自定义实现)
  • 监控
    • 监控线程
    • 监控线程池 ThreadPoolExecutor 提供 get 系列方法支持状态获取。 继承线程池来自定义线程池。 重写线程池 beforeExecute、afterExecute 和 terminated 方法。
线程预热
默认状态通过 execute 方法提交任务后,创建工作线程执行,创建线程需要一定启动成本,需要提升运行时间,最好提供线程预热。 预热核心线程,非核心线程需要依赖 BlockingQueue 状态
  • prestartAllCoreThreads
  • prestartCoreThreads

Executors

使用工具类创建。 不推荐,因为这样会掩盖线程池的参数设置,有些参数可能会导致内存溢出。
  • FixedThreadPool 固定线程数量的线程池。 当一个新任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
    • LinkedBlockingQueue 无界队列可能堆积大量任务导致内存溢出。
  • SingleThreadExecutor 只有一个线程的线程池。 若多于一个任务被提交到线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行任务队列中的任务。
    • LinkedBlockingQueue 无界队列可能堆积大量任务导致内存溢出。
  • CachedThreadPool 任意线程数量的线程池。 若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 线程空闲时间累计keepAliveTime 则销毁。
    • SynchronousQueue
    • corePoolSize = 0
    • maximumPoolSize = Integer.MAX_VALUE 极端情况下任务数量过多而且执行速度较慢可能创建大量线程导致内存溢出。
    • keepAliveTime = 60L

interface Future

FutureTask

阻塞的获取任务结果。
implements Future, Runnable
Future 表示异步计算结果,Runnable 允许 FutureTask 被提交到线程池执行。
  • futureTask.run() 允许手动执行。
  • futureTask.get() 阻塞当前线程,获取结果。
  • futureTask.cancel(true) 中断正在执行任务。

CompletableFuture

编排异步任务的流程。
响应式编程组件。 CompletableFuture 具备异步和可编排的特性。 RxJava 与 Reactor 也有相似的特性,而且具备更多功能(操作融合、延迟执行和回压)。
implements Future, CompletionStage. Future 表示异步计算结果,CompletionStage 表示异步执行过程。
基础
  • 创建
    • CompletableFuture.completedFuture(T value) 立即返回 CompletableFuture (result = value)
    • CompletableFuture.supplyAsync(Supplier<U> supplier) & CompletableFuture.supplyAsync(Supplier<U> supplier, Executor executor) 异步执行有返回值任务(可指定线程池)
    • CompletableFuture.runAsync(Runnable runnable) & CompletableFuture.runAsync(Runnable runnable, Executor executor) 异步执行无返回值任务(可指定线程池)
  • 组合
    • completedFuture.thenCombine(CompletionStage<? extends U> other, BiFunction<? super T, ? super U, ? extends V> fn) 等待两个 CompletableFuture 完成,BiFunction 接收他们的返回值生成新值作为最终结果返回。
    • completedFuture.allOf(CompletableFuture<?>... cfs) 等待所有给定的 CompletableFuture 完成,但不保留它们的结果。
    • completedFuture.anyOf(CompletableFuture<?>... cfs) 只要有一个 CompletableFuture 完成就返回,但是它只返回最先完成的那个 CompletableFuture 的结果。
  • 响应
    • completablefuture.get() 阻塞当前线程并获取 CompletableFuture 的结果,可能会抛出 ExecutionException 异常。
    • completablefuture.join() 阻塞当前线程并获取 CompletableFuture 的结果,可能会抛出 CompletionException 异常。
    • completablefuture.getNow(T valueIfAbsent) 不阻塞当前线程。 如果 CompletableFuture 尚未完成,则返回给定的默认值。否则返回实际结果。
  • 链式调用
    • .thenApply(Function<? super T,? extends U> fn) 完成时应用给定函数,并返回新的 CompletableFuture 其结果是该函数的结果。
    • thenAccept(Consumer<? super T> action) 完成时消费其结果,不返回新的值。
    • .thenRun(Runnable action) 完成时执行给定操作,不返回新值。
    • thenCompose(Function<? super T, ? extends CompletionStage<U>> fn) 完成时将结果作为参数传递给另一个 CompletableFuture 并返回。
  • 处理异常
    • handle(BiFunction<? super T, Throwable, ? extends U> fn) 无论成功与否,都会被调用。
    • whenComplete(BiConsumer<? super T, ? super Throwable> action) 类似于 handle,但它不返回新的 CompletableFuture。
    • exceptionally(Function<Throwable, ? extends T> fn) 失败时调用。
原理
此处是浅尝辄止,若有更多原理上的疑问参考。 CompletableFuture原理与实践-外卖商家端API的异步化
notion image
result 存储当前 CF 结果。 stack 表示当前 CF 完成后需要触发的依赖动作 Dependency Action。 设计思想类似观察者模式
  • CompletableFuture 被观察者 stack 存储注册的所有观察者,当观察者执行完后会弹栈 stack,依次通知注册到其中的所有观察者。
  • Completion 观察者 回调方法接收函数类型的参数 f,生成 Completion 对象,并将入参函数 f 赋值给 fn,然后检查当前 CompletableFuture 是否处于完成状态,是则直接触发 fn,否则将 Completion 加入 CompletableFuture 的观察者链 stack,再次尝试触发。
    • next 观察者链表
    • dep 指向对应 CompletableFuture
    • src 指向依赖 CompletableFuture
    • fn 存储回调函数。
实践总结
  • 执行
    • 同步方法(不带Async后缀) 注册时被依赖操作已经执行完成,则直接由当前线程执行。否则由回调线程执行。
    • 异步方法 如果传递线程池参数则使用该线程池执行,否则使用 CommonPool 执行。 建议强制传线程池,且根据实际情况做线程隔离。
      • CommonPool threadExecutorPool = ForkJoinPool.commonPool() corePoolSize = N-1 Java 8 引入的共用线程池,主要用于 ForkJoinTask 的并行计算,但是也广泛用于需要并行计算和异步任务执行的场景,比如 Java 8 的并行流
       
 

悲观锁: 认为共享资源每次被访问就会出现问题,所以在每次获取资源操作时都会上锁。synchronized 和 ReentrantLock 就属于此类锁。 乐观锁: 认为共享资源访问时不会出现问题,线程无需加锁和等待,只需要在提交修改时校验数据是否被修改过。系统层面可以通过版本号机制实现,Java 层面通过内联汇编 C++ 方式实现 CAS 原子操作。

volatile

确保所有线程看到 volatile 修饰的变量一致。
  • 保证变量可见性 如果我们将变量声明为 volatile,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。volatile 关键字能保证数据的可见性,但不能保证数据的原子性。 synchronized 关键字两者都能保证。
  • 禁止指令重排序 如果我们将变量声明为 volatile,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。
Java 通过 volatile 和 synchronized 隐式插入内存屏障,开发者无需直接操作底层指令。 在写入volatile变量后插入StoreStore屏障和StoreLoad屏障,确保写入对其他线程可见。 在读取volatile变量前插入LoadLoad屏障和LoadStore屏障,确保读取到最新值。 操作系统层面,内存屏障主要体现在缓存一致性协议对Buffer、Cache和Memory 的强制同步,同时也限制编译器对屏障前后的指令重排序 CPU 执行写操作时可能将数据暂存于 StoreBuffer 等待其他 CPU 对于共享数据修改的确认,而非 Cache 或者 Memory,导致其他线程无法感知,StoreBarrier 强制将该处缓存刷新到缓存或者主存。 CPU 写操作执行后,当前 Cache 会被标记无效,但标记无效请求可能被暂存在 Invalidation Queue 中延迟处理,LoadBarrier 会强制处理这些无效请求,避免读到过期数据。
 

synchronized

保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
synchronized 底层原理
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。
Java 早期版本中,synchronized 属于 重量级锁,效率低下。 这是因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。
Java 6 之后, synchronized 引入了大量的优化。 如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让 synchronized 锁的效率提升了很多。其中偏向锁在后续版本被移除。
  • 修饰实例方法 给当前对象实例加锁,进入同步代码前要获得当前对象实例的锁 。
  • 修饰静态方法 给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得当前 Class 的锁。
  • 修饰代码块 对括号里指定的对象/类加锁:
    • synchronized(object) 表示进入同步代码前要获得给定对象的锁。
    • synchronized(类.class) 表示进入同步代码前要获得给定 Class 的锁。
      • 由于类锁是全局的,所有线程都会竞争同一个锁,因此在高并发场景下,类锁可能会成为性能瓶颈。为了提高性能,可以考虑双重检查锁定(Double-Checked Locking)或者静态内部类懒加载。
JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步。

interface Lock

定义锁的基本操作,Lock 接口实现基本上都是聚合 AQS 子类完成线程访问控制。
提供与 synchronized 相似功能,但是需要显式获取和释放锁,不过可以尝试非阻塞的获取锁、能被中断的获取锁以及超时获取锁。

AbstractQueuedSynchronizer Class

构建锁和其他同步组件的基础框架。
核心思想是: 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。 如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁 实现的,即将暂时获取不到锁的线程加入到队列中。
AQS 组成
  • 同步状态变量(volatile int state)
  • CLH(Craig,Landin,and Hagersten) 队列 线程获取同步状态失败,同步器会将当前线程以及等待状态等信息构造成 Node 加入同步队列,同时阻塞对应线程,当同步状态释放后唤醒 header 节点中线程再次获取同步状态。
    • notion image
常见同步器
  • Semaphore synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,而Semaphore 可以用来控制同时访问特定资源的线程数量。
  • CountDownLatch CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,它不能再次被使用。
  • CyclicBarrier CyclicBarrier 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活
ReentrantLock ReentrantLock 实现了 Lock 接口,是一个可重入且独占式的锁。 功能和 synchronized 关键字类似,但是在基础功能之外增加了高级功能:
  • lock.lockInterruptibly() 等待可中断.
    • 可中断锁: 获取锁的过程中可以被中断,不需要一直等到获取锁之后才能进行其他逻辑处理。
      不可中断锁: 一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。synchronized 就属于是不可中断锁。
  • new ReentrantLock(true) 公平锁
    • ReentrantLock 默认使用非公平锁,也可以通过构造器来显式的指定使用公平锁(new ReentrantLock(true))。 非公平锁: 锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。 公平锁: 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
  • lock.newCondition() 选择性通知,允许创建多个 Condition 实例,每个 Condition 都有自己的等待线程队列,可实现只唤醒等待在特定 Condition 上的线程( condition.signal() )。
不同的是前者依赖于 JDK,后者依赖于 JVM,也就意味着 synchronized 对程序员透明。
 
使用 AQS 构建自定义同步器
  • 定义静态内部类继承同步器
  • 根据需要重写方法
    • 独占锁(ReentrantLock) tryAcquire、tryRelease tryAcquireNanos、tryAcquireInterruptibly
    • 共享锁(Semaphore 、CountDownLatch) tryAcquireShared、tryReleaseShared tryAcquireSharedNanos、tryAcquireSharedNanos
  • 调用同步器模版方法实现自定义逻辑
Sample

并发容器

ConcurrentHashMap

并发安全 HashMap
  • Segment Array + 哈希表 + 链表 Java 8 之前的ConcurrentHashMap 通过分段锁的设计实现了高并发性能。 它将哈希表划分为多个段,并使用细粒度的锁来控制对每个段的访问。
    • notion image
    • Segment Array 初始化 ConcurrentHashMap 时,会初始化 SegmentArray,并指定初始容量和负载因子。 默认初始容量为 16,意味着默认支持 16 线程并发。每个 Segment 的初始容量和负载因子与整个 ConcurrentHashMap 相同。 此外还会为每个 Segment 分配一个锁用于控制对该 Segment 的并发访问需要注意的是,虽然每个Segment都有自己的锁,但整个ConcurrentHashMap的并发性能并不完全取决于锁的数量。实际上,锁的竞争程度、哈希函数的分布性以及负载因子等因素都会对并发性能产生影响。
    • HashEntry HashEntry 与 HashMap类似,采用链表解决哈希冲突。
      • hashcode
      • rehash
        当某个Segment的负载因子超过阈值时,会触发扩容操作。 扩容时,会创建一个新的 Segment 数组,并将原有 Segment 中的键值对重新散列到新的Segment数组中。这个过程涉及到大量的数据复制和重哈希计算。
        ConcurrentHashMap采用了分段扩容的策略减少扩容对并发性能的影响。 它每次只处理一个 Segment,并且在扩容过程中仍然允许其他线程访问未处理的Segment。这样确保了扩容操作不会阻塞整个ConcurrentHashMap的并发访问。
        ConcurrentHashMap 在扩容过程中还采用“转移策略”的技术来避免死锁和饥饿问题。 具体来说,当某个线程正在处理一个 Segment 时,如果该 Segment 需要扩容,那么扩容操作会由另一个线程来完成。这样确保了处理线程不会因等待扩容而阻塞过长时间。
  • NodeArray + 链表/红黑树
    • notion image
    • NodeArray CAS 操作被广泛应用于节点的添加、删除和更新等场景,以确保并发修改的安全性。
      • CAS(Compare-and-Swap)是一种无锁化的算法,它包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置的值更新为新值B,返回TRUE。否则,处理器不做任何操作,返回FALSE。
    • 链表/红黑树 冲突链表达到一定长度的时候,链表会转化成红黑树。
      • hashcode
        Java 8 中的 ConcurrentHashMap 也使用哈希算法来计算键的哈希值,并根据哈希值来定位数组中的索引位置。
        不同的是,Java 8 中的哈希计算过程更加复杂和精细,以减少哈希冲突和提高空间利用率。此外,当发生哈希冲突时,新的键值对会添加到链表或红黑树的末尾,而不是像之前版本那样使用头插法。
        rehash
        当 ConcurrentHashMap 中的元素数量超过数组的容量阈值时,就会触发扩容操作。 在扩容过程中,会创建一个新的数组,并将原有数组中的键值对重新散列到新的数组中。
        Java 8 中的扩容操作不再需要对整个数组进行锁定,而是采用了更细粒度的并发控制策略。具体来说,它将数组划分为多个小段(每个小段包含多个桶),并允许多个线程同时处理不同的小段。这样设计可以减少锁的竞争和提高扩容操作的并发性能。

ConcurrentLinkedQueue

并发安全的非阻塞 Queue
CAS 实现

BlockingQueue

并发安全的阻塞 Queue
提供可阻塞的插入和移除方法。 队列容器满时生产者线程会被阻塞,队列容器为空时消费者线程会被阻塞。
BlockingQueue 对插入、移除和获取元素操作提供四种不同方法用于不同场景:
Throws exception
Special value
Blocks
Time out
Insert
add(e)
offer(e)
put(e)
offer(e, time, unit)
Remove
remove()
poll()
take()
poll(time, uint)
Examine
element()
peek()
not applicable
not applicable
  • ArrayBlockingQueue 有界队列,数组实现。 读写都需要获取 AQS 独占锁。 这意味着任意时刻只有单个线程能工作。 如果队列为空,读线程进入读线程队列,等待元素写入后唤醒读线程队列的首个等待线程。 如果队列已满,写线程进入写线程队列,等待元素移除后唤醒写线程队列的首个等待线程。
    • LinkedBlockingQueue 无界队列 & 有界队列,链表实现。 读写操作获取各自的锁进行工作,并拥有各自的等待队列。
      • 默认无界队列,capacity 初始化为 Integer.MAX_VALUE,可通过有参构造器指定容量。
    • PriorityBlockingQueue 无界队列,二叉堆实现。 只能指定初始队列大小,空间不足会触发自动扩容机制,使用基于数组的二叉堆来存放数据。 如果节点数量小于 64,增加 oldCap + 2 容量,否则增加 oldCap / 2.
      • SynchronousQueue 不存储元素。 写入操作不会立刻返回,而是等待读线程执行读,读取操作同理。 公平模式 TransferQueue,非公平模式 TransferStack。
         

        CopyOnWriteArrayList

        并发安全 List
        无界队列
        Copy-On-Write 核心思想是如果有多个调用者同时请求相同资源,它们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源内容时,系统才会复制一份专用副本给该调用者,而其他调用者所见到的最初资源仍然保持不变。 这过程对其他调用者透明,主要优点是如果调用者没有修改资源,就不会有副本被创建,因此多个调用者只是读取操作时可以共享同一份资源。 可以看出写时复制机制非常适合读多写少的并发场景,但仍然存在注入内存占用过多、写操作开销过大和数据一致性等问题tips:该技术广泛应用于计算机操作系统,Redis 执行 RDB 持久化时也会用到该技术。
        当需要修改 CopyOnWriteArrayList 内容时,会创建底层数组副本数组,对副本数组进行修改,修改完之后再将修改后的数组重新赋值,读取操作和写入操作实现分离。
        读取操作不需要进行同步控制,多线程操作但是弱一致性,读取操作分两步进行,通过 getArray 获得当前数组引用,直接从数组中获取下标为 index 元素。
        写入操作同步控制为线性操作,只允许单个线程写入写入会创建副本,副本数组长度为原本数组+1,这也是为什么 CopyOnWriteArrayList 没有扩容机制的原因,删除操作类似。
        写入操作执行完成后重新赋值的数组对所有线程立即可见。
         

        ConcurrentSkipListMap

        线程安全的、基于跳表数据结构实现的有序映射。它提供了与 TreeMap 类似的功能,但在并发环境下的性能通常更好。 使用细粒度锁定和 CAS 操作来实现高并发的读写操作。读操作是无锁的,写操作通过锁定相关节点来确保线程安全性。支持高效的并发修改,多个线程可以同时进行插入、删除和查找操作。 由于其线程安全性和高效的并发性能,ConcurrentSkipListMap 非常适合用于多线程环境中需要频繁读写操作的场景
         

        并发工具

        原子操作

        Atomic

        更新基本类
        • AtomicBoolean
        • AtomicLong
        • AtomicInteger
          • .addAndGet()
          • .getAndIncrement()
          • .lazySet(int newValue)
          • .compareAndSet(int expect, int update)
            • Unsafe 只提供3种CAS方法 compareAndSwapObject()、compareAndSwapInt()、compareAndSwapLong()
        更新数组
        • AtomicIntegerArray
        • AtomicLongArray
        • AtomicReferenceArray
        更新引用类型
        • AtomicReference
        • AtomicReferenceFieldUpdater
        • AtomicMarkableReference
        更新字段
        • AtomicIntegerFieldUpdater
        • AtomicLongFieldUpdater
        • AtomicStampedReference

        并发工具

        • CountDownLatch 允许一个或多个线程等待其他线程完成操作
          • new CountDownLatch (num)
          • .countDown()
          • .await()
        • CyclicBarrier 允许一个或多个线程等待其他线程完成操作,可以重复使用。
          • new CyclicBarrier(int parties) & new CyclicBarrier(int parties, Runnable barrier-Action) barrier-Action:线程到达屏障时,优先执行 barrier-Action
          • .await() 表示到达屏障
          • .reset() CountDownLatch 的计数器只能使用一次,而 CyclicBarrier 的计数器可以使用 reset() 方法重置。
        • Semaphore 控制同时访问特定资源的线程数量。
          • new Semaphore(int peimits)
          • .aquire()
          • .release()
        • Exchanger 线程间数据交换
          • .exchange()
         
        上一篇
        MySQL
        下一篇
        SQL 慢查询优化