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. Week2 题解

    T1及题解 T2 CF1207E 首先,异或这个位运算有个很好的性质:x^y^y=x 于是,可以有两种解决方法: 法一 既然让猜原来的数是多少,当它异或了一个其他值val的时候,再异或val就会变回原 ...

  2. Hive的分桶表

    [分桶概述] Hive表分区的实质是分目录(将超大表的数据按指定标准细分到指定目录),且分区的字段不属于Hive表中存在的字段:分桶的实质是分文件(将超大文件的数据按指定标准细分到分桶文件),且分桶的 ...

  3. 再谈java线程

    线程状态 描述 当线程被创建并启动之后,它既不是已启动就进入到了执行状态,也不是一直处于执行状态.在线程的声明周期中有六中状态. java api中java.lang.Thread.State这个枚举 ...

  4. scrum项目冲刺_day06总结

    摘要:今日完成任务. 1.服务器部署完成 2.由于将数据库放到了服务器上,搜索功能需要修改 总任务: 一.appUI页面(已完成) 二.首页功能: 1.图像识别功能(已完成) 2.语音识别功能(已完成 ...

  5. minix3使用轻快入门

    minix3是一款迷你的unix作业系统,但又不在at&t代码的基础上构建.当年开发这款作业系统的作者仅仅是拿来自用,给学生上课使用的. 如果你已经安装了minix3,你还需要安装openss ...

  6. 开源物联网平台(Thingsboard)-编译

    环境准备 Jdk8+ (3.2.2版本开始使用Jdk11) Maven3.2.1+ release-3.2分支 获取代码 ##get source from mirror git clone http ...

  7. 一些PHP选项参数相关的函数

    关于 PHP 的配置,我们大多数情况下都是去查看 php.ini 文件或者通过命令行来查询某些信息,其实,PHP 的一些内置函数也可以帮助我们去查看或操作这些配置参数.比如之前我们学习过的 关于php ...

  8. 彻底搞明白PHP的中引用的概念

    之前我们其实已经有过几篇文章讲过引用方面的问题,这次我们来全面的梳理一下引用在PHP到底是怎么回事,它和C中的指针有什么不同,在使用的时候要注意些什么. 什么是引用? 在 PHP 中引用意味着用不同的 ...

  9. Jmeter系列(29)- 性能指标(2) | 并发数

    并发数 概念 同时承载正常使用系统功能的用户数量:系统能够同时处理请求的数目. 通过问题详解 问题:网站的并发数,究竟指的同时提交请求的用户数目,还是用户同时提交的请求的数目? 答案:根据这句描述&q ...

  10. windows10 升级并安装配置 jmeter5.3

    一.安装配置JDK Jmeter5.3依赖JDK1.8+版本,JDK安装百度搜索JAVA下载JDK,地址:https://www.oracle.com/technetwork/java/javase/ ...