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:容器类线程不安全的更多相关文章

  1. java 容器类大集结

    这个世界是程序员的世界,归根到底是数据的世界,要统治这个世界,首先要学会征服数据. 没有最好的,只有最合适的,如何在不同的环境先选择最优的存储的结构呢?且看下文分解: 以下内容部分来自网络,参考: h ...

  2. 【转】java 容器类使用 Collection,Map,HashMap,hashTable,TreeMap,List,Vector,ArrayList的区别

    原文网址:http://www.360doc.com/content/15/0427/22/1709014_466468021.shtml java 容器类使用 Collection,Map,Hash ...

  3. Java容器类List、ArrayList、Vector及map、HashTable、HashMap的区别与用法

    Java容器类List.ArrayList.Vector及map.HashTable.HashMap的区别与用法 ArrayList 和Vector是采用数组方式存储数据,此数组元素数大于实际存储的数 ...

  4. java容器类---概述

    1.容器类关系图 虚线框表示接口. 实线框表示实体类. 粗线框表示最经常使用的实体类. 点线的箭头表示实现了这个接口. 实线箭头表示类能够制造箭头所指的那个类的对象. Java集合工具包位于Java. ...

  5. java之线程

    java之线程 一:线程: 线程是什么呢?线程,有时被称为轻量级进程是程序执行流的最小单元.一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成.另外,线程是进程中的一个实体,是被系统 ...

  6. Java 使用线程方式Thread和Runnable,以及Thread与Runnable的区别

    一. java中实现线程的方式有Thread和Runnable Thread: public class Thread1 extends Thread{ @Override public void r ...

  7. Java的线程安全

    线程安全 我们这里讨论的线程安全,就限定于多个线程之间存在共享数据访问这个前提,因为如果一段代码根本不会与其他线程共享数据,那么从线程安全的角度来看,程序是串行执行还是多线程执行对它来说是完全没有区别 ...

  8. 深入理解Java之线程池

    原作者:海子 出处:http://www.cnblogs.com/dolphin0520/ 本文归作者海子和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则 ...

  9. java中线程分两种,守护线程和用户线程。

    java中线程分为两种类型:用户线程和守护线程. 通过Thread.setDaemon(false)设置为用户线程: 通过Thread.setDaemon(true)设置为守护线程. 如果不设置次属性 ...

随机推荐

  1. WebService学习总结(四)--基于CXF的服务端开发

    本节将实践目前最流行的第二种web service 发布和调试框架  CXF Apache CXF 是一个开放源代码框架,提供了用于方便地构建和开发 Web 服务的可靠基础架构.它允许创建高性能和可扩 ...

  2. 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: ...

  3. adb 常用命令大全(1)- 汇总

    adb 常用命令大全系列 基础命令 查看手机设备信息 应用管理 日志相关 模拟按键输入 其他实用功能

  4. 性能测试必备命令(2)- uptime

    性能测试必备的 Linux 命令系列,可以看下面链接的文章哦 https://www.cnblogs.com/poloyy/category/1819490.html 介绍 系统启动up了(运行了)多 ...

  5. Git 系列教程(13)- 分支管理

    查看分支列表 $ git branch iss53 * master testing  注意 master 分支前的 * 字符:它代表现在 checkout 的那一个分支(也就是说,当前 HEAD 指 ...

  6. 反射应用和获取Class对象的三种方式

    一.写一个"框架",可以创建任何对象运行任何方法 1.配置文件 2.使用类加载器ClassLoader,Properties集合是可以和IO流结合使用完成读取和写入数据的集合,方法 ...

  7. CentOS 7操作系统安装

    1.关于运维小伙伴可以采用何种方式安装操作系统 下面列举的只是我会用到的安装方式,在运维过程中并不一定是最优解,只是自己运维过程中的一些经验. (1)物理服务器,可以通过连接管理口来安装操作系统,管理 ...

  8. python3.x内置函数

    函数 返回值类型 函数详情 abs(x) int|float 求绝对值,若是复数则返回复数的模 all(iterable) bool 若所有元素为真则返回True(非0,非空,非None) any(i ...

  9. rootfs -根文件系统制作

    目录 目录 目录 概述 概念 根文件系统是什么 根文件系统中有什么 根文件系统的形式 Busybox 简介 什么是 linuxrc VFS 简介 Busybox 工具 Busybox 目录结构 Men ...

  10. FastAPI(4)- get 请求 - 路径参数 Path Parameters

    什么是路径 假设一个 url 是: http://127.0.0.1:8080/items/abcd 那么路径 path 就是 /items/abcd 路径参数 就是将路径上的某一部分变成参数,可通过 ...