1. ConcurrentHashMap
数据结构
- 哈希表:底层采用数组(桶)存储元素,每个桶可以链接一个链表或红黑树。
- 链表与红黑树转换:当链表长度超过一定阈值(默认为8),链表会转换成红黑树以加速查找。
实现细节与优化
-
细粒度锁与CAS操作
- CAS操作:使用
sun.misc.Unsafe
提供的原子操作(如compareAndSwapObject
)来保证并发安全性。这允许在没有锁定的情况下执行更新操作。 - 细粒度锁:自Java 8起,放弃了分段锁机制,转而使用对节点或桶加锁的方式,减少了锁定范围,提高了并发性。
- CAS操作:使用
-
扩容机制
- 支持动态扩容,在扩容过程中能够处理并发请求,确保在高并发环境下仍能高效运行。扩容时,新插入的数据可以直接放入新数组中,旧数组中的数据会在后续逐步迁移。
-
无锁化读取
- 大多数读操作不需要获取锁,因为它们依赖于volatile变量保证可见性。在读操作期间,如果遇到正在被修改的节点,会尝试重新读取直到获得稳定的视图。
工作流程
- 初始化:创建一个初始容量的哈希表。
- 插入操作:
- 计算哈希值找到对应的桶。
- 如果该桶为空,则直接插入;否则,使用CAS操作尝试添加到链表或红黑树中。
- 查找操作:
- 计算哈希值找到对应的桶。
- 在链表或红黑树中查找目标键值对。
- 删除操作:
- 计算哈希值找到对应的桶。
- 使用CAS操作从链表或红黑树中移除目标键值对。
- 扩容操作:
- 当负载因子达到阈值时,触发扩容。
- 创建新的更大容量的数组,并将原有数据迁移到新数组中。
示例代码
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
// 插入数据
map.put("key1", "value1");
// 获取数据
String value = map.get("key1");
System.out.println(value); // 输出: value1
// 并发环境下的操作
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
map.put("key" + i, "value" + i);
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(map.size()); // 输出: 2000
}
}
适用场景
- 高并发读写场景:由于采用了细粒度锁和CAS操作,使得ConcurrentHashMap在高并发环境下表现出色,特别是对于读多写少的场景,如缓存系统。
- 需要高效查找和更新的场景:如分布式系统的共享状态管理。
2. CopyOnWriteArrayList 和 CopyOnWriteArraySet
数据结构
- 基于数组的数据结构:所有修改操作都会创建底层数组的一个新副本。读操作直接从现有数组中读取数据,无需加锁。
实现细节与优化
- ReentrantLock
- 写操作使用重入锁保护底层数组的访问,确保线程安全。
- 弱一致性迭代器
- 迭代器不会抛出
ConcurrentModificationException
,因为它总是基于数组的一个固定快照。
- 迭代器不会抛出
- 内存消耗与垃圾回收压力
- 频繁的写操作会导致较高的内存消耗和垃圾回收压力,因为每次写操作都需要复制整个数组。
工作流程
- 初始化:创建一个初始容量的数组。
- 插入操作:
- 获取重入锁。
- 复制当前数组并在副本上执行插入操作。
- 更新引用指向新数组。
- 查找操作:
- 直接从现有数组中读取数据,无需加锁。
- 删除操作:
- 获取重入锁。
- 复制当前数组并在副本上执行删除操作。
- 更新引用指向新数组。
示例代码
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListExample {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// 插入数据
list.add("item1");
// 获取数据
System.out.println(list.get(0)); // 输出: item1
// 并发环境下的操作
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
list.add("item" + i);
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size()); // 输出: 2001
}
}
适用场景
- 读多写少的场景:如日志记录系统或缓存更新不频繁的情况下,由于每次写操作都需要复制整个数组,因此写操作的成本较高,特别是在数组较大时。
- 需要避免
ConcurrentModificationException
的场景:如在迭代过程中可能有其他线程修改集合的情况。
3. BlockingQueue 接口的实现类
典型实现
- LinkedBlockingQueue, ArrayBlockingQueue
数据结构
- 队列结构:支持先进先出(FIFO)原则,
LinkedBlockingQueue
使用链表作为底层存储,而ArrayBlockingQueue
使用固定大小的数组。
实现细节与优化
- 阻塞机制
- 提供了阻塞插入和移除方法,确保如果队列满了,尝试添加元素的操作会被阻塞直到空间可用;同样,如果队列为空,尝试移除元素的操作也会被阻塞直到有元素可移除。
- 公平性设置
- 可以选择是否按照FIFO顺序处理请求,影响生产者和消费者之间的交互方式。
工作流程
- 初始化:创建一个队列,指定其容量(对于
ArrayBlockingQueue
)或不指定(对于LinkedBlockingQueue
)。 - 插入操作:
- 如果队列未满,直接插入元素。
- 如果队列已满,当前线程将被阻塞,直到有空间可用。
- 移除操作:
- 如果队列非空,直接移除并返回元素。
- 如果队列为空,当前线程将被阻塞,直到有元素可移除。
示例代码
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueExample {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
// 生产者线程
Thread producerThread = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
queue.put("Item" + i);
System.out.println("Produced: Item" + i);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 消费者线程
Thread consumerThread = new Thread(() -> {
try {
while (true) {
String item = queue.take();
System.out.println("Consumed: " + item);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producerThread.start();
consumerThread.start();
producerThread.join();
consumerThread.interrupt();
}
}
适用场景
- 生产者-消费者模式的任务调度和资源管理:特别是在需要限制任务队列大小的情况下。
- 需要控制任务处理顺序的场景:如任务队列中的优先级处理。
4. SynchronousQueue
数据结构
- 不实际存储元素的特殊队列:它作为一个中介,每个插入操作必须等待一个对应的移除操作,反之亦然。
实现细节与优化
- 双向链表
- 用于组织等待中的生产者和消费者,以便匹配相应的插入和移除操作。
- Transferer抽象类
- 定义了传递元素的基本框架,具体实现根据公平性与否有所不同。
工作流程
- 插入操作:
- 如果有一个等待的消费者,直接传递元素给消费者。
- 否则,当前线程将被阻塞,直到有一个消费者准备接收元素。
- 移除操作:
- 如果有一个等待的生产者,直接从生产者处接收元素。
- 否则,当前线程将被阻塞,直到有一个生产者准备传递元素。
示例代码
import java.util.concurrent.SynchronousQueue;
public class SynchronousQueueExample {
public static void main(String[] args) throws InterruptedException {
SynchronousQueue<String> queue = new SynchronousQueue<>();
// 生产者线程
Thread producerThread = new Thread(() -> {
try {
queue.put("Task");
System.out.println("Produced a task.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 消费者线程
Thread consumerThread = new Thread(() -> {
try {
String task = queue.take();
System.out.println("Consumed: " + task);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
consumerThread.start(); // 先启动消费者
producerThread.start(); // 然后启动生产者
producerThread.join();
consumerThread.join();
}
}
-
性能考量:
- 高效的直接传递:适合需要快速任务分配或数据传输的场景
适用场景
- 需要快速任务分配或数据传输的场景:如Executor框架中的工作窃取算法。
- 直接在线程间传递数据的场景:减少中间存储的需求。
性能考虑与最佳实践
- 选择合适的集合类型:基于应用的具体需求(如读写比例、并发程度等),选择最适合的集合类型至关重要。
- 避免过度同步:虽然线程安全集合提供了内置的同步机制,但在某些情况下,额外的同步措施可能会导致不必要的性能开销。
- 注意内存消耗:像
CopyOnWriteArrayList
这样的集合在频繁写操作的情况下会导致较高的内存消耗和垃圾回收压力。