线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
我们都指定Java中集合类(Collection、Map)都未考虑线程安全,从源码的角度看一下ArrayList类为什么不是线程安全的类。如该类中在列表末尾插入元素的方法为
/**
* Appends the specified element to the endof this list.
*
* @param e element to be appended to thislist
* @return true (asspecified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
ArrayList的实现主要就是用了一个Object的数组,用来保存所有的元素,以及一个size变量用来保存当前数组中已经添加了多少元素。
一个 ArrayList在末尾添加一个元素的时候,它可能会有两步来完成:
首先,调用ensureCapacityInternal()这个方法,它的作用就是判断如果将当前的新元素加到列表后面,当前elementData数组的大小是否满足,如果size + 1的这个需求长度大于了elementData这个数组的长度,那么就要对这个数组进行扩容。
其次,在elementData对应位置上设置值。
在单线程运行的情况下,这个方法中的流程是一气呵成的。
第二步中elementData[size++] = e操作不是一个原子操作,它由如下两步操作构成:
elementData[size] = e;
size = size + 1;
如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度让线程A暂停(只完成了第一步),线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0,所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,线程A开始将size的值增加为1,线程B开始将size的值增加为2,都增加 Size 的值。现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2,下标1的位置上什么都没有。
还有一种情况:线程A和B同时调用add方法,假设A执行add方法已经到了elementData[size] = e这一行,但此时调度器将A置回到Runnable状态,将B置于Running状态,而B执行add已完成了elementData[size++] = e,此时size刚好等于数组长度。若此时调度器将B置回到Runnable状态,将A置于Running状态,于是A开始执行elementData[size] = e,但size的长度等于数组长度,这必然造成ArrayIndexOutOfBoundsException。
这就是“线程不安全”了。 线程存取同一对象相同资源(本例为size)时就会引发这种情况(称为竞速情况,race condition)。
网友评论