线程不安全类
如果一个类的对象同时可以被多个线程访问,并且你不做特殊的同步或并发处理,那么它就很容易表现出线程不安全的现象。比如抛出异常、逻辑处理错误等。下面列举一下常见的线程不安全的类及对应的线程安全类:
StringBuilder 与 StringBuffer
StringBuilder是线程不安全的,而StringBuffer是线程安全的。StringBuffer的方法使用了synchronized
关键字修饰。
1 | //StringBuffer的方法使用了synchronized关键字修饰。 |
SimpleDateFormat 与 jodatime插件
SimpleDateFormat 类在处理时间的时候,如下写法是线程不安全的:
1 | private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd"); |
我们可以使用线程封闭等手段,也可以使用jodatime插件来转换时间,Joda 类具有不可变性,因此它们的实例无法被修,可以保证线程安全性
1 | private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyyMMdd"); |
ArrayList,HashSet,HashMap…
像ArrayList,HashSet,HashMap 等Collection类均是线程不安全的,我们以ArrayList举例分析一下源码:
首先看看这个类所拥有的部分属性字段:
1 | public class ArrayList<E> extends AbstractList<E> |
所以通过这两个字段我们可以看出,ArrayList的实现主要就是用了一个Object的数组,用来保存所有的元素,以及一个size变量用来保存当前数组中已经添加了多少元素。
接着我们看下最重要的add操作时的源代码:
1 | public boolean add(E e) { |
ensureCapacityInternal()
这个方法的详细代码我们可以暂时不看,它的作用就是判断如果将当前的新元素加到列表后面,列表的elementData
数组的大小是否满足,如果size + 1的这个需求长度大于了elementData
这个数组的长度,那么就要对这个数组进行扩容。
由此看到add元素时,实际做了两个大的步骤:
- 判断
elementData
数组容量是否满足需求 - 在
elementData
对应位置上设置值
这样也就出现了第一个导致线程不安全的隐患,在多个线程进行add操作时可能会导致elementData
数组越界。具体逻辑如下:
- 列表大小为9,即size=9
- 线程A开始进入add方法,这时它获取到size的值为9,调用
ensureCapacityInternal
方法进行容量判断。 - 线程B此时也进入add方法,它获取到size的值也为9,也开始调用
ensureCapacityInternal
方法。 - 线程A发现需求大小为10,而
elementData
的大小就为10,可以容纳。于是它不再扩容,返回。 - 线程B也发现需求大小为10,也可以容纳,返回。
- 线程A开始进行设置值操作,
elementData[size++] = e
操作。此时size变为10。 - 线程B也开始进行设置值操作,它尝试设置
elementData[10] = e
,而elementData
没有进行过扩容,它的标最大为9。于是此时会报出一个数组越界的异常ArrayIndexOutOfBoundsException
.
另外第二步 elementData[size++] = e
设置值的操作同样会导致线程不安全。从这儿可以看出,这步操作也不是一个原子操作,它由如下两步操作构成:
elementData[size] = e;
size = size + 1;
也就是说,当多线程环境下执行时,又可能会发生第二个导致线程不安全的隐患:一个线程的值覆盖另一个线程添加的值。
对应的线程安全类有哪些呢?接下来就涉及到我们同步容器。
同步容器
同步容器分两类,一种是Java提供好的类,另一类是Collections类中的相关同步方法。
Vector
Vector实现了List接口,Vector实际上就是一个数组,和ArrayList非常的类似,但是内部的方法都是使用synchronized
修饰过的方法。
1 | //vector 的add 方法实现 |
但是Vector也不是完全的线程安全的,比如:
删除与获取并发操作
1 | public class VectorExample { |
运行结果:报错java.lang.ArrayIndexOutOfBoundsException: Array index out of range
原因分析:同时发生获取与删除的操作。当两个线程在同一时间都判断了vector的size,假设都判断为9,而下一刻线程1执行了remove操作,随后线程2才去get,所以就出现了错误。synchronized关键字可以保证同一时间只有一个线程执行该方法,但是多个线程同时分别执行remove、add、get操作的时候就无法控制了。
使用foreach\iterator遍历Vector的时候进行增删操作,也会出现问题
1 | public class VectorExample3 { |
解决办法:在使用iterator进行增删操作的时候,加上Lock或者synchronized同步措施或者并发容器
HashTable
1 | public synchronized V put(K key, V value) { |
源码分析
- 使用了synchronized修饰
- 保证安全性不允许空值
- HashMap和HashTable都使用哈希表来存储键值对
Collections类中的相关同步方法
1 | //定义 |