JUC包下的并发容器
java的集合容器框架主要有四大类别:List、Set、Queue、Map,都是非线程安全的。
java先提供了同步容器供用户使用,可以简单地理解为通过synchronized来实现同步的容器,比如Vector、Hashtable以及SynchronizedList。
java.util.concurrent包中提供了多种并发类容器,解决同步容器的性能问题。
- Map
- ConcurrentHashMap 代替Hashtable、synchronizedMap,支持复合操作
- JDK6中采用一种更加细粒度的加锁机制Segment“分段锁”,JDK8中采用CAS无锁算法。
- 应用场景
- 共享数据的线程安全:在多线程编程中,如果需要进行共享数据的读写,可以使用 ConcurrentHashMap 保证线程安全。
- 缓存:ConcurrentHashMap 的高并发性能和线程安全能力,使其成为一种很好的缓存实现方案。在多线程环境下,使用 ConcurrentHashMap 作为缓存的数据结构,能够提高程序的并发性能,同时保证数据的一致性。
- ConcurrentSkipListMap 代替synchronizedSortedMap(TreeMap)
- Skip list(跳表)是一种可以代替平衡树的数据结构,默认是按照Key值升序的。
- ConcurrentHashMap 代替Hashtable、synchronizedMap,支持复合操作
- List
- CopyOnWriteArrayList 代替Vector、synchronizedList
- 利用高并发往往是读多写少的特性,对读操作不加锁,对写操作,先复制一份新的集合,在新的集合上面修改,然后将新集合赋值给旧的引用,并通过volatile 保证其可见性,当然写操作的锁是必不可少的了。
- 应用场景
- 读多写少,因为读不需要加锁
- 不需要实时更新数据,因为读取的可能不是最新的
- 缺陷
- 由于写操作的时候会拷贝数组,会消耗额外内存
- 因为拷贝数组,新增元素都需要时间,不能用于实时读的场景
- 因为add/set都需要复制数组,注意数组大小,容易引起故障
- fail-fast 机制是java集合(Collection)中的一种错误机制。抛出 ConcurrentModificationException
- 方案一:在遍历过程中所有涉及到改变modCount 值的地方全部加上synchronized 或者直接使用 Collection#synchronizedList
- 方案二:使用CopyOnWriteArrayList 替换 ArrayList,推荐使用该方案(即fail-safe)。
- CopyOnWriteArrayList 代替Vector、synchronizedList
- Set
- CopyOnWriteArraySet 代替synchronizedSet
- 基于CopyOnWriteArrayList实现,其唯一的不同是在add时调用的是CopyOnWriteArrayList的addIfAbsent方法,其遍历当前Object数组,如Object数组中已有了当前元素,则直接返回,如果没有则放入Object数组的尾部,并返回。
- ConcurrentSkipListSet
- CopyOnWriteArraySet 代替synchronizedSet
- Queue
- BlockingQueue
- ConcurrentLinkedQueue
- BlockingDeque
- ConcurrentLinkedDeque
import sun.misc.Unsafe;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
public class ConcurrentDemo {
private static final ConcurrentDemo d = new ConcurrentDemo();
// private static ArrayList<String> arr = new ArrayList<>();
private static CopyOnWriteArrayList<String> arr = new CopyOnWriteArrayList<>();
public static void main(String[] args) {
//cas案例
// casDemo();
//统计文件中英文字母出现的总次数
mapDemo();
arr.add("h");
arr.add("e");
arr.add("l");
arr.add("l");
arr.add("o");
var t1 = new Thread(new AddElement());
var t2 = new Thread(new AddElement());
t1.start();
t2.start();
System.out.println(arr.size());
//java.util.ConcurrentModificationException
for (String s : arr) {
System.out.print(s);
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
static class AddElement implements Runnable {
@Override
public void run() {
var start = 0;//97
for (int i = 0; i < start + 1; i++) {
// try {
// Thread.sleep(5);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
// arr.add(Character.toString(i));
arr.add(Integer.toString(i));
}
}
}
static void casDemo() {
var d2 = d.new HashMapTest();
for (int i = 50; i < 100; i++) {
int num = i;
new Thread(() -> {
try {
if (d2.demo(1, num))
System.out.println(num + " updated success");
} catch (NoSuchFieldException | NoSuchMethodException | InvocationTargetException |
InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}).start();
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(d2);
}
static void mapDemo() {
var d2 = d.new ConcurrentHasMapDemo();
// d2.test();
d2.demo();
}
class ConcurrentHasMapDemo {
private static ConcurrentHashMap<String, AtomicLong> counter = new ConcurrentHashMap<>();
private static CountDownLatch countDownLatch = new CountDownLatch(3);
private static String[] words = {"you", "and", "me"};
public void demo() {
var task = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// 模拟从文本文件中读取到的单词
String word = words[new Random().nextInt(words.length)];
// 尝试获取全局统计结果
AtomicLong number = counter.get(word);
// 在未获取到的情况下,进行初次统计结果设置
if (number == null) {
// 在设置时发现如果不存在则初始化
AtomicLong newNumber = new AtomicLong(0);
number = counter.putIfAbsent(word, newNumber);
if (number == null) {
number = newNumber;
}
}
// 在获取到的情况下,统计次数直接加1
number.incrementAndGet();
System.out.println(Thread.currentThread().getName() + ":" + word + " 出现第 " + number + " 次");
}
countDownLatch.countDown();
}
};
new Thread(task, "线程1").start();
new Thread(task, "线程2").start();
new Thread(task, "线程3").start();
try {
countDownLatch.await();
System.out.println(counter.toString());
} catch (Exception e) {
}
}
public void test() {
var map = new ConcurrentHashMap<>();
map.put("key", "value");
map.put("hello", "world");
System.out.println(map.size());
System.out.println(map);
//如果 key 对应的 value 不存在,则 put 进去,返回 null。否则不 put,返回已存在的 value。
Object o = map.putIfAbsent("hello", "world2");
System.out.println(o.toString());
//如果 key 对应的值是 value,则移除 K-V,返回 true。否则不移除,返回 false。
boolean remove = map.remove("hello", "world2");
System.out.println("remove:" + remove);
//如果 key 对应的当前值是 oldValue,则替换为 newValue,返回 true。否则不替换,返回 false。
Object replace = map.replace("hello", "world", "world2");
System.out.println("replace:" + replace);
System.out.println(map);
}
}
class HashMapTest {
private int value = 1;
public boolean demo(int expectedValue, int newValue) throws NoSuchFieldException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Constructor<Unsafe> unsafe = Unsafe.class.getDeclaredConstructor();
unsafe.setAccessible(true);
Unsafe u = unsafe.newInstance();
var offset = u.objectFieldOffset(HashMapTest.class.getDeclaredField("value"));
return u.compareAndSwapInt(this, offset, expectedValue, newValue);
}
@Override
public String toString() {
return "HashMapTest{" +
"value=" + value +
'}';
}
}
}
跳表
跳表是一种基于有序链表的数据结构,支持快速插入、删除、查找操作,其时间复杂度为O(log n),比普通链表的O(n)更高效。
https://cmps-people.ok.ubc.ca/ylucet/DS/SkipList.html
跳表的特性有这么几点:
- 一个跳表结构由很多层数据结构组成。
- 每一层都是一个有序的链表,默认是升序。也可以自定义排序方法。
- 最底层链表包含了所有的元素。
- 如果每一个元素出现在LevelN的链表中(N>1),那么这个元素必定在下层链表出现。
- 每一个节点都包含了两个指针,一个指向同一级链表中的下一个元素,一个指向下一层级别链表中的相同值元素。
电商场景中并发容器的选择
- 案例一:电商网站中记录一次活动下各个商品售卖的数量。
- 选型:Hashtable 不推荐,锁太重,选ConcurrentHashMap 确保高并发下多线程的安全性
- 案例二:在一次活动下,为每个用户记录浏览商品的历史和次数。
- 如果对数据有强一致要求,则需使用Hashtable;在大部分场景通常都是弱一致性的情况下,使用ConcurrentHashMap 即可;如果数据量级很高,且存在大量增删改操作,则可以考虑使用ConcurrentSkipListMap。
- 案例三:在活动中,创建一个用户列表,记录冻结的用户。一旦冻结,不允许再下单抢购,但是可以浏览。
- 综合业务场景,选CopyOnWriteArrayList,会占空间,但是也仅仅发生在添加新冻结用户的时候。绝大多数的访问在非冻结用户的读取和比对上,不会阻塞。