并发编程知识点
重入锁
可重入锁(Reentrant Lock)是一种允许同一个线程多次获取同一把锁而不会被阻塞的同步机制。在 Java 中,ReentrantLock
类和 synchronized
关键字实现的锁都是可重入的。其核心特性是线程对锁的重入不会导致死锁,而是通过计数器记录锁的获取次数,释放时需匹配相同次数才能完全解锁。
核心机制
-
计数器(State)
锁内部维护一个状态计数器(如 AQS 中的state
变量):- 初始值为 0:表示锁未被持有。
- 线程首次获取锁:计数器加 1(如从 0→1),记录持有者线程。
- 同一线程重入:计数器递增(如 1→2→3...),无需等待锁释放。
- 释放锁时:计数器递减,直至为 0 时锁才真正释放。
-
线程标识
锁需记录当前持有者线程,仅允许该线程重入。其他线程请求锁时会被阻塞。
代码示例分析
在你提供的代码中,重入锁的实现逻辑如下:
else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;
}
- 条件判断:
current == getExclusiveOwnerThread()
检查当前线程是否已持有锁。 - 计数器递增:
nextc = c + acquires
将状态值累加(通常acquires=1
)。 - 溢出检查:防止重入次数过多导致整数溢出(
nextc < 0
)。 - 更新状态:通过
setState(nextc)
更新重入次数,无需 CAS 操作(因已持有锁)。
为什么需要可重入锁?
-
避免死锁
若锁不可重入,同一线程多次请求同一把锁会导致自己等待自己释放,造成死锁。
示例:public synchronized void methodA() {methodB(); // 若锁不可重入,此处会阻塞,导致死锁 }public synchronized void methodB() {// 业务逻辑 }
-
简化代码逻辑
允许在递归或嵌套调用中自然使用锁,无需担心锁的获取次数。
对比不可重入锁
特性 | 可重入锁 | 不可重入锁 |
---|---|---|
同一线程重入 | 允许(计数器递增) | 阻塞(导致死锁) |
释放逻辑 | 需匹配相同次数的释放操作 | 单次释放即可 |
典型实现 | ReentrantLock , synchronized | 早期的 AbstractOwnableSynchronizer |
AQS加锁的过程
- 方法声明
protected final boolean tryAcquire(int acquires)
这是一个受保护的最终方法,用于尝试获取独占锁。acquires
参数表示请求获取锁的数量 ,一般在独占锁场景下 acquires
为 1
。返回值为 boolean
,获取锁成功返回 true
,失败返回 false
。
2. 获取当前线程和状态
final Thread current = Thread.currentThread();
int c = getState();
Thread.currentThread()
获取当前执行该方法的线程,赋值给current
。getState()
方法获取AQS
中表示同步状态的volatile
变量state
的值,赋值给c
。state
用于记录锁的持有状态,在独占锁场景下,state = 0
表示锁未被占用,state > 0
表示锁已被占用。
- 锁未被占用时尝试获取锁
if (c == 0) {if (isFirst(current) &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}
}
- 当
c == 0
,说明锁当前未被任何线程持有。 isFirst(current)
方法判断当前线程是否具备获取锁的资格(比如在公平锁模式下,需要判断是否是等待队列中第一个线程 )。非平锁模式下不需要。compareAndSetState(0, acquires)
使用 CAS(Compare - And - Swap,比较并交换)操作尝试将state
从0
更新为acquires
。如果 CAS 操作成功,说明当前线程成功获取到锁,调用setExclusiveOwnerThread(current)
将当前线程设置为独占锁的持有线程,并返回true
。
- 锁已被当前线程持有时的处理
else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;
}
- 当
c != 0
且current
等于当前持有锁的线程(通过getExclusiveOwnerThread()
获取 ),说明是锁的重入情况。 - 计算重入后的新状态
nextc = c + acquires
。 - 如果
nextc < 0
,说明发生了整数溢出,抛出Error
。正常情况下,更新state
为nextc
,并返回true
,表示重入成功。
- 获取锁失败
return false;
如果前面的条件都不满足,即锁被其他线程持有且当前线程既不是持有锁的线程也不满足初次获取锁条件,返回 false
,表示获取锁失败。
java线程安全的类有哪些
Java线程安全的类
Java提供了多种线程安全的类,主要分布在java.util.concurrent
包以及部分基础类中。以下是一些常见的线程安全类及其用途:
集合类
ConcurrentHashMap
线程安全的哈希表,性能优于Hashtable
,支持高并发读写。
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);
CopyOnWriteArrayList
线程安全的列表,适用于读多写少的场景,写操作时会复制新数组。
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("value");
CopyOnWriteArraySet
基于CopyOnWriteArrayList
实现的线程安全集合。
CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();
set.add("element");
Vector
早期的线程安全列表,方法通过synchronized
实现同步,性能较差。
Vector<String> vector = new Vector<>();
vector.add("item");
Hashtable
线程安全的哈希表,所有方法通过synchronized
实现同步。
Hashtable<String, Integer> table = new Hashtable<>();
table.put("key", 1);
阻塞队列
ArrayBlockingQueue
基于数组的有界阻塞队列,支持公平锁和非公平锁。
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
queue.put("item");
LinkedBlockingQueue
基于链表的阻塞队列,可选有界或无界。
LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>();
queue.offer("item");
PriorityBlockingQueue
线程安全的优先级队列,无界。
PriorityBlockingQueue<Integer> queue = new PriorityBlockingQueue<>();
queue.add(5);
同步工具类
CountDownLatch
允许一个或多个线程等待其他线程完成操作。
CountDownLatch latch = new CountDownLatch(3);
latch.countDown();
CyclicBarrier
让一组线程到达屏障时被阻塞,直到所有线程到达。
CyclicBarrier barrier = new CyclicBarrier(3);
barrier.await();
Semaphore
控制同时访问资源的线程数量。
Semaphore semaphore = new Semaphore(3);
semaphore.acquire();
原子类
AtomicInteger
支持原子操作的整数类。
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
AtomicLong
原子操作的长整型类。
AtomicLong counter = new AtomicLong(0L);
counter.addAndGet(5);
AtomicReference
原子操作的引用类型。
AtomicReference<String> ref = new AtomicReference<>("initial");
ref.compareAndSet("initial", "updated");
其他线程安全类
StringBuffer
线程安全的字符串拼接类,方法通过synchronized
实现同步。
StringBuffer buffer = new StringBuffer();
buffer.append("text");
Collections.synchronizedXXX
通过Collections
工具类包装的线程安全集合,如:
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
注意事项
ConcurrentHashMap
和CopyOnWriteArrayList
适用于高并发场景,性能优于传统同步类(如Vector
)。- 原子类通过CAS(Compare-And-Swap)实现无锁线程安全,适合计数器等场景。
- 阻塞队列常用于生产者-消费者模式,避免手动实现线程同步。
concurrent包下的类都是安全的,主要归功于其底层采用了多种高效的线程安全机制。这些机制在保证线程安全的同时,尽可能减少了锁竞争带来的性能损耗。
线程安全的单例模式
1. 类定义与成员变量
public class Singleton {private volatile static Singleton singleton; // 单例实例,volatile确保多线程可见性
private
:隐藏实例,防止外部直接访问。
static
:实例属于类,全局唯一。volatile
:禁止指令重排序,确保实例初始化完成后对所有线程可见(解决多线程下的 “半初始化” 问题)。
2. 私有构造方法
private Singleton() {} // 私有构造,禁止外部new创建实例
确保类无法被实例化,只能通过getSingleton()
方法获取唯一实例。
3. 单例获取方法(核心逻辑)
public static Singleton getSingleton() {if (singleton == null) { // 第一次检查:快速返回(避免每次进入同步块)synchronized (Singleton.class) { // 同步锁:确保同一时间只有一个线程执行初始化if (singleton == null) { // 第二次检查:防止多线程同时通过第一次检查后重复创建实例singleton = new Singleton(); // 创建实例(volatile保证此处初始化完成后对所有线程可见)}}}return singleton;
}
- 第一次检查(外层
if
):
若实例已存在,直接返回,避免每次进入同步块(提高性能,减少锁竞争)。 - 同步块(
synchronized
):
对类对象加锁,确保只有一个线程能执行初始化逻辑(线程安全的关键)。 - 第二次检查(内层
if
):
防止多线程在第一次检查通过后(此时singleton
仍为null
),进入同步块后重复创建实例(例如:线程 A 和线程 B 同时通过外层if
,线程 A 先获取锁创建实例,线程 B 进入后通过内层if
判断,避免重复创建)。
4. 线程安全的保证
volatile
关键字:
禁止 JVM 对singleton = new Singleton()
进行指令重排序(避免 “半初始化” 问题:即实例内存已分配,但构造方法未执行完毕,其他线程误认为实例已创建,访问到未初始化的对象)。- 双重检查:
结合外层快速检查和内层同步检查,既保证线程安全,又减少锁的开销(仅在首次创建实例时加锁,后续获取实例无需同步)。
5. 与传统单例的对比
- 懒汉式(非线程安全):
未加锁,多线程下可能创建多个实例。 - 饿汉式(线程安全,但提前初始化):
静态变量直接初始化(private static Singleton singleton = new Singleton();
),类加载时创建实例,无需同步,但可能浪费资源(若实例从未被使用)。 - 双重检查锁定(线程安全,延迟初始化):
结合懒汉式的延迟初始化和同步锁的线程安全,通过volatile
和双重检查,实现高效且安全的单例。
总结
- 适用场景:需要延迟初始化(节省资源)且多线程环境下的单例需求。
- 优势:线程安全、延迟加载、性能优化(仅首次创建时加锁)。
- 注意事项:JDK 5+ 才支持
volatile
的内存可见性语义(之前版本可能失效,需谨慎)。