JUC并发编程基础学习之List集合线程安全问题

前言

之前我们在使用ArrayList的时候,觉得它并没有不安全,是因为我们是在单线程环境下使用的,如果在多线程环境下,那么ArrayList就不够安全了!

1.1 测试List集合是否安全

1.单线程下测试ArrayList集合
1-1 源码分析
package java.util;

/**
 * @author  Josh Bloch
 * @author  Neal Gafter
 * @see     Collection
 * @see     List
 * @see     LinkedList
 * @see     Vector
 * @since   1.2 ArrayList从JDK1.2版本开始使用 
 */
//ArrayList集合
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

    /**
     * 在当前list集合的尾部追加指定的元素
     *
     * @param 被追加到该list集合中的一个元素
     * @return 布尔型值true
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    /**
     * 在当前list集合的尾部追加指定的元素
     *
     * @param e 被追加到该list集合中的一个元素
     * @return 布尔型值true
     */
    public void add(int index, E e) {
        rangeCheckForAdd(index);
        checkForComodification();
        parent.add(parentOffset + index, e);
        this.modCount = parent.modCount;
        this.size++;
    }
}
1-2 测试代码
package com.kuang.unsafe;

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * @ClassName ListTest
 * @Description List安全性测试
 * @Author 狂奔の蜗牛rz
 * @Date 2021/7/29
 */
public class ListTest {

    public static void main(String[] args) {

        //1.使用新型语法方式
        //获取一个ArrayList
        List<String> list = Arrays.asList("1","2","3");
        //打印输出数组集合(forEach底层是一个函数式接口)
        list.forEach(System.out::println);
        
}
1-3 测试结果

在这里插入图片描述

结果执行成功,没有出现异常!

2.多线程下测试ArrayList集合
2-1 测试代码
package com.kuang.unsafe;

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * @ClassName ListTest
 * @Description List安全性测试
 * @Author 狂奔の蜗牛rz
 * @Date 2021/7/29
 */
public class ListTest {

    public static void main(String[] args) {

        //2.使用传统的语法方式
        //获取ArrayList集合
       List<String> list = new ArrayList<>();
        //使用for循环模拟多线程环境
        for (int i = 1; i <= 10; i++) {
            //使用Lambda表达式创建和启动线程
            new Thread(()->{
                /**
                 * 将字符串添加到list集合中
                 * 使用UUID.randomUUID().toString()获取UUID随机字符串
                 * 使用substring(0,5),返回的字符串中下标索引从0到5的子字符串
                 */
                list.add(UUID.randomUUID().toString().substring(0,5));
                //打印输出list集合
                System.out.println(list);
            //String.valueOf(i)表示获取下标为i的字符串
            },String.valueOf(i)).start();
        }
    }
}
2-2 测试结果

在这里插入图片描述

结果出现异常报错:java.util.ConcurrentModificationException(并发修改异常)!

测试结论并发情况下,ArrayList线程不安全

1.2 解决List线程不安全问题

1.将ArrayList替换为Vector集合

2.使用Collections集合工具类的synchronizedList方法

3.使用CopyOnWriteArrayList(写时复制数组集合)

1.使用Vector集合
1-1 源码分析
package java.util;

/**
 * @author  Lee Boynton
 * @author  Jonathan Payne
 * @see Collection
 * @see LinkedList
 * @since JDK1.0 Vector从JDK1.0就开始使用
 */
public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{    

   // ...(省略前面部分代码)...
   
	/**
     * 追加指定的元素到该Vector集合尾部
     *
     * @param e 被追加到当前Vector集合中的一个元素
     * @return 布尔型值true
     * @since 1.2
     */
	//使用synchronized同步锁修饰add方法,保证线程安全
    public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

    // ...(省略后面部分代码)...

}
  • 测试代码
package com.kuang.unsafe;

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * @ClassName ListTest
 * @Description List安全性测试
 * @Author 狂奔の蜗牛rz
 * @Date 2021/7/29
 */
public class ListTest {

    public static void main(String[] args) {

        //2.使用传统方式
        //获取ArrayList集合
//        List<String> list = new ArrayList<>();
        //方案1: 使用Vector集合
        List<String> list = new Vector<>();
        //使用for循环模拟多线程环境
        for (int i = 1; i <= 10; i++) {
            //使用Lambda表达式创建和启动线程
            new Thread(()->{
                /**
                 * 将字符串添加到list集合中
                 * 使用UUID.randomUUID().toString()获取UUID随机字符串
                 * 使用substring(0,5),返回的字符串中下标索引从0到5的子字符串
                 */
                list.add(UUID.randomUUID().toString().substring(0,5));
                //打印输出list集合
                System.out.println(list);
            //String.valueOf(i)表示获取下标为i的字符串
            },String.valueOf(i)).start();
        }
    }
}
1-2 测试结果

在这里插入图片描述

结果执行成功,没有出现异常!

2.使用Collections工具类的synchronizedList方法
2-1 源码分析

查看Collections工具类部分相关源码:

package java.util;

/**
 *
 * @author  Josh Bloch
 * @author  Neal Gafter
 * @see     Collection
 * @see     Set
 * @see     List
 * @see     Map
 * @since   1.2
 */
//Collections工具类
public class Collections {

    // 抑制默认的构造函数, 确保其非实例化
    private Collections() {
    }

   // ...(省略前面部分代码)...

    /**
     * 如果定义的集合是序列化的,返回的集合也将会序列化
     * @param  <T> list集合中的Object类
     * @param  list list集合被包装在同步的list集合中
     * @return 指定集合的同步视图
     */
    public static <T> List<T> synchronizedList(List<T> list) {
        /**
         * 返回值是使用三元运算符来获取
         * list instanceof RandomAccess ?
                new SynchronizedRandomAccessList<>(list) :
                new SynchronizedList<>(list)
         * 判断list对象是否是RandomAccess(随机访问接口)的实例,若是,则创建一个SynchronizedRandomAccessList对象(同步随机访问集合), 否则将创建一个同步集合SynchronizedList, 二者的参数都是list集合
         */
        return (list instanceof RandomAccess ?
                new SynchronizedRandomAccessList<>(list) :
                new SynchronizedList<>(list));
    }
    
    // ...(省略后面部分代码)...
    
}

instanceof定义

instanceof是Java的一个二元操作符,类似于 ==,>,< 等操作符。
instanceof是Java的保留关键字。

作用:测试它左边的对象是否是它右边的类的实例,返回boolean的数据类型。

查看RandomAccess接口的源码:

package java.util;

/**
 * @since 1.4
 */
//随机访问接口
public interface RandomAccess {
}
  • RandomAccess接口是一个标志接口,实现了该接口的List集合,可以支持快速随机访问
  • 如果实现了该接口的List集合,使用for循环的方式获取数据将优于迭代器获取:
    使用for循环形式
    for (int i=0, n=list.size(); i &lt; n; i++)
    list.get(i);
    
    使用迭代器形式
    for (Iterator i=list.iterator(); i.hasNext(); )
    i.next();
    
2-2 测试代码
package com.kuang.unsafe;

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * @ClassName ListTest
 * @Description List安全性测试
 * @Author 狂奔の蜗牛rz
 * @Date 2021/7/29
 */
public class ListTest {

    public static void main(String[] args) {

        //2.使用传统方式
        //获取ArrayList集合
//        List<String> list = new ArrayList<>();
        //方案2: 使用Collections集合工具类的synchronizedList方法
	    List<String> list = Collections.synchronizedList(new ArrayList<>());
        //使用for循环模拟多线程环境
        for (int i = 1; i <= 10; i++) {
            //使用Lambda表达式创建和启动线程
            new Thread(()->{
                /**
                 * 将字符串添加到list集合中
                 * 使用UUID.randomUUID().toString()获取UUID随机字符串
                 * 使用substring(0,5),返回的字符串中下标索引从0到5的子字符串
                 */
                list.add(UUID.randomUUID().toString().substring(0,5));
                //打印输出list集合
                System.out.println(list);
            //String.valueOf(i)表示获取下标为i的字符串
            },String.valueOf(i)).start();
        }
    }
}
2-3 测试结果

在这里插入图片描述

结果执行成功,没有出现异常!

3.CopyOnWriteArrayList简单了解
3-1 什么是CopyOnWriteArrayList?

CopyOnWriteArrayList:顾名思义,写入时进行复制

在这里插入图片描述

3-2 什么是写入时复制?

写入时复制(全称为Copy On Write,简称为COW),是计算机程序设计领域的一种优化策略;

核心思想是:如果有多个调用者同时请求相同资源(如内存或磁盘上的数据存储),它们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的。

此作法主要的优点是如果调用者没有修改该资源,就不会有副本被创建,因此多个调用者只是读取操作时可以共享同一份资源。

简而言之,就是读操作直接在正本上进行,一旦有写操作,就复制一份副本出来,并在副本上做修改

3-3 CopyOnWriteArrayList的简单使用介绍

在这里插入图片描述

4.使用CopyOnWriteArrayList集合
4-1 源码分析
package java.util.concurrent;

/**
 * Java集合框架
 * @since 1.5
 * @author Doug Lea
 * @param <E> 用于容纳这个collection集合的元素类型
 */
//CopyOnWriteArrayList(写时复制数组集合), 实现了List<E>(List集合),RandomAccess(快速访问)接口和序列化接口
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    //私有静态其最终的连续版本UID
    private static final long serialVersionUID = 8673264195747942595L;
      
    /** 
     * lock锁保护所有的调整器
     * 我们发现它使用transient来修饰lock锁:
     * 简单理解:将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会被序列化
     */
    final transient ReentrantLock lock = new ReentrantLock();

   /** 
    * 数组, 只能通过getArray或者setArray方法获取 
    */
    private transient volatile Object[] array;

   /** 
    * 获取数组. 非私有的也可以通过CopyOnWriteArraySet类访问
    */
    final Object[] getArray() {
        return array;
    }

    /**
     * 设置数组
     */
    final void setArray(Object[] a) {
        array = a;
    }

    /**
     * 无参构造方法
     * 创建一个空list集合
     */
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }
    
   // ...(中间省略部分代码)...
        
    /**
     * 在集合后面追加指定元素
     *
     * @param e 被追加到当前集合的指定元素
     * @return 布尔型值true
     */
    public boolean add(E e) {
        //获取ReentrantLock(重入锁)
        final ReentrantLock lock = this.lock;
        //首先上锁
        //lock作用:使得不会进行覆盖,复制时写入是防止遍历时出现异常
        lock.lock();
        //执行添加操作的业务代码
        try {
            //调用getArray方法获取元素数组elements
            Object[] elements = getArray();
		   //设置数组长度	
            int len = elements.length;
            //复制一份原数组(elements)元素到新数组(newElements)中
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //新数组中的要被追加的指定元素
            newElements[len] = e;
            //设置数组为newElements
            setArray(newElements);
            //返回真值
            return true;
        } finally {
            //最后解锁
            lock.unlock();
        }
    }    
    
   // ...(后面省略部分代码)...
        
}

transient关键字使用总结

1、transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。
2、被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。
3、一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。也可以认为在将持久化的对象反序列化后,被transient修饰的变量将按照普通类成员变量一样被初始化。

4-2 测试代码
package com.kuang.unsafe;

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * @ClassName ListTest
 * @Description List安全性测试
 * @Author 狂奔の蜗牛rz
 * @Date 2021/7/29
 */
public class ListTest {

    public static void main(String[] args) {

        //2.使用传统方式
        //获取ArrayList集合
//        List<String> list = new ArrayList<>();
        //方案3: 使用CopyOnWriteArrayList(写时复制数组集合)
        List<String> list = new CopyOnWriteArrayList<>();
        /**
         * 多个线程调用时,list集合读取时是固定的,写入时可能存在覆盖,
         * 为了避免写入时被覆盖,造成数据问题,可以进行读写分离
         */
        //使用for循环模拟多线程环境
        for (int i = 1; i <= 10; i++) {
            //使用Lambda表达式创建和启动线程
            new Thread(()->{
                /**
                 * 将字符串添加到list集合中
                 * 使用UUID.randomUUID().toString()获取UUID随机字符串
                 * 使用substring(0,5),返回的字符串中下标索引从0到5的子字符串
                 */
                list.add(UUID.randomUUID().toString().substring(0,5));
                //打印输出list集合
                System.out.println(list);
            //String.valueOf(i)表示获取下标为i的字符串
            },String.valueOf(i)).start();
        }
    }
}
4-3 测试结果

在这里插入图片描述

结果执行成功,没有出现异常!

到这里,今天的有关List集合线程安全的学习就结束了,欢迎大家学习和讨论!

参考视频链接:https://www.bilibili.com/video/BV1B7411L7tE (B站UP主遇见狂神说的JUC并发编程基础)

参考博客链接

https://baijiahao.baidu.com/s?id=1636557218432721275&wfr=spider&for=pc (Java中的关键字transient,这篇文章你再也不愁了)

https://www.cnblogs.com/liuling/archive/2013/05/05/transient.html (Java transient关键字使用小结)

https://blog.csdn.net/nazeniwaresakini/article/details/104473981 (写时复制(COW)技术与其在Linux、Java中的应用)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

狂奔の蜗牛rz

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值