1、线程不安全的类
如果一个类的对象同时可以被多个线程访问,并且你不做特殊的同步或并发处理,那么它就很容易表现出线程不安全的现象。比如抛出异常、逻辑处理错误…
下面列举一下常见的线程不安全的类及对应的线程安全类:
(1)StringBuilder 与 StringBuffer
StringBuilder是线程不安全的,而StringBuffer是线程安全的。分析源码:StringBuffer的方法使用了synchronized关键字修饰。
1 |
|
(2)SimpleDateFormat 与 jodatime插件
SimpleDateFormat 类在处理时间的时候,如下写法是线程不安全的:
1 | private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd"); |
但是我们可以变换其为线程安全的写法:在每次转换的时候使用线程封闭,新建变量
1 | private static void update() { |
另外我们也可以使用jodatime插件来转换时间:其可以保证线程安全性
Joda 类具有不可变性,因此它们的实例无法被修改。(不可变类的一个优点就是它们是线程安全的)
1 | private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyyMMdd"); |
分析源码:(不可变性)
1 | public class DateTimeFormatter { |
(3)ArrayList,HashSet,HashMap 等Collection类
像ArrayList,HashSet,HashMap 等Collection类均是线程不安全的,我们以ArrayList举例分析一下源码:
1、ArrayList的基本属性
:
在声明时使用了transient 关键字,此关键字意为在采用Java默认的序列化机制的时候,被该关键字修饰的属性不会被序列化。而ArrayList实现了序列化接口,自己定义了序列化方法(在此不描述)。
//对象数组:ArrayList的底层数据结构
private transient Object[] elementData;
//elementData中已存放的元素的个数
private int size;
//默认数组容量
private static final int DEFAULT_CAPACITY = 10;
2、初始化
1 | /** |
3、添加方法(重点)
1 | //每次添加时将数组扩容1,然后再赋值 |
总结:ArrayList每次对内容进行插入操作的时候,都会做扩容处理,这是ArrayList的优点(无容量的限制),同时也是缺点,线程不安全。(以下例子取材于鱼笑笑博客)
一个 ArrayList ,在添加一个元素的时候,它可能会有两步来完成:在 Items[Size] 的位置存放此元素; 增大 Size 的值。 在单线程运行的情况下,如果 Size =
0,添加一个元素后,此元素在位置 0,而且 Size=1; 而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置
0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0
(注意,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加
Size 的值。 那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于
2。这就是“线程不安全”了。那么如何将其处理为线程安全的?或者说对应的线程安全类有哪些呢?接下来就涉及到我们同步容器。
2、同步容器
同步容器分两类,一种是Java提供好的类,另一类是Collections类中的相关同步方法。
(1)ArrayList的线程安全类:Vector,Stack
Vector实现了List接口,Vector实际上就是一个数组,和ArrayList非常的类似,但是内部的方法都是使用synchronized修饰过的方法。
Stack它的方法也是使用synchronized修饰了,继承了Vector,实际上就是栈
使用举例(Vector):
1 | //定义 |
运行结果:报错java.lang.ArrayIndexOutOfBoundsException: Array index out of
range
原因分析:同时发生获取与删除的操作。当两个线程在同一时间都判断了vector的size,假设都判断为9,而下一刻线程1执行了remove操作,随后线程2才去get,所以就出现了错误。synchronized关键字可以保证同一时间只有一个线程执行该方法,但是多个线程同时分别执行remove、add、get操作的时候就无法控制了。
1 | 错误[2]:使用foreach\iterator遍历Vector的时候进行增删操作 |
解决办法:在使用iteratir进行增删操作的时候,加上Lock或者synchronized同步措施或者并发容器
(2)HashMap的线程安全类:HashTable
使用举例:
1 | //定义 |
源码分析:
保证安全性:使用了synchronized修饰 不允许空值(在代码中特殊做了判断)
HashMap和HashTable都使用哈希表来存储键值对。在数据结构上是基本相同的,都创建了一个继承自Map.Entry的私有的内部类Entry,每一个Entry对象表示存储在哈希表中的一个键值对。
Entry对象唯一表示一个键值对,有四个属性:
-K key 键对象
-V value 值对象
-int hash 键对象的hash值
-Entry entry 指向链表中下一个Entry对象,可为null,表示当前Entry对象在链表尾部
1 | public synchronized V put(K key, V value) { |
(3)Collections类中的相关同步方法
Collections类中提供了一系列的线程安全方法用于处理ArrayList等线程不安全的Collection类
1 | 使用方法: |