juc下的线程安全集合类,可以发现他们的规律,里面包含三类关键词:Blocking、CopyOnWrite、Concurrent
- Blocking大部分实现基于锁,并提供用来阻塞的方法
- CopyOnWrite之类容器修改开销相对较重,适合读多写少的场景
- concurrent类型的容器
- 内部很多操作使用cas优化,一般可以提供较高吞吐量
- 弱一致性
- 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续运行遍历,这时内容是旧的
- 求大小弱一致性,size操作未必是100%准确
- 读取弱一致性
遍历时如果发生了修改,对于非安全容器来讲,使用fail-fast机制也是就让遍历立刻失败,抛出ConcurrentModificationException,不在继续遍历
ConcurrentHashMap
示例:计算每个字母出现的次数
生成26个文件每个文件都有200个随机字母,并且每个字母都有200个,便于查看统计结果是否有线程安全问题。
public static void createData(){
final String ALPHA = "abcdefghijklmnopqrstuvwxyz";
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++) {
// File tmp = new File("tmp");
// String path = tmp.getPath();
// System.out.println(path);
try (PrintWriter out = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream("tmp/" + i + ".txt")
)
)){
// String collect = list.subList(i * count,(i +1) * count).stream().c
String collect = list.subList(i*count,(i+1) * count).stream().collect(Collectors.joining("\n"));
out.print(collect);
}catch (IOException e){
e.printStackTrace();
}
}
}
读取每个文件的字母并存入list集合中
public static List<String> readFromFile(int i) {
ArrayList<String> words = new ArrayList<>();
try (BufferedReader in = new BufferedReader(new InputStreamReader(Files.newInputStream(Paths.get("tmp/" + i + ".txt"))))) {
while (true) {
String word = in.readLine();
if (word == null) {
break;
}
words.add(word);
}
return words;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
对集合中的元素进行操作,通过使用函数式编程的方式,由调用者决定如何处理这些字母元素
private static <V> void demo(Supplier<Map<String, V>> supplier, BiConsumer<Map<String, V>, List<String>> consumer) {
Map<String, V> counterMap = supplier.get();
// key value
// a 200
// b 200
List<Thread> ts = new ArrayList<>();
for (int i = 0; i < 26; i++) {
int idx = i;
Thread thread = new Thread(() -> {
List<String> words = readFromFile(idx);
consumer.accept(counterMap, words);
});
ts.add(thread);
}
ts.forEach(t -> t.start());
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(counterMap);
}
错误的使用ConcurrentHashMap
demo(
// 创建 map 集合
// 创建 ConcurrentHashMap 对不对?
// () -> new ConcurrentHashMap<String, LongAdder>(8,0.75f,8),
() -> new ConcurrentHashMap<String, Integer>(),
(map, words) -> {
for (String word : words) {
// 检查 key 有没有
Integer counter = map.get(word);
int newValue = counter == null ? 1 : counter + 1;
// 没有 则 put
map.put(word, newValue);
}
}
);

测试会发现最终每一个字母的出现频率并不是200,因为虽然ConcurrentHashMap 本身的每一步操作都是原子的线程安全的,但是合在一块就不在是线程安全的了,处于临界区的代码为
// 检查 key 有没有
Integer counter = map.get(word);
int newValue = counter == null ? 1 : counter + 1;
// 没有 则 put
map.put(word, newValue);
最简单的解决方式就是对临界区代码加锁
synchronized (map) {
// 检查 key 有没有
Integer counter = map.get(word);
int newValue = counter == null ? 1 : counter + 1;
// 没有 则 put
map.put(word, newValue);
}
但是这种方式太过笨重,会降低吞吐量,降低性能
ConcurrentHashMap 提供了很多线程安全的方法并且由于使用分段加锁对性能的损耗也会降低
computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)方法:如果集合中没有key,那么会通过mappingFunction的计算将key和计算结果value放入map中,至于如何自增可以使用juc提供的自增类LongAdder(LongAdder底层通过使用分步计算共同求和的方式自增,不仅可以保证线程安全还可以提升效率)来实现,他们都是线程安全的。
实例
demo(
// 创建 map 集合
// 创建 ConcurrentHashMap 对不对?
() -> new ConcurrentHashMap<String, LongAdder>(),
(map, words) -> {
for (String word : words) {
// 如果缺少一个 key,则计算生成一个 value , 然后将 key value 放入 map
// 如果集合没有则创建一个LongAdder 线程安全累加器
LongAdder longAdder = map.computeIfAbsent(word, key -> new LongAdder());
// 加一
longAdder.increment();
}
}
);
1532

被折叠的 条评论
为什么被折叠?



