1 集合
1.1 集合类的继承图
通过观察类结构图的继承关系我们发现,集合的顶层接口Collection继承Iterable接口。
而在Iterable接口中有一个Iterator方法,它返回一个Itertator对象。
1.2 各类子类的具体实现
hashset: 基于hashmap实现,只存储key,value是固定的Object
TreeSet:基于treemaps实现,只存储key,value是固定的Object
arraylist: 基于数组实现,双倍扩容。
linkedList: 双向链表
hashmap: 链表数组<64,链表<8链表。如果大于8则扩展为红黑树
treeMap: 红黑树
1.1.1 快失败
快速失败”(fail-fast) 是一种错误检测机制,主要用于非线程安全的集合(如 ArrayList、LinkedList、HashSet、HashMap 等)。当使用迭代器(包括增强 for 循环)遍历集合时,如果在遍历过程中直接对集合结构进行了修改(如调用 add()、remove()、clear() 等方法,而不是迭代器自身的 remove() 方法),迭代器会立即抛出 ConcurrentModificationException,以避免后续不确定的行为。
1.2 红黑树
在 Java 中引入红黑树,主要是为了解决 HashMap 在哈希冲突严重时的性能退化问题,同时也作为 TreeMap / TreeSet 底层实现提供有序性保障 红黑树引入红色和黑色,核心目的是用更少的代价维护树的近似平衡,而不像 AVL 树那样追求绝对平衡。
1.3 hashcode
1.与equals强绑定。
2.与对象存储的内存地址有关。
3.重写equals就有必须重写hashcode.
1.4 concurrenhashtMap实现
1.5 hashtable
2 多线程
2.1 多线程基础
线程与进程:
进程是一个执行中的程序的实例。它包含程序代码和其当前活动的状态。每个进程都有自己的内存空间,包括代码段、堆栈段和数据段。在操作系统中,进程是资源分配和执行的基本单位。例如,在Windows系统中,一个运行的应用程序,如xx.exe,就是一个进程。
线程是进程中的一个实体,是被系统独立调度和分派的基本单位。线程在进程中并发执行,可以共享进程的资源,如内存和文件资源。每个线程有自己的程序计数器(PC)、虚拟机栈和本地方法栈,但是它们共享进程的堆和方法区资源。
由于线程是jvm执行基本单位,所有在整个java当中都尤为关键。
2.2 关键字
2.2.1 synchronized
是一种内置的 Java 关键字,它用于实现线程的同步。当一个线程进入synchronized块或方法时,它获得了锁,这会阻止其他线程同时进入相同的synchronized块或方法,从而确保了共享资源的互斥访问。synchronized 是 Java 中用于实现线程同步的关键字。它提供了一种 独占锁 的机制,用于确保多个线程之间的互斥访问共享资源。
synchronized有三种主要用法:非静态方法、静态方法、代码块。
修饰非静态方法,锁的是this,既可理解为锁单实例。
public synchronized void synchronizedInstanceMethod() {
// 同步的代码块
}
修饰静态方法则是锁的整个对象。相当于对当前类的Class对象加锁,当前类的Class对象作为对象监视器。这意味着只有一个线程可以同时执行该静态方法,以确保对该类的互斥访问。
public static synchronized void synchronizedStaticMethod() {
// 同步的代码块
}
修饰代码块时 方式一:
当你使用 synchronized(class) 时,你锁定的是整个类的对象,而不是实例对象。这意味着无论多少实例对象存在,它们都会竞争同一个锁。
Object lock = new Object();
synchronized (lock) {
// 同步的代码块
}
方式二:
当你使用 synchronized(this) 时,你锁定的是当前实例对象(this)。这意味着同一实例的不同方法调用会相互排斥,但不同实例之间的方法调用不会相互排斥。
synchronized (this) {
// 同步的代码块
}
synchronized 的底层实现原理可以概括为以下几点:
- synchronized 通过监视器锁来实现线程同步。
- 每个 Java 对象都有一个监视器锁。
- 线程在获取了对象的监视器锁后,可以执行被修饰的代码。
- 线程在释放了对象的监视器锁后,其他线程可以尝试获取监视器锁。
ynchronized在JDK1.6之后进行了优化,引入了偏向锁,轻量级锁,自旋锁等概念,用来提高性能和减少阻塞开销。以下是一些常见的优化详细说明:
- 偏向锁:JDK引入了偏向锁,它会将锁定的对象与线程相关联,当一个线程获得锁时,它会标记对象为已偏向该线程,以后再次进入同步块时,不需要竞争锁,而是直接获得。这对于减少无竞争情况下的锁开销非常有用。
- 轻量级锁:在低竞争情况下,JDK使用轻量级锁来减小锁开销。轻量级锁采用自旋方式来等待锁的释放,而不是进入阻塞状态。
- 自旋锁:当轻量级锁尝试获取锁失败时,JDK可以选择使用自旋锁。自旋锁不会使线程进入阻塞状态,而是一直尝试获取锁,通常在短时间内完成。这对于低竞争锁非常有用。
- 适应性自旋:JDK中的锁可以根据历史性能数据来调整自旋等待的次数,以达到更好的性能。
这些优化措施有助于提高synchronized的性能,使其在不同的竞争场景中更加高效。但请注意,优化是基于JVM和硬件平台的,因此在不同的环境中表现可能会有所不同。
2.2.2 monitor
Monitor 是 synchronized 关键字的底层实现,用于保证多线程访问共享资源时的线程安全。每个 Java 对象都可以关联一个 Monitor,充当锁的角色。每个java对象
Monitor,即监视器,是一种用于实现线程同步的机制。在Java中,每个对象都有一个与之关联的Monitor。当一个线程访问某个对象的同步方法或同步代码块时,它需要先获取该对象的Monitor。只有获取到Monitor的线程才能进入同步区域执行代码,其他线程则需要在Monitor的入口队列中等待,直到当前持有Monitor的线程释放它。### 对象头(Object Header)
在 HotSpot 虚拟机中,每个 Java 对象在内存布局上分为三部分:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
对象头中包含 Mark Word,它是实现锁的关键数据结构。
Mark Word 内容示例:
| 位数 | 内容 |
|---|---|
| 25 | 哈希码(HashCode) |
| 31 | GC 分代年龄 |
| 2 | 锁标志位(01、00、10 等) |
| 1 | 是否偏向锁标志 |
-
Monitor 像一个厕所的智能门锁:
- 有人用时,标上“占用”(Owner)。
- 有人排队时,记下名单(EntryList)。
- 有人出去喝咖啡时,记在休息区(WaitSet)。
- 支持“熟客”反复进出(重入),还能喊人回来(notify)。
2.2.3 线程状态相关
线程创建方式
1.继承thread类
package atguigu.java;
//1.创建一个继承于Thread类的子类
class MyThread extends Thread {
//2.重写Thread类的run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
2.实现Runnable接口
//1.创建一个实现了Runnable接口的类
class MThread implements Runnable {
//2.实现类去实现Runnable中的抽象方法:run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
//3.创建实现类的对象
MThread mThread = new MThread();
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(mThread);
t1.setName("线程1");
//5.通过Thread类的对象调用start():① 启动线程 ②调用当前线程的run()-->调用了Runnable类型的target的run()
t1.start();
//再启动一个线程,遍历100以内的偶数
Thread t2 = new Thread(mThread);
t2.setName("线程2");
t2.start();
}
}
3:实现Callable接口
package com.atguigu.java2;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//1.创建一个实现Callable的实现类
class NumThread implements Callable {
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
//把100以内的偶数相加
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
try {
//6.获取Callable中call方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
- 继承
Thread:简单但不灵活,工程中避免使用。 - 实现
Runnable:最常用,无返回值,解耦任务与线程。 - 实现
Callable:需要返回结果或抛出异常时的首选。
join、yield、interrupt
```
package org.mt.thread;
import org.mt.MybatisExApplication;
import org.springframework.boot.SpringApplication;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.*;
/**
* @author yzx
* @description
* @date 2026/5/1
*/
public class thread {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask1 = new FutureTask<>(new MyCallable("任务1"));
FutureTask<String> futureTask2 = new FutureTask<>(new MyCallable("任务2"));
FutureTask<String> futureTask3 = new FutureTask<>(new MyCallable("任务3"));
Thread thread1 = new Thread(futureTask1, "线程-1");
thread1.join(); //线程1执行完毕之后再执行线程2
Thread thread2 = new Thread(futureTask2, "线程-2");
thread2.yield(); //线程2让其他线程先执行
Thread thread3 = new Thread(futureTask3, "线程-3");
thread1.start();
thread2.start();
thread3.start();
System.out.println("任务1结果: " + futureTask1.get());
System.out.println("任务2结果: " + futureTask2.get());
System.out.println("任务3结果: " + futureTask3.get());
}
static class MyCallable implements Callable<String> {
private final String taskName;
public MyCallable(String taskName) {
this.taskName = taskName;
}
@Override
public String call() throws Exception {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String currentTime = sdf.format(new Date());
System.out.println(Thread.currentThread().getName() + " 正在执行: " + taskName + ",开始时间: " + currentTime);
Thread.sleep(2000);
String endTime = sdf.format(new Date());
return taskName + " 执行完成,结束时间: " + endTime;
}
}
}
中断是专为“唤醒那些卡在 sleep/wait/join/阻塞队列 等操作上的线程”而设计的标准信号机制。
它不是为了暴力结束线程,而是为了让阻塞中的线程有机会响应停止请求。
如果你不打算在代码里检查中断或处理 InterruptedException,那么调用 interrupt() 确实没用;
但一旦你写了健壮的多线程代码,中断就是不可或缺的协作工具。
await、notify、notifyAll
package org.mt.thread;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author yzx
* @description 演示 wait/notify 和 wait/notifyAll 的使用
* @date 2026/5/1
*/
public class thread {
public static void main(String[] args) throws InterruptedException {
System.out.println("========== 示例1: wait/notify ==========");
waitNotifyExample();
Thread.sleep(1000);
System.out.println("\n========== 示例2: wait/notifyAll ==========");
waitNotifyAllExample();
}
static void waitNotifyExample() throws InterruptedException {
Object lock = new Object();
Thread producer = new Thread(() -> {
synchronized (lock) {//使用lock对象锁是因为需要在同一个监视器
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(new Date()) + " - 生产者: 开始生产...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sdf.format(new Date()) + " - 生产者: 生产完成,通知消费者");
lock.notify();
}
}, "生产者线程");
Thread consumer = new Thread(() -> {
synchronized (lock) {//使用lock对象锁是因为需要在同一个监视器
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(new Date()) + " - 消费者: 等待产品...");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sdf.format(new Date()) + " - 消费者: 收到通知,开始消费");
}
}, "消费者线程");
consumer.start();
Thread.sleep(100);
producer.start();
producer.join();//在实例二之前打印
consumer.join();//在实例二之前打印
}
static void waitNotifyAllExample() throws InterruptedException {
Object lock = new Object();
Thread worker1 = new Thread(() -> {
synchronized (lock) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(new Date()) + " - 工人1: 等待开始信号...");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sdf.format(new Date()) + " - 工人1: 收到信号,开始工作");
}
}, "工人1");
Thread worker2 = new Thread(() -> {
synchronized (lock) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(new Date()) + " - 工人2: 等待开始信号...");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sdf.format(new Date()) + " - 工人2: 收到信号,开始工作");
}
}, "工人2");
Thread worker3 = new Thread(() -> {
synchronized (lock) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(new Date()) + " - 工人3: 等待开始信号...");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sdf.format(new Date()) + " - 工人3: 收到信号,开始工作");
}
}, "工人3");
Thread boss = new Thread(() -> {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock) {
System.out.println(sdf.format(new Date()) + " - 老板: 所有人都可以开始工作了!");
lock.notifyAll();
}
}, "老板");
worker1.start();
worker2.start();
worker3.start();
Thread.sleep(100);
boss.start();
boss.join();
worker1.join();
worker2.join();
worker3.join();
}
}
2.4 LOCK
多线程编程中,线程安全是一个至关重要的问题。当多个线程同时访问共享资源时,可能会导致数据不一致等问题。Java 提供了 synchronized 关键字来实现线程同步,但在某些场景下,Lock 接口及其实现类提供了更灵活和强大的同步机制。本文将详细介绍 Java Lock 的基础概念、使用方法、常见实践以及最佳实践,帮助读者深入理解并高效使用 Java Lock。
Lock 是 Java 中用于控制多个线程对共享资源访问的工具。与 synchronized 关键字不同,Lock 提供了更灵活的锁机制,允许开发者手动控制锁的获取和释放。
Lock 是一个接口,定义了锁的基本操作,主要方法如下:
void lock():获取锁,如果锁不可用,则当前线程将被阻塞,直到锁被释放。void unlock():释放锁。boolean tryLock():尝试获取锁,如果锁可用,则获取锁并返回true;否则返回false,不会阻塞当前线程。boolean tryLock(long time, TimeUnit unit):在指定的时间内尝试获取锁,如果在该时间内锁可用,则获取锁并返回true;否则返回false。void lockInterruptibly():获取锁,如果锁不可用,则当前线程将被阻塞,直到锁被释放或者线程被中断。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final Lock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockExample example = new ReentrantLockExample();
// 创建并启动多个线程
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
t1.start();
t2.start();
// 等待线程执行完毕
t1.join();
t2.join();
System.out.println("Count: " + example.getCount());
}
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReentrantReadWriteLockExample {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
private int data = 0;
public int readData() {
readLock.lock();
try {
return data;
} finally {
readLock.unlock();
}
}
public void writeData(int newData) {
writeLock.lock();
try {
data = newData;
} finally {
writeLock.unlock();
}
}
public static void main(String[] args) {
ReentrantReadWriteLockExample example = new ReentrantReadWriteLockExample();
// 多个读线程
for (int i = 0; i < 3; i++) {
new Thread(() -> {
int value = example.readData();
System.out.println("Read value: " + value);
}).start();
}
// 写线程
new Thread(() -> {
example.writeData(100);
System.out.println("Data written");
}).start();
}
}
转账避免死锁
两个账户之间转账,需要同时锁定两个账户。如果使用 synchronized 或简单 lock,可能发生死锁(A锁住账户1等账户2,B锁住账户2等账户1)。用 tryLock 解决:
public void transfer(Account from, Account to, int amount) {
// 始终按固定顺序尝试加锁(此处简单用hashCode决定顺序)
ReentrantLock lock1 = getLock(from);
ReentrantLock lock2 = getLock(to);
while (true) {
if (lock1.tryLock()) {
try {
if (lock2.tryLock()) {
try {
// 成功获取两个锁,执行转账
from.withdraw(amount);
to.deposit(amount);
return;
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
// 没同时拿到两个锁,释放已经拿到的锁,短暂等待后重试
Thread.sleep(10);
}
}
| 特性 | synchronized | Lock (以 ReentrantLock 为例) |
|---|---|---|
| 获取锁的方式 | 隐式(进入同步块/方法时自动获取) | 显式(调用 lock() 等方法) |
| 释放锁 | 隐式(退出同步块/方法时自动释放) | 显式(必须手动调用 unlock(),通常在 finally) |
| 可中断性 | 不可中断(等待锁时无法响应中断) | 可中断(lockInterruptibly() 支持中断) |
| 超时获取 | 不支持 | 支持(tryLock(timeout)) |
| 尝试获取 | 不支持,只能一直阻塞 | 支持(tryLock() 非阻塞尝试) |
| 公平性 | 非公平锁(不能保证等待最久的线程获锁) | 可选择公平或非公平(构造参数 true 为公平锁) |
| 条件变量 | 只有 wait/notify(与锁对象绑定) | 可创建多个 Condition,更灵活 |
| 性能 | 早期较差,现代 JVM 优化后相差不大 | 在高竞争下有时略优,但差异已不明显 |
| 使用难度 | 简单,不易出错 | 需要小心在 finally 中解锁,否则可能死锁 |
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedBuffer {
private final Queue<String> buffer = new LinkedList<>();
private final int capacity = 10;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 缓冲区未满条件
private final Condition notEmpty = lock.newCondition(); // 缓冲区非空条件
public void put(String item) throws InterruptedException {
lock.lock();
try {
while (buffer.size() == capacity) {
notFull.await(); // 缓冲区满,等待未满条件
}
buffer.add(item);
notEmpty.signal(); // 通知等待的非空条件(可以取了)
} finally {
lock.unlock();
}
}
public String take() throws InterruptedException {
lock.lock();
try {
while (buffer.isEmpty()) {
notEmpty.await(); // 缓冲区空,等待非空条件
}
String item = buffer.poll();
notFull.signal(); // 通知等待的未满条件(可以放了)
return item;
} finally {
lock.unlock();
}
}
}
2.5 threadlocal
ThreadLocal 为每个线程提供独立的变量副本,线程之间互不干扰。常用于传递上下文(如用户ID、请求ID)、线程安全的日期格式化、数据库连接等。
public class Demo {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
threadLocal.set(100); // 主线程设置
System.out.println(threadLocal.get()); // 100
new Thread(() -> {
threadLocal.set(200); // 子线程设置
System.out.println(threadLocal.get()); // 200
}).start();
System.out.println(threadLocal.get()); // 主线程仍是 100
}
}
典型应用场景
- 数据库连接 / 事务管理:Spring 的
TransactionSynchronizationManager就用 ThreadLocal 存放当前线程的事务资源。 - SimpleDateFormat:因为非线程安全,每个线程持有一个自己的
SimpleDateFormat实例。 - Web 请求上下文:在同一个请求链路(同一个线程)中传递用户信息、追踪 ID 等。
- 避免参数传递:跨层传递某些公共参数而无须修改方法签名。
2.6 atomicinteger
AtomicInteger 和 AtomicBoolean 位于 java.util.concurrent.atomic 包,提供了原子操作,用于无锁编程,保证在多线程环境下对变量的读写、自增等操作是线程安全的,性能通常优于 synchronized
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private final AtomicInteger count = new AtomicInteger(0);
public int increment() {
return count.incrementAndGet();
}
public int getCount() {
return count.get();
}
// 模拟多线程调用
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
threads[i].start();
}
for (Thread t : threads) t.join();
System.out.println(counter.getCount()); // 正确输出 10000
}
}
2.7 线程池
在并发编程中,如果每次处理任务都 new Thread().start(),会带来两个严重问题:
- 资源消耗大:线程的创建和销毁需要时间与内存,高频创建会导致系统开销剧增。
- 不可控性:无限创建线程会耗尽系统资源,导致 OOM 或系统崩溃。
线程池正是为了解决这些痛点而设计,它具备以下优势:
- 降低资源消耗:复用已创建的线程,减少创建/销毁的开销。
- 提高响应速度:任务到达时,无需等待线程创建即可直接执行。
- 提高线程可管理性:统一分配、监控、调优,防止资源耗尽。
线程池的精髓在于:核心线程 → 队列 → 最大线程 → 拒绝策略 这一系列流转。
线程池工作流程
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
四种内置拒绝策略对比
| 策略 | 行为 | 风险/场景 |
|---|---|---|
| AbortPolicy(默认) | 直接抛出 RejectedExecutionException(运行时异常) | 适合关键业务,不允许任务丢失,调用方需捕获并处理异常 |
| CallerRunsPolicy | 由提交任务的调用者线程自己执行该任务 | 提供背压机制,减缓任务提交速度;适合对延迟不敏感且允许降级的场景 |
| DiscardPolicy | 静默丢弃当前被拒绝的任务,不抛异常,不记录 | 适合可容忍少量任务丢失的业务(如日志、监控打点) |
| DiscardOldestPolicy | 丢弃队列头部(最旧的未处理任务),然后重新尝试提交当前任务(可能再次失败) | 适合追求新任务优先,老旧任务可放弃的场景 |
| 任务执行方法 |
| 方法 | 说明 |
|---|---|
execute(Runnable command) | 提交无返回值任务 |
submit(Callable<T> task) | 提交有返回值任务,返回 Future<T> |
submit(Runnable task, T result) | 提交 Runnable,完成后返回指定的 result |
submit(Runnable task) | 提交 Runnable,返回 Future<?>(get 返回 null) |
invokeAll(Collection) | 批量提交,阻塞直到所有任务完成 |
invokeAny(Collection) | 批量提交,返回第一个成功结果,其他取消 |
提交一个新任务时,ThreadPoolExecutor 按以下步骤处理: |
-
核心线程是否已满?
- 未满 → 创建新核心线程直接执行任务。
- 已满 → 进入下一步。
-
工作队列是否已满?
- 未满 → 任务进入队列排队等待。
- 已满 → 进入下一步。
-
线程池是否已达最大线程数?
- 未满 → 创建新的非核心线程执行任务。
- 已满 → 进入下一步。
-
执行拒绝策略
- 根据设置的
RejectedExecutionHandler处理任务。
- 根据设置的
常见线程池
| 方法 | 描述 | 队列 | 风险 |
|---|---|---|---|
newFixedThreadPool(n) | 固定大小线程池,core = max = n | 无界 LinkedBlockingQueue | 队列无限,可能导致 OOM |
newCachedThreadPool() | 可缓存线程池,60s 回收,core=0, max=Integer.MAX_VALUE | 同步移交 SynchronousQueue | 线程可无限创建,导致 OOM |
newSingleThreadExecutor() | 单线程池,保证任务顺序执行 | 无界 LinkedBlockingQueue | 队列无限,可能导致 OOM |
newScheduledThreadPool(n) | 支持定时与周期性任务 | 延迟队列 DelayedWorkQueue | 线程数无界(默认) |
| 为什么阿里的 Java 开发手册禁止直接用 Executors? |
FixedThreadPool和SingleThreadPool允许的请求队列长度为Integer.MAX_VALUE,可能堆积大量请求,导致 OOM。CachedThreadPool允许创建的线程数量为Integer.MAX_VALUE,可能创建大量线程,导致 OOM。
结论:应该手动创建 ThreadPoolExecutor,以便更精确地控制参数。
1.线程池状态的现实意义
资源管控:线程池需要在不同状态下合理管理线程资源,避免无限制创建线程导致OOM(如RUNNING状态限制最大线程数,SHUTDOWN状态逐步回收线程)。
任务调度:状态决定了是否接受新任务(如SHUTDOWN状态拒绝新任务但继续执行队列中的任务,STOP状态则直接中断所有任务)。
优雅停机:在线程池关闭时,状态机确保任务不丢失、资源不泄漏(如TIDYING和TERMINATED状态的清理机制)。
2. 状态管理不当的典型问题
线程泄漏:未正确关闭线程池(如忘记调用shutdown()),导致线程无法回收,最终耗尽系统资源。
任务丢失:错误使用shutdownNow(),导致队列中的任务被丢弃,业务逻辑不完整。
响应延迟:线程池卡在TIDYING状态,未正确进入TERMINATED,影响服务重启或资源释放。
3. 状态管理的设计挑战
无锁高性能:线程池需要在多线程环境下高效切换状态,避免使用重量级锁(如synchronized)。
状态一致性:确保线程数、任务队列、运行状态三者同步,避免竞态条件(如RUNNING状态下同时修改线程数和任务队列)。
不可逆状态机:状态流转必须是单向的(如RUNNING→SHUTDOWN→STOP→TIDYING→TERMINATED),防止逻辑混乱。
2.8 线程池例子
例子1: 两个线程交替从1打印到100
package org.mt.thread;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author yzx
* @description 演示 wait/notify 和 wait/notifyAll 的使用
* @date 2026/5/1
*/
public class thread {
static int count = 0;
static Boolean flag = true;
public static void main(String[] args) throws InterruptedException {
PrintClass printClass = new PrintClass();
new Thread(new Runnable() {
@Override
public void run() {
printClass.print1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
printClass.print2();
}
}).start();
}
static class PrintClass {
void print1() {
synchronized (this) {
for (int i = 0; i < 50; i++) {
//if 改为whlie防止虚假唤醒
if(flag) {
try {
flag = false;
System.out.println("print1: " + count++);
notify();
wait();//暂停整个循环
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
notifyAll(); //唤醒最后一个wait
}
}
void print2() {
synchronized (this) {
for (int i = 0; i < 50; i++) {
if(!flag) {
try {
flag = true;
System.out.println("print2: " + count++);
notify();
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
例子2:线程池打印1-100
线程池所有线程都是共享的,当执行完一个任务,这个线程就会被等待接受新的任务。
package org.mt.thread;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author yzx
* @description 演示 wait/notify 和 wait/notifyAll 的使用
* @date 2026/5/1
*/
public class thread {
static int count = 0;
static Boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Ticker ticker = new Ticker();
ticker.run();
}
static class Ticker {
void run() {
int coreSize = 2;
int maxSize = 4;
long keepAlive = 30L;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(10);
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
ThreadFactory namedFactory = new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("my-pool-thread-" + threadNumber.getAndIncrement());
t.setDaemon(false);
return t;
}
};
ThreadPoolExecutor executor = new ThreadPoolExecutor(
coreSize, maxSize, keepAlive, unit,
workQueue, namedFactory, handler
);
AtomicInteger count = new AtomicInteger(0);
for (int i = 0; i < 100; i++) {
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + ": " + count.getAndIncrement());
});
}
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
}
}
CountDownLatch
| 特性 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 计数器递减/递增 | 递减,直到0 | 递增(或可重置),达到指定数量后自动重置 |
| 可重用性 | 不可重用 | 可重用(通过 reset()) |
| 线程阻塞 | 一个或多个线程等待计数归零 | 多个线程相互等待,直到达到屏障点 |
| 常用场景 | 主线程等待子线程完成 | 多个线程互相等待,到达同一状态后再一起继续 |
| 是否可触发额外动作 | 否 | 可以通过构造函数的 Runnable 参数指定到达屏障时的动作 |
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 初始化计数器为 3
CountDownLatch latch = new CountDownLatch(3);
// 创建并启动 3 个工作线程
for (int i = 1; i <= 3; i++) {
new Thread(new Worker(latch, i)).start();
}
// 主线程等待,直到计数器归零
latch.await();
System.out.println("所有工作都已完成,主线程继续执行...");
}
static class Worker implements Runnable {
private final CountDownLatch latch;
private final int id;
Worker(CountDownLatch latch, int id) {
this.latch = latch;
this.id = id;
}
@Override
public void run() {
try {
// 模拟任务耗时
Thread.sleep((long) (Math.random() * 1000));
System.out.println("Worker " + id + " 完成工作");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 每个线程完成自己的任务后调用 countDown
latch.countDown();
}
}
}
}
Semaphore
Semaphore(信号量)是 Java 并发包中的一个同步工具类,用于控制同时访问某个特定资源的线程数量。可以把它理解为一个“许可证管理器”:线程需要先获取许可证才能执行,执行完毕后归还许可证。它通常用来限制对共享资源(如数据库连接池、文件写操作等)的并发访问数。
- 核心概念
- 许可证(permits) :Semaphore 内部维护一个计数器,表示当前可用的许可证数量。
- 获取许可证:线程通过
acquire()方法获取许可证,如果此时没有可用的许可证(计数器为0),线程就会阻塞,直到有其他线程释放许可证。 - 释放许可证:线程通过
release()方法释放许可证,计数器加1,并唤醒可能正在等待的线程。 - 公平与非公平模式:Semaphore 构造时可以指定是否公平(fair)。公平模式下,线程按照请求的顺序获取许可证;非公平模式下,可能发生“插队”,提高吞吐量但可能导致线程饥饿。
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
public static void main(String[] args) {
// 最多允许 3 个线程同时访问
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 10; i++) {
new Thread(new Worker(semaphore, i)).start();
}
}
static class Worker implements Runnable {
private final Semaphore semaphore;
private final int id;
Worker(Semaphore semaphore, int id) {
this.semaphore = semaphore;
this.id = id;
}
@Override
public void run() {
try {
semaphore.acquire(); // 获取许可证
System.out.println("线程 " + id + " 开始工作");
Thread.sleep(1000); // 模拟工作
System.out.println("线程 " + id + " 结束工作");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release(); // 释放许可证
}
}
}
}
2.9 CAS
Java 多线程 CAS 原理
CAS(Compare-And-Swap,比较并交换)是一种实现无锁(lock-free)并发算法的关键技术。它利用硬件层面的原子操作来保证多线程环境下对共享变量的更新是线程安全的,避免了使用重量级锁(如 synchronized)带来的上下文切换和线程阻塞开销。
1. CAS 的核心思想
CAS 包含三个操作数:
- 内存位置 V(对应 Java 中的共享变量)
- 预期值 A(期望该位置当前持有的值)
- 新值 B(想要写入的新值)
执行逻辑:当且仅当 V 的值等于 A 时,才将 V 的值原子地更新为 B;否则什么都不做。无论是否更新成功,都会返回 V 的当前值(通常是原子操作前的值)。整个过程是原子性的,不能被中断。
伪代码表示(非原子版本,仅用于理解逻辑):
int cas(int* addr, int expected, int newValue) {
int old = *addr;
if (old == expected) {
*addr = newValue;
}
return old;
}
2. 硬件层面的实现
CAS 的原子性需要 CPU 的硬件支持。现代处理器提供了多种原子指令来实现 CAS 功能:
- x86 架构:
CMPXCHG指令(Compare and Exchange)。 - ARM 架构:
LDREX/STREX指令对。 - RISC-V:
LR/SC指令对。
这些指令会锁住内存总线或缓存行,保证比较和交换操作不可分割。Java 中的 Unsafe 类通过 JNI 调用这些硬件指令来完成 CAS。
3. Java 中的 CAS 实现
Java 对 CAS 的封装主要在 sun.misc.Unsafe 类中。该类提供了 compareAndSwapInt、compareAndSwapLong、compareAndSwapObject 等 native 方法,但这些方法不能直接调用(通常通过反射或由 JVM 内部使用)。开发者更常用的是 java.util.concurrent.atomic 包下的原子类,如 AtomicInteger、AtomicLong、AtomicReference 等,它们内部基于 Unsafe 的 CAS 操作实现。
示例:AtomicInteger 的 incrementAndGet() 源码片段
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// getAndAddInt 内部循环调用 compareAndSwapInt
public final int getAndAddInt(Object obj, long offset, int delta) {
int v;
do {
v = this.getIntVolatile(obj, offset); // 读取当前值
} while(!this.compareAndSwapInt(obj, offset, v, v + delta));
return v;
}
4. CAS 的工作流程(以 AtomicInteger 为例)
假设有两个线程 T1 和 T2 同时对共享变量 count 进行 incrementAndGet 操作,初始值为 0。
- T1 和 T2 同时读取到
count的当前值v = 0。 - T1 执行 CAS:期望
count == 0,所以成功将count更新为 1,T1 返回 1。 - T2 执行 CAS:此时
count已经变为 1,不等于它之前读取的预期值 0,因此 CAS 失败,返回旧值 1。 - T2 进入循环,重新读取
count(此时为 1),再次尝试 CAS(期望 1,新值 2)。 - 该循环一直持续到 CAS 成功为止。这种失败后重试的方式称为自旋(spin)。
5. CAS 的优缺点
-
低开销:无锁操作避免了线程阻塞和上下文切换,在低到中等竞争程度下性能远高于
synchronized。 -
无死锁风险:因为不会有线程被挂起等待锁。
-
细粒度控制:可以只针对单个变量进行原子操作。
-
ABA 问题:如果变量值从 A 变为 B 再变回 A,CAS 会认为值没有变化,但实际发生了两次改动。解决方法:使用带版本号的引用(
AtomicStampedReference或AtomicMarkableReference)。 -
自旋 CPU 开销:在高竞争场景下,CAS 会反复失败并重试,消耗大量 CPU 资源。
-
只能保证单个共享变量的原子性:无法像
synchronized那样保护多个变量的操作一致。对于多个变量的复合操作,仍需使用锁或其他协调机制。
6. ABA 问题详解与解决
- 问题描述:线程 T1 准备将变量 V 从 A 改为 C;期间 T2 将 V 从 A 改为 B,又改回 A。T1 的 CAS 会成功,但它不知道中间状态发生了变化。在某些场景(如栈顶指针操作)会导致逻辑错误。
- 解决:使用
AtomicStampedReference(带时间戳/版本号)或AtomicMarkableReference(带布尔标记)。每次修改时版本号递增,CAS 时同时检查版本号。
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
int[] stampHolder = new int[1];
String oldRef = ref.get(stampHolder);
int oldStamp = stampHolder[0];
boolean success = ref.compareAndSet(oldRef, "B", oldStamp, oldStamp + 1);
7. 总结
CAS 是 Java 并发包(JUC)的基石,它不仅被 AtomicInteger 等原子类使用,还广泛应用于 ConcurrentHashMap、ConcurrentLinkedQueue、ReentrantLock 的内部(AQS 中的状态管理)。理解 CAS 原理有助于写出更高效的并发代码,同时也要注意其在高度竞争下的局限性,结合锁或自旋策略来设计健壮的系统。
2.10 各种锁概念以及例子
好的,我们基于之前讨论的 18 个锁概念 以及补充内容,从多个维度进行分类、比较和解释。这样能更清晰地看到它们之间的关系与区别。
一、按锁的设计策略分类(最宏观)
| 分类 | 代表锁 | 核心思想 | 比较说明 |
|---|---|---|---|
| 乐观锁 | CAS、AtomicInteger、StampedLock 乐观读 | 操作前不加锁,更新时检查冲突 | 适合读多写少、冲突概率低的场景;性能高但需要重试机制 |
| 悲观锁 | synchronized、ReentrantLock | 操作前先加锁,阻塞其他线程 | 适合写多、冲突严重的场景;保证强一致性但并发性能较低 |
比较:乐观锁看重“最终成功”,悲观锁看重“绝对互斥”。两者是理念之争,不互斥(如
StampedLock同时提供乐观读和悲观写)。
二、按锁的获取与释放机制分类
| 分类 | 实现方式 | 特点 | 典型例子 |
|---|---|---|---|
| 自旋锁 | 忙等(循环检测) | 避免线程切换,适合锁持有时间极短 | AtomicBoolean 自旋实现、AQS 中的短暂自旋 |
| 非自旋锁 | 阻塞(挂起线程) | 适合锁持有时间长,避免浪费 CPU | synchronized 重量级状态、ReentrantLock 阻塞等待 |
比较:自旋锁不交出 CPU,适用于锁几乎马上被释放;非自旋锁让出 CPU,但需要上下文切换开销。现代 JVM 会自适应自旋(先自旋,再阻塞)。
三、按锁的可重入性分类
| 分类 | 说明 | 代表 | 比较 |
|---|---|---|---|
| 可重入锁 | 同一线程可多次获取,计数释放 | synchronized、ReentrantLock | 避免死锁(如递归调用)。 |
| 不可重入锁 | 同一线程再次获取会阻塞(或死锁) | 自定义实现(例如自己写的不可重入互斥锁) | Java 原生锁几乎都是可重入的,不可重入锁极少使用。 |
比较:可重入锁是更友好的设计;若需要不可重入语义,可用
Semaphore(1)并自行控制。
四、按锁的公平性分类
| 分类 | 特点 | 优点 | 缺点 | 实现 |
|---|---|---|---|---|
| 公平锁 | 严格按请求顺序分配 | 避免饥饿 | 吞吐量低(更多挂起/唤醒) | new ReentrantLock(true) |
| 非公平锁 | 允许插队(尝试抢占) | 吞吐量高,减少上下文切换 | 可能使某些线程饥饿 | synchronized、new ReentrantLock(false)(默认) |
比较:除非需要防止饥饿,通常优先使用非公平锁。
synchronized只支持非公平。
五、按锁的共享程度分类
| 分类 | 访问权限 | 代表 | 比较 |
|---|---|---|---|
| 独占锁 | 一次仅一个线程持有 | synchronized、ReentrantLock、写锁 | 保证写操作的原子性。 |
| 共享锁 | 多线程可同时持有 | 读锁、Semaphore | 提高读并发。 |
| 读写锁 | 读共享、写独占 | ReentrantReadWriteLock、StampedLock | 是独占+共享的混合体,适合读多写少场景。 |
比较:纯粹的共享锁(如信号量)不限资源类型,读写锁把共享与独占施加在“读/写”操作上。
六、按锁的粒度 / 作用范围分类
| 分类 | 锁对象 | 影响范围 | 典型场景 |
|---|---|---|---|
| 对象锁 | 实例对象 | 单个实例的方法/块 | synchronized 实例方法 |
| 类锁 | Class 对象 | 所有实例的静态方法 | synchronized 静态方法 |
| 分段锁 | 数据段(如数组的段) | 段内数据 | ConcurrentHashMap 1.7 |
| 锁粗化(优化) | 多个相邻锁合并为一个 | 减少频繁加锁 | JIT 编译优化 |
比较:粒度越小并发度越高,但管理开销越大。分段锁是折中方案。
七、按锁的性能开销(JVM 对 synchronized 的优化状态)
这是从轻到重的三个级别,锁只能单向升级(不能降级):
| 状态 | 实现方式 | 竞争程度 | 开销 |
|---|---|---|---|
| 偏向锁 | 对象头标记持有线程ID | 无竞争 | 几乎为零 |
| 轻量级锁 | CAS + 自旋 | 轻度交替 | 用户态,无阻塞 |
| 重量级锁 | 操作系统互斥量(mutex) | 激烈竞争 | 内核态,开销大 |
比较:这是 JVM 的自适应锁升级机制。JDK 15 后偏向锁默认关闭,因为维护成本高。
八、按锁的使用工具/接口分类(显式 vs 隐式)
| 分类 | 关键字 / 类 | 控制方式 | 灵活性 |
|---|---|---|---|
| 隐式锁 | synchronized | 自动获取/释放 | 差(不可中断、超时) |
| 显式锁 | Lock 接口及实现(ReentrantLock) | 手动 lock/unlock | 强(可中断、轮询、超时、多条件) |
| 读写锁 | ReadWriteLock | 读锁共享、写锁独占 | 强 |
| 工具锁 | Semaphore、CountDownLatch | 非传统锁,用于控制并发数或等待 | 特定场景 |
比较:能用
synchronized尽量用(简单可靠),需要高级特性(如超时、公平、多条件)时用ReentrantLock。
九、特殊概念:死锁、锁消除、锁粗化
| 概念 | 类型 | 说明 |
|---|---|---|
| 死锁 | 故障状态 | 不是锁实现,而是多锁资源导致的互相等待。 |
| 锁消除 | JIT 优化 | JVM 检测到不可能有竞争时直接去掉锁(如局部 StringBuffer)。 |
| 锁粗化 | JIT 优化 | 将多次加锁/解锁合并为一次,减少开销。 |
比较:锁消除和锁粗化都是 JVM 自动优化,对开发者透明;死锁是必须避免的编程错误。
十、补充的高级锁工具(相对独立)
| 锁/工具 | 主要特征 | 与经典锁比较 |
|---|---|---|
StampedLock | 支持乐观读、读写锁模式、可转换锁模式 | 性能优于 ReentrantReadWriteLock,但不可重入,易用性差 |
Condition | 与 Lock 配合,实现多路等待/通知 | 比 wait/notify 更灵活(可精确唤醒) |
CyclicBarrier | 栅栏,等待所有线程到达后一起放行 | 与 CountDownLatch 类似但可循环使用 |
Exchanger | 两个线程交换数据点 | 特殊场景(双线程数据对接) |
总结:怎么记住这些锁?
- 先分策略:乐观 vs 悲观
- 再看实现细节:公平性、可重入性、共享程度
- 考虑 JVM 内部:偏向/轻量/重量级锁升级
- 选择合适的工具:
synchronized→ReentrantLock→ReadWriteLock→StampedLock - 别忘了优化概念:锁消除、锁粗化
- 避免死锁:使用
tryLock或固定锁顺序
锁特性对比表格(常用核心锁)
| 锁类型 | 可重入 | 公平选项 | 共享/独占 | 是否可中断 | 是否支持超时 | 多条件等待 | 适用场景 | 性能特点 |
|---|---|---|---|---|---|---|---|---|
synchronized | ✅ | ❌ 非公平 | 独占 | ❌ | ❌ | ❌ (wait/notify 粗糙) | 简单同步,代码块/方法 | JVM 自动升级(偏向→轻量→重量),竞争激烈时性能下降 |
ReentrantLock | ✅ | ✅ 公平/非公平 | 独占 | ✅ (lockInterruptibly) | ✅ (tryLock) | ✅ (Condition) | 需要高级锁控制 | 非公平模式下吞吐量高,公平模式开销大 |
ReentrantReadWriteLock | ✅ | ✅ 公平/非公平 | 读共享,写独占 | ❌ (读锁可中断需注意) | ✅ (可设置) | ✅ | 读多写少,读操作耗时 | 读锁并发高,写锁独占;写锁饥饿风险 |
StampedLock | ❌ | ❌ | 读/写/乐观读 | ❌ | ❌ | ❌ | 读远多于写,允许乐观重试 | 乐观读无锁,性能极高;不可重入,使用复杂 |
Semaphore | ❌ | ✅ 公平/非公平 | 共享(许可数>1) | ✅ | ✅ | ❌ | 限制并发线程数(连接池、限流) | 轻量级,适用资源池场景 |
CountDownLatch | ❌ | — | 线程协调 | ✅ | ❌ | ❌ | 等待一组线程完成(一次性) | 倒计数阻塞,不可重用 |
CyclicBarrier | ❌ | — | 线程协调 | ✅ | ❌ | ❌ | 多线程相互等待到达屏障(可重用) | 栅栏,可重复使用 |
AtomicXXX (CAS乐观锁) | — | — | 无锁(CAS) | — | — | — | 单个变量原子更新,轻量级竞争 | 无阻塞,极高吞吐量,但不适合复杂代码块 |
3. 按 JVM 锁升级状态对比(仅对 synchronized)
| 状态 | 实现机制 | 竞争程度 | 开销 | 晋升方向 |
|---|---|---|---|---|
| 偏向锁 (JDK15+ 默认关闭) | 对象头存线程ID | 无竞争 | 几乎为0 | → 轻量级 |
| 轻量级锁 | CAS + 自旋 | 轻度交替 | 用户态无阻塞 | → 重量级 |
| 重量级锁 | 操作系统 mutex | 激烈竞争 | 内核态,线程阻塞 | 终点 |
4. 补充:锁优化技术对比
| 优化技术 | 作用 | 触发时机 | 开发者可控性 |
|---|---|---|---|
| 锁消除 | 去掉不可能有竞争的锁(如局部 StringBuffer) | JIT 逃逸分析 | 无,自动 |
| 锁粗化 | 将多个连续加锁/解锁合并为一个锁范围 | JIT 检测相邻同步块 | 无,自动 |
| 自适应自旋 | 先自旋一定次数,再阻塞 | 轻量级锁失败时 | 可通过 JVM 参数调整 |
5. 快速决策口诀(三句半)
- 简单同步选
synchronized,代码干净少出错 - 高级控制
ReentrantLock,中断超时多条件 - 读多写少用读写锁,极致乐观
Stamped - 跨 JVM 就分布式,死锁预防用
tryLock
3 IO流(非重点)
4 JVM
4.1 基本组成
jvm主要是分为三大块内容:1.类加载器,负责将class文件代码转为字节码。2.运行时数据区,java运行是数据存储交互都在数据区中。3.执行引擎和内部库,负责、解释器执行命令、垃圾回收和提供底层方法调用
方法区
JVM 的方法区(Method Area) 主要用于存储类的结构信息,具体包括:
- 类元数据
类的全限定名、父类名、访问修饰符(public、abstract 等)、实现的接口列表等。 - 运行时常量池
存放编译期生成的各种字面量(如字符串常量、final 常量值)和符号引用(如类名、方法名、字段名)。 - 字段信息
类中声明的每个字段的名称、类型、访问修饰符。 - 方法信息
每个方法的名称、返回类型、参数类型、访问修饰符、字节码指令、异常表等。 - 静态变量(static 变量)
类的所有实例共享的静态变量,存储在方法区中(注意:Java 7 及之前某些实现将静态变量存放在堆中,但逻辑上属于方法区;Java 8 后元空间中仍存储静态变量)。 - 类加载器引用
指向加载该类的类加载器实例,用于类卸载时的判定。 - 即时编译器(JIT)优化后的代码缓存(部分实现中也会在方法区或代码缓存区存放)
在jdk7以前,习惯上把方法区称为永久代。jdk8开始,使用元空间取代了永久代。- 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。
堆
在 JVM 中,堆(Heap) 是运行时数据区中最大的一块内存,也是垃圾回收(GC) 的主要管理区域。它的核心作用是:
- 所有类的实例对象(通过
new创建的对象) - 数组(包括对象数组和基本类型数组)
几乎所有的对象都在堆上分配内存(极少数情况如 JIT 编译后的标量替换可能不会在堆分配)。
栈和本地方法栈
虚拟机栈(Java Virtual Machine Stack)
主要存储内容:每个 Java 方法调用都会创建一个栈帧(Stack Frame) ,栈帧中存储:
-
局部变量表
- 存放方法内的基本数据类型(boolean、byte、char、short、int、long、float、double)和对象引用(reference)。
- long 和 double 占用两个局部变量槽(slot)。
-
操作数栈
- 用于存放计算过程中的中间结果、方法参数、返回值等。
- 例如执行
iadd指令时,会从操作数栈弹出两个整数,相加后再压入结果。
-
动态链接
- 指向运行时常量池中该方法的符号引用,用于将符号引用解析为直接引用(例如调用其他方法时)。
-
方法返回地址
- 方法正常或异常退出后,需要返回到调用方的地址。
注意:栈帧随着方法调用而创建,随着方法结束而销毁。局部变量表所需的内存在编译时即可完全确定,不会在运行时改变。
本地方法栈(Native Method Stack)
主要存储内容:与虚拟机栈类似,但它是为 Native 方法(用 C/C++ 等非 Java 语言编写的方法,如 JNI 调用)服务的。
- 每个 Native 方法调用也会对应一个栈帧(具体结构由 JVM 实现定义),用于存储该 Native 方法的局部变量、操作数等。
- 许多 JVM 实现(如 HotSpot)将虚拟机栈和本地方法栈合并为同一个栈区域,只是逻辑上区分。
JVM与线程之间的关系
JVM 与线程的关系可以概括为:JVM 中的线程本质上映射到操作系统(OS)的内核线程,由 OS 负责调度;JVM 为每个线程提供独立的运行时数据区域(如程序计数器、虚拟机栈、本地方法栈),并协同管理线程的生命周期与资源。
| 内存区域 | 线程私有? | 是否为新线程单独创建 |
|---|---|---|
| 程序计数器 | ✅ | 是 |
| Java 虚拟机栈 | ✅ | 是(分配新栈内存) |
| 本地方法栈 | ✅ | 是(通常与 Java 栈合并) |
| 堆 | ❌ | 否(直接共享) |
| 方法区 | ❌ | 否(直接共享) |
| 直接内存 | ❌ | 否(需主动分配,非线程自动创建) |
综上所述,JVM为Java线程提供了一个运行时环境,负责线程的创建、调度、同步、通信和内存管理等关键功能。线程是JVM中执行代码的基本单位,而JVM则负责为线程提供必要的资源和调度以保证程序的并发执行。
java虚拟机(JVM)和线程之间的关系是密不可分的。以下是它们之间关系的几个关键点:
- 线程的创建和管理:
线程创建:在Java中,线程是通过继承 Thread 类或者实现 Runnable 接口来创建的。当 start() 方法被调用时,JVM 会负责创建实际的操作系统级别的线程。
线程管理:JVM 负责管理线程的生命周期,包括线程的创建、运行、阻塞、等待、计时等待和终止。JVM 内部有一个线程调度器(Thread Scheduler),它负责决定哪个线程应当执行以及何时执行。
- 线程调度:
线程调度器:JVM 中的线程调度器是基于优先级和线程饥饿策略来工作的。它确保所有可运行状态的线程都有机会执行。
调度策略:调度策略可以是抢占式的(基于线程优先级)或者是协作式的(线程主动让出CPU时间)。
- 线程状态:
线程状态:JVM 定义了线程的多种状态,如新建(New)、可运行(Runnable)、阻塞(Blocked)、等待(Waiting)、计时等待(Timed Waiting)和终止(Terminated)。
状态转换:线程的状态转换由JVM控制,如线程执行完毕、等待锁、等待其他线程的通知等。
- 内存管理:
线程栈:每个线程在JVM中都有自己的栈空间,用于存储局部变量、方法调用的上下文等。
堆空间:所有线程共享JVM堆空间,堆空间用于存储Java对象实例。
- 同步和通信:
同步:JVM 提供了 synchronized 关键字和 java.util.concurrent.locks 包中的锁机制,用于线程之间的同步。
通信:JVM 提供了 wait(), notify() 和 notifyAll() 方法,用于线程之间的通信。
- 异常处理:
异常处理:线程在执行过程中如果遇到未捕获的异常,JVM 会负责处理这个异常,可能会导致线程的终止。
- JVM对线程的支持:
线程局部变量:JVM 支持线程局部变量(ThreadLocal),允许创建线程私有的变量副本。
线程池:JVM 提供了线程池(如 ExecutorService),可以有效地管理线程资源,提高性能。
4.2 类加载器
双亲委派模型
双亲委派模型是 Java 类加载器(ClassLoader)的一种工作委派机制。它要求除了顶层的启动类加载器(Bootstrap ClassLoader)外,其余的类加载器在加载一个类之前,都会先委派给自己的父加载器去尝试加载,只有当父加载器无法加载时,子加载器才会自己尝试加载。
🧬 1. 类加载器的层次结构
Java 中主要有三层类加载器(JDK 8 及之前):
| 加载器 | 实现语言 | 加载范围 |
|---|---|---|
| 启动类加载器 (Bootstrap) | C++ (JVM内部) | JAVA_HOME/lib 中的核心类(如 rt.jar, java.lang.*) |
| 扩展类加载器 (Extension) | Java (sun.misc.Launcher$ExtClassLoader) | JAVA_HOME/lib/ext 或系统属性 java.ext.dirs 指定的目录 |
| 应用类加载器 (Application/System) | Java (sun.misc.Launcher$AppClassLoader) | CLASSPATH 或 -cp 指定的用户类路径 |
这三者形成父子关系:
Bootstrap ← Extension ← Application(注意:Bootstrap 是 Extension 的父加载器,但 Bootstrap 并非 Java 对象,通过 null 表示)
⚙️ 2. 双亲委派的工作流程
当应用类加载器收到一个类加载请求(比如 ClassLoader.loadClass("com.example.MyClass"))时:
- 检查缓存:当前加载器先查看自己是否已经加载过这个类,如果是则直接返回。
- 委派给父加载器:如果没有加载过,则调用父加载器的
loadClass()方法。 - 父加载器重复该过程:父加载器同样先检查缓存,再委托给自己的父加载器… 一直传递到 Bootstrap 类加载器。
- 父加载器无法加载:如果 Bootstrap 加载器找不到这个类(不在其搜索路径中),则会抛出
ClassNotFoundException,然后子加载器(Extension)尝试自己加载;如果 Extension 也找不到,再回到 Application 加载器尝试。 - 自己尝试加载:当前加载器最终在自己的搜索路径中查找并定义类。
用一句话概括:先向上委托,再向下尝试。
🛡️ 3. 为什么需要双亲委派?
-
避免类的重复加载
同一个类(全限定名)在整个 JVM 中只会被加载一次。父加载器加载过的类,子加载器不会再去加载。 -
保证核心类的安全
防止用户自定义的类“冒充”核心类库(例如定义java.lang.String)。因为java.lang.String会被 Boostrap 加载器优先加载,自定义的同名类永远不会被加载,从而保护了 JVM 的核心 API。 -
隔离机制(也是类加载器的“沙箱”安全模型的一部分)
不同层次的类加载器负责不同路径的类,维持了类型系统的唯一性和稳定性。
🚨 4. 例外与破坏双亲委派的场景
- Java 的 SPI(Service Provider Interface):如 JDBC、JNDI 等,它们需要调用由应用类加载器加载的实现类,但 SPI 的核心接口由 Bootstrap 加载。为了解决这个“父加载器调用子加载器”的矛盾,引入了线程上下文类加载器(Thread Context ClassLoader),可以设置和应用类加载器。
- Tomcat / 各类 Web 容器:为了实现 Web 应用之间的类隔离(多个应用可以使用不同版本的同一个库),它们破坏了双亲委派模型。例如,一个 Web 应用会优先从自己的
WEB-INF/classes中加载类,而不是向上委派。 - OSGi / 模块化系统:完全自定义了类加载的图结构,打破了层次委派。
注意:虽然存在破坏,但双亲委派仍然是 JVM 默认的标准模型。
📜 5. 核心代码示意(java.lang.ClassLoader.loadClass)
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查是否已加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 2. 存在父加载器,则委派给父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 3. 没有父加载器(说明自己是启动类加载器的孩子),调用本地方法查找 Bootstrap 路径
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父类加载器无法加载
}
if (c == null) {
// 4. 父加载器加载不到,自己尝试加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
🧠 总结
- 双亲委派模型是 JVM 类加载器的一种协作契约,确保类的唯一性和核心库的安全。
- 它形成了一个由下向上委托,再由上向下加载的树状结构。
- 虽然在某些场景(如 Web 容器、OSGi)需要被打破,但它是理解 Java 类加载机制的基础。
4.3 垃圾回收器
4.3.1 使用的算法
JVM 垃圾回收(GC)中,常见的算法主要有以下几种,它们是不同垃圾回收器实现的基础:
🔖 1. 标记-清除(Mark-Sweep)
原理:
- 标记:从 GC Roots 出发,遍历所有可达对象,并打上标记。
- 清除:遍历堆内存,回收未被标记的对象的内存。
优点:
- 无需移动对象,实现简单。
缺点:
- 内存碎片:回收后内存不连续,可能导致大对象无法分配而提前触发 Full GC。
- 效率问题:标记和清除阶段都需要遍历整个堆(或分代区域),暂停时间较长。
典型应用:CMS 回收器的“并发清除”阶段本质是标记-清除。
📦 2. 复制算法(Copying)
原理:
- 将内存划分为两个大小相等的区域(如 Eden + Survivor 的 From/To 区)。
- 每次只使用其中一个区域,当该区域满了,将存活对象复制到另一个区域,并清空原区域。
优点:
- 无碎片:复制后内存连续。
- 高效:只需处理存活对象,不需要遍历整个区域。
缺点:
- 内存利用率低:始终有一半空间闲置(HotSpot 优化为 Eden:Survivor=8:1,浪费降至 10%)。
- 不适于存活率高的场景:大量复制会降低效率。
典型应用:新生代 GC(如 Serial、ParNew、Parallel Scavenge)。
🔧 3. 标记-整理(Mark-Compact)
原理:
- 标记:同标记-清除,标记存活对象。
- 整理:将所有存活对象向一端移动,然后直接清理边界以外的内存。
优点:
- 无碎片:内存连续,利于大对象分配。
- 无需额外空间:相比复制算法,不需要双倍内存。
缺点:
- 移动对象需要更新引用,暂停时间长(但次数少)。
典型应用:老年代 GC(如 Serial Old、Parallel Old)。
⏳ 4. 分代收集算法(Generational Collection)
原理:
- 根据对象存活周期的不同,将堆分为新生代(Young Generation)和老年代(Old Generation)。
- 新生代使用复制算法(对象存活率低,回收效率高)。
- 老年代使用标记-清除或标记-整理(对象存活率高,移动成本高,但需避免碎片)。
优点:
- 扬长避短:针对不同区域采用最优算法,兼顾吞吐量与内存利用率。
- 事实上的工业标准(几乎所有商业 JVM 都采用分代思想)。
典型应用:几乎所有主流 GC(G1、ZGC 虽不严格分代,但仍有逻辑分代概念)。
🧩 5. 增量收集(Incremental GC)& 并发收集(Concurrent GC)
思想:
- 增量收集:将一次完整的垃圾回收切分成多次小停顿,避免长时间 STW。
- 并发收集:GC 线程与应用线程同时运行(如并发标记、并发清理),进一步减少暂停。
优点:降低最大停顿时间,适合对延迟敏感的应用。
缺点:实现复杂、可能增加 CPU 开销和浮动垃圾。
典型应用:CMS(并发标记清除)、G1(并发标记 + 局部回收)、ZGC(全并发)。
📊 算法对比总结
| 算法 | 内存碎片 | 额外空间 | 移动对象 | 适用场景 |
|---|---|---|---|---|
| 标记-清除 | 有 | 无 | 否 | 老年代(配合并发) |
| 复制 | 无 | 需要(浪费空间) | 是 | 新生代 |
| 标记-整理 | 无 | 无 | 是 | 老年代 |
| 分代 | 视具体算法 | 视具体算法 | 视具体算法 | 综合(主流) |
| 增量/并发 | 视具体算法 | 视具体算法 | 可能 | 低延迟应用 |
4.3.2 G1和ZGG
G1的核心思想是从“全局清理”转向 “优先回收收益最高的区域”(Garbage-First) ,让你可以设定一个期望的停顿时间目标(-XX:MaxGCPauseMillis),G1会尽力去达成。
-
工作流程:G1的回收过程像一个三步走的策略:
-
核心技术:
- 染色指针 (Colored Pointers) :ZGC将GC所需的标记信息直接存储在64位指针的某些位上,而不是传统的对象头里。这样在并发回收时,只需要修改指针本身的颜色标记,就能指明对象的状态。
- 读屏障 (Load Barrier) :当Java线程读取一个对象引用时,ZGC会插入一段轻量级的屏障代码,检查该染色指针。如果对象正在被移动,读屏障会“修正”线程拿到的地址,保证它总能访问到对象最新的正确位置[]
-
分代化演进:在JDK 21中,ZGC正式引入了分代ZGC(Generational ZGC) 。它和G1一样,在逻辑上重新将堆划分为年轻代和老年代,并优先、快速地回收年轻代中的“朝生夕灭”的对象,从而显著提升了整体效率
4.4 JVM调优实践
4.4.1 调优参数
好的,基于刚才给出的完整参数列表,下面挑出最常用、实际调优中最高频出现的 JVM 参数。掌握这些就足以应对绝大多数线上问题排查和性能优化场景。
🧠 堆内存(必设)
| 参数 | 说明 | 典型示例 |
|---|---|---|
-Xms | 初始堆大小 | -Xms2g |
-Xmx | 最大堆大小(最最重要) | -Xmx4g |
-XX:MaxMetaspaceSize | 元空间上限(防止膨胀) | -XX:MaxMetaspaceSize=256m |
建议将
-Xms和-Xmx设为相同值,避免运行时动态扩容带来的性能损耗。
♻️ 垃圾回收器选择
| 参数 | 说明 | 适用场景 |
|---|---|---|
-XX:+UseG1GC | 启用 G1(JDK9+ 默认,但显式指定更稳妥) | 通用型,适合大多数服务端应用 |
-XX:+UseParallelGC | 启用 Parallel GC(JDK8 默认) | 批处理、离线任务,对吞吐量要求高 |
-XX:+UseZGC | 启用 ZGC(JDK15+) | 超低延迟场景,如实时交易 |
⏱️ GC 调优(最常用)
| 参数 | 说明 | 默认值 | 适用 GC |
|---|---|---|---|
-XX:MaxGCPauseMillis | 期望最大 GC 停顿时间(毫秒) | 200ms | G1、Parallel、ZGC 等 |
-XX:ParallelGCThreads | 并行 GC 线程数 | CPU 核心数 | 所有并行 GC |
-XX:ConcGCThreads | 并发 GC 线程数 | 约 ParallelGCThreads 的 1/4 | G1、ZGC |
调优时先只设
-XX:MaxGCPauseMillis,让 GC 自己适应,不要一上来就调太多参数。
🧵 线程栈
| 参数 | 说明 | 默认 | 何时需要 |
|---|---|---|---|
-Xss | 每个线程栈大小 | 1MB(Linux) | 线程数非常多时减小(如 -Xss512k);递归深度大时增大 |
📝 GC 日志(排障必备)
JDK8 及以前
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
JDK9+(统一 -Xlog)
-Xlog:gc*:file=gc.log:time,uptime:filecount=5,filesize=10m
强烈建议生产环境开启 GC 日志,对分析内存问题帮助极大。
🔍 OOM 自动诊断
| 参数 | 说明 |
|---|---|
-XX:+HeapDumpOnOutOfMemoryError | OOM 时自动 dump 堆内存 |
-XX:HeapDumpPath=/path/to/dump | 指定 dump 文件保存路径 |
示例:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/logs/
⚙️ 其他常用
| 参数 | 说明 | 使用频率 |
|---|---|---|
-XX:+PrintCommandLineFlags | 启动时打印 JVM 最终生效的参数 | 排查问题时非常有用 |
-XX:+AlwaysPreTouch | 启动时申请并填充物理内存,减少运行时缺页故障 | 对延迟敏感的服务 |
-XX:+DisableExplicitGC | 禁止 System.gc() 触发 Full GC | 防止第三方代码随意 GC(慎用) |
-XX:+UseContainerSupport | 使 JVM 能感知容器内存/CPU 限制(JDK10+ 默认开启) | 容器化环境 |
示例
java -Xms4g -Xmx4g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=100 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/data/logs/ \
-Xlog:gc*:file=/data/logs/gc.log:time,uptime:filecount=5,filesize=10m \
-jar app.jar
✅ 小结
| 类别 | 最常用参数 |
|---|---|
| 堆 | -Xms, -Xmx, -XX:MaxMetaspaceSize |
| GC 选择 | -XX:+UseG1GC / -XX:+UseZGC |
| GC 调优 | -XX:MaxGCPauseMillis |
| 日志 | JDK8: -XX:+PrintGCDetails -Xloggc: JDK9+: -Xlog:gc*:file= |
| OOM | -XX:+HeapDumpOnOutOfMemoryError |
| 诊断 | -XX:+PrintCommandLineFlags |
掌握这些,你已经能覆盖 90% 的日常 JVM 调优需求了。如果需要针对特定 GC(如 ZGC)或特定问题(如内存泄漏)的进阶参数,再按需查阅即可。
4.4.2 调优工具
这些是JDK自带的轻量级工具,通常在线上服务器使用。它们无需图形界面,能快速定位CPU飙升、内存泄漏、线程死锁等问题,是排查线上问题的首选。使用前,通常先用 jps -l 查看Java进程ID。
- jps (JVM Process Status Tool) :JDK自带,用于查看当前用户下所有Java进程的进程ID (PID) 和主类名。
- jstat (JVM Statistics Monitoring Tool) :JDK自带,用于实时监控JVM的GC、类加载、JIT编译等运行状态统计数据。常用命令有
jstat -gcutil <pid> 1000(每秒输出一次GC概况) - Arthas:阿里巴巴开源的线上诊断利器,支持在线热更新。
- async-profiler:低开销的Java采样分析器,常与Arthas配合,生成CPU和内存的火焰图,精准定位性能热点。
4.4.3 内存泄漏排查思路
4.4.4 CPU飙高排查思路
线上CPU飙高是常见的性能问题,可能导致系统响应缓慢甚至宕机。以下是逐步排查和解决问题的详细方法。
- 系统级初步定位
检查整体CPU使用情况:
- 使用 top 或 htop 命令查看CPU占用最高的进程:
top
定位高CPU线程:
- 查看进程内线程的CPU占用:
top -Hp
- 应用级深入分析
获取线程堆栈信息:
- 使用 jstack 分析Java应用线程状态:
jstack | grep -A 20 <十六进制线程ID>
生成火焰图分析瓶颈:
- 使用 perf 工具采样并生成火焰图:
perf record -F 99 -p -g -- sleep 30
perf script | ./FlameGraph/stackcollapse-perf.pl | ./FlameGraph/flamegraph.pl > cpu-flamegraph.svg
- 常见原因与解决方案
- 死循环或高频计算: 检查代码逻辑是否存在死循环或复杂算法。 优化算法,避免高复杂度操作。
- 垃圾回收频繁(GC): 查看GC日志或使用 jstat 检查GC频率: jstat -gcutil 1000 优化JVM参数,例如使用G1 GC: -XX:+UseG1GC -XX:MaxGCPauseMillis=200
- 锁竞争或线程阻塞: 使用 jstack 查看阻塞线程,优化锁粒度或改用无锁数据结构。
- I/O操作频繁: 使用 iotop 检查磁盘I/O,减少不必要的读写操作。
- 临时应急措施
- 限流与降级: 暂时关闭非核心功能,限制请求流量。
- 重启异常进程: 如果问题无法快速解决,可考虑重启服务。
- 长期优化与监控
- 搭建监控系统(如 Prometheus + Grafana),实时监控CPU、GC、线程状态等。
- 定期进行性能测试,优化代码和数据库查询。
通过以上步骤,可以从系统到代码逐步定位并解决CPU飙高问题,有效保障系统稳定性。