Java:容器类线程不安全
Java:容器类线程不安全
本笔记是根据bilibili上 尚硅谷 的课程 Java大厂面试题第二季 而做的笔记
1. Collection 线程不安全的举例
前言
1、当我们执行下面语句的时候,底层进行了什么操作
new ArrayList<Integer>();
底层创建了一个空的数组,伴随着初始值为 10
当执行 add 方法后,如果超过了 10,那么会进行扩容,扩容的大小为原值的一半,也就是 5 个,使用下列方法扩容
Arrays.copyOf(elementData, netCapacity)
单线程环境下
单线程环境的 ArrayList 是不会有问题的
public class ArrayListNotSafeDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
for(String element : list) {
System.out.println(element);
}
}
}
多线程环境
为什么 ArrayList 是线程不安全的?因为在进行写操作的时候,方法上为了保证并发性,是没有添加synchronized 修饰,所以并发写的时候,就会出现问题
/**
* 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;
}
当我们同时启动30个线程去操作List的时候
/**
* 集合类线程不安全举例
*/
public class ArrayListNotSafeDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
这个时候出现了错误,也就是 java.util.ConcurrentModificationException
这个异常是 并发修改的异常
解决方案
方案一:Vector
第一种方法,就是不用 ArrayList 这种不安全的 List 实现类,而采用 Vector,线程安全的
关于 Vector 如何实现线程安全的,而是在方法上加了锁,即 synchronized
/**
* Adds the specified component to the end of this vector,
* increasing its size by one. The capacity of this vector is
* increased if its size becomes greater than its capacity.
*
* <p>This method is identical in functionality to the
* {@link #add(Object) add(E)}
* method (which is part of the {@link List} interface).
*
* @param obj the component to be added
*/
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}
这样就每次只能够一个线程进行操作,所以不会出现线程不安全的问题,但是因为加锁了,导致并发性基于下降
方案二:Collections.synchronizedXXX
List<String> list = Collections.synchronizedList(new ArrayList<>());
采用 Collections 集合工具类,在 ArrayList 外面包装一层同步机制
方案三:采用 JUC 里面的方法
CopyOnWriteArrayList:写时复制,主要是一种读写分离的思想
写时复制,CopyOnWrite 容器即写时复制的容器,往一个容器中添加元素的时候,不直接往当前容器 Object[]
添加,而是先将 Object[]
进行 copy,复制出一个新的容器 object[] newElements
,然后新的容器 Object[] newElements
里添加原始,添加元素完后,在将原容器的引用指向新的容器 setArray(newElements);
这样做的好处是可以对 copyOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不需要添加任何元素。
所以 CopyOnWrite 容器也是一种读写分离的思想,读和写不同的容器,就是写的时候,把 ArrayList 扩容一个出来,然后把值填写上去,再通知其他的线程,ArrayList 的引用指向扩容后的容器。
查看底层 add()
方法源码
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
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();
}
}
首先需要加锁
final ReentrantLock lock = this.lock;
lock.lock();
然后在末尾扩容一个单位
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
然后在把扩容后的空间,填写上需要add的内容
newElements[len] = e;
最后把内容set到Array中
setArray(newElements);
2. HashSet 线程不安全
CopyOnWriteArraySet
加了马甲的 CopyOnWriteArrayList
底层还是使用 CopyOnWriteArrayList 进行实例化
public class CopyOnWriteArraySet<E> extends AbstractSet<E>
implements java.io.Serializable {
private static final long serialVersionUID = 5457747651344034263L;
private final CopyOnWriteArrayList<E> al;
/**
* Creates an empty set.
*/
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
}
HashSet 底层结构
同理HashSet的底层结构就是HashMap
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
问:但是为什么我调用 HashSet.add()的方法,只需要传递一个元素,而HashMap是需要传递key-value键值对?
首先我们查看hashSet的add方法
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
// 其中:
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
我们能发现但我们调用 add 的时候,存储一个值进入 map 中,只是作为 key 进行存储,而 value 存储的是一个 Object 类型的常量,也就是说 HashSet 只关心 key,而不关心 value
3. HashMap 线程不安全
同理 HashMap 在多线程环境下,也是不安全的
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
for(int i = 0; i < 30; i++){
new Thread(() -> {
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 8));
System.out.println(map);
}, String.valueOf(i)).start();
}
}
解决方法
1、使用 Collections.synchronizedMap(new HashMap<>());
2、使用 ConcurrentHashMap
Map<String, String> map = new ConcurrentHashMap<>();
Java:容器类线程不安全的更多相关文章
- java 容器类大集结
这个世界是程序员的世界,归根到底是数据的世界,要统治这个世界,首先要学会征服数据. 没有最好的,只有最合适的,如何在不同的环境先选择最优的存储的结构呢?且看下文分解: 以下内容部分来自网络,参考: h ...
- 【转】java 容器类使用 Collection,Map,HashMap,hashTable,TreeMap,List,Vector,ArrayList的区别
原文网址:http://www.360doc.com/content/15/0427/22/1709014_466468021.shtml java 容器类使用 Collection,Map,Hash ...
- Java容器类List、ArrayList、Vector及map、HashTable、HashMap的区别与用法
Java容器类List.ArrayList.Vector及map.HashTable.HashMap的区别与用法 ArrayList 和Vector是采用数组方式存储数据,此数组元素数大于实际存储的数 ...
- java容器类---概述
1.容器类关系图 虚线框表示接口. 实线框表示实体类. 粗线框表示最经常使用的实体类. 点线的箭头表示实现了这个接口. 实线箭头表示类能够制造箭头所指的那个类的对象. Java集合工具包位于Java. ...
- java之线程
java之线程 一:线程: 线程是什么呢?线程,有时被称为轻量级进程是程序执行流的最小单元.一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成.另外,线程是进程中的一个实体,是被系统 ...
- Java 使用线程方式Thread和Runnable,以及Thread与Runnable的区别
一. java中实现线程的方式有Thread和Runnable Thread: public class Thread1 extends Thread{ @Override public void r ...
- Java的线程安全
线程安全 我们这里讨论的线程安全,就限定于多个线程之间存在共享数据访问这个前提,因为如果一段代码根本不会与其他线程共享数据,那么从线程安全的角度来看,程序是串行执行还是多线程执行对它来说是完全没有区别 ...
- 深入理解Java之线程池
原作者:海子 出处:http://www.cnblogs.com/dolphin0520/ 本文归作者海子和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则 ...
- java中线程分两种,守护线程和用户线程。
java中线程分为两种类型:用户线程和守护线程. 通过Thread.setDaemon(false)设置为用户线程: 通过Thread.setDaemon(true)设置为守护线程. 如果不设置次属性 ...
随机推荐
- WebService学习总结(四)--基于CXF的服务端开发
本节将实践目前最流行的第二种web service 发布和调试框架 CXF Apache CXF 是一个开放源代码框架,提供了用于方便地构建和开发 Web 服务的可靠基础架构.它允许创建高性能和可扩 ...
- Appium问题解决方案(7)- Could not find 'adb.exe' in PATH. Please set the ANDROID_HOME environment variable with the Android SDK root directory path
背景:运行代码提示找不到ADB An unknown server-side error occurred while processing the command. Original error: ...
- adb 常用命令大全(1)- 汇总
adb 常用命令大全系列 基础命令 查看手机设备信息 应用管理 日志相关 模拟按键输入 其他实用功能
- 性能测试必备命令(2)- uptime
性能测试必备的 Linux 命令系列,可以看下面链接的文章哦 https://www.cnblogs.com/poloyy/category/1819490.html 介绍 系统启动up了(运行了)多 ...
- Git 系列教程(13)- 分支管理
查看分支列表 $ git branch iss53 * master testing 注意 master 分支前的 * 字符:它代表现在 checkout 的那一个分支(也就是说,当前 HEAD 指 ...
- 反射应用和获取Class对象的三种方式
一.写一个"框架",可以创建任何对象运行任何方法 1.配置文件 2.使用类加载器ClassLoader,Properties集合是可以和IO流结合使用完成读取和写入数据的集合,方法 ...
- CentOS 7操作系统安装
1.关于运维小伙伴可以采用何种方式安装操作系统 下面列举的只是我会用到的安装方式,在运维过程中并不一定是最优解,只是自己运维过程中的一些经验. (1)物理服务器,可以通过连接管理口来安装操作系统,管理 ...
- python3.x内置函数
函数 返回值类型 函数详情 abs(x) int|float 求绝对值,若是复数则返回复数的模 all(iterable) bool 若所有元素为真则返回True(非0,非空,非None) any(i ...
- rootfs -根文件系统制作
目录 目录 目录 概述 概念 根文件系统是什么 根文件系统中有什么 根文件系统的形式 Busybox 简介 什么是 linuxrc VFS 简介 Busybox 工具 Busybox 目录结构 Men ...
- FastAPI(4)- get 请求 - 路径参数 Path Parameters
什么是路径 假设一个 url 是: http://127.0.0.1:8080/items/abcd 那么路径 path 就是 /items/abcd 路径参数 就是将路径上的某一部分变成参数,可通过 ...