您的位置:首页 > 编程语言

并发编程(二):非线程安全集合类

2017-08-14 21:48 507 查看

前言

Java集合时所讲的ArrayList 、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都线程不安全的,当多个并发线程向这些集合中存取元素时,就可能会破坏这些集合的完整性。那么究竟是在什么情况下才会出现问题呢?
线程安全就是说多线程访问同一代码(对象、变量等),不会产生不确定的结果;


线程不安全的集合类

ArrayList:

package 线程不安全;

import java.util.ArrayList;
import java.util.List;

public class ArrayListInThread implements Runnable {

//线程不安全
private List<String> threadList = new ArrayList<String>();
//线程安全
//private List<String> threadList = Collections.synchronizedList(new ArrayList<String>());

@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异常。


LinkedList:

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的相关方法就会抛出异常。这是相对容易发现的由线程竞争造成的错误。

HashSet:

测试代码:

想要实现的效果:

创建两个线程,共享一个target,这样共享线程内的实例变量,输出结果应该是set中有5000个整数;

package 线程不安全;

import java.util.HashSet;
import java.util.Set;

public class TestHashSet implements Runnable{

// 实现Runnable 让该集合能被多个线程访问
Set<Integer> set = new HashSet<Integer>();
// 线程的执行就是插入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:

TreeSet 底层是通过 TreeMap 来实现的(如同HashSet底层是是通过HashMap来实现的一样);

HashMap:

测试Demo :两个线程同时往声明的hashmap中存储数据;线程安全下,所有的map的key==value。

package 线程不安全;

import java.util.HashMap;

public class TestHashMap {

public static final HashMap<String, String> firstHashMap=new HashMap<String, String>();

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<K,V> 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<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(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作为原始数组,这样也会有问题。

TreeMap:

测试Demo:如同HashMap一般,填充数据;

’`package 线程不安全;

import java.util.HashMap;
import java.util.TreeMap;

public class TestHashMap {

// public static final HashMap<String, String> firstHashMap=new HashMap<String, String>();
public static final TreeMap<String, String> firstHashMap=new TreeMap<String, String>();

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<K,V> 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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息