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. java:volatile关键字原理

    volatile说明 在变量中声明后,能够在所有线程中共享改变量.并且volatile关键字能防止指令重排,即程序读取到volatile时,则不会将程序执行顺序修改. 先了解下内存模型 cpu内存模型 ...

  2. wireshark 解密加密报文

    wireshark 解密IPSec加密后的报文 序言 wireshark作为一款非常优秀的抓包工具,支持了各种各样的网络协议,成为了网络开发中必不可少的工具之一.一般而言,对于普通的网络数据包,wir ...

  3. SpringSecurity-Shiro-初见

    目录 简介 实战环境搭建 SpringSecurity 认证和授权 权限控制和注销 记住我 Shiro 快速上手 shiro整合mybais 简介 在 Web 开发中,安全一直是非常重要的一个方面. ...

  4. python库--requests

    requests 方法 返回 参数 方法详情 .get()  r  url  get请求 params  url?后面的内容会以'key=value'的方式接到url后面 proxies 设置代理ip ...

  5. 用Java实现红黑树

    红黑树是众多"平衡的"搜索树模式中的一种,在最坏情况下,它相关操作的时间复杂度为O(log n). 1.红黑树的属性 红黑树是一种二分查找树,与普通的二分查找树不同的一点是,红黑树 ...

  6. JD 评论晒图爬虫

    JD 评论晒图爬虫 #coding=utf-8 import requests import re import os __author__ = 'depy' """ j ...

  7. 聊一聊开闭原则(OCP).

    目录 简述 最早提出(梅耶开闭原则) 重新定义(多态开闭原则) 深入探讨 OCP的两个特点 对外扩展开放(Open for extension) 对内修改关闭 抽象 关闭修改.对外扩展? 简述 在面向 ...

  8. HiveServer2的负载均衡高可用与ActicePassive高可用浅析

    负载均衡的高可用 最近在工作中遇到了hiveserver2需要部署高可用的场景,去网上搜索了解过后,用了绝大多数人推荐的共同方法: Property_name Property_value Descr ...

  9. PHP的OpenSSL加密扩展学习(一):对称加密

    我们已经学过不少 PHP 中加密扩展相关的内容了.而今天开始,我们要学习的则是重点中的重点,那就是 OpenSSL 加密扩展的使用.为什么说它是重点中的重点呢?一是 OpenSSL 是目前 PHP 甚 ...

  10. centos7.6,nginx1.18,php-7.4.6,mysql-5.7.30 安装

    #1.下载,来自各官网 nginx-1.18.0.tar.gz php-7.4.6.tar.gz mysql-5.7.30-linux-glibc2.12-x86_64.tar.gz #下载到本地再传 ...