1.同步容器类
(1)为什么会出现同步容器
- Java集合框架中,主要有四大类别:List、Set、Queue、Map。
- List、Set、Queue接口分别继承了Collection接口,Map本身是一个接口。
注意Collection和Map是一个顶层接口,而List、Set、Queue则继承了Collection接口,分别代表数组、集合和对列这三大容器。
像ArrayList、LinkedList都是实现List接口,HashSet实现了Set接口,而Deque继承了Queue接口,PriorityQueue实现了Queue接口。另外LinkedList实现了Deque接口。
像ArrayList、LinkedList、HashMap这些容器都是非线程安全的。如果有多个线程并发地访问这些容器时,就会出现问题。
因此,在编写程序时,必须要求程序员手动地在任何访问到这些容器的地方进行同步处理,这样导致在使用这些容器的时候非常地不方便。
所以,Java提供了同步容器供用户使用。
(2)Java中的同步容器类
Vector、Stack、HashTable
Collections类中提供的静态工厂方法创建的类
Vector实现了List接口,Vector实际上就是一个数组,和ArrayList类似,但是Vector中的方法都是synchronized方法,即进行了同步措施。
Stack也是一个同步容器,它的方法也是用synchronzied进行了同步,继承Vector类。
HashTable实现了Map接口,他和HashMap相似,但是HashTable进行了同步处理,而HashMap没有。
Collections类是一个工具提供类,注意,它和Collection不同,Collection是一个顶层的接口。在Collections类中提供了大量的方法,比如对集合或者容器进行排序、查找等操作。最重要的是,在它里面提供了几个静态工厂方法来创建同步容器类
(3)同步容器的缺陷
1.性能问题:传统的非同步容器和同步容器的性能差异,我们以ArrayList和Vector为例
public class Demo1 { public static void main(String[] args) { ArrayList<Integer> arrayList = new ArrayList<>(); Vector<Integer> vector = new Vector<>(); long startTime = System.currentTimeMillis(); for (int i = 0; i < 100000; i++) { vector.add(i); } long endTime = System.currentTimeMillis(); System.out.println("vector循环10万次添加元素消耗的时间:"+(endTime-startTime)+"ms"); long startTime1 = System.currentTimeMillis(); for (int i = 0; i < 100000; i++) { arrayList.add(i); } long endTime1 = System.currentTimeMillis(); System.out.println("ArrayList循环10万次添加元素消耗的时间:"+(endTime1-startTime1)+"ms"); } }
进行同样多的插入操作,Vector的耗时是ArrayList的两倍。
这只是其中的一方面性能问题上的反映。
另外,由于Vector中的add方法和get方法都进行了同步,因此,在有多个线程进行访问时,如果多个线程都只是进行读取操作,那么每个时刻就只能有一个线程进行读取,其他线程便只能等待,这些线程必须竞争同一把锁。
因此为了解决同步容器的性能问题,在Java 1.5中提供了并发容器,位于java.util.concurrent目录下。
2.同步容器的安全性问题
Vector中的方法都进行了同步处理,那么一定就是线程安全的,事实上这可不一定。
public class Demo2 { private static Vector<Integer> vector = new Vector<>(); public static void main(String[] args) { while (true) { for (int i = 0; i < 10; i++) { vector.add(i); } Thread thread1 = new Thread(() -> { for (int i = 0; i < vector.size(); i++) { vector.remove(i); } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < vector.size(); i++) { vector.get(i); } }); thread1.start(); thread2.start(); } } }
正如大家所看到的,这段代码报错了:数组下标越界。
也许有朋友会问:Vector是线程安全的,为什么还会报这个错?很简单,对于Vector,虽然能保证每一个时刻只能有一个线程访问它,但是不排除这种可能:
当某个线程在某个时刻执行这句时:
for(int i= 0;i < vector.size();i++) vector.get(i);
假若此时vector的size方法返回的是10,i的值为9
然后另外一个线程执行了这句:
for(int i= 0;i < vector.size();i++) vector.remove(i);
将下标为9的元素删除了。
那么通过get方法访问下标为9的元素肯定就会出问题了。
因此为了保证线程安全,必须在方法调用端做额外的同步措施,如下面所示:
public class Demo2 { private static Vector<Integer> vector = new Vector<>(); private static Object object = new Object(); public static void main(String[] args) { while (true) { for (int i = 0; i < 10; i++) { vector.add(i); } Thread thread1 = new Thread(() -> { synchronized (object) { for (int i = 0; i < vector.size(); i++) { vector.remove(i); } } }); Thread thread2 = new Thread(() -> { synchronized (object) { for (int i = 0; i < vector.size(); i++) { vector.get(i); } } }); thread1.start(); thread2.start(); } } }
3.ConcurrentModificationException异常
在对Vector等容器并发地进行迭代修改时,会报ConcurrentModificationException异常,关于这个异常将会在后续文章中讲述。但是在并发容器中不会出现这个问题。
2.并发容器类
(1)ConcurrectHashMap
HashMap、HashTable与ConcurrentHashMap都是实现的哈希表数据结构,在随机读取的时候效率很高。HashTable实现同步是利用synchronzied关键字进行锁定的,其实针对整张Hash表进行锁定,及每次锁住整张表让线程独占,在线程安全的背后是巨大的浪费。
ConcurrentHashMap和HashTable主要的区别就在于围绕这锁的粒度进行区别以及如何区锁定。
上图中,左边是HashTable的实现方式,可以看到锁住的是整张哈希表,而右边是ConcurrectHashMap的实现方式,单独锁住每一个桶(segment),ConcurrentHashMap将哈希表分为16个桶(默认值),诸如get()、put()、remove()等常用操作之锁定当前需要的桶,而size()才锁定整张表。原来只能一个线程进入,现在却能同时接受16个写线程并发进入(写线程需要锁定,而读线程几乎不受限制),并发性的提升是显而易见的。
而在迭代时,ConcurrentHashMap使用了不同于传统集合的快速失败迭代器(fast-fail iterator)的另外一种迭代方式,称为弱一致迭代器。在这种迭代方式中,当iterator被创建后集合在发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时实例化出新的数据从而不影响原有的数据,iterator完成后在将头指针替换成为新的数据,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。
public class Demo3 { public static void main(String[] args) { //Map<String,String> map = new ConcurrentHashMap<>(); //84ms //Map<String,String> map = new ConcurrentSkipListMap<>(); //108ms //Map<String,String> map = new Hashtable<>(); //92ms Map<String,String> map = new HashMap<>(); //139ms Random random = new Random(); Thread[] threads = new Thread[100]; CountDownLatch latch = new CountDownLatch(threads.length); long startTime = System.currentTimeMillis(); for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(()->{ for (int j = 0; j < 10000; j++) { map.put(random.nextInt(1000000)+"",random.nextInt()+""); latch.countDown(); } }); } Arrays.asList(threads).forEach(thread -> thread.start()); try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } long endTime = System.currentTimeMillis(); System.out.println(endTime-startTime+"ms"); } }
运行结果:
ConcurrentHashMap:84ms ConcurrentSkipListMap:108ms Hashtable:92ms HashMap:139ms
启动100个线程,向每个容器中添加100000个元素,最终发现,ConcurrentHashMap要比HashMap效率高,ConcurrentHashMap是将大锁分成若干小锁,实现多个线程共同运行,锁以效率有很大差距。ConcurrentSkipListMap较ConcurrentHashMap除了实现高并发外还能对元素进行排序。