前言:
之前我们在使用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 < 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中的应用)