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

前言

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

线程不安全的集合类

ArrayList:

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  

结果二:
并发编程(二):非线程安全集合类_第1张图片

抛出异常: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 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());
    }
}

结果一:
并发编程(二):非线程安全集合类_第2张图片
结果二:
并发编程(二):非线程安全集合类_第3张图片

现象:好多结果都不是预想的5000;

分析原因:
打印结果大部分出现大于5000的情况。这就出现了之前提到的情况,证明了HashSet不是线程安全的类。 其实查看源代码发现HashSet内部维护数据的采用的是HashMap,根本原因是HashMap不是线程安全的类。导致了HashSet的非线程安全。

并发编程(二):非线程安全集合类_第4张图片

TreeSet:

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

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

        }


}

结果:
经过多次测试后,发现如图:

并发编程(二):非线程安全集合类_第5张图片

分析:
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作为原始数组,这样也会有问题。

TreeMap:

测试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("线程安全!");
                }
            }

        }


}

结果:

并发编程(二):非线程安全集合类_第6张图片

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

你可能感兴趣的:(java-并发编程,线程安全,并发,编程,arraylist,JAVA)