ArrayList 线程安全问题

版权声明:欢迎转载,转载请说明出处. 大数据Github项目地址https://github.com/SeanYanxml/bigdata。 https://blog.csdn.net/u010416101/article/details/88720974

前言

在观看多线程书籍的时候,经常会看到大家提及ArrayListHashMap时候.皆会说明, ArrayListHashMap类型都不是线程安全的. 那么,在传统的集合包内的集合类到底为什么线程非安全呢?在新的JUC包类又有什么可以替代呢? 由于篇幅问题, 我们将这个问题分成两章进行分析.本章中, 我们主要分析下ArrayList的部分.

本章主要包括如下几个部分:

  • 为什么ArrayList是线程非安全的?
  • 替代措施(Vector类 / Colletions封装 / JUC类)

ArrayList 与 非线程安全

ArrayList线程不安全详解

我们先执行如下例子:

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

/**
 * ArrayList的非线程安全演示.
 * 
 * */
class UnsafeArrayListThread extends Thread{
	public void run(){
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		UnsafeArrayList.arrayList.add(Thread.currentThread().getName()+" "+System.currentTimeMillis());
	}
}
public class UnsafeArrayList {
	public static List arrayList = new ArrayList();
	public static void main(String[] args) {
		Thread []threadArray = new Thread[1000];
		for(int i=0;i<threadArray.length;i++){
			threadArray[i] = new UnsafeArrayListThread();
			threadArray[i].start();
			
		}
		
		for(int i=0;i<threadArray.length;i++){
			try {
				threadArray[i].join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}			
		}
		
		for(int i=0;i<arrayList.size();i++){
			System.out.println(arrayList.get(i));
		}
		
	}
}

在输出时,我们会遇到这样的几种情况:

  1. 输出值为null;
  2. 数组越界异常;
  3. 某些线程没有输出值;
1. 输出值为null;
//null
//Thread-11553158258958
//Thread-51553158258958
//Thread-71553158258958
//Thread-61553158258959

3. 某些线程没有输出值;
// 某些输出缺少.

2. 数组越界异常;
//Exception in thread "Thread-874" java.lang.ArrayIndexOutOfBoundsException: 823
//at java.util.ArrayList.add(ArrayList.java:441)
//at com.yanxml.multithreading.art.collection.UnsafeArrayListThread.run(UnsafeArrayList.java:17)

这些都是在多线程中使用ArrayList类所会遇到的问题.下面我们分别根据上面进行一一解答:

我们看下ArrayList的源码

	
    public boolean add(E e) {
    	// 确保ArrayList的长度足够
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // ArrayList加入
        elementData[size++] = e;
        return true;
    }
    
	private void ensureCapacityInternal(int minCapacity) {
        if (elementData == EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }
    
	private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
    // 如果超过界限 数组长度增长
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

在上述过程中,会出问题的部分在于: 1. 增加元素 2. 扩充数组长度;

增加元素过程中较为容易出现问题的部分在于elementData[size++] = e;.赋值的过程可以分为两个步骤elementData[size] = e;size++;

我们分别使用两个线程来模拟插入过程.例如有两个线程,分别加入数字1与2.

在这里插入图片描述
运行的过程如下所示:

  1. 线程1 赋值 element[1] = 1; 随后因为时间片用完而中断;
  2. 线程2 赋值 element[1] = 2; 随后因为时间片用完中断;
  • 此处导致了之前所说的一个问题(有的线程没有输出); 因为后续的线程将前面的线程的值覆盖了.
  1. 线程1 自增 size++; (size=2)
  2. 线程2 自增 size++; (size=3)
  • 此处导致了某些值为null的问题.因为原size=1, 但是因为线程1与线程2都将值赋值给了element[1],导致了element[2]内没有值,被跳过了.指针index指向了3.所以,导致了某些情况下值为null的情况.

数组越界情况. 我们将上方的线程运行图更新下进行演示:
在这里插入图片描述
前提条件: 当前size=2 数组长度为2.

  1. 线程1 判断数组是否越界.因为size=2 长度为2,没有越界.将进行赋值操作.但是因为时间片问题导致了中断.
  2. 线程2 判断数组是否越界.因为size=2 长度为2,没有越界.将进行赋值操作.但是因为时间片问题导致了中断.
  3. 线程1 重新获取到主动权.上文判断了长度刚刚好够用.进行赋值操作element[size]=1,并且size++
  4. 线程2 因为上文判断了数组没有越界.所以进行赋值操作.但是此时的size=3了.再执行element[3]=2. 导致了数组越界了.
  • 由此处可以看出因为数组的当前指向size并未进行加锁的操作,导致了数组越界的情况出现.

所以, ArrayList类是非线程安全的类!


解决措施

  • 使用Vector类进行替换.
    将上文的public static List arrayList = new ArrayList();替换为public static List arrayList = new Vector<>();

原理:使用了synchronized关键字进行了加锁处理.

public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
 }
  • 使用Collections.synchronizedList(List)进行替换
    public static List arrayList = Collections.synchronizedList(new ArrayList());
    原理: 使用mutex锁进行维护处理.
# 转化方法如下
    public static <T> List<T> synchronizedList(List<T> list) {
        return (list instanceof RandomAccess ?
                new SynchronizedRandomAccessList<>(list) :
                new SynchronizedList<>(list));
    }
# SynchronizedList类如下
    static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
        	// 其中的add方法如下
        	public void add(int index, E element) {
            synchronized (mutex) {list.add(index, element);}
        	}
        
        }
  • 使用JUC中的CopyOnWriteArrayList类进行替换.(具体见JUC的集合框架篇)

后记

我们放上集合类与安全性的比较图.
在这里插入图片描述


Reference

[1]. ArrayList线程不安全详解
[2]. Java中 Vector的使用详解
[3]. Set、List、Map线程安全问题
[4]. Set与线程安全

猜你喜欢

转载自blog.csdn.net/u010416101/article/details/88720974