在Java的集合容器框架中,主要有四大类别:List、Set、Queue、Map。
List、Set、Queue接口分别继承了Collection接口,分别代表数组、集合和队列这三大类容器。Map本身是一个接口。
但是,由于缺少安全机制,像ArrayList、LinkedList、HashMap这些容器都是非线程安全的。
java语言为此设计了同步容器。内置了Vector、Stack、HashTable等同步容器。同步容器中的方法采用了synchronized进行了同步。
也可以使用Collections工具类将不安全的容器封装了同步容器。
List<String> list = new ArrayList<>();
Set<String> set = new HashSet<>();
Map<String,String> map = new HashMap();
List<String> list1 = Collections.synchronizedList(list);
Set<String> set1 = Collections.synchronizedSet(set);
Map<String,String> map1 = Collections.synchronizedMap(map);
同步容器中的大量的方法采用了synchronized进行了同步,在并发环境中都会争抢这一把锁,同时只能有一个线程对容器进行访问,这必然会影响到执行性能。
public class ListConcurrentTest2 {
static Vector<Integer> vector = new Vector<Integer>();
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
vector.add(1);
}
Thread thread1 = new Thread() {
public void run() {
for (int i = 0; i < vector.size(); i++)
vector.remove(-1);
}
};
Thread thread2 = new Thread() {
public void run() {
for (int i = 0; i < vector.size(); i++)
vector.get(i);
}
};
thread1.start();
thread2.start();
}
}
运行结果,由于线程删除了vector的元素,导致thread2 遍历时,下标元素被删除不存在,发生越界错误。
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: -1
at java.util.Vector.elementData(Vector.java:737)
at java.util.Vector.remove(Vector.java:835)
at com.concurrent.juc.ch13.ListConcurrentTest2$1.run(ListConcurrentTest2.java:26)
Process finished with exit code 0
在对Vector等容器并发地进行迭代修改时,会报ConcurrentModificationException异常,但是在并发容器中不会出现这个问题。 比如:
public class ListConcurrentTest3 {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
Integer integer = iterator.next();
list.remove(integer);
}
}
}
此时,由于iterator
在迭代时修改了list内容,报错
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.concurrent.juc.ch13.ListConcurrentTest3.main(ListConcurrentTest3.java:22)
同步容器将所有对容器状态的访问都串行化了,以保证了线程的安全性,但是安全隐患仍然存在,而且严重降低了并发性,当多个线程竞争容器时,性能急剧下降。
因此Java5.0开始针对多线程并发访问设计,提供了并发性能较好的并发容器,有以下优点。
ConcurrentHashMap可以做到读取数据不加锁,对数据分桶,在内部采用了一个叫做Segment数组,Segment元素指向一个Map。ConcurrentHashMap需要两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部,因此,这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长,但是带来的好处是写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment。
ConcurrentHashMap的源码值得深入研究,可以参考《JAVA多线程源码篇》
Copy-On-Write是一种用于程序设计中的优化策略。其基本思路是,在读时共享数据,但是当有线程要修改元素时会将内部数组拷贝一份,对拷贝数组进行修改,拷贝完成后再更正链接指向。JUC容器里有CopyOnWriteArrayList和CopyOnWriteArraySet,可以在非常多的并发场景中使用到。
BlockingQueue常用于生产者消费者场景。队列满时,生产者会阻塞,队列空时,消费者会阻塞。可以方便地实现线程协同工作。
ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。有界也就意味着,它不能够存储无限多数量的元素。它有一个同一时间能够存储元素数量的上限。
LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。
PriorityBlockingQueue 是一个无界的并发队列。它使用了和类 java.util.PriorityQueue 一样的排序规则。你无法向这个队列中插入 null 值。所有插入到 PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。
SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。
DelayQueue 对元素进行持有直到一个特定的延迟到期。在每个元素的 getDelay() 方法返回的值的时间段之后才释放掉该元素。如果返回的是 0 或者负值,延迟将被认为过期,该元素将会在 DelayQueue 的下一次 take 被调用的时候被释放掉。
Java5.0提供了并发性能较好的并发容器,根据具体场景进行设计,尽量避免synchronized,提供并发性。 定义了一些并发安全的复合操作,并且保证并发环境下的迭代操作不会出错。
多线程系列在github上有一个开源项目,主要是本系列博客的实验代码。
https://github.com/forestnlp/concurrentlab
如果您对软件开发、机器学习、深度学习有兴趣请关注本博客,将持续推出Java、软件架构、深度学习相关专栏。
您的支持是对我最大的鼓励。