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

来源:互联网 发布:网络电视直播pc版 编辑:程序博客网 时间:2024/06/09 17:13

前言

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) + "  ");        }    }}

结果一:

9null  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

阅读全文
1 0
原创粉丝点击