0%

线程不安全类与同步容器

线程不安全类

如果一个类的对象同时可以被多个线程访问,并且你不做特殊的同步或并发处理,那么它就很容易表现出线程不安全的现象。比如抛出异常、逻辑处理错误等。下面列举一下常见的线程不安全的类及对应的线程安全类:

StringBuilder 与 StringBuffer

StringBuilder是线程不安全的,而StringBuffer是线程安全的。StringBuffer的方法使用了synchronized关键字修饰。

1
2
3
4
5
6
7
//StringBuffer的方法使用了synchronized关键字修饰。
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}

SimpleDateFormat 与 jodatime插件

SimpleDateFormat 类在处理时间的时候,如下写法是线程不安全的:

1
2
3
4
5
6
7
8
9
10
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");

//线程调用方法
private static void update() {
try {
simpleDateFormat.parse("20180208");
} catch (Exception e) {
log.error("parse exception", e);
}
}

我们可以使用线程封闭等手段,也可以使用jodatime插件来转换时间,Joda 类具有不可变性,因此它们的实例无法被修,可以保证线程安全性

1
2
3
4
5
private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyyMMdd");

private static void update(int i) {
log.info("{}, {}", i, DateTime.parse("20180208", dateTimeFormatter).toDate());
}

ArrayList,HashSet,HashMap…

像ArrayList,HashSet,HashMap 等Collection类均是线程不安全的,我们以ArrayList举例分析一下源码:

首先看看这个类所拥有的部分属性字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
/**
* 列表元素集合数组
* 如果新建ArrayList对象时没有指定大小,那么会将EMPTY_ELEMENTDATA赋值给elementData,
* 并在第一次添加元素时,将列表容量设置为DEFAULT_CAPACITY
*/
transient Object[] elementData;

/**
* 列表大小,elementData中存储的元素个数
*/
private int size;
}

所以通过这两个字段我们可以看出,ArrayList的实现主要就是用了一个Object的数组,用来保存所有的元素,以及一个size变量用来保存当前数组中已经添加了多少元素。

接着我们看下最重要的add操作时的源代码:

1
2
3
4
5
6
7
8
9
10
11
public boolean add(E e) {

/**
* 添加一个元素时,做了如下两步操作
* 1.判断列表的capacity容量是否足够,是否需要扩容
* 2.真正将元素放在列表的元素数组里面
*/
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

ensureCapacityInternal()这个方法的详细代码我们可以暂时不看,它的作用就是判断如果将当前的新元素加到列表后面,列表的elementData数组的大小是否满足,如果size + 1的这个需求长度大于了elementData这个数组的长度,那么就要对这个数组进行扩容

由此看到add元素时,实际做了两个大的步骤:

  1. 判断elementData数组容量是否满足需求
  2. elementData对应位置上设置值

这样也就出现了第一个导致线程不安全的隐患,在多个线程进行add操作时可能会导致elementData数组越界。具体逻辑如下:

  1. 列表大小为9,即size=9
  2. 线程A开始进入add方法,这时它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断。
  3. 线程B此时也进入add方法,它获取到size的值也为9,也开始调用ensureCapacityInternal方法。
  4. 线程A发现需求大小为10,而elementData的大小就为10,可以容纳。于是它不再扩容,返回。
  5. 线程B也发现需求大小为10,也可以容纳,返回。
  6. 线程A开始进行设置值操作, elementData[size++] = e 操作。此时size变为10。
  7. 线程B也开始进行设置值操作,它尝试设置elementData[10] = e,而elementData没有进行过扩容,它的标最大为9。于是此时会报出一个数组越界的异常ArrayIndexOutOfBoundsException.

另外第二步 elementData[size++] = e 设置值的操作同样会导致线程不安全。从这儿可以看出,这步操作也不是一个原子操作,它由如下两步操作构成:

  1. elementData[size] = e;
  2. size = size + 1;

也就是说,当多线程环境下执行时,又可能会发生第二个导致线程不安全的隐患:一个线程的值覆盖另一个线程添加的值

对应的线程安全类有哪些呢?接下来就涉及到我们同步容器。

同步容器

同步容器分两类,一种是Java提供好的类,另一类是Collections类中的相关同步方法。

Vector

Vector实现了List接口,Vector实际上就是一个数组,和ArrayList非常的类似,但是内部的方法都是使用synchronized修饰过的方法。

1
2
3
4
5
6
7
//vector 的add 方法实现
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}

但是Vector也不是完全的线程安全的,比如:

删除与获取并发操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class VectorExample {

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() {
public void run() {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
};
Thread thread2 = new Thread() {
public void run() {
for (int i = 0; i < vector.size(); i++) {
vector.get(i);
}
}
};
thread1.start();
thread2.start();
}
}
}

运行结果:报错java.lang.ArrayIndexOutOfBoundsException: Array index out of range
原因分析:同时发生获取与删除的操作。当两个线程在同一时间都判断了vector的size,假设都判断为9,而下一刻线程1执行了remove操作,随后线程2才去get,所以就出现了错误。synchronized关键字可以保证同一时间只有一个线程执行该方法,但是多个线程同时分别执行remove、add、get操作的时候就无法控制了。

使用foreach\iterator遍历Vector的时候进行增删操作,也会出现问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class VectorExample3 {

// 报错java.util.ConcurrentModificationException
private static void test1(Vector<Integer> v1) { // foreach
for(Integer i : v1) {
if (i.equals(3)) {
v1.remove(i);
}
}
}

// 报错java.util.ConcurrentModificationException
private static void test2(Vector<Integer> v1) { // iterator
Iterator<Integer> iterator = v1.iterator();
while (iterator.hasNext()) {
Integer i = iterator.next();
if (i.equals(3)) {
v1.remove(i);
}
}
}

public static void main(String[] args) {
Vector<Integer> vector = new Vector<>();
vector.add(1);
vector.add(2);
test1(vector);
test2(vector);
}
}

解决办法:在使用iterator进行增删操作的时候,加上Lock或者synchronized同步措施或者并发容器

HashTable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}

// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}

addEntry(hash, key, value, index);
return null;
}

源码分析

  • 使用了synchronized修饰
  • 保证安全性不允许空值
  • HashMap和HashTable都使用哈希表来存储键值对

Collections类中的相关同步方法

synContainer

1
2
3
4
5
6
//定义
private static List<Integer> list = Collections.synchronizedList(Lists.newArrayList());
//多线程调用方法
private static void update(int i) {
list.add(i);
}