教程:https://www.bilibili.com/video/BV16J411h7Rd
HashTable,Vector

修改开销相对较重,基于 synchronized,适用于读多写少的场景

大部分实现基于锁(ReentrantLock),并提供用来阻塞的方法

内部很多操作使用 CAS 优化,一般可以提供较高的吞吐量

Note:
对于非安全容器来讲,遍历过程中发生修改,使用了 fail-fast 的机制会让遍历立刻失败,抛出ConcurrentModificationException,不再继续遍历。
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
new Thread(() -> {
list.forEach(System.out::println);
}).start();
new Thread(() -> list.add(6)).start();
new Thread(() -> list.add(7)).start();
new Thread(() -> list.add(8)).start();

随机生成 26 个字母,每个字母生成 200 次,打乱后装入 26 个文件中。
static final String ALPHA = "abcedfghijklmnopqrstuvwxyz";
public static void main(String[] args) {
int length = ALPHA.length();
int count = 200;
List<String> list = new ArrayList<>(length * count);
for (int i = 0; i < length; i++) {
char ch = ALPHA.charAt(i);
for (int j = 0; j < count; j++) {
list.add(String.valueOf(ch));
}
}
Collections.shuffle(list);
for (int i = 0; i < 26; i++) {
try (PrintWriter out = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream("G://tmp/" + (i + 1) + ".txt")))) {
String collect = String.join("\n", list.subList(i * count, (i + 1) * count));
out.print(collect);
} catch (IOException e) {
e.printStackTrace();
}
}
}

使用 HashMap 统计,可以看到结果不正确,正确应该是每个字母出现次数都是 200:
public static void main(String[] args) {
demo(
// 创建 map 集合
// 创建 ConcurrentHashMap 对不对?
() -> new HashMap<String, Integer>(),
// 进行计数
(map, words) -> {
for (String word : words) {
Integer counter = map.get(word);
int newValue = counter == null ? 1 : counter + 1;
map.put(word, newValue);
}
}
);
}
private static <V> void demo(Supplier<Map<String, V>> supplier,
BiConsumer<Map<String, V>, List<String>> consumer) {
Map<String, V> counterMap = supplier.get();
List<Thread> ts = new ArrayList<>();
for (int i = 1; i <= 26; i++) {
int idx = i;
Thread thread = new Thread(() -> {
List<String> words = readFromFile(idx);
consumer.accept(counterMap, words);
});
ts.add(thread);
}
ts.forEach(Thread::start);
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(counterMap);
}
public static List<String> readFromFile(int i) {
ArrayList<String> words = new ArrayList<>();
try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream("G://tmp/"
+ i + ".txt")))) {
while (true) {
String word = in.readLine();
if (word == null) {
break;
}
words.add(word);
}
return words;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

假如把 HashMap 换成 ConcurrentHashMap 正确吗?
() -> new ConcurrentHashMap<String, Integer>()

可以看到结果也不正确!什么情况?

() -> new ConcurrentHashMap<String, Integer>(),
// 进行计数
(map, words) -> {
for (String word : words) {
synchronized (map) {
Integer counter = map.get(word);
int newValue = counter == null ? 1 : counter + 1;
map.put(word, newValue);
}
}
}
这样确实能解决问题,但是并发度不高。
() -> new ConcurrentHashMap<String, Integer>(),
// 进行计数
(map, words) -> {
for (String word : words) {
map.putIfAbsent(word, 0);
map.computeIfPresent(word, (key, value) -> value + 1);
}
}
() -> new ConcurrentHashMap<String, LongAdder>(),
// 进行计数
(map, words) -> {
for (String word : words) {
LongAdder value = map.computeIfAbsent(word, (key) -> new LongAdder());
value.increment();
}
}
经过大量测试,大部分情况都是第三种方案更胜一筹,第一种方案次之,最差的就是第二种方案:

比较形象的例子:jdk1.7中 hashmap为什么会发生死链? - 磊哥的回答 - 知乎
两个线程同时插入尾节点,出现顶替的情况,造成数据丢失。以及两个线程同时修改数据的情况。
第一,代码中不允许!

第二,这会带来并发场景下的歧义:
举个例子:
map.get(key) 方法后的结果返回 null,我们要判断 ① key 对应的值是 null,② 还是 map 中没有这个 key,有这两种情况。这时候我们可以使用 map.containsKey(key) 方法看返回的是 true 还是 false。如果是 true,代表 key 对应的值为 null,如果是 false,说明 map 中没有这个 key。单线程情况下确实可以使用额外的方法判定消除歧义。key 和获取 key 对应的值两个方法因为是分开的操作,这样无法保证一个线程下执行两个方法过程中是否有另外一个线程进行捣乱。假如说初始状态下 map 中 key 对应 "hello"。| 时刻 | 线程1 | 线程2 |
|---|---|---|
| 1 | map.containsKey(key) 返回 true | |
| 2 | map.put(key, null) | |
| 3 | map.get(key),期待:"hello",实际:null |
底层实现采用了写入时拷贝的思想,增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,这时不影响其他线程的并发读,读写分离。
读-读,读-写都可以并发执行,写-写是互斥的。
适用于读多写少的场景。写过程太多会一直创建新的数组,典型的以空间换时间。
@SneakyThrows
public static void main(String[] args) {
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> iter = list.iterator();
new Thread(() -> {
list.remove(0);
System.out.println(list);
}).start();
TimeUnit.SECONDS.sleep(1);
while (iter.hasNext()) {
System.out.println(iter.next());
}
}

弱一致性: