Java集合时所讲的ArrayList 、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都线程不安全的,当多个并发线程向这些集合中存取元素时,就可能会破坏这些集合的完整性。那么究竟是在什么情况下才会出现问题呢?
线程安全就是说多线程访问同一代码(对象、变量等),不会产生不确定的结果;
package 线程不安全;
import java.util.ArrayList;
import java.util.List;
public class ArrayListInThread implements Runnable {
//线程不安全
private List threadList = new ArrayList();
//线程安全
//private List threadList = Collections.synchronizedList(new ArrayList());
@Override
public void run() {
try {
Thread.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
//把当前线程名称加入list中
threadList.add(Thread.currentThread().getName());
}
public static void main(String[] args) throws InterruptedException{
ArrayListInThread listThread = new ArrayListInThread();
for(int i = 0; i < 10; i++){
Thread thread = new Thread(listThread, String.valueOf(i));
thread.start();
}
//等待子线程执行完
Thread.sleep(2000);
System.out.println(listThread.threadList.size());
//输出list中的值
for(int i = 0; i < listThread.threadList.size(); i++){
if(listThread.threadList.get(i) == null){
System.out.println();;
}
System.out.print(listThread.threadList.get(i) + " ");
}
}
}
结果一:
9
null
null 0 2 1 6 7 8 9
抛出异常:ArrayIndexOutofBoundsException异常;
现象:出现null值;
出现输出不全的现象;
抛出异常;
原因:
ArrayList中的add方法:
//添加元素e
public boolean add(E e) {
// 确定ArrayList的容量大小
ensureCapacity(size + 1); // Increments modCount!!
// 添加e到ArrayList中
elementData[size++] = e;
return true;
}
// 确定ArrarList的容量。
// 若ArrayList的容量不足以容纳当前的全部元素,设置 新的容量=“(原始容量x3)/2 + 1”
public void ensureCapacity(int minCapacity) {
// 将“修改统计数”+1,该变量主要是用来实现fail-fast机制的
modCount++;
int oldCapacity = elementData.length;
// 若当前容量不足以容纳当前的元素个数,设置 新的容量=“(原始容量x3)/2 + 1”
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
int newCapacity = (oldCapacity * 3)/2 + 1;
//如果还不够,则直接将minCapacity设置为当前容量
if (newCapacity < minCapacity)
newCapacity = minCapacity;
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
赋值语句为:elementData[size++] = e,这条语句可拆分为两条:
1. elementData[size] = e;
2. size ++;
假设A线程执行完第一条语句时,CPU暂停执行A线程转而去执行B线程,此时ArrayList的size并没有加一,这时在ArrayList中B线程就会覆盖掉A线程赋的值,而此时,A线程和B线程先后执行size++,便会出现值为null的情况;
至于结果中出现的ArrayIndexOutOfBoundsException异常,则是A线程在执行ensureCapacity(size+1)后没有继续执行,此时恰好minCapacity等于oldCapacity,B线程再去执行,同样由于minCapacity等于oldCapacity,ArrayList并没有增加长度,B线程可以继续执行赋值(elementData[size] = e)并size ++也执行了,此时,CPU又去执行A线程的赋值操作,由于size值加了1,size值大于了ArrayList的最大长度,
因此便出现了ArrayIndexOutOfBoundsException异常。
Java中LinkedList是线程不安全的,在多线程程序中有多个线程访问LinkedList的话会抛出ConcurrentModificationException;另外JDK代码里,ListItr的add(), next(), previous(), remove(), set()方法都会跑出ConcurrentModificationException。
LinkedList的底层方法:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
代码中,modCount记录了LinkedList结构被修改的次数。Iterator初始化时,expectedModCount=modCount。任何通过Iterator修改LinkedList结构的行为都会同时更新expectedModCount和modCount,使这两个值相等。
通过LinkedList对象修改其结构的方法只更新modCount。所以假设有两个线程A和B。A通过Iterator遍历并修改LinkedList,而B,与此同时,通过对象修改其结构,造成modCount加了两次,而expectedModCount只做了一次修改,形成modCount != expectedModCount;那么Iterator的相关方法就会抛出异常。这是相对容易发现的由线程竞争造成的错误。
测试代码:
想要实现的效果:
创建两个线程,共享一个target,这样共享线程内的实例变量,输出结果应该是set中有5000个整数;
package 线程不安全;
import java.util.HashSet;
import java.util.Set;
public class TestHashSet implements Runnable{
// 实现Runnable 让该集合能被多个线程访问
Set set = new HashSet();
// 线程的执行就是插入5000个整数
@Override
public void run() {
for (int i = 0;i < 5000;i ++) {
set.add(i);
}
}
public static void main(String[] args){
TestHashSet run2 = new TestHashSet();
// 实例化两个线程
Thread t6 = new Thread(run2);
Thread t7 = new Thread(run2);
// 启动两个线程
t6.start();
t7.start();
// 当前线程等待加入到调用线程后
try {
t6.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
try {
t7.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 打印出集合的size
System.out.println(run2.set.size());
}
}
现象:好多结果都不是预想的5000;
分析原因:
打印结果大部分出现大于5000的情况。这就出现了之前提到的情况,证明了HashSet不是线程安全的类。 其实查看源代码发现HashSet内部维护数据的采用的是HashMap,根本原因是HashMap不是线程安全的类。导致了HashSet的非线程安全。
TreeSet 底层是通过 TreeMap 来实现的(如同HashSet底层是是通过HashMap来实现的一样);
测试Demo :两个线程同时往声明的hashmap中存储数据;线程安全下,所有的map的key==value。
package 线程不安全;
import java.util.HashMap;
public class TestHashMap {
public static final HashMap firstHashMap=new HashMap();
public static void main(String[] args) throws InterruptedException {
//线程一
Thread t1=new Thread(){
public void run() {
for(int i=0;i<25;i++){
firstHashMap.put(String.valueOf(i), String.valueOf(i));
}
}
};
//线程二
Thread t2=new Thread(){
public void run() {
for(int j=25;j<50;j++){
firstHashMap.put(String.valueOf(j), String.valueOf(j));
}
}
};
t1.start();
t2.start();
//主线程休眠1秒钟,以便t1和t2两个线程将firstHashMap填装完毕。
Thread.currentThread().sleep(1000);
for(int l=0;l<50;l++){
//如果key和value不同,说明在两个线程put的过程中出现异常。
if(!String.valueOf(l).equals(firstHashMap.get(String.valueOf(l)))){
System.err.println(String.valueOf(l)+":"+firstHashMap.get(String.valueOf(l)));
}
}
}
}
结果:
经过多次测试后,发现如图:
分析:
HashMap初始容量大小为16,一般来说,当有数据要插入时,都会检查容量有没有超过设定的thredhold,如果超过,需要增大Hash表的尺寸,但是这样一来,整个Hash表里的元素都需要被重算一遍。这叫rehash,而在rehash的时候,如果有多个线程访问,就会容易导致出错。
通过查看HashMap底层的实现:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
其中addEntry()方法:
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry e = table[bucketIndex];
table[bucketIndex] = new Entry(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
从代码中,可以看到,如果发现哈希表的大小超过阀值threshold,就会调用resize方法,扩大容量为原来的两倍,而扩大容量的做法是新建一个Entry[]:
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
结论:两个线程同时遇到HashMap的扩容(Rehash)情况下,结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的table作为原始数组,这样也会有问题。
测试Demo:如同HashMap一般,填充数据;
’`package 线程不安全;
import java.util.HashMap;
import java.util.TreeMap;
public class TestHashMap {
// public static final HashMap firstHashMap=new HashMap();
public static final TreeMap firstHashMap=new TreeMap();
public static void main(String[] args) throws InterruptedException {
//线程一
Thread t1=new Thread(){
public void run() {
for(int i=0;i<25;i++){
firstHashMap.put(String.valueOf(i), String.valueOf(i));
}
}
};
//线程二
Thread t2=new Thread(){
public void run() {
for(int j=25;j<50;j++){
firstHashMap.put(String.valueOf(j), String.valueOf(j));
}
}
};
t1.start();
t2.start();
//主线程休眠1秒钟,以便t1和t2两个线程将firstHashMap填装完毕。
Thread.currentThread().sleep(1000);
for(int l=0;l<50;l++){
//如果key和value不同,说明在两个线程put的过程中出现异常。
if(!String.valueOf(l).equals(firstHashMap.get(String.valueOf(l)))){
System.err.println(String.valueOf(l)+":"+firstHashMap.get(String.valueOf(l)));
System.out.println("线程不安全!啊啊啊啊");
}else{
System.out.println("线程安全!");
}
}
}
}
结果:
TreeMap的put方法的底层:
public V put(K key, V value) {
Entry t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
}
分析:如HashMap一般,TreeMap的put方法中调用了Entry()方法,而且是新建Entry();
在Java里,线程安全一般体现在两个方面:
1、多个thread对同一个java实例的访问(read和modify)不会相互干扰,它主要体现在关键字synchronized。如ArrayList和Vector,HashMap和Hashtable
(后者每个方法前都有synchronized关键字)。如果你在interator一个List对象时,其它线程remove一个element,问题就出现了。
2、每个线程都有自己的字段,而不会在多个线程之间共享。它主要体现在java.lang.ThreadLocal类,而没有Java关键字支持,如像static、transient那样。
如果程序中有多个线程可能访问这些集合,就可以用Collections提供的类方法,它们可以把这些集合包装成线程安全的集合。例如:
Collection synchronizedCollection(Collection c):返回指定collection对应的线程安全的Collection。
Static List synchronizedList(List list):返回指定List对象对应的线程安全的List对象;
Static Set synchronizedSet(Set s):返回指定set对象对应的线程安全的Set对象;等方法;
例如:
1.
//使用Collection的synchronizedMap方法将一个普通的HashMap包装成线程安全的类
HashMap m=Collection.synchronizedMap(new HashMap());
2.
list list =Collections.synchronizedList(new ArrayList)来创建一个ArrayList对象。
参考资料:
http://blog.csdn.net/zhangxin961304090/article/details/46804065
http://blog.csdn.net/zhouxinhong/article/details/7361233
http://blog.csdn.net/micro_hz/article/details/51839246
http://blog.csdn.net/qq991029781/article/details/50930209