GitHub 7.7k Star 的Java工程师成神之路 ,不来了解一下吗?java
GitHub 7.7k Star 的Java工程师成神之路 ,真的不来了解一下吗?git
GitHub 7.7k Star 的Java工程师成神之路 ,真的肯定不来了解一下吗?github
为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,好比:同步容器、并发容器、阻塞队列等。安全
最多见的同步容器就是Vector和Hashtable了,那么,同步容器的全部操做都是线程安全的吗?多线程
这个问题不知道你有没有想过,本文就来深刻分析一下这个问题,一个很容易被忽略的问题。并发
在Java中,同步容器主要包括2类:工具
本文拿相对简单的Vecotr来举例,咱们先来看下Vector中几个重要方法的源码:spa
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
public synchronized E remove(int index) {
modCount++;
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
E oldValue = elementData(index);
int numMoved = elementCount - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--elementCount] = null; // Let gc do its work
return oldValue;
}
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
复制代码
能够看到,Vector这样的同步容器的全部公有方法全都是synchronized的,也就是说,咱们能够在多线程场景中放心的使用单独这些方法,由于这些方法自己的确是线程安全的。线程
可是,请注意上面这句话中,有一个比较关键的词:单独code
由于,虽然同步容器的全部方法都加了锁,可是对这些容器的复合操做没法保证其线程安全性。须要客户端经过主动加锁来保证。
简单举一个例子,咱们定义以下删除Vector中最后一个元素方法:
public Object deleteLast(Vector v){
int lastIndex = v.size()-1;
v.remove(lastIndex);
}
复制代码
上面这个方法是一个复合方法,包括size()和remove(),乍一看上去好像并无什么问题,不管是size()方法仍是remove()方法都是线程安全的,那么整个deleteLast方法应该也是线程安全的。
可是时,若是多线程调用该方法的过程当中,remove方法有可能抛出ArrayIndexOutOfBoundsException。
Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 879
at java.util.Vector.remove(Vector.java:834)
at com.hollis.Test.deleteLast(EncodeTest.java:40)
at com.hollis.Test$2.run(EncodeTest.java:28)
at java.lang.Thread.run(Thread.java:748)
复制代码
咱们上面贴了remove的源码,咱们能够分析得出:当index >= elementCount时,会抛出ArrayIndexOutOfBoundsException ,也就是说,当当前索引值再也不有效的时候,将会抛出这个异常。
由于removeLast方法,有可能被多个线程同时执行,当线程2经过index()得到索引值为10,在尝试经过remove()删除该索引位置的元素以前,线程1把该索引位置的值删除掉了,这时线程一在执行时便会抛出异常。
为了不出现相似问题,能够尝试加锁:
public void deleteLast() {
synchronized (v) {
int index = v.size() - 1;
v.remove(index);
}
}
复制代码
如上,咱们在deleteLast中,对v进行加锁,便可保证同一时刻,不会有其余线程删除掉v中的元素。
另外,若是如下代码会被多线程执行时,也要特别注意:
for (int i = 0; i < v.size(); i++) {
v.remove(i);
}
复制代码
因为,不一样线程在同一时间操做同一个Vector,其中包括删除操做,那么就一样有可能发生线程安全问题。因此,在使用同步容器的时候,若是涉及到多个线程同时执行删除操做,就要考虑下是否须要加锁。
前面说过了,同步容器直接保证耽搁操做的线程安全性,可是没法保证复合操做的线程安全,遇到这种状况时,必需要经过主动加锁的方式来实现。
并且,除此以外,同步容易因为对其全部方法都加了锁,这就致使多个线程访问同一个容器的时候,只能进行顺序访问,即便是不一样的操做,也要排队,如get和add要排队执行。这就大大的下降了容器的并发能力。
针对前文提到的同步容器存在的并发度低问题,从Java5开始,java.util.concurent包下,提供了大量支持高效并发的访问的集合类,咱们称之为并发容器。
针对前文提到的同步容器的复合操做的问题,通常在Map中发生的比较多,因此在ConcurrentHashMap中增长了对经常使用复合操做的支持,好比"若没有则添加":putIfAbsent(),替换:replace()。这2个操做都是原子操做,能够保证线程安全。
另外,并发包中的CopyOnWriteArrayList和CopyOnWriteArraySet是Copy-On-Write的两种实现。
Copy-On-Write容器即写时复制的容器。通俗的理解是当咱们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,而后新的容器里添加元素,添加完元素以后,再将原容器的引用指向新的容器。
CopyOnWriteArrayList中add/remove等写方法是须要加锁的,而读方法是没有加锁的。
这样作的好处是咱们能够对CopyOnWrite容器进行并发的读,固然,这里读到的数据可能不是最新的。由于写时复制的思想是经过延时更新的策略来实现数据的最终一致性的,并不是强一致性。
可是,做为代替Vector的CopyOnWriteArrayList并无解决同步容器的复合操做的线程安全性问题。
本文介绍了同步容器和并发容器。
同步容器是经过加锁实现线程安全的,而且只能保证单独的操做是线程安全的,没法保证复合操做的线程安全性。而且同步容器的读和写操做之间会互相阻塞。
并发容器是Java 5中提供的,主要用来代替同步容器。有更好的并发能力。并且其中的ConcurrentHashMap定义了线程安全的复合操做。
在多线程场景中,若是使用并发容器,必定要注意复合操做的线程安全问题。必要时候要主动加锁。
在并发场景中,建议直接使用java.util.concurent包中提供的容器类,若是须要复合操做时,建议使用有些容器自身提供的复合方法。