Synchronized关键字
Java 中的 synchronized 是一种常见的同步机制,用于实现线程间的互斥访问,确保共享资源在多线程环境中的操作是安全的。
作用:
保证线程安全:
- 保证在多线程的环境下,同一时间只有一个线程可以访问被synchronized修饰的代码块或者方法
实现原子性:
- 防止线程切换造成的中间状态问题
可见性:
- 线程对共享变量的修改对其他线程可见。
使用方法
Java 中 synchronized 关键字有三种主要使用方式:
1.修饰实例方法
同步当前实例对象 (this) 的方法。
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
特点:
- 同步的是整个方法,锁定当前实例对象。
- 同一对象的不同线程调用此方法时,必须等待当前线程释放锁。
2.修饰静态方法
同步当前类 (Class 对象)。
public class Counter {
private static int count = 0;
public static synchronized void increment() {
count++;
}
public static synchronized int getCount() {
return count;
}
}
特点:
- 锁定的是 Class 对象,而非实例。
- 所有访问此类的线程会竞争同一个锁。
3.修饰代码块
同步代码块,指定锁对象。
public class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
public int getCount() {
synchronized (lock) {
return count;
}
}
}
特点:
- 可以灵活控制锁定的范围,减少锁的粒度,提高并发效率。
- 锁对象可以是任意对象,如 this、类对象 (Class) 或自定义的锁对象。
锁粒度
锁的粒度
粗粒度:
- 锁定方法或较大的代码块,容易导致线程阻塞,性能下降。
细粒度:
- 锁定更小的代码块,提高线程并发性,但需要小心死锁问题。
synchronized 的底层原理
1.JVM 实现:
- synchronized 是基于 JVM 实现的,依赖 monitor 对象。
- 每个对象都有一个关联的监视器锁 (monitor),当线程获取锁时,进入 monitor 的 ENTRY。
2.字节码层面
- synchronized 方法会在字节码中标记为 ACC_SYNCHRONIZED。
- synchronized 代码块使用 monitorenter 和 monitorexit 指令。
3.重量级锁到轻量级锁的优化:
- 偏向锁:如果只有一个线程竞争,锁会偏向该线程。
- 轻量级锁:当有少量线程竞争时,通过自旋锁避免线程阻塞。
- 重量级锁:竞争激烈时,升级为重量级锁,阻塞线程。
线程之间的同步于互斥
互斥:
- synchronized 确保同一时间只有一个线程进入临界区。
同步:
- 线程对共享变量是可见的
注意事项
避免死锁:
* 多线程中,如果两个线程互相持有对方需要的锁,可能导致死锁。
性能问题:
* 粒度太大的锁会降低并发性能。尽量缩小同步范围。
适配场景:
* 对读多写少的场景,可以考虑 ReentrantReadWriteLock 替代 synchronized。
ReentrantLock
特性
- 可重入性:同一线程可以多次获取同一把锁,而不会发生死锁(与 synchronized 行为一致)。
公平锁和非公平锁
- 公平锁:线程按先后顺序获取锁,先等待的线程优先。
- 非公平锁(默认):可能会有线程插队获取锁,提升吞吐量。
尝试锁(tryLock)
- 支持尝试获取锁而不会导致线程阻塞。
中断锁
- 可以通过 lockInterruptibly 方法在获取锁时响应中断。
条件变量(Condition)
- 可以通过 newCondition() 创建条件变量,用于实现线程的精确等待和唤醒。
基本用法
- 简答的加锁
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void criticalSection() {
lock.lock(); // 获取锁
try {
System.out.println(Thread.currentThread().getName() + " is in critical section");
Thread.sleep(1000); // 模拟操作
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 确保最终释放锁
}
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
Runnable task = example::criticalSection;
Thread t1 = new Thread(task, "Thread-1");
Thread t2 = new Thread(task, "Thread-2");
t1.start();
t2.start();
}
}
- 公平锁与非公平锁
import java.util.concurrent.locks.ReentrantLock;
public class FairLockExample {
private final ReentrantLock lock;
// 构造函数,指定公平锁或非公平锁
public FairLockExample(boolean fair) {
this.lock = new ReentrantLock(fair);
}
public void criticalSection() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " acquired lock");
Thread.sleep(1000); // 模拟操作
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
FairLockExample example = new FairLockExample(true); // true表示公平锁
for (int i = 1; i <= 5; i++) {
new Thread(example::criticalSection, "Thread-" + i).start();
}
}
}
- 尝试锁(tryLock)
import java.util.concurrent.locks.ReentrantLock;
public class TryLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void tryLockExample() {
if (lock.tryLock()) { // 尝试获取锁
try {
System.out.println(Thread.currentThread().getName() + " acquired lock");
Thread.sleep(1000); // 模拟操作
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " could not acquire lock");
}
}
public static void main(String[] args) {
TryLockExample example = new TryLockExample();
Thread t1 = new Thread(example::tryLockExample, "Thread-1");
Thread t2 = new Thread(example::tryLockExample, "Thread-2");
t1.start();
t2.start();
}
}
4.响应中断
lockInterruptibly 允许线程在等待锁时可以响应中断。
import java.util.concurrent.locks.ReentrantLock;
public class InterruptibleLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void criticalSection() {
try {
lock.lockInterruptibly(); // 可中断的锁
System.out.println(Thread.currentThread().getName() + " acquired lock");
Thread.sleep(2000); // 模拟操作
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " was interrupted");
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
public static void main(String[] args) {
InterruptibleLockExample example = new InterruptibleLockExample();
Thread t1 = new Thread(example::criticalSection, "Thread-1");
Thread t2 = new Thread(() -> {
try {
Thread.sleep(500);
t1.interrupt(); // 中断 t1
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
}
}
- 条件变量(Condition)
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionExample {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void await() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " waiting");
condition.await(); // 线程进入等待状态
System.out.println(Thread.currentThread().getName() + " resumed");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void signal() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " signaling");
condition.signal(); // 唤醒等待线程
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ConditionExample example = new ConditionExample();
Thread t1 = new Thread(example::await, "Thread-1");
t1.start();
Thread.sleep(2000);
Thread t2 = new Thread(example::signal, "Thread-2");
t2.start();
}
}
ReentrantLock和synchronized的对比
特性 | ReentrantLock | synchronized | |
---|---|---|---|
锁的属性 | 显式锁 | 隐式锁 | |
可中断 | 支持 | 不支持 | |
尝试锁 | 支持 | 不支持 | |
公平性 | 支持 | 不支持,仅非公平 | |
条件变量 | 支持 | 不支持 | |
性能 | 高并发性能由于synchronized | 一般场景性能较好 |
锁的升降级
锁的升降级和降级是与 Java 锁的实现(如 synchronized 和 ReentrantLock) 以及 JVM 的优化策略(如偏向锁、轻量级锁、重量级锁)密切相关的。以下是锁的升降级与降级的规则和机制:
锁的级别
Java 中的锁根据性能和功能的不同,通常分为以下几种级别:
- 无锁(No Lock):线程之间不涉及竞争。
- 偏向锁(Biased Lock):一个线程获取锁后,锁会偏向该线程,减少加锁和解锁的代价。
- 轻量级锁(Lightweight Lock):多线程竞争时,采用自旋的方式尝试获取锁,避免线程上下文切换的开销。
- 重量级锁(Heavyweight Lock):当自旋失败或竞争激烈时,锁会升级为重量级锁,涉及线程挂起和唤醒。
锁的升级规则
锁升级是 JVM 的一种优化策略,自动进行、不可逆。升级规则如下:
无锁 → 偏向锁:
- 如果锁对象是初次被一个线程访问,JVM 会将该对象标记为偏向锁,线程以后访问时无需加锁操作。
- 偏向锁在没有竞争的情况下效率极高。
偏向锁 → 轻量级锁:
- 当有其他线程尝试获取偏向锁时,偏向锁会被撤销。
- 进入轻量级锁阶段,多个线程尝试自旋获取锁。
轻量级锁 → 重量级锁:
- 当自旋的线程数量较多(超过一定次数,默认 10 次)或竞争激烈时,轻量级锁会升级为重量级锁。
- 升级为重量级锁后,线程会被挂起,避免进一步的 CPU 消耗。
锁的降级规则
锁的降级是 JVM 的另一种优化策略,用来降低锁的开销。降级规则如下:
重量级锁 → 轻量级锁:不会发生。
- 锁的升级是不可逆的,重量级锁不能回退到轻量级锁。
轻量级锁 → 偏向锁:
- 锁没有被释放,且竞争已经结束时,JVM 会尝试将锁恢复为偏向锁。
- 这个过程通常在偏向锁的重新分配机制中发生。
偏向锁 → 无锁:
- 偏向锁释放后,如果后续没有线程访问锁对象,则锁可能恢复到无锁状态。
原子性
原子操作是指一个不可中断的操作,保证操作的完整性,即使在多线程环境中也不会被其他线程干扰。换句话说,原子操作要么全部完成,要么全部失败,中间不会有任何状态暴露。
Java 的原子性
- 定义
原子性是多线程编程中的一种重要性质,用于描述某些操作在执行期间不会被线程切换中断。这是线程安全的基础之一。
Java 中的原子操作实现方式
(1)使用 synchronized
synchronized 关键字可以确保一个代码块或方法是原子的。它通过线程排队和锁机制,保证同一时刻只有一个线程能够执行同步代码块。
public synchronized void increment() {
i++;
}
缺点:
- 上下文切换开销较大。
- 多线程竞争时可能导致性能下降。
(2)使用 java.util.concurrent.atomic 包
Java 的 java.util.concurrent.atomic 包提供了一组线程安全的原子类,如 AtomicInteger、AtomicLong 等,内部使用 CAS (Compare-And-Swap) 实现高效的原子操作。
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
private static final AtomicInteger atomicInt = new AtomicInteger(0);
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
int newValue = atomicInt.incrementAndGet(); // 原子性自增
System.out.println("Thread " + Thread.currentThread().getName() + ": " + newValue);
}).start();
}
}
}
常用方法:
- incrementAndGet():自增并返回新值。
- decrementAndGet():自减并返回新值。
- compareAndSet(expect, update):如果当前值等于预期值,则更新为新值。
(3)使用锁(Lock 和 ReentrantLock)
Java 的 java.util.concurrent.locks.Lock 接口提供了一种显式锁机制,可以更细粒度地控制线程之间的访问
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
优点:
- 可以中断等待锁的线程(lockInterruptibly)。
- 更灵活的锁定机制,比如非阻塞获取锁 (tryLock)。
(4)Volatile 与原子性
volatile 修饰符只能保证 可见性 和 禁止指令重排,但不能保证操作的原子性。例如,以下代码不是线程安全的:
private volatile int count = 0;
public void increment() {
count++;
}
原子操作底层原理
Java 中的原子操作通常是通过 CPU 指令和底层内存模型实现的,核心机制包括
- CAS(Compare-And-Swap):
CAS 是一种硬件级别的原子操作,通过比较变量当前值和预期值是否相等来决定是否更新。如果相等,则执行更新操作;否则,重试直到成功。 - CPU 指令支持:
大多数现代处理器提供支持原子操作的指令,例如 x86 架构中的 lock cmpxchg。 - 内存屏障(Memory Barrier):
保证线程对共享变量的修改对其他线程可见,并禁止编译器和 CPU 指令重排序。
应用场景
- 计数器: 使用 AtomicInteger 实现并发计数器。
- 多线程环境下的 ID 生成: 确保唯一性。
- 无锁数据结构: 使用原子类实现高效、线程安全的队列或栈。
voliate
volatile 是 Java 中的一个修饰符,用于修饰变量,保证 可见性 和 有序性,但不能保证操作的 原子性。它主要用于多线程环境下的变量共享,确保一个线程对变量的修改对其他线程立即可见。
作用
- 可见性
当一个线程修改了 volatile 修饰的变量,新的值会立即被刷新到主内存中。其他线程读取该变量时会直接从主内存获取最新值,而不是从自己的工作内存中读取过期的缓存值。 - 有序性
volatile 可以防止指令重排序,确保在它之前的操作不会被编译器或 CPU 调整到它之后。
等待/通知机制
等待/通知机制是多线程编程中的一种重要通信方式,主要用于线程之间协调工作。通过一个线程等待条件满足,另一个线程在条件满足时通知,避免忙等(不断轮询消耗 CPU 资源)。
在 Java 中,等待/通知机制依赖于 Object 类的以下三个方法:
- wait():让当前线程进入等待状态,释放锁,并等待其他线程的通知。
- notify():唤醒一个正在等待同一对象的线程。
- notifyAll():唤醒所有正在等待同一对象的线程。
这些方法必须在同步块(synchronized)中调用,因为它们需要先获得对象的监视器锁。
基本原理
- 等待线程:
调用 wait() 后,线程释放当前对象的锁,并进入等待队列。 - 通知线程:
调用 notify() 或 notifyAll() 后,从等待队列中唤醒一个或全部线程,使它们尝试重新获得锁并继续执行。
代码示例:等待/通知
class Buffer {
private int data = 0; // 缓冲区数据
private boolean hasData = false; // 缓冲区状态标记
public synchronized void produce() throws InterruptedException {
while (hasData) { // 缓冲区满时等待
wait();
}
data++; // 生产数据
System.out.println("Produced: " + data);
hasData = true;
notify(); // 通知消费者
}
public synchronized void consume() throws InterruptedException {
while (!hasData) { // 缓冲区为空时等待
wait();
}
System.out.println("Consumed: " + data);
hasData = false;
notify(); // 通知生产者
}
}
public class WaitNotifyExample {
public static void main(String[] args) {
Buffer buffer = new Buffer();
// 生产者线程
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
buffer.produce();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 消费者线程
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
buffer.consume();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}
wait 和 notify 的注意事项
必须在同步块或方法内调用:
- 调用 wait() 或 notify() 的对象必须是当前线程持有的锁对象。
- 否则,会抛出 IllegalMonitorStateException 异常。
释放锁:
- wait() 会释放当前对象的锁,但并不会释放其他锁。
- 唤醒后,线程需重新竞争锁才能继续执行。
避免虚假唤醒:
- 使用 while 循环检查条件,而不是 if,以防止虚假唤醒。
线程间通信必须基于同一对象:
- wait() 和 notify() 调用的对象应该是同一个,否则线程无法正确协调。
notify 和 notifyAll 的区别
方法 | 作用 |
---|---|
单元 1 | 唤醒一个正在等待同一对象锁的线程,线程具体由 JVM 选择。 |
单元 3 | 唤醒所有正在等待同一对象锁的线程,优先级高的线程先执行。 |
建议:如果只有一个线程在等待,可以用 notify;多个线程等待时,用 notifyAll 以避免线程饥饿。
等待/通知机制 VS 其他同步工具
- wait/notify:是较底层的机制,需要手动管理同步逻辑。
- Lock 和 Condition:是更高级的工具,提供了更加灵活的线程等待与通知机制。
- BlockingQueue:是高层抽象,内置生产者/消费者模式,无需显式调用等待或通知。
ThreadLocal
ThreadLocal 是 Java 提供的一种线程本地存储机制,用于为每个线程创建一个独立的变量副本。每个线程可以独立地操作自己的副本,互不影响,适合在多线程环境下实现线程隔离。
ThreadLocal 的主要方法
get():
- 获取当前线程绑定到 ThreadLocal 的值。
- 如果当前线程没有值,则返回 null 或初始化的默认值。
set(T value):
- 将值绑定到当前线程的 ThreadLocal。
remove():
- 移除当前线程绑定的值,避免内存泄漏。
initialValue():
- 定义当前线程值的初始值,默认返回 null,可通过继承 ThreadLocal 并重写此方法实现。
ThreadLocal 的应用场景
- 用户信息隔离:在 Web 应用中保存每个线程对应的用户会话信息。
- 数据库连接管理:为每个线程分配独立的数据库连接。
- 事务管理:在多线程中保存当前事务上下文。
- 线程安全对象:如 SimpleDateFormat。
示例
- 使用 ThreadLocal 管理用户信息
public class UserContext {
private static ThreadLocal<String> userThreadLocal = ThreadLocal.withInitial(() -> "Unknown User");
public static void setUser(String user) {
userThreadLocal.set(user);
}
public static String getUser() {
return userThreadLocal.get();
}
public static void removeUser() {
userThreadLocal.remove();
}
}
public class ThreadLocalDemo {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
UserContext.setUser("User1");
System.out.println(Thread.currentThread().getName() + " - " + UserContext.getUser());
UserContext.removeUser();
});
Thread t2 = new Thread(() -> {
UserContext.setUser("User2");
System.out.println(Thread.currentThread().getName() + " - " + UserContext.getUser());
UserContext.removeUser();
});
t1.start();
t2.start();
}
}
ThreadLocal 内存泄漏问题
原因
- ThreadLocal 的值存储在 Thread 的 ThreadLocalMap 中,使用的是弱引用。
- 如果线程存活时间较长,而没有手动调用 remove(),则可能导致内存泄漏。
解决方式
- 显式调用 remove():
ThreadLocal工作流程
内部实现结构:
- 每个线程对象 (Thread) 都有一个 ThreadLocalMap。
- ThreadLocalMap 是一个以 ThreadLocal 为键的弱引用键值对。
存储过程:
- 当调用 threadLocal.set(value) 时,当前线程的 ThreadLocalMap 会保存 ThreadLocal -> value 的映射。
获取值:
- 调用 threadLocal.get() 时,通过当前线程的 ThreadLocalMap 找到对应的值。
清理:
- 因为 ThreadLocal 的键是弱引用,当 ThreadLocal 被回收后,ThreadLocalMap 的值无法再被访问,但值仍然会占用内存,需手动调用 remove()。
ThreadLoacl优缺点
优点
- 线程隔离:每个线程独立持有变量,避免数据共享导致的线程安全问题。
- 简单易用:在特定场景下代替复杂的同步机制。
缺点
- 内存泄漏风险:长时间运行的线程未清理 ThreadLocal 值会造成内存泄漏。
- 生命周期管理复杂:需要显式调用 remove(),否则容易出错。
ReentrantReadWriteLock
ReentrantReadWriteLock 是 Java 并发包 (java.util.concurrent.locks) 提供的一个读写锁实现,允许多个读线程同时访问,但在写线程访问时,所有线程(包括读线程)都会被阻塞。
组成
读锁 (ReadLock):
- 支持多个线程并发读取。
- 前提是没有线程持有写锁。
- 使用场景:数据只读时,提升并发性能。
写锁(WriteLock)
- 独占锁,同时只允许一个线程持有。
- 在写锁持有期间,其他线程(包括读线程和写线程)都会被阻塞。
- 使用场景:需要修改数据时,确保线程安全。
主要方法
- 读锁相关方法
- lock():获取读锁,如果当前有写锁,会阻塞。
- tryLock():尝试获取读锁,立即返回结果。
- unlock():释放读锁。
- 写锁相关方法
- lock():获取写锁,其他线程必须等待释放后才能获取。
- tryLock():尝试获取读锁,立即返回结果。
- unlock():释放写锁。
- 状态查询方法
- getReadLockCount():当前持有读锁的线程数。
- isWriteLocked():写锁是否被某线程持有。
- getWriteHoldCount():当前线程持有写锁的次数(重入次数)。
- getReadHoldCount():当前线程持有读锁的次数。
特性
支持可重入
- 持有锁的线程可以重复获取同一个锁。
- 支持读锁、写锁的分别重入。
写锁降级为读锁
- 允许持有写锁的线程获取读锁,这称为锁降级。
锁降级通过以下步骤完成:
- 先获取写锁。
- 再获取读锁。
- 最后释放写锁。
写锁不能升级为读锁
- 持有读锁的线程尝试获取写锁会导致死锁。
优势
提高读性能:
- 允许多个读线程并发读取,适合读多写少的场景。
读写分离:
- 写线程不会阻塞读线程,除非写线程持有锁。
线程安全:
- 提供线程级别的独占性和互斥控制。
注意事项
锁饥饿问题:
- 如果有大量的读线程,可能导致写线程长期等待写锁。
性能开销:
- 对于简单的线程竞争场景,ReentrantLock 可能比 ReentrantReadWriteLock 性能更优。
锁降级:
- 推荐在写操作后立刻降级为读锁,提高读操作性能。
LockSupport
LockSupport 是 Java 并发包中的一个工具类,主要用于线程的阻塞和唤醒。它提供了一种灵活而高效的线程同步机制,相较于 Object.wait() 和 Object.notify(),LockSupport 更加底层和轻量。
核心方法
LockSupport 的核心是两个静态方法:park() 和 unpark(Thread thread)。
1.park()
- 使当前线程阻塞,直到被唤醒。
- 阻塞状态是基于许可(permit)的。每个线程都有一个与之关联的许可,park() 会消费许可。
- 如果许可不可用,则线程会阻塞;如果许可已存在,则调用 park() 不会阻塞线程。
2.unpark(Thread thread)
- 唤醒指定线程,使其可以从 park() 状态中恢复。
- 如果目标线程尚未被 park(),则 unpark 会预先授予一次许可。
- 每个线程最多只能持有一个许可,重复调用 unpark() 不会累计。
基本用法
防止死锁的公平队列实现
import java.util.concurrent.locks.LockSupport;
class FairLock {
private Thread currentThread = null;
public void lock() {
Thread thread = Thread.currentThread();
while (currentThread != null) {
LockSupport.park(); // 挂起当前线程
}
currentThread = thread; // 获取锁
}
public void unlock() {
if (Thread.currentThread() == currentThread) {
currentThread = null;
LockSupport.unpark(Thread.currentThread()); // 唤醒等待线程
}
}
}
注意事项
1.许可特性:
* unpark() 提供的许可是一次性的,且不会累计。重复调用 park() 会阻塞线程。
2.避免死锁:
* 如果线程在 park() 之前没有调用 unpark(),线程会一直阻塞。因此,需要确保调用 unpark() 的逻辑不会遗漏。
3.中断兼容:
* park() 会响应线程的中断状态。如果线程被中断,park() 不会阻塞,但不会抛出 InterruptedException。
4.线程安全:
* LockSupport 的操作是线程安全的,可以在多线程环境下安全地使用。
应用场景
- 阻塞队列的实现
在高性能阻塞队列如 LinkedBlockingQueue 中,LockSupport 常用于线程等待和唤醒。 - 线程池实现
Java 中的线程池使用 LockSupport 来管理线程的挂起和激活。 - 自定义锁
基于 LockSupport 可以实现更加灵活的锁机制,如公平锁或非公平锁。 - JUC(Java并发包)工具类
LockSupport 是很多 JUC 工具类的基础,如 CountDownLatch、Semaphore、ReentrantLock 等。
阻塞队列
阻塞队列是一种支持线程阻塞的队列,当队列为空或已满时,操作队列的线程会自动阻塞,直到条件满足后被唤醒。
- 主要功能
- 当队列为空,获取操作(take())会阻塞,直至队列之中有元素为止
- 当队列已满,插入操作(put())会阻塞,直至队列有空闲空间为止
阻塞队列的应用场景
生产-消费者模型
- 生产者不断将数据加入队列中,消息从队列中取出
- 阻塞队列自动处理生产者和消费者的速度差,避免繁琐的锁操作
线程池任务队列
- 在线程池中,任务会被提交到阻塞队列之中
限流控制
- 通过限制队伍容量,可以有效控制数据处理的速度,防止系统过载
阻塞队列的实现
Java 提供了多种阻塞队列实现,主要位于 java.util.concurrent 包中。
- ArrayBlockingQueue:有界队列,基于数组,容量固定
- LinkedBlockingQueue:可选有界队列,基于链表,适合数据流入和流出速度不同的场景
- PriorityBlockingQueue: 基于优先级的无界队列,元素按照优先级排列,但插入操作稍慢
- DeleyQueue:储存接口实现了Delayed接口的元素,只有到期以后才能被取出
- SynchronuosQueue:不存储元素,每次插入操作必须等待相应的移除操作完成,适合高同步场景
阻塞队列的基本方法
插入元素
- put(E e): 如果队列已满,线程阻塞直到有空闲空间为止
- offer(E e,long timeout,TimeUnit nuit): 尝试在指定的时间内插入,失败返回false
获取元素
- take():如果队列为空,阻塞直到有可用元素
- poll(long time, TimeUnit unit):尝试在指定时间内获取元素,超时返回 null。
其他方法
- size():获取队列当中的元素个数
- remainingCapacity() :获取队列剩余容量
阻塞队列的实现原理
基于锁和条件变量
- 阻塞队列通常使用重入锁(ReentrantLock)和条件变量(Condition)实现线程的阻塞与唤醒
- 插入和获取操作分别有条件变量notEmpty和notFull,用于在条件不满足时阻塞
无锁实现(如 ConcurrentLinkedQueue):
- 部分无界队列使用 CAS(Compare-And-Swap) 操作实现无锁线程安全。
- CAS 能确保操作的原子性,提高并发性能。
阻塞队列的优缺点
- 优点:
- 自动管理线程同步,避免显式锁的使用
- 支持多线程高效并发,性能优越
- 提供多种实现,满足不同场景的需求(有界,无界,优先级)
- 缺点:
- 在高负载的情况下会出现队列耗尽或者插入过慢的情况
- 部分阻塞队列的性能可能低于无锁实现
说一说CAS
CAS(Compare-And-Swap)的底层实现主要依赖于 CPU 的硬件支持,通过特定的原子指令完成操作,从而确保线程安全性
底层原理
CAS 是一种 乐观锁机制,它基于“先比较,后交换”的思想,完成并发情况下的变量更新。
它需要三个参数:
- V:内存中的变量值(预期值)。
- E:期望值(Expected value)。
- N:需要更新的新值(New value)。
过程:
- 如果变量的当前值 V 等于期望值 E,则将变量更新为新值 N。
- 如果 V 不等于 E,说明变量已被其他线程修改过,则不进行更新。
这种方式通过硬件指令保证整个操作的原子性。
CAS 底层实现
在Java中,CAS是由Unsafe类的本地方法实现的,其核心是依赖于CPU提供的原子性操作指令(如x86的cmpxchg)。
实现方式:
CPU指令支持
- 在 x86 架构下,CAS 是通过 cmpxchg(Compare and Exchange)指令实现的。
- 该指令对一个内存地址的值进行比较和条件性替换,这个过程是原子的
Unsafe 类
- Java 提供了 sun.misc.Unsafe 类作为 CAS 操作的入口。
- Unsafe 类的方法 compareAndSwapInt、compareAndSwapLong、compareAndSwapObject 分别对整型、长整型和对象引用进行原子操作。
JNI 实现
- compareAndSwapXxx 是用 JNI 调用底层的 C/C++ 实现的,与硬件直接交互。
- 在底层,它会调用 cmpxchg 或其他架构对应的原子性指令。
CAS 的优缺点
优点:
- 无锁操作:避免了线程阻塞,提高了性能。
- 原子性:由硬件保证 CAS 操作的原子性,线程安全。
- 高效:尤其在冲突较少的情况下性能优势明显。
缺点:
ABA 问题:
- 如果一个变量的值从 A 改成 B,又从 B 改回 A,CAS 无法识别这种情况。
- 解决方法:可以结合 版本号 或 引用对象+版本号的 AtomicStampedReference。
自旋消耗 CPU:
- 如果 CAS 操作失败,会不断重试,消耗 CPU 资源。
只能保证单个变量的原子性:
- 对于多个变量的操作,需要使用锁或者其他机制。
并发容器
常见的并发容器
- ConcurrentHashMap
- 用途:线程安全的哈希表,用于高效地存储和访问键值对。
底层实现:
Java 7:
- 基于分段锁实现,将数据分成多个段,每个段是一个小哈希表,拥有自己的锁,减少锁的争用
- 默认分为16段
Java 8:
- 使用 CAS 和 synchronized代替分段锁
- 数据结构是数组 + 链表 + 红黑树
- 当链表长度超过阈值(8)时,会转换为红黑树,降低时间复杂度
- 读操作:使用voliate关键字和CAS确保操作无锁
- 写操作:采用 CAS 或 synchronized,必要时对单个桶加锁,减少锁粒度。
- CopyOnWriteArrayList
- 用途:线程安全的动态数组,适用于读多写少的场景
底层实现:
- 所有修改操作(如 add、remove)会复制一份底层数组,操作完成后会用新数组替换原数组
- 读操作可以无锁进行,因为读操作直接使用快照数组
- 修改的性能较差,因需创建新数组,但读性能优异。
- CopyOnWriteArraySet
- 用途:线程安全的 Set 实现,基于 CopyOnWriteArrayList。
底层实现:
- 使用CopyOnWriteArrayList存储元素
- 写入元素的时候会检查是否存在重复元素,避免重复
- ConcurrentSkipListMap 和 ConcurrentSkipListSet
- 用途:线程安全的有序 Map 和 Set,基于跳表实现,支持高效的范围查找和排序。
底层实现:
- 使用跳表(Skip List)数据结构。
- 插入、删除和搜索操作通过 CAS 实现线程安全。
- 跳表通过多层索引提升查找性能,平均时间复杂度为 O(log N)。
- BlockingQueue(ReentrantLock + Condition)
- 用途:线程安全的队列,适合生产者-消费者模式
实现类:
ArrayBlockingQueue:
- 基于数组的有界阻塞队列,使用单一锁机制。
LinkedBlockingQueue
- 基于链表的阻塞队列,分别为生产者和消费者设置锁,减少锁竞争
PriorityBlockingQueue:
- 基于堆实现的优先级队列,无届队列
SynchronousQueue:
- 不存储元素,生产者和消费者必须直接交接数据。
DelayQueue
- 基于 PriorityQueue 实现的队列,元素到期后才能取出
底层实现:
- 使用 ReentrantLock 和 Condition 实现线程安全和阻塞。
- 生产者使用 notFull 条件变量,消费者使用 notEmpty 条件变量。
- ConcurrentLinkedQueue和ConcurrentLinkedDeque
- 用途:线程安全的无界非阻塞队列(Queue)和双端队列(Deque)。
底层实现:
- 使用 CAS 和链表实现。
- 每次入队或出队操作都会使用 CAS 更新头节点或尾节点。
- 避免了锁的使用,适合高并发场景。
常见容器底层实现机制
- CAS
锁机制
- 分段锁:Java7 ConcurrentHashMap 一般分为16段
- ReentrantLock:支持公平锁和非公平锁,细化锁粒度,提高性能。
- synchronized: Java8 ConcurrentHashMap偶尔使用
volatile
- 用于保证变量的可见性
Condition
- 用于阻塞队列
Copy-on-Write
- 开辟新的数组供数据写,修改操作基于数组快照,避免了锁的竞争。
跳表
- 使用跳表代替红黑树,利用多层索引实现高效查找
Fork/Join
Java 7 中引入的一种用于并行编程的框架,专为分治任务而设计。它在 java.util.concurrent 包中提供,允许程序将大任务拆分成多个小任务,并将这些小任务分配到多个线程中执行,从而充分利用多核 CPU 的并行计算能力。
Fork/Join 框架的核心概念
- 分治思想
Fork/Join 框架基于 分治法:
- 分解(Fork):将复杂的大任务拆分成若干小任务。
- 处理(Compute):如果任务足够小,直接计算。
- 合并(Join):将小任务的结果合并,形成最终结果。
ForkJoinPool
- ForkJoinPool是Fork/Join框架的执行器
- 它管理一组工作线程,通过一种工作窃取算法来高效执行任务
- 每个线程都有一个双端队列,用于存储要执行的任务
ForkJoinTask
- ForkJoinTask 是任务的基本单元,是一个抽象类
子类
- RecursiveTask:用于有返回值的任务
- RecursiveAction:用于无返回值的任务
Fork/Join 框架的工作流程
- 主任务(ForkJoinTask)被提交到 ForkJoinPool。
- 主任务通过 fork() 方法将任务拆分成子任务,子任务进一步拆分直到足够小。
- 每个子任务被分配给线程处理,任务完成后通过 join() 方法合并结果。
- 如果线程空闲,会尝试从其他线程的队列中“窃取”任务以提高效率(Work-Stealing)。
优势
- 高效利用多核 CPU:通过分治法和工作窃取算法最大化线程利用率。
- 任务动态分配:通过工作窃取算法解决负载不均问题。
- 简单易用:开发者只需专注于任务的拆分和合并逻辑。
缺点
- 适用范围有限:适合计算密集型任务,不适合 I/O 密集型任务。
- 任务拆分开销:过多的小任务可能导致性能下降。
- 调试复杂性:并行化后的代码调试和排查问题较为复杂。
工作窃取算法
工作窃取算法是 ForkJoinPool 的核心优化机制:
- 每个线程有一个双端队列,存储它的任务。
- 如果线程完成了自己的任务,会从其他线程的队列尾部窃取任务,以减少空闲时间。
- 这种方式有效平衡了线程之间的负载,提升了性能。
CountDownLatch
CountDownLatch 是 Java 并发工具包(java.util.concurrent)中提供的一个同步辅助类,用于管理多个线程之间的协作。它通过一个计数器实现,线程可以等待计数器变为零再继续执行,或者通过减少计数器的值来释放其他线程的等待。
主要方法
CountDownLatch(int count)
- 创建一个 CountDownLatch 实例,count 是计数器的初始值。
void await()
- 让当前线程等待,直到计数器为零,或者线程被中断。
void countDown()
- 减少计数器的值。如果计数器变为零,所有调用了 await() 的线程会被唤醒。
long getCount()
- 获取当前计数器的值(通常用于监控或调试)。
工作原理
- 初始化时设置计数器的值(比如 new CountDownLatch(3))。
- 主线程调用 await() 方法进入等待状态。
- 每个工作线程在完成任务后调用 countDown() 将计数器减一。
- 当计数器减为零时,所有调用 await() 的线程会被唤醒,继续执行。
应用场景
- 线程启动协调:主线程等待多个线程完成初始化后再执行任务。
- 并行任务等待:多个任务并行执行,主线程等待所有任务完成后汇总结果。
- 控制服务启动顺序:多个服务按照依赖关系启动,只有当依赖的服务启动完成后,才能启动当前服务。
注意事项
- 计数器只能减,不能加:CountDownLatch 的计数器是一次性的,无法重置或增加。
- 线程安全:countDown() 和 await() 是线程安全的,可以在多个线程中调用。
- 不能重复使用:如果需要复用,建议使用 CyclicBarrier。
CyclicBarrier
CyclicBarrier 是 Java 并发包 (java.util.concurrent) 中的一个同步辅助类,用于让一组线程彼此等待,直到所有线程都到达一个公共屏障点(barrier)。它适合于多线程协作的场景,例如分阶段计算或任务同步。
核心特性
可重用
- 与CountDownLatch不同,CyclicBarrier在释放线程后可以被重置并重新使用。
屏障动作(Barrier Action)
- 在所有线程到达屏障点时,可以指定一个额外的任务来执行,这个任务由一个线程完成。
主要方法
CyclicBarrier(int parties)
- 创建一个 CyclicBarrier,指定参与线程的数量。
CyclicBarrier(int parties, Runnable barrierAction)
- 创建一个 CyclicBarrier,指定参与线程的数量,并定义一个在所有线程到达屏障点时要执行的动作。
int await()
- 线程调用此方法等待其他线程到达屏障点。
- 当所有线程都调用了 await(),屏障点打开,线程继续执行。
int getNumberWaiting()
- 返回当前有多少线程正在等待。
int getParties()
- 返回 CyclicBarrier 的参与线程总数。
boolean isBroken()
- 如果屏障点被打破(如线程被中断),返回 true。
工作原理
- 初始化时设置屏障线程数量(例如 new CyclicBarrier(3))。
- 每个线程调用 await() 方法后进入等待状态。
当所有线程到达屏障点时:
- 可选:执行屏障动作(barrierAction)。
- 屏障点打开,所有线程继续执行。
- 执行完成后,屏障会被重置,可以复用。
应用场景
- 分阶段任务
多线程任务分为多个阶段,每个阶段需要所有线程同步后才能进入下一阶段。 - 并行计算
将大任务分解为多个线程并行处理,所有线程完成后再汇总结果。 - 模拟比赛
所有参赛选手准备好后一起开始比赛。
注意
- 复用机制
CyclicBarrier 可以被复用,但只有当所有线程都通过屏障点后才能重置。 - 屏障损坏
如果一个线程被中断或超时,CyclicBarrier 会进入损坏状态(BrokenBarrierException),其他线程也无法继续通过屏障。 - 屏障动作异常
如果屏障动作(barrierAction)抛出异常,所有线程会抛出 BrokenBarrierException。
CyclicBarrier 与 CountDownLatch 的区别
特征 | CyclicBarrier | CountDownLatch |
---|---|---|
重用性 | 可重用,每次线程通过屏障后可以重置。 | 不可重用,计数器减到零后失效。 |
线程数量 | 需要所有线程都到达屏障点。 | 主线程可以等待多个线程完成任务。 |
屏障动作 | 支持屏障动作,线程同步后可执行操作。 | 不支持屏障动作。 |
典型场景 | 多阶段任务,分块并行计算。 | 任务初始化,主线程等待结果汇总。 |
Semaphore
Semaphore 是 Java 并发包(java.util.concurrent)中的一个同步工具类,用于控制同时访问特定资源的线程数量。它可以用于实现流量控制、资源池管理等场景。
核心特征
- 许可(Permit):信号量通过维护一个许可的计数来限制线程访问共享资源的数量。
- 可公平性(Fairness):支持公平模式,保证线程按照申请的顺序获取许可。
- 阻塞/非阻塞:支持线程获取许可时进行阻塞等待,或者尝试非阻塞获取许可。
构造方法
- Semaphore(int permits)
- 创建一个非公平的信号量,指定可用的许可数(permits)。
- Semaphore(int permits, boolean fair)
创建信号量,指定许可数和公平性:
- true:公平模式,线程按申请顺序获取许可
- false:非公平模式,可能发生插队
主要方法
void acquire()
- 从信号量中获取一个许可,若没有可用的许可,则阻塞等待。
void acquire(int permits)
- 获取指定数量的许可,若没有足够的许可,则阻塞等待。
boolean tryAcquire()
- 尝试获取一个许可,成功返回 true,失败返回 false,不阻塞。
boolean tryAcquire(int permits, long timeout, TimeUnit unit)
- 尝试在指定时间内获取许可,超时则返回 false。
void release()
- 释放一个许可,将其返回信号量。
void release(int permits)
- 释放指定数量的许可。
int availablePermits()
- 返回当前可用的许可数。
int getQueueLength()
- 返回正在等待许可的线程数。
代码实现
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
// 创建一个信号量,最多允许 3 个线程访问
Semaphore semaphore = new Semaphore(3);
// 创建并启动 6 个线程
for (int i = 1; i <= 6; i++) {
new Thread(new Task(semaphore, "Thread-" + i)).start();
}
}
}
class Task implements Runnable {
private final Semaphore semaphore;
private final String name;
public Task(Semaphore semaphore, String name) {
this.semaphore = semaphore;
this.name = name;
}
@Override
public void run() {
try {
System.out.println(name + " is trying to acquire a permit...");
semaphore.acquire(); // 获取许可
System.out.println(name + " acquired a permit. Performing task...");
Thread.sleep(2000); // 模拟任务执行
System.out.println(name + " finished task. Releasing permit...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放许可
}
}
}
应用场景
- 资源池管理如:数据库连接池、线程池,限制最大并发资源访问数。
- 流量控制:限制访问某些资源(如 API 请求、文件写入)的速率。
- 任务限流:控制某一类任务的并发执行数量。
注意事项
非公平模式性能更高:
- 非公平模式下,线程获取许可的效率更高,但可能导致某些线程“饥饿”。
许可释放需在 finally 块中
- 确保发生异常时也能释放许可,避免信号量泄漏。
许可数量控制
- 创建信号量时应合理设置许可数,过小可能导致任务阻塞过多。
Exchanger
Exchanger 是 Java 并发包 (java.util.concurrent) 中提供的一种线程同步工具,设计用于在两个线程之间交换数据。它可以让两个线程在一个同步点(同步屏障)相互交换数据,通常用于需要数据共享或双向通信的场景。
核心特性
- 一对一交换
Exchanger 仅支持两个线程之间交换数据。 阻塞同步
- 当一个线程调用 exchange() 时,会阻塞等待另一个线程到达同步点。
- 两个线程都到达后,它们会交换数据并继续执行。
- 线程安全
由 Exchanger 管理的交换过程是线程安全的。
构造方法
Exchanger<T>()
创建一个可以交换类型为 T 的数据的 Exchanger 对象。
主要方法
- T exchange(T data)
当前线程将 data 传递给另一个线程,并接收另一个线程的 data。此方法会阻塞直到交换完成。 - T exchange(T data, long timeout, TimeUnit unit)
当前线程将 data 传递给另一个线程,并接收另一个线程的 data。如果在指定时间内未完成交换,会抛出 TimeoutException。
代码实现
import java.util.concurrent.Exchanger;
public class ExchangerExample {
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<>();
Thread thread1 = new Thread(() -> {
try {
String data = "Data from Thread-1";
System.out.println(Thread.currentThread().getName() + " is exchanging: " + data);
String received = exchanger.exchange(data);
System.out.println(Thread.currentThread().getName() + " received: " + received);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
});
Thread thread2 = new Thread(() -> {
try {
String data = "Data from Thread-2";
System.out.println(Thread.currentThread().getName() + " is exchanging: " + data);
String received = exchanger.exchange(data);
System.out.println(Thread.currentThread().getName() + " received: " + received);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
});
thread1.start();
thread2.start();
}
}
应用场景
- 双缓冲优化
一个线程生成数据,一个线程消费数据,通过 Exchanger 在两个线程之间切换缓冲区。 - 数据处理和验证
两个线程处理同一数据的不同部分,并在同步点交换结果以整合最终输出。 - 任务分工与结果汇总
两个线程各自执行不同的任务,并通过 Exchanger 共享计算结果。
注意事项
一对一线程配对
Exchanger 只能用于两个线程,若只有一个线程调用 exchange(),它会一直阻塞。
超时控制
使用带超时的 exchange() 避免线程无限期阻塞。
可能的死锁
如果两个线程未能正确调用 exchange(),可能导致线程阻塞。例如,一个线程提前退出或抛出异常。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。