JUC中的线程安全的集合
所谓的线程安全的集合,指的是集合中的每一个方法是原子操作,例如
get()
、put()
等。但这并不表示使用了线程安全的集合就不会造成线程安全问题,正如事务不是简单地由一系列原子操作堆叠就可以实现的一样,只有正确地使用线程安全的集合,才能保证线程安全问题。
Blocking 类
Blocking 大部分基于锁,并提供阻塞的方法
CopyOnWrite 类
CopyOnWrite类的容器使用修改时拷贝,在修改时开销比较大,适用于读多写少的场景
Concurrent 类
内部使用CAS操作进行优化,一般可以提供比较高的吞吐量,性能相对较高
弱一致性
遍历时弱一致性
当利用迭代器进行遍历时,如果Concurrent容器发生修改,迭代出来的数据还是旧值。
对于非安全容器来讲,遍历时发生修改会利用 fail-fast 机制让遍历立刻失败,抛出 ConcurrentModificationException 异常
public class NoSafeCollectionMain {
// 迭代次数应该设置得稍微大些, 保证在执行 forEach() 方法时还有线程没有加入到集合中, 这样才能正常显示错误
public static final int ITER_COUNTS = 200;
public static void main(String[] args) {
// 使用非线程安全的集合类
List<String> threadNames = new ArrayList<>();
for (int i = 0; i < ITER_COUNTS; i++) {
new Thread(() -> {
threadNames.add(Thread.currentThread().getName());
}, "thread-" + i).start();
}
// 主线程的 threadNames 进行遍历时, 可能其它线程对 threadNames 这个集合进行修改, 此时报错 ConcurrentModificationException
threadNames.forEach(System.out::println);
}
}
求大小时弱一致性
size()
获取的值未必准确,例如另一个线程对容器进行修改
读取时弱一致性
使用 ConcurrentHashMap 进行 WordCount 案例
生成测试数据
import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; public class ConcurrentHashMapMain { public static final String ALPHA = "abcdefghijklmnopqrstuvwxyz"; public static int COUNT = 200; public static void main(String[] args) { List<String> words = new ArrayList<>(); for (char ch : ALPHA.toCharArray()) { for (int i = 0; i < COUNT; i++) { words.add(String.valueOf(ch)); } } Collections.shuffle(words); for (int i = 0; i < 26; i++) { PrintWriter printWriter = null; try { printWriter = new PrintWriter(new OutputStreamWriter(new FileOutputStream("G:/java/juc/src/main/resources/words/" + (i + 1) + ".txt"))); } catch (FileNotFoundException e) { e.printStackTrace(); } String collect = words.subList(i * COUNT, (i + 1) * COUNT) .stream() .collect(Collectors.joining("\n")); printWriter.print(collect); // 注意这里的flush(), 否则生成的文件中没有数据 printWriter.flush(); } } }
正确地使用 ConcurrentHashMap
ConcurrentHashMap 相较于 synchronized 等锁的优势在于锁的粒度更细,可以提供更高的并发度,这是使用 ConcurrentHashMap 来替代 synchronized + HashMap 的原因。和 Redis 中通过 Lua 脚本来将多条非原子指令拼接成一条指令的操作类似,应该去寻找 ConcurrentHashMap 中是否提供某些方法,将多个方法拼接成一个原子方法。
ConcurrentHashMap 提供在多线程环境下还能够正常使用的集合(不需要显式加锁),搭配 CAS 操作的其他类型或方法才能够完成正确的并发控制。
多线程下扩容时的并发死链问题
产生原因
JDK7 中的 HashMap 在遇到哈希冲突时,使用头插法。
这样可能会造成 e 节点两次被访问,第一次访问 e.next = null,第二次访问 e.next = next,而 next.next = e,则可能产生链表环路。
测试案例
需要在 JDK7 的环境下进行测试,因为 JDK7 时 HashMap 的拉链法是使用的头插法,而 JDK8 中使用的是尾插法。另外 JDK8 对于扩容机制和 hash 值的计算方法都发生的改变
测试代码
- 找到扩容前和扩容后桶下标相同的key
- 触发扩容:元素个数达到阈值(总容量的3/4),初始容量是16