在 [8.Condition.md](8.Condition.md) 这一节中,我们实现了一个 `BlockingQueue`(阻塞队列)。
BlockingQueue
的意思就是说,当一个线程调用这个 TaskQueue
的 getTask()
方法时,该方法内部可能会让线程变成等待状态,直到队列条件满足不为空,线程被唤醒后,getTask()
方法才会返回。
因为 BlockingQueue
非常有用,所以我们不必自己编写,可以直接使用 Java 标准库的 java.util.concurrent
包提供的线程安全的集合:ArrayBlockingQueue
。
除了 BlockingQueue
外,针对 List
、Map
、Set
、Deque
等,java.util.concurrent
包也提供了对应的并发集合类:
| interface | non-thread-safe | thread-safe | | —- | —- | —- |
| List | ArrayList | CopyOnWriteArrayList |
| Map | HashMap | ConcurrentHashMap,ConcurrentSkipListMap |
| Set | HashSet / TreeSet | CopyOnWriteArraySet,ConcurrentSkipListSet |
| Queue | ArrayDeque / LinkedList | ArrayBlockingQueue / LinkedBlockingQueue |
| Deque | ArrayDeque / LinkedList | LinkedBlockingDeque |
还有一些没有列出来的并发集合类:PriorityBlockingQueue
、
ArrayBlockingQueue
有一个可选的参数来指定是否需要公平性。若设置了公平参数,则等待了最长时间的线程会优先得到处理。通常,公平性会降低性能,只有在确实非常需要时才使用它。
使用这些并发集合与使用非线程安全的集合类完全相同。我们以 ConcurrentHashMap
为例:
Map<String, String> map = new ConcurrentHashMap<>();
// 在不同的线程读写:
map.put("A", "1");
map.put("B", "2");
map.get("A");
因为所有的同步和加锁的逻辑都在集合内部实现,对外部调用者来说,只需要正常按接口引用,其他代码和原来的非线程安全代码完全一样。java.util.Collections
工具类还提供了一个旧的线程安全集合转换器,可以这么用:
Map unsafeMap = new HashMap();
Map threadSafeMap = Collections.synchronizedMap(unsafeMap);
但是它实际上是用一个包装类包装了非线程安全的 Map
,然后对所有读写方法都用 synchronized
加锁,这样获得的线程安全集合的性能比 java.util.concurrent
集合要低很多,所以不推荐使用。
线程安全集合只能保证使用过程中不破坏数据结构。比如,在多线程中操作普通的 HashMap
会破坏内部的链表数组:有些连接可能丢失,或者甚至会构成循环,使得该数据结构不再可用。当线程集合类不会发生这样的情况。
但是使用线程安全集合类,并不一定就能让代码线程安全:
Long oldValue = map.get(word); // map -> ConcurrentHashMap
Long newValue = oldValue == null ? 1 : oldValue + 1;
map.put(word, newValue);
上述代码中虽然使用 ConcurrentHashMap
类,但是由于上述操作并不是原子的,所以结果不可预知,线程不安全。
小结
使用 java.util.concurrent
包提供的线程安全的并发集合可以大大简化多线程编程:
多线程同时读写并发集合是安全的;
尽量使用 Java 标准库提供的并发集合,避免自己编写同步代码。