Java多线程编程——初探线程不安全的集合

在Java中,一些集合是线程不安全的,如ArrayList、HashSet、TreeSet、HashMap等。本文,我们以ArrayList为例探究线程不安全的现象与原因。

一、ArrayList追加元素的原理

首先我们节选出ArrayList中add方法的源码:

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return <tt>true</tt> (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

上述程序首先检查数组elementData是否开辟了足以存储size+1个元素的空间,之后在已存放数据末尾的后一个位置放置新元素e,并随之增加数组中已有元素数量多一个。第一步检查是否有足够空间的程序如下:

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_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);
}

/**
 * Increases the capacity to ensure that it can hold at least the
 * number of elements specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
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.5倍的新数组,然后将数据从旧数组拷贝到新数组中。

二、ArrayList线程不安全的体现

现在我们创建两个线程,同时分别向一个ArrayList的列表中追加元素,来分析出现的数据不一致的现象。具体程序如下:

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        ArrayList<Integer> list = new ArrayList<>();

        new ManipulateArrayList(list).start();
        new ManipulateArrayList(list).start();

        Thread.sleep(10000);

        for (int i = 0; i < list.size(); i++) {
            System.out.println("list[" + i + "]: " + list.get(i));
        }
    }
}

class ManipulateArrayList extends Thread {
    private ArrayList<Integer> list;

    public ManipulateArrayList(ArrayList<Integer> list) {
        this.list = list;
    }

    @Override
    public void run() {
        for (int i = 0; i < 500; i++) {
            list.add(i);
        }
    }
}

下面我们一一分析该程序可能出现的错误。

(一)数组越界异常

Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: 244
	at java.util.ArrayList.add(ArrayList.java:459)
	at ManipulateArrayList.run(Main.java:29)

1. 当前数组内已存入的元素个数为243。

2. 线程1先执行ensureCapacityInternal(size + 1)检查数组大小是否足够,其读取的size值为243。

3. 线程2开始执行ensureCapacityInternal方法进行容量判断,其读取的size值也为243。

4. 线程1发现数组开辟的空间大小为244,足够存放一个新元素,所以无需进行扩容操作。

5. 线程2发现数组开辟的空间大小为244,也足够在已有243个元素的基础上再添加一个新元素,所以也没有进行扩容操作。

6. 线程1执行elementData[size++] = e语句,即在下标为243的位置放置新元素,然后size加一变为244。

7. 线程2也执行elementData[size++] = e语句,此时读取的size为244,于是线程2试图在下标为244的位置存入元素e。然而,elementData数组开辟的空间仅为244,不足以放下第245个元素,于是程序抛出异常:无法在下标为244的位置插入元素,因为数组越界了!

(二)元素值为空以及数值被覆盖

......
list[103]: 103
list[104]: 104
list[105]: 0
list[106]: 105
list[107]: 106
list[108]: 1
list[109]: null
list[110]: 2
list[111]: 3
......

可以看到,数组中下标为109位置的元素为null。出现这种情况的原因是ArrayList的add方法不是原子操作。语句elementData[size++] = e事实上包含了两步操作:

1. elementData[size] = e

2. size++

在两个线程交替执行的情形下就有可能出现如下的问题:

1. 线程1读取到size为108,于是将e元素(即108)添加到了下标为108的位置上。

2. 线程2也读取到size为108,于是它也将它所要赋予的值(即1)放到下标108的位置上。

3. 线程1执行size++操作,size变成了109。

4. 线程2执行size++操作,size变成了110。

5. 线程2读取到size为110,于是将e元素(即2)添加到了下标为110的位置上。

不难看出,数组下标为109的位置没有被赋值,所以该元素依旧留空。此外,数组下标为108的位置被两次赋值,线程1先写入的元素被线程2覆盖了,于是线程1的写入就丢失了。

三、线程安全的CopyOnWriteArrayList集合

在java.util.concurrent下提供了线程安全的CopyOnWriteArrayList集合。我们修改上述程序,使用CopyOnWriteArrayList集合。

import java.util.concurrent.CopyOnWriteArrayList;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();

        new ManipulateArrayList(list).start();
        new ManipulateArrayList(list).start();

        Thread.sleep(10000);

        for (int i = 0; i < list.size(); i++) {
            System.out.println("list[" + i + "]: " + list.get(i));
        }
    }
}

class ManipulateArrayList extends Thread {
    private CopyOnWriteArrayList<Integer> list;

    public ManipulateArrayList(CopyOnWriteArrayList<Integer> list) {
        this.list = list;
    }

    @Override
    public void run() {
        for (int i = 0; i < 500; i++) {
            list.add(i);
        }
    }
}

此时,程序不会再出现各种数据不一致的现象。程序的运行结果如下:

list[0]: 0
list[1]: 1
list[2]: 2
list[3]: 3
......
list[254]: 250
list[255]: 251
list[256]: 4
list[257]: 5
......
list[319]: 66
list[320]: 67
list[321]: 253
list[322]: 254
......
list[945]: 446
list[946]: 499
list[947]: 447
......
list[998]: 498
list[999]: 499
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值