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)设置为守护线程. 如果不设置次属性 ...
随机推荐
- MySQL——SQL语句入门
1.DDL: 数据库定义语言 定义对象:库.表 何为定义: 库的定义: 创建 删除 修改---->修改本身以及库中的对象(表.视图.函数.触发器...) 表的定义: 创建---->定义表的 ...
- Appium问题解决方案(4)- Error while obtaining UI hierarchy XML file: com.android.ddmlib.SyncException
背景 操作步骤 运行 uiautomatorviewer.bat 点击左上角的 Device ScreensShot 报错 截图 解决方法 网上还是有很多方法的,可能造成的原因不同,我是第六种方法解决 ...
- iframe 内容适用高度
HTML: <div class="content"> <iframe id="frameObj" src="链接" fr ...
- Vue组件传值(二)之 非父子组件传值
Vue中非父子组件之间是如何实现通信的? 本章主要讲的是非父子组件传值,父子组件传值请看上一篇文章. 1.创建新的Vue实例引入项目中,通过$emit.$on来实现非父子组件传值: 1 <!DO ...
- Linux目录同步到阿里云OSS工具ossutil
Linux目录同步到阿里云OSS工具ossutil 背景 最近公司服务用户激增,常规文件服务器不能满足需求,严重影响性能,决定将静态文件迁移到阿里云OSS,用来解决性能问题,提高用户体验.毕竟之前 ...
- Tomcat配置支持war包部署
Tomcat配置支持war包部署 #cat /data/tomcat/conf/server.xml <?xml version='1.0' encoding='utf-8'?> < ...
- Ts基本数据类型
TS的基本数据类型 string let str : string str = 1 //报错 会提示num是字符串 不是数字 str = 'test' //正确 //拼接字符串 let str2 : ...
- springboot整合jsp报错
今天在学springboot整合jsp时遇到了一个问题,虽然jsp不被spring官方推荐使用,但抱着学习的心态还是想解决一下这个问题.在写好了需要pom文件之后,访问网站得到了500的错误提示,后台 ...
- webrtc源码阅读理解一
webrtc是一个比较成熟的实时音视频处理开源项目,一上来老大就扔给我一本webrtc native实践,虽然狠下心"翻"完了一遍,但是还是云里雾里的,在经过几个月的摸索之后,我大 ...
- cgroup之cpu关键参数
cpu.cfs_period_us specifies a period of time in microseconds (µs, represented here as "us" ...