IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> ArrayList线程安全问题以及解决方案 -> 正文阅读

[Java知识库]ArrayList线程安全问题以及解决方案

1. 多线程下的ArrayList

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

        Runnable runnable = () -> {
            for (int i = 0; i < 10000; i++) {
                list.add(i);
            }
        };
        
        for (int i = 0; i < 2; i++) {
            new Thread(runnable).start();
        }
        
        Thread.sleep(500);
        System.out.println(list.size());
    }
}

上述运行可能会出现
代码中循环创建了两个线程,这两个线程都执行10000次数组的添加操作,理论上最后输出的结果应该为20000,但经过多次尝试,最后只出现了两种结果:

1.数组索引越界异常
2.list.size()不为20000

看一下add()方法的代码:

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

产生了null值 以及数据丢失的问题

数组越界情况. 我们将上方的线程运行图更新下进行演示:

在这里插入图片描述

前提条件: 当前size=2 数组长度为2.

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

核心问题就在于 elementData[size++] = e;不是原子操作

2.线程安全的list解决方案:

有三种方式:

  1. vector 在add方法上加上synchronized锁

  2. Collections的静态方法synchronizedList(List< T> list)

  3. copyonwritearraylist写时复制的思想

1.使用Vector容器

Vector类实现了可扩展的对象数组,并且它是线程安全的。Vector的解决方案很简单,它采用了同步关键词synchronized修饰方法。

public void add(int index, E element) {
    insertElementAt(element, index);
}
...
// 使用了synchronized关键词修饰
public synchronized void insertElementAt(E obj, int index) {
        modCount++;
        if (index > elementCount) {
            throw new ArrayIndexOutOfBoundsException(index
                                                     + " > " + elementCount);
        }
        ensureCapacityHelper(elementCount + 1);
        System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);
        elementData[index] = obj;
        elementCount++;
    }

可以看出,Vector在通用方法的实现上ArrayList并没有什么区别(这里不比较扩容方式等细节)

2. Collections.synchronizedList(List< T> list)

/**
     * Returns a synchronized (thread-safe) list backed by the specified
     * list.  In order to guarantee serial access, it is critical that
     * <strong>all</strong> access to the backing list is accomplished
     * through the returned list.<p>
     *
     * It is imperative that the user manually synchronize on the returned
     * list when iterating over it:
     * <pre>
     *  List list = Collections.synchronizedList(new ArrayList());
     *      ...
     *  synchronized (list) {
     *      Iterator i = list.iterator(); // Must be in synchronized block
     *      while (i.hasNext())
     *          foo(i.next());
     *  }
     * </pre>
     * Failure to follow this advice may result in non-deterministic behavior.
     *
     * <p>The returned list will be serializable if the specified list is
     * serializable.
     *
     * @param  <T> the class of the objects in the list
     * @param  list the list to be "wrapped" in a synchronized list.
     * @return a synchronized view of the specified list.
     */
    public static <T> List<T> synchronizedList(List<T> list) {
        return (list instanceof RandomAccess ?
                new SynchronizedRandomAccessList<>(list) :
                new SynchronizedList<>(list));
    }

因为ArrayList实现了RandomAccess接口,因此该方法返回一个SynchronizedRandomAccessList实例。
该类的add实现:

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

其中,mutex是final修饰的一个对象:

final Object mutex;

我们可以看到,这种线程安全容器是通过同步代码块来实现的,基础的add方法任然是由ArrayList实现。

我们再来看看它的读方法:

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

和写方法没什么区别,同样是使用了同步代码块。线程同步的实现原理非常简单!

通过上面的分析可以看出,无论是读操作还是写操作,它都会进行加锁,当线程的并发级别非常高时就会浪费掉大量的资源,因此某些情况下它并不是一个好的选择。针对这个问题,我们引出第三种线程安全容器的实现。

3. CopyOnWriteArrayList

CopyOnwriteArrayList 实现了 读 读操作和 读写操作不互斥

实现

public boolean add(E e) {
    final ReentrantLock lock = this.lock
    lock.lock();
    try{
        Object[] elements = getArray();
        int len = elements.length;
        //复制数组
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //赋值
        newElements[len] = e;
        //再把引用变为 这个新复制的数组 也就是覆盖原数组
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
    
}

可以看到是通过JUC包下的lock来实现线程间的同步的,

怎么实现读写不互斥的呢?

在面临写操作的时候,CopyOnwriteArrayList会先复制原来的数组并且在新数组上进行修改,最后再将原数组覆盖。如果写操作过程中发生了线程切换。并且切换到读线程,因为此时数组并未发生覆盖,读操作读取的还是原数组。

另外,数组定义private transient volatile Object[] array,其中采用volatile修饰,保证内存可见性,读取线程可以马上知道这个修改。

private transient volatile Object[] array;

也就是说当读写并发时 读操作是在旧数组中 读到的旧值

三种方式的性能比较

1. 首先我们来看看三种方式在写操作的情况:

public class ConcurrentList {
    public static void main(String[] args) {
        testVector();
        testSynchronizedList();
        testCopyOnWriteArrayList();
    }

    private static void testVector() {

        Vector vector = new Vector();
        long time1 = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            vector.add(i);
        }
        long time2 = System.currentTimeMillis();
        System.out.println("vector: "+(time2-time1));

    }

    public static void testSynchronizedList(){
        List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());
        long time1 = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            list.add(i);
        }
        long time2 = System.currentTimeMillis();
        System.out.println("synchronizedList: "+(time2-time1));
    }

    public static void testCopyOnWriteArrayList(){
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        long time1 = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            list.add(i);
        }
        long time2 = System.currentTimeMillis();
        System.out.println("copyOnWriteArrayList: "+(time2-time1));
    }

}

在代码中我让Vector和SynchronizedList两种实现方式进行写操作10000000次,而CopyOnWriteArrayList仅仅只有100000次,与前两种方式少了100倍!但是耗时缺差不多

在这里插入图片描述

第三种方式使用的时间远大于前两种,写操作越多,时间差就越明显。

看似出乎意料,实则意料之中,copyOnWriteArrayList每进行一次写操作都会复制一次数组,这是非常耗时的操作,因此在面临巨大的写操作量时才会差异这么大。

而对于 vector和SynchronizedList的性能上的差别 主要是由于synchronized加载实例方法和加载代码块上的差别引起的,同步方法直接在方法上加synchronized实现加锁,同步代码块则在方法内部加锁,很明显,同步方法锁的范围比较大,而同步代码块范围要小点,一般同步的范围越大,性能就越差,一般需要加锁进行同步的时候,肯定是范围越小越好,这样性能更好。

如果开两个线程的情况下

public class ConcurrentList {
    public static void main(String[] args) throws InterruptedException {
        testVector();
        testSynchronizedList();
        testCopyOnWriteArrayList();
    }

    private static void testVector() throws InterruptedException {
        Vector vector = new Vector();
        Semaphore semaphore = new Semaphore(0);
        long time1 = System.currentTimeMillis();

        Runnable runnable = new Runnable() {
            @Override
            public void run() {

                for (int i = 0; i < 10000000; i++) {
                    vector.add(i);
                }
                semaphore.release();

            }
        };

        for (int i = 0; i < 2; i++) {
            new Thread(runnable).start();
        }
        semaphore.acquire(2);
        long time2 = System.currentTimeMillis();
        System.out.println("vector: "+(time2-time1));

    }

    public static void testSynchronizedList() throws InterruptedException {
        List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());
        Semaphore semaphore = new Semaphore(0);
        long time1 = System.currentTimeMillis();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000000; i++) {
                    list.add(i);
                }
                semaphore.release();
            }
        };
        for (int i = 0; i < 2; i++) {
            new Thread(runnable).start();
        }
        semaphore.acquire(2);
        long time2 = System.currentTimeMillis();
        System.out.println("synchronizedList: "+(time2-time1));
    }

    public static void testCopyOnWriteArrayList() throws InterruptedException {
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        Semaphore semaphore = new Semaphore(0);
        long time1 = System.currentTimeMillis();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++) {
                    list.add(i);
                }
                semaphore.release();
            }
        };
        for (int i = 0; i < 2; i++) {
            new Thread(runnable).start();
        }
        semaphore.acquire(2);
        long time2 = System.currentTimeMillis();
        System.out.println("copyOnWriteArrayList: "+(time2-time1));
    }

}

vector和synchorinzedList性能差不多

在这里插入图片描述

2. 我们再来看看三种方式在读操作的情况:

public class ConcurrentList2 {
    public static void main(String[] args) throws InterruptedException {
        testVector();
        testSynchronizedList();
        testCopyOnWriteArrayList();
    }

    public static void testVector() throws InterruptedException {
        Vector<Integer> vector = new Vector<>();
        vector.add(0);
        Semaphore semaphore = new Semaphore(0);
        long time1 = System.currentTimeMillis();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000000; i++) {
                    vector.get(0);
                }
                semaphore.release(1);
            }
        };
        for (int i = 0; i < 2; i++) {
            new Thread(runnable).start();
        }
        semaphore.acquire(2);
        long time2 = System.currentTimeMillis();
        System.out.println("vector: "+(time2-time1));
    }

    public static void testSynchronizedList() throws InterruptedException {
        List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());
        list.add(0);
        Semaphore semaphore = new Semaphore(0);
        long time1 = System.currentTimeMillis();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000000; i++) {
                    list.get(0);
                }
                semaphore.release(1);
            }
        };
        for (int i = 0; i < 2; i++) {
            new Thread(runnable).start();
        }
        semaphore.acquire(2);
        long time2 = System.currentTimeMillis();
        System.out.println("synchronizedList: "+(time2-time1));
    }

    public static void testCopyOnWriteArrayList() throws InterruptedException {
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        list.add(0);
        Semaphore semaphore = new Semaphore(0);
        long time1 = System.currentTimeMillis();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000000; i++) {
                    list.get(0);
                }
                semaphore.release(1);
            }
        };
        for (int i = 0; i < 2; i++) {
            new Thread(runnable).start();
        }
        semaphore.acquire(2);
        long time2 = System.currentTimeMillis();
        System.out.println("copyOnWriteArrayList: "+(time2-time1));
    }
}

在这里插入图片描述

测试方式不严谨哈 读写并发就不测了

可以看出 CopyOnWriteArrayList由于读不加锁 在读的性能上 非常好

总结

  1. 获取线程安全的List我们可以通过Vector、Collections.synchronizedList()方法和CopyOnWriteArrayList三种方式
  2. 得益于读写不冲突,以及读不加锁,在读多写少的情况下,推荐使用CopyOnWriteArrayList方式
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-04-24 09:15:04  更:2022-04-24 09:17:56 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 2:33:17-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码