同步容器出现的原因?
在Java的集合容器框架中,主要四大类是List、Set、Queue、Map。其中List、Set、Queue分别继承了Collection顶层接口,Map本身是一个顶层接口。我们常用的ArrayList、LinkedList、HashMap这些容器都是非线程安全的,如果有多个线程并发访问这些容器时,就会出现问题。因此,编写程序时,必须要求开发者手动在任何访问到这些容器的地方进行同步处理,这样导致使用这些容器时的不便,所以,Java提供了同步容器供用户使用。
Java中的同步容器类:
1、Vector、Stack、HashTable
2、Collections类中提供的静态工厂方法创建的类
Vector实现了List接口,其实际上就是一个类似于ArrayList的数组,但Vector中的方法都是synchronized方法。Stack也是同步容器,实际上Stack是继承于Vector。HashTable也进行了同步处理,但HashMap没有。Collections类是一个工具提供类,其中包含对集合或容器进行排序、查找等操作的方法。重要的是,它也提供了几个静态工厂方法来创建同步容器类。
同步容器存在的缺陷:
1、同步容器中的方法采用了synchronized进行同步,这会影响执行性能。
2、同步容器不一定是真正完全的线程安全,同步容器中的方法是线程安全的,但对这些集合类的符合操作无法保证其线程安全性,仍旧需要通过主动加锁来保证。
3、ConcurrentModificationException异常
Vector等容器迭代时同时对其修改,会报ConcurrentModificationException异常。
ConcurrentModificationException异常详解:
public class Test { public static void main(String[] args) { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(2); Iterator<Integer> iterator = list.iterator(); while(iterator.hasNext()){ Integer integer = iterator.next(); if(integer==2) list.remove(integer); } } }
//结果
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.testdemo.demo.Test.main(Test.java:20)
执行上述代码后,会报该异常。并发现错误发生在checkForComodification()方法中,我们不直接看该方法内容。先了解一下ArrayList中itreator方法的具体实现。该方法存在于父类AbstractList中。
public Iterator<E> iterator() {
return new Itr();
}
该方法返回一个指向Itr类型对象的引用。看看其具体实现。
private class Itr implements Iterator<E> {
//表示下一个要访问的元素的索引 int cursor = 0;
//表示上一个要访问的元素的索引 int lastRet = -1;
//expectedModCount表示对ArrayList修改次数的期望值,初始值为modCount
//modCount是AbstractList类中的一个成员变量。表示对List的修改次数。每次调用add或remove方法都会对modCount进行+1操作 int expectedModCount = modCount;
//判断是否还有元素未被访问
public boolean hasNext() { return cursor != size(); }
//获取到下标为0的元素 public E next() { checkForComodification(); try { E next = get(cursor); lastRet = cursor++; return next; } catch (IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(); } } public void remove() { if (lastRet == -1) throw new IllegalStateException(); checkForComodification(); try { AbstractList.this.remove(lastRet); if (lastRet < cursor) cursor--; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException e) { throw new ConcurrentModificationException(); } } //如果 final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
首先调用list.iterator()返回一个Iterator后,通过hashNext方法判刑是否还有元素未被访问。该方法通过对比下一个访问的元素下标和ArrayList的大小。不等于时说明还有元素需要访问。
然后通过next方法获取到下标为0的元素。该方法中先调用checkForComodification()方法,根据cursor的值获取到元素。将cursor值赋给lastRet。初始时,cursor为0,lastRet为-1.调用一次后curosr的值为1,lastRet为0.此时modCount为0,expectedModCount也为0。
当元素值为2时,调用list.remove()方法。我们看一下ArrayList中remove方法的源码。
public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } /* * Private remove method that skips bounds checking and does not * return the value removed. */ private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work }
remove方法删除元素实际是调用fastRemove()方法,在该方法中,首先对modCount进行加1,表示对集合修改了一次。然后删除元素,最终将size进行减1,并将引用设置为null方便垃圾回收。此时对于iterator,expectedModCount为0,cursor为1,lastRet为0。对于list,其modCount为1,size为0。当执行完删除操作,继续while循环。调用hasNext()方法。此时的cursor为1,size为0,返回true继续执行循环,调用next()方法。next方法首先调用checkForComodification()对比modCount和expectedModCount。此时值不一样,抛出ConcurrentModificationException异常。关键之处在于:list.remove会导致modCount和expectedModCount不一致。
ConcurrentModificationException异常解决:
单线程:单线程情况下,采用迭代器提供的remove方法进行删除就不会抛出异常。
多线程:在使用iterator迭代的时候,使用synchronized或lock同步、使用并发容器CopyOnWriteArrayList代替ArrayList和Vector
并发容器
同步容器将所有对容器状态的访问都串行化了,保证线程安全性的同时严重降低了并发性,当多个线程竞争容器时,吞吐量严重降低。从JDK5开始针对多线程并发访问设计,提供了并发性能较好的并发容器,引入了java.util.concurrent 包。
与Vector和Hashtable、Collection.synchronizedXxx()同步容器等相比,util.concurrent中的并发容器主要解决了两个问题:
1、根据具体场景设计,尽量避免synchronized,提供并发性
2、定义一些并发安全的复合操作,并且保证并发环境下的迭代操作不会出错。util.concurrent中容器在迭代时,可以不封装在synchronized中,可以保证不抛异常,但未必每次看到的都是最新的数据。
并发容器简单介绍:
ConcurrentHashMap代替同步的Map。HashMap是根据散列值分段存储,同步Map在同步时锁住了所有的段,而ConcurrentHashMap加锁的时候根据散列值锁住了散列值对应的那段,提高了并发性能。ConcurrentHashMap提供了对常用符合操作的支持。比如"若没有则添加":putIfAbsent(),替换:replace()。这2个操作都是原子操作。
HashMap和ConcurrentHashMap的区别: 1、HashMap不是线程安全的,而ConcurrentHashMap是线程安全的。 2、ConcurrentHashMap采用锁分段技术,将整个Hash桶进行了分段segment,也就是将这个大的数组分成了几个小的片段segment,而且每个小的片段segment上面都有锁存在,那么在插入元素的时候就需要先找到应该插入到哪一个片段segment,然后再在这个片段上面进行插入,而且这里还需要获取segment锁。 3、ConcurrentHashMap可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,不用对整个ConcurrentHashMap加锁,并发性能更好。
CopyOnWriteArrayList和CopyOnWriteArraySet分别代替List和Set。主要是在遍历操作为主的情况下代替同步的List和同步的Set。
ConcurrentLinedQueue是一个先进先出的非阻塞队列。
ConcurrentSkipListMap可以在高效并发中替代SoredMap、ConcurrentSkipListSet可以在高效并发中替代SoredSet。