十二、线程安全的集合
除了使用锁来保护共享数据结构,也可以直接使用一些实现了线程安全的对象
常见的线程安全集合
// 以下集合都可以被多线程安全访问
// 无边界非阻塞队列
ConcurrentLinkedDeque<String> concurrentLinkedDeque = new ConcurrentLinkedDeque<>();
// 哈希映射
ConcurrentHashMap<Integer,String> concurrentHashMap = new ConcurrentHashMap<>();
// 16: 桶数
// 0.75f: 负载因子
// 16(第三个参数):并发写线程估计数,默认为 16 ,同一时间超过这个线程数,其他线程将被阻塞
ConcurrentHashMap<Integer, String> concurrentHashMap2 = new ConcurrentHashMap<>(16,0.75f,16);
// 有序集,(自动按大小排序,由小到大)
ConcurrentSkipListSet<String> concurrentSkipListSet = new ConcurrentSkipListSet<>();
// 有序映射 (键的大小顺序)
ConcurrentSkipListMap<Integer,String> concurrentSkipListMap = new ConcurrentSkipListMap();
线程安全集合之 Set
ConcurrentHashMap<Integer,Integer> concurrentHashMap=new ConcurrentHashMap<>();
concurrentHashMap.put(3,3);
concurrentHashMap.put(4,4);
// ConcurrentHashMap.KeySetView<Object, Boolean> objects = concurrentHashMap.newKeySet();
// 获得的Set 是 concurrentHashMap 的一个包装器,对 set 进行操作并不会影响 concurrentHashMap 的实际值
Set<Integer> set = concurrentHashMap.newKeySet();
set.add(1);
set.remove(3);
set.remove(4);
System.out.println(set); // [1]
System.out.println(concurrentHashMap); // {3=3, 4=4}
// 获得的 Set 是 Map 的映射的键集,对 Set 的操作会影响到实际的 Map
// 可以设置一个默认 value ,当添加的 set 不存在时,会为map设置此默认值为 value
// 如果使用不设置此默认值的方法,则无法使用 add 方法
Set<Integer> set2=concurrentHashMap.keySet(0);
set2.add(2);
set2.remove(3);
set2.add(3);
set2.remove(4);
System.out.println(set2); // [2, 3]
System.out.println(concurrentHashMap); // {2=0, 3=0}
map 的原子更新
ConcurrentHashMap<Integer, String> concurrentHashMap = new ConcurrentHashMap<>();
concurrentHashMap.put(1,"1 oldValue");
String oldValue;
String newValue;
oldValue=concurrentHashMap.get(1);
// put 方法虽然是线程安全的,但是在这个位置可能会被其他线程插入,concurrentHashMap.get(1) 被改变
newValue=oldValue.replaceAll("old","new");
concurrentHashMap.put(1,newValue);
//////////// 线程安全的更新 方式 1
newValue="1 newValue";
do {
oldValue=concurrentHashMap.get(1);
}while (!concurrentHashMap.replace(1,oldValue,newValue));
//concurrentHashMap.put(1,null);
//////////// 线程安全的更新 方式 2
// compute方法,为第二个参数传入 新值获得过程
// ConcurrentHashMap 不允许 null 作为值,所以可以用 null 判断键对应值是否存在
// 处理 键和值
concurrentHashMap.compute(1,(k,v)->{
return v==null?"原来没有值":v+"newValue";
});
// 处理 旧值新值
concurrentHashMap.merge(1,"新值",(existValue,newValuex)->{
return existValue+newValuex;
});
concurrentHashMap.merge(1,"新值",String::concat);
System.out.println(concurrentHashMap.get(1));
//////////// 线程安全的更新 方式 3
ConcurrentHashMap<Integer, LongAdder> concurrentHashMap3 = new ConcurrentHashMap<>();
// 直接使用线程安全的对象做 value
// 若果 键对应值存在,则返回值,不存在则插入新值并返回 null 值
concurrentHashMap3.putIfAbsent(1,new LongAdder());
concurrentHashMap3.get(1).increment();
批操作 HashMap
ConcurrentHashMap<Integer,Integer> concurrentHashMap = new ConcurrentHashMap();
for (int i = 8; i < 15; i++) {
concurrentHashMap.put(i,i);
}
// 对每个条目都做lambda表达式的操作,并返回第一个 非 null 的结果,如果所有结果都为 null 则返回 null
// 参数:parallelismThreshold,映射元素超过阀值,就会并行的完成批操作,
// 因此如果希望尽可能的多的线程运行,使用 1 作为阈值,希望单线程运行,使用 Long.MAX_VALUE 作为阈值
Integer result=concurrentHashMap.search(1,(k, v)->{
return v > 9 ? v*10 : null;
});
System.out.println(result); // 输出 100 120 140 ··· ,HashMap 的键没有顺序,而且多线程运行的话不一定是哪个线程先找到
// forEach 有一个 不需要传入 线程阈值 的版本
concurrentHashMap.forEach((k,v)-> System.out.println(v));
concurrentHashMap.forEach(1L,(k,v)-> System.out.println(v));
// 下面这个操作会报错 java.lang.UnsupportedOperationException ,这个是不能修改的
// concurrentHashMap.forEachEntry(1,entry->entry.setValue(10));
// forEach 第三个版本,可以在第 2 个参数传入一个转换器,然后将转换的结果交给第 3 个参数处理,转换器返回 null 的时候会跳过
concurrentHashMap.forEach(1L,(k,v)-> v>10?v*10:null,System.out::println);
//
Integer reduce = concurrentHashMap.reduce(1L, (k,v)-> v > 10 ? v* 10 : null,Integer::sum);
System.out.println(reduce); // 500
// 可以把输出结果转为其他类型,这里第三个参数 basis ,必须写累加器的 零元素(描述是说什么在将返回类型装为long的时候是通过于默认值累加,多线程也会影响结果,所以必须用 0 作为 零元素,否则结果可能不受控制)
long sum=concurrentHashMap.reduceToLong(1L,(k,v)-> v > 10 ? v* 10L : 0L,0L,Long::sum);
System.out.println(sum); //500
// 三种操作都还有只操作 key value entry 的版本
//concurrentHashMap.searchKeys();
//concurrentHashMap.searchValues();
//concurrentHashMap.searchEntries();
//concurrentHashMap.forEachKey();
//concurrentHashMap.forEachValue();
//concurrentHashMap.forEachEntry();
//concurrentHashMap.reduceKeys();
//concurrentHashMap.reduceValues();
//concurrentHashMap.reduceEntries();
通过拷贝数组来写入的 CopyOnWriteArrayList 和 CopyOnWriteArraySet
这两个类在读取的时候,不加锁,获取底层数据数组的指针(类似 C 的指针概念)来读
在写入的时候,加锁,先将底层的数据数组拷贝,在拷贝的数组上进行修改,修改结束后直接将数据数组的指针替换成这个拷贝的数组指针
适合经常读很少写的集合,且由于读不加锁,拿指针来读,所以在存在写入的后,仍指向旧数组,使用的是旧的但是能保持一致性的数据
并行的数组算法
Arrays 提供了一些方法,可以并行的对数组做一些操作
int[] ints={1,2,3,4};
// 用数组每个元素的前一个元素值 和当前元素的值做运算,结果赋给当前下标元素,(第一个元素跳过)
// x 为前一个元素值,y 为当前元素值
// 需要满足 (a op b) op c == a op (b op c) 才可能实现并行运算
Arrays.parallelPrefix(ints,(x,y)->x+y);
System.out.println(Arrays.toString(ints)); // [1, 3, 6, 10]
int[] nums={5,3,1,4,8,6};
// 并行排序
Arrays.parallelSort(nums);
System.out.println(Arrays.toString(nums)); // [1, 3, 4, 5, 6, 8]
int[] nums2=new int[20];
// 并行设置值
// i 为数组下标 源码段:for (int i = 0; i < array.length; i++) array[i] = generator.applyAsInt(i);
Arrays.setAll(nums2, i->i%6);
System.out.println(Arrays.toString(nums2)); // [0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1]
较早线程安全集合
// 线程安全的动态数组
Vector<Integer> vector=new Vector<>();
// 线程安全的散列表
Hashtable<Integer,Integer> hashtable=new Hashtable<>();
// 通过 Collections提供的方法,包装出一个线程安全的集合
List<Object> synchronizedList = Collections.synchronizedList(new ArrayList<>());
// Collections.synchronizedMap();
// Collections.synchronizedSet();
// ······
网友评论