如何实现ArrayList的线程安全

本文介绍了三种实现ArrayList线程安全的方法:Vector、Collections.synchronizedList和CopyOnWriteArrayList,并对比了它们的性能。Vector通过方法同步实现;Collections.synchronizedList通过包装集合实现;CopyOnWriteArrayList采用写时复制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        ArrayList用的太多了,几乎所有人都知道它是线程不安全的,但实际使用中,我们的多线程实现,普遍都是基于一些同步方法或者锁,很多场景其实并不需要关注ArrayList本身的线程安全。网上可以找到三种主流的实现ArrayList线程安全的手段,他们分别是什么样的思路,还是值得简单了解和记录的。

Vector

        Vector 是矢量队列,它是JDK1.0版本添加的类,历史比ArrayList(since 1.2)更为悠久。其具体历史已不太可考,个人只能简单猜测是在java初期从其他语言直接借鉴的名字及相关概念。其底层和ArrayList一样是数组,除线程安全外,大多数实现方法和ArrayList逻辑基本一致。值得一提的是,从jdk1.2以后,Java提供了系统的集合框架,就将Vector改为实现List接口,从而导致Vector里有一些重复的方法,例如:addElement(Object obj),实际上这个方法和add(Object obj)没什么区别。

        Vector 的实现,就是在方法上都加上synchronized(即使get也不例外)。康康部分源码。

public synchronized E get(int index) {
	if (index >= elementCount)
		throw new ArrayIndexOutOfBoundsException(index);
	return elementData(index);
}

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

关于Vector为什么被弃用

  1. 所有方法都有同步开销,非多线程下,效率不如ArrayList;
  2. 一些老代码,导致有重复的方法,以及风格和新的集合类格格不入;
  3. 线程安全的实现,可以通过新的Collections.synchronizedList之类的调用来替换。

Collections.synchronizedList

        基于集合类的实现,可以简单的使用函数来操作,例如,List<Integer> list2 = Collections.synchronizedList(new ArrayList<Integer>())。先看看函数的定义。

public static <T> List<T> synchronizedList(List<T> list) {
    return (list instanceof RandomAccess ?
    	new Collections.SynchronizedRandomAccessList<>(list) :
    	new Collections.SynchronizedList<>(list));
}

        显然,里面还藏了一些list类,我们以SynchronizedRandomAccessList接着看看这个类是如何操作的。

//Collections中的静态类1号,主要实现在SynchronizedList中
static class SynchronizedRandomAccessList<E>
	extends Collections.SynchronizedList<E>
    implements RandomAccess {
        ... //绝大部分方法都没有单独的实现
}
//Collections中的静态类2号
static class SynchronizedList<E>
        extends Collections.SynchronizedCollection<E>
        implements List<E> {
    private static final long serialVersionUID = -7754090372962971524L;

    final List<E> list;

		...

    //可以看到,大部分方法的处理方式,是多了一个synchronized (mutex),
    //mutex是SynchronizedCollection中的一个Object变量
    public boolean equals(Object o) {
        if (this == o)
            return true;
        synchronized (mutex) {
            return list.equals(o);
        }
    }

    public int hashCode() {
        synchronized (mutex) {
            return list.hashCode();
        }
    }

    public E get(int index) {
        synchronized (mutex) {
            return list.get(index);
        }
    }

    public E set(int index, E element) {
        synchronized (mutex) {
            return list.set(index, element);
        }
    }

    public void add(int index, E element) {
        synchronized (mutex) {
            list.add(index, element);
        }
    }

    public E remove(int index) {
        synchronized (mutex) {
            return list.remove(index);
        }
    }

    public int indexOf(Object o) {
        synchronized (mutex) {
            return list.indexOf(o);
        }
    }

    public int lastIndexOf(Object o) {
        synchronized (mutex) {
            return list.lastIndexOf(o);
        }
    }

    public boolean addAll(int index, Collection<? extends E> c) {
        synchronized (mutex) {
            return list.addAll(index, c);
        }
    }

...

    //官方注释明确提出,对于使用 Iterator遍历列表时,Collections.synchronizedList可能发生错误
    //还需要手动去确保线程安全
    public ListIterator<E> listIterator() {
        return list.listIterator(); // Must be manually synched by user
    }

    public ListIterator<E> listIterator(int index) {
        return list.listIterator(index); // Must be manually synched by user
    }
}

CopyOnWriteArrayList

        CopyOnWriteArrayList是1.5后引入,属于JUC的一部分。他基本的原理还是和ArrayList一样,涉及线程安全的部分,是通过写时复制的方式来实现(从名字中就可以看出)。它内部有个volatile数组来保持数据。在“添加/修改/删除”数据时,会先获取互斥锁,再新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给volatile数组,然后再释放互斥锁。以get和set为例看看代码。

public class CopyOnWriteArrayList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;

    //互斥锁
    final transient ReentrantLock lock = new ReentrantLock();

    //存储数据的volatile数组,入口仅限于类中函数getArray/setArray。
    private transient volatile Object[] array;

    public E get(int index) {
        return get(getArray(), index);
    }

    public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            E oldValue = get(elements, index);

            if (oldValue != element) {
                int len = elements.length;
				//直接开始复制一格数组,修改某个值,将型数组赋值给CopyOnWriteArrayList中的array
                Object[] newElements = Arrays.copyOf(elements, len);
                newElements[index] = element;
                setArray(newElements);
            } else {
                // Not quite a no-op; ensures volatile write semantics
                // 官方介绍为,并非无效的操作,是为了保证volatile的写语义。
                // 这里有点搞心态,翻译一下参考文档6,按照其中一个回答,这里的代码jdk 8中存在,jdk 11中已经移除。但我在jdk 14中又再次发现。
                // 参考热门回答,主要是为了确保一些逻辑中CopyOnWriteArrayList外的非 volatile变量,由于指令重排导致的执行顺序问题。
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }
}

因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。

性能对比

        我尝试的代码主要来自参考文档4,就不再贴出来了。

CopyOnWriteArrayList add method cost time is 17156
Collections.synchronizedList add method cost time is 23
Vector add method cost time is 5
CopyOnWriteArrayList get method cost time is 2
Collections.synchronizedList get method cost time is 4
Vector get method cost time is 5
---- 10万级数据

CopyOnWriteArrayList add method cost time is 133
Collections.synchronizedList add method cost time is 1
Vector add method cost time is 0
CopyOnWriteArrayList get method cost time is 0
Collections.synchronizedList get method cost time is 0
Vector get method cost time is 1
---- 万级数据

        以上代码又多跑了几次,大同小异。

        结论:

  1. 写时CopyOnWriteArrayList性能较差,且随着数据量的增大,几何级下跌。读操作各方式基本没有区别。
  2. CopyOnWriteArrayList,适用于以读为主,读操作远远大于写操作的场景中使用,比如缓存。
  3. Collections.synchronizedList则可以用在CopyOnWriteArrayList不适用,但是有需要同步的地方使用, 比如读写操作都比较均匀的地方。
  4. 不得不含泪承认,从简单的几次跑数中,Vector的读写都很优秀。但既然已经不建议使用,就忘了它吧。

参考文档

1. Java集合(五)应该弃用的Vector和Stack,Java集合(五)应该弃用的Vector和Stack_weixin_33695082的博客-CSDN博客

2. vector过时的代替建议,vector过时的代替建议_忧伤的可乐鸡-CSDN博客_vector的替代

3. 简单理解Collections.synchronizedList,简单理解Collections.synchronizedList_walker-CSDN博客_collections.synchronizedlist

4. CopyOnWriteArrayList与Collections.synchronizedList的性能对比,https://blog.csdn.net/yangzl2008/article/details/39456817

5. Java多线程系列--“JUC集合”02之 CopyOnWriteArrayList,Java多线程系列--“JUC集合”02之 CopyOnWriteArrayList - 如果天空不死 - 博客园

6. Why setArray() method call required in CopyOnWriteArrayList,java - Why setArray() method call required in CopyOnWriteArrayList - Stack Overflow

ArrayList本身是线程不安全的数据结构。这意味着当多个线程同时访问和修改ArrayList时,可能会导致数据不一致或者发生其他的并发问题。然而,我们可以采取一些措施来使ArrayList变得线程安全。以下是几种常见的线程安全ArrayList实现方式: 1. 使用Collections类的synchronizedList方法:通过使用Collections类的synchronizedList方法,可以将ArrayList转换为线程安全的List。这个方法返回一个线程安全的List,它会在每个公共方法上加锁,从而保证多线程访问的安全性。但是,这种方式会造成性能的下降,因为它会对整个List进行加锁,导致多个线程不能并发执行。 2. 使用CopyOnWriteArrayList类:CopyOnWriteArrayListJava.util.concurrent包下提供的线程安全ArrayList实现。它的特点是,在进行写操作(如添加、删除元素)时,会创建一个新的数组,并将原来的数组复制过去,然后再进行修改。这样可以保证在写操作期间,其他线程可以继续读取原来的数组而不会出现问题。然而,由于写操作需要复制整个数组,因此性能相对较低。适用于读操作频繁,写操作较少的场景。 3. 使用线程安全的List实现:除了CopyOnWriteArrayList,还可以使用其他实现线程安全的List接口的类,如Vector和ConcurrentLinkedArrayList。它们都是线程安全的,但在性能和用法上可能会有所不同。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值