JAVA多线程-集合类线程不安全问题

ArrayList线程不安全

案例

package JUC;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * @author zhaolimin
 * @date 2021/11/13
 * @apiNote ArrayList类不安全测试
 */

public class ArrayListNotSafeDemo {

    public static void main(String[] args) {

        List<String> list = new ArrayList<>();
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0,8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
        // java.util.ConcurrentModificationException 出现了快速失败机制
    }
}

故障现象

  • 抛出 java.util.ConcurrentModificationException 出现了并发修改期望值与修改值不同,快速失败机制。

故障原因

  • 多线程添加操作下,出现了快速失败机制。
  • 原因是add方法对于ArrayList来说是线程不安全的。
  • 并发争抢修改导致的问题,一个线程正在写,另一个线程抢夺,导致数据不一致问题。

解决方案

将ArrayList类换成Vector类

  • 可以解决,但是并发性急剧下降。
 public static void main(String[] args) {

        List<String> list = new Vector<>();
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0,8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }

运用集合工具类Collections

Collections.synchronizedList方法
  • 可以解决,但是并发性急剧下降。
   public static void main(String[] args) {

        List<String> list = Collections.synchronizedList(new ArrayList<>());
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0,8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }

运用JUC下的CopyOnWriteArrayList类(重点)

  • 主要用于读多写少的场景。
  • 写时复制
    • 读的是原数组,锁的是副本
    • 一般来说读写不能并发,但使用写时复制的话,就允许了,提高了并发能力
  • 缺点
    • 典型的空间换取时间
    • 会产生大量无效的对象引用碎片
    public static void main(String[] args) {
        
        List<String> list = new CopyOnWriteArrayList<>();

        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0,8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
CopyOnWriteArrayList类核心内容选读
  • 与添加有关的方法
/**
	追加一个特殊的元素到这个List集合的尾部。
*/
public boolean add(E e) {
    // 用 ReentrantLock 加锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    // 注:JDK9以后已经改为 synchronized 加锁了。
    try {
        // 拿到旧表
        Object[] elements = getArray();
        // 拿到旧表的长度
        int len = elements.length;
        // 在旧表的基础上拷贝并长度 + 1 新表
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 在新表的末尾追加要添加的元素
        newElements[len] = e;
        // 将当前表设置为新表
        setArray(newElements);
        // 返回添加成功
        return true;
    } finally {
        // 解锁
        lock.unlock();
    }
}

HashSet线程不安全

案例

package JUC;

import java.util.HashSet;
import java.util.UUID;

/**
 * @author zhaolimin
 * @date 2021/11/14
 * @apiNote HashSet线程不安全测试。
 */
public class HashSetNotSafeDemo {

    public static void main(String[] args) {

        HashSet<String> strings = new HashSet<>();

        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                strings.add(UUID.randomUUID().toString().substring(0,8));
                System.out.println(strings);
            }, String.valueOf(i)).start();
        }
    }
}

故障现象

  • 抛出 java.util.ConcurrentModificationException 出现了并发修改异常,快速失败机制。

故障原因

  • 期望修改次数与真实修改次数值不同。
  • 多线程添加操作下,出现了快速失败机制。
  • 原因是add方法对于HashSet来说是线程不安全的。
  • 并发争抢修改导致的问题,一个线程正在写,另一个线程抢夺,导致数据不一致问题。

解决方案

运用集合工具类Collecitons

Collections.synchronizedSet方法
  • 线程安全但是并发性下降
public static void main(String[] args) {

    Set<String> strings = Collections.synchronizedSet( new HashSet<>());

    for (int i = 0; i < 50; i++) {
        new Thread(() -> {
            strings.add(UUID.randomUUID().toString().substring(0,8));
            System.out.println(strings);
        }, String.valueOf(i)).start();
    }
}
运用JUC下的CopyOnWriteArraySet类(重点)
  • 同样是使用了写时复制类。
public static void main(String[] args) {

    CopyOnWriteArraySet<String> strings = new CopyOnWriteArraySet<>();

    for (int i = 0; i < 50; i++) {
        new Thread(() -> {
            strings.add(UUID.randomUUID().toString().substring(0,8));
            System.out.println(strings);
        }, String.valueOf(i)).start();
    }
}
CopyOnWriteArraySet类核心内容选读
  • 属性
private final CopyOnWriteArrayList<E> al; // 可以看到数据结构其实是一个 CopyOnWriteArrayList 类的对象

// 让我联想到了 HashSet 和 HashMap 的关系。
public CopyOnWriteArraySet() {
    // 调用的是 CopyOnWriteArrayList 的构造函数
    al = new CopyOnWriteArrayList<E>();
}

HashMap线程不安全

案例

package JUC;

import java.util.*;

/**
 * @author zhaolimin
 * @date 2021/11/14
 * @apiNote HashMap线程不安全
 */
public class HashMapNotSafeDemo {

    public static void main(String[] args) {

        HashMap<String, String> map = new HashMap<>();

        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,8));
                System.out.println(map);
            }, String.valueOf(i)).start();
        }
    }
}

故障现象

  • 抛出 java.util.ConcurrentModificationException 出现了并发修改异常,快速失败机制。

故障原因

  • 期望修改次数与真实修改次数值不同。
  • 多线程添加操作下,出现了快速失败机制。
  • 原因是add方法对于HashSet来说是线程不安全的。
  • 并发争抢修改导致的问题,一个线程正在写,另一个线程抢夺,导致数据不一致问题。

解决方案

运用集合工具类Collecitons

public static void main(String[] args) {

    Map<Object, Object> objectObjectMap = Collections.synchronizedMap(new HashMap<>());
    for (int i = 0; i < 40; i++) {
        new Thread(() -> {
            objectObjectMap.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,8));
            System.out.println(objectObjectMap);
        }, String.valueOf(i)).start();
    }
}

运用JUC下的ConcurrentHashMap类(重点)

  • 该类详解看之前的笔记。
public static void main(String[] args) {

    ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
    for (int i = 0; i < 40; i++) {
        new Thread(() -> {
            map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,8));
            System.out.println(map);
        }, String.valueOf(i)).start();
    }
}

传值问题 (重要基本功)

案例

  • person类
public class Person {

    private Integer id;
    private String personName;

    public Person() {
    }

    public Person(String personName) {
        this.personName = personName;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getPersonName() {
        return personName;
    }

    public void setPersonName(String personName) {
        this.personName = personName;
    }
}
  • 测试类
public class TestTransferValueDemo {

    public void changeValue1(int age) {
        age = 30;
    }

    public void changeValue2(Person person) {
        person.setPersonName("zlm");
    }

    public void changeValue3(String str) {
        str = "wl";
    }

    // main线程在栈内存启动
    public static void main(String[] args) {
        
        TestTransferValueDemo testTransferValueDemo = new TestTransferValueDemo();
		
        // 基本数据类型
        int age = 20;
        testTransferValueDemo.changeValue1(age);
        System.out.println("-------- age = " + age);

        Person person = new Person("xxx");
        testTransferValueDemo.changeValue2(person);
        System.out.println("-------- personName = " + person.getPersonName());

        String str = "xxx";
        testTransferValueDemo.changeValue3(str);
        System.out.println("-------- str = " + str);
    }
}
  • 打印结果
/*
	-------- age = 20
    -------- personName = zlm
    -------- str = xxx
*/

为什么会出现这样的结果

  • 栈管运行,堆管存储

  • 栈空间是线程私有,堆的是数据共享

  • age的结果

    • 是因为自始至终,基本数据类型都是传递的数据副本,副本改变根本影响不到原件的值,所谓的值传递
  • person的结果

    • 传递的是引用,函数参数以及main中引用,指向的都是同一堆内存中的地址。
    • 也就是对象在堆内存中的地址,修改了那个地址上的值。
  • str的结果

    • String 类型字符数组 是 final 修饰的,说明是一个不可更改的常量,只能新建
    • 对于 changeValue3 方法中的 str,它原本是和 main 中的 str 一样指向常量池里的 “xxx”
    • 但是 changeValue3 中的 str 又被强行指向了一个叫 “wl” 的字符串常量,但是在常量池里并没有这个字符串
    • 由于字符串是常量,不能修改只能新建。
    • 于是常量池中会新建一个字符串常量 “wl” 然后 changeValue3 中的 str 将指向这个新建 “wl” 字符串
    • 由于 “wl” 字符串和 “xxx” 字符串在常量池中的地址不同,就导致了 main 中的 strchangeValue3 中的 str 不是指向同一个常量池地址
    • 而题中我们要打印的是 main 中的 str 所指的堆内存常量池中的内容为 “xxx”,而不是 “wl”
    • 所以我们打印的是 “xxx”
    • 对于String str = new String("hello world");运行期创建就存储在堆中
    • 对于 String str = "hello world;" 编译期创建的放在常量池

总结

  • String类型特殊,需要单独看待。
  • 引用类型复制的是地址。
  • 对于基础类型的变量和常量:变量和引用存储在栈中,常量存储在常量池中。

码云仓库同步笔记,可自取欢迎各位star指正:https://gitee.com/noblegasesgoo/notes

如果出错希望评论区大佬互相讨论指正,维护社区健康大家一起出一份力,不能有容忍错误知识。
										—————————————————————— 爱你们的 noblegasesgoo

你可能感兴趣的:(JAVA学习,JAVA八股文,java,开发语言,juc,多线程,线程安全)