摩尔投票法 Opencv 双重检验锁 Transformer docker安装 arrays replace types solr datagridview tree vue代码规范 vue前端开发 后台管理界面模板 matlab图像识别 tomcat调优和jvm调优 cmd清空命令 python计算器 python配置 搭建java环境 java编译环境 java获取时间 linux服务器登录 vbs脚本 gtx1030 电脑必备软件排行榜 iphone滚动截屏 html5网页制作 图解深度学习 linux多线程编程 视频相册制作软件 win10有几个版本 ps扭曲变形 正当防卫4存档 电脑待机费电吗 ajaxpro android应用开发入门 调试js cadworx id页码怎么设置
当前位置: 首页 > 学习教程  > 编程语言

【JUC】集合类多线程操作不安全的三种解决方案

2020/12/5 10:00:27 文章标签:

现在属于一个查漏补缺的阶段,之前京东的面试中问到我,关于多线程操作集合时,集合不安全该如何解决? 当时就只想到了(可能因为紧张,我承认比较菜 )使用使用实现安全的集合类和使用Collections。s…

现在属于一个查漏补缺的阶段,之前京东的面试中问到我,关于多线程操作集合时,集合不安全该如何解决?

当时就只想到了(可能因为紧张,我承认比较菜 )使用使用实现安全的集合类和使用Collections。synchronizedxxx的集合安全类来解决,现在回想起来自己当时的确回答的不好,这两种方式并不能保证“真正的线程安全”。查漏补缺,将掌握不熟练的知识一定要多练习,返回回顾知识,熟能生巧。
而且最近越来越感受到,每当看一些曾经掌握的知识的源码,就发现那些前辈真的很厉害,有一些设计很巧妙。

文章目录

  • List集合不安全
    • 方式1:直接使用多线程安全的集合类
      • 如何回答Vector是否是线程安全的集合类?
    • 方式2:使用Collections工具类修饰的集合类
    • 方式3:使用集合安全类
      • CopyOnWriteArrayList是如何保证线程安全的?
      • CopyOnWriteArrayList中可能出现的问题:
      • CopyOnWriteArrayList内部时如何实现的(梳理版)?
      • synchronizedArrayList的实现和CopyOnWriteArrayList有什么不同?
  • Set不安全
    • 使用Collections工具包下的synchronizedSet
    • 使用同步的Set集合
  • Map不安全
    • 使用Hashtable
    • 使用Collections.synchronizedMap
    • 使用同步Map

List集合不安全

当前企业的开发都会考虑到高并发的问题,博主我在今年秋招时也被多次问到集合类在多线程中使用的问题。这部分还是很重要的。

我们熟悉的ArrayList、HashMap等等集合类很多都不是线程安全的。也就是说在多线程情况下是可能造成多线程问题,因此有需求也必须让使用的集合类变安全。

通常能够想到的使用多线程安全的集合类的方式有三种:

  1. 直接使用本身就是线程安全的集合类,比如使用Vector,HashTable等等。
  2. 使用集合类工具类Collections类下的工具类对集合类进行修饰。
  3. 使用JUC包下的多线程安全集合类,如CopyOnWriteArrayList等。

接下来,我将三种不同的多线程安全的使用集合类做代码演示。

先演示一下普通的集合类会造成多线程安全问题

package jucTest2;

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

/**
 * @author 雷雨
 * @date 2020/12/4 17:02
 * 集合类在多线程下操作的不安全性
 */
public class UnFireCollectionDemo {

    public static void main(String[] args) {
        ArrayList<String> arrayList = new ArrayList<>();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                arrayList.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(arrayList);
            }
        },"A线程").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                arrayList.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(arrayList);
            }
        },"B线程").start();
    }
}

image-20201204170521163

出现了java.util.ConcurrentModificationException异常(同步修改异常),也就是说在多线程同时操作集合时出现了多线程操作的问题。

方式1:直接使用多线程安全的集合类

package jucTest2;

import java.util.UUID;
import java.util.Vector;

/**
 * @author 雷雨
 * @date 2020/12/4 16:55
 * 直接使用多线程安全的集合类
 */
public class FireCollectionDemo1 {

    public static void main(String[] args) {

        Vector<String> vector  = new Vector<>();

        new Thread(()->{
            for (int i = 1; i <= 10 ; i++) {
                vector.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(vector);
            }
        },"线程A").start();

        new Thread(()->{
            for (int i = 1; i <= 10 ; i++) {
                vector.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(vector);
            }
        },"线程B").start();

    }
}

image-20201204165935861

有观察结果会发现:没有出现异常。也就是说在集合Vector进行多线程操作时没有发生多线程问题。

为什么Vector是线程安全的?

简单的讲,Vector是线程安全的,因为Vector中的每个方法都使用了synchronized修饰,从而保证访问 vector 的任何方法都必须获得对象的 intrinsic lock (或叫 monitor lock),也即,在vector内部,其所有方法不会被多线程所访问。

Vector一定不存在多线程安全问题吗?

if (!vector.contains(element)) 
vector.add(element); 
...
}

其实Vector也可能存在多线程安全安全问题,虽然在Vector的内部的方法都使用了synchronized修饰,保证了在Vector内部使用时,Vector是线程安全的,但是如果如上述代码所示,是在外部环境中使用的,仍然存在锁竞争,对应上述代码,虽然contains和add方法都是原子性的操作,但是在if条件判断为真之后,关于contains的锁释放了,在多线程的环境中,其他线程有可能与add线程竞争并获取了锁资源后修改了其状态,而add线程在当时正在等待,只有其他线程释放锁资源后,add线程拿到了锁,add线程才执行(而在add方法执行时,它已经是基于一个错误的假设了)。

单个方法的synchronized了并不代表组合方法调用的原子性。

如何回答Vector是否是线程安全的集合类?

Vector 和 ArrayList 实现了同一接口 List, 但所有的 Vector 的方法都具有 synchronized 关键修饰。但对于复合操作,Vector 仍然需要进行同步处理。

方式2:使用Collections工具类修饰的集合类

package jucTest2;

import java.util.*;

/**
 * @author 雷雨
 * @date 2020/12/4 17:28
 * 直接使用多线程安全的集合类
 */
public class FireCollectionDemo2 {

    public static void main(String[] args) {

        List<String> list = Collections.synchronizedList(new ArrayList<String>());

        new Thread(()->{
            for (int i = 1; i <= 10 ; i++) {
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(list);
            }
        },"线程A").start();
        

        new Thread(()->{
            for (int i = 1; i <= 10 ; i++) {
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(list);
            }
        },"线程B").start();


    }
}

结果正常输出,没有发生多线程安全问题。

image-20201204173042447

关于Collections.synchronizedList和Vector的区别:

  1. 在源码中Vector中线程安全的实现是使用了synchronized锁住了整个方法(也就是使用了同步方法的方式),而在Collections.synchronizedList中是使用synchronized锁了当前的mutex对象,而mutex对象指向的是当前的实例。
  2. 那么Vector锁的对象是调用者,而Collections.synchronizedList锁的是synchronizedList本身的实例对象。

方式3:使用集合安全类

比如使用CopyOnWriteArrayList

package jucTest2;


import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * @author 雷雨
 * @date 2020年12月4日20:45:23
 * 直接使用多线程安全的集合类CopyOnWriteArratList
 */
public class FireCollectionDemo3 {

    public static void main(String[] args) {

        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

        new Thread(()->{
            for (int i = 1; i <= 10 ; i++) {
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(list);
            }
        },"线程A").start();


        new Thread(()->{
            for (int i = 1; i <= 10 ; i++) {
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(list);
            }
        },"线程B").start();




    }
}

多次运行,发现没有出现异常,该集合类是一个多线程安全的类。

image-20201204204600305

CopyOnWriteArrayList是如何保证线程安全的?

CopyOnWriteArrayList直接翻译就是写的时候复制,也就是说在写操作的时候是创建一个新的容器进行写操作,写完之后,再将原容器的引用指向新容器,整个过程加锁,保证了写的线程安全。

整个过程都使用Lock加锁,是线程安全的

//添加元素操作
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;
        //将array引用指向新数组
        setArray(newElements);
        return true;
    } finally {
        //解锁
        lock.unlock();
    }
}
//删除操作
public E remove(int index) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        E oldValue = get(elements, index);
        int numMoved = len - index - 1;
        if (numMoved == 0)
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            Object[] newElements = new Object[len - 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index + 1, newElements, index,
                             numMoved);
            setArray(newElements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

而因为读操作的时候不会对当前容器做任何处理,所以我们可以对容器进行并发的读,而不需要加锁,也就是读写分离。

public E get(int index) {
    return get(getArray(), index);
}

CopyOnWriteArrayList中可能出现的问题:

CopyOnWriteArrayList虽然实现了读写分离,提高了效率,并且在需要写操作的地方使用了ReentrantLock保证了线程的同步,但是仍然是存在问题的:

  1. 由于写操作是通过复制原数组,会消耗内存,如果原数组的数据量较大,可能会导致频繁的minor GC。
  2. 不能用于实时性的场景,因为是读写分离的,而且在写操作中采用的方式是通过复制写的操作,那么就会有耗时,可能会在写入数据的过程中,有读取的操作,那么可能导致读取的数据还是旧的数据。CopyOnWriteArrayList能保证最终一致性,但是却不能满足实时性的要求。

从第二点也就说明了CopyOnWriteArrayList其实比较适用于读多写少的场景,但是还是慎用,不能保证每次写入的操作的数据量,可能会导致读取到旧的数据的可能性。

小结:

CopyOnWriteArrayList的思想:

1、读写分离,读和写分开
2、最终一致性
3、使用另外开辟空间的思路,来解决并发冲突

CopyOnWriteArrayList内部时如何实现的(梳理版)?

  • 写操作的实现

再梳理一遍源码(以添加操作为例)

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;
        //将array引用指向新数组
        setArray(newElements);
        return true;
    } finally {
        //解锁
        lock.unlock();
    }
}

首先CopyOnWriteArrayList的写操作的实现:

  1. 首先在写操作的内部创建了一个ReentrantLock同步锁。
  2. 另外在写操作,在加锁的情况下还使用了复制写的思想。复制一个新的数据,添加元素之后,将引用指向新数组。

为什么要使用ReentrantLock?

是为了保证多线程的同步操作。对于多线程同步,采用加锁,能够解决多线程的问题。

为什么要使用复制写的操作?

因为关于集合的操作不仅有写操作还有读操作,如何不采用复制写的思想,那么就要对读操作也要加锁,不然就可能会造成线程安全问题(锁竞争机制)。但是CopyOnWriteArrayList为了保证读写分离(保证读的操作的效率),因此没有对读操作加锁。

如果没有复制,写时加锁,读取不加锁,那么就会造成并发读写问题,产生不可预期的错误,造成ConcurrentModificationException问题。(是因为为了保证并发读写的安全性,在集合中维护了一个ModConcurrent用来计数集合修改次数)如果在写时,进行了读取操作,ModConcurrent变化了,就会抛出ConcurrentModificationException。

  • 可能会问,为什么CopyOnWriteArrayList中采用写操作,读不加锁?

如果写操作加锁,读操作也使用ReentrantLock加锁,那么就退化为synchronized,读性能大大减弱。

synchronizedArrayList的实现和CopyOnWriteArrayList有什么不同?

synchronizedArrayList的实现是使用了synchronized关键字在方法的内部对操作进行加锁(同步代码块)的方式实现线层同步,CopyOnWriteArrayList的内部使用的是ReentrantLock(同步锁),CopyOnWriteArrayList底层保存元素的数组使用了volatile保证而来线程间的可见性。

Set不安全

package jucTest2;

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

/**
 * @author 雷雨
 * @date 2020/12/4 21:53
 * Set 集合不安全
 */
public class UnFireCollectionSet {

    public static void main(String[] args) {

        Set<String> set = new HashSet<>();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(set);
            }
        },"A线程").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(set);
            }
        },"B线程").start();


    }
}

image-20201204215634344

结果可以看到发生了CurrentModifcationException异常(同步修改异常)。

使用Collections工具包下的synchronizedSet

package jucTest2;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

/**
 * @author 雷雨
 * @date 2020/12/5 8:47
 * 使用集合安全的类  set
 * Collections工具包下的集合安全类
 */
public class FireCollectionSet2 {
    public static void main(String[] args) {
        Set<String> set = Collections.synchronizedSet(new HashSet<String>());

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(set);
            }
        },"线程A").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(set);
            }
        },"线程B").start();
    }
}

image-20201205085311515

使用同步的Set集合

使用CopyOnWriteSet

package jucTest2;

import java.util.UUID;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * @author 雷雨
 * @date 2020/12/5 8:53
 * 使用同步Set
 */
public class FireCollectionSet3 {
    public static void main(String[] args) {
        CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<String>();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(set);
            }
        },"线程A").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(set);
            }
        },"线程B").start();
    }
}

CopyOnWriteSet的底层使用CopyOnWriteList来实现的,因此也能保证线程安全。

Map不安全

HashMap在多线程操作下不安全

package jucTest2;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
 * @author 雷雨
 * @date 2020/12/5 9:03
 * Map在多线程下操作不安全
 *
 */
public class UnFireCollectionMap {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                int temp = i;
                map.put(UUID.randomUUID().toString().substring(0,5),temp);
                System.out.println(map);
            }
        },"线层A").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                int temp = i;
                map.put(UUID.randomUUID().toString().substring(0,5),temp);
                System.out.println(map);
            }
        },"线程B").start();
    }
}

使用Hashtable

使用HashTable

package jucTest2;

import java.util.Hashtable;
import java.util.UUID;

/**
 * @author 雷雨
 * @date 2020/12/5 9:10
 * 使用本身就是多线程安全的类
 */
public class FireCollectionMap1 {
    public static void main(String[] args) {
        Hashtable<String,Integer> map  = new Hashtable<>();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                int temp = i;
                map.put(UUID.randomUUID().toString().substring(0,5),temp);
                System.out.println(map);
            }
        },"线层A").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                int temp = i;
                map.put(UUID.randomUUID().toString().substring(0,5),temp);
                System.out.println(map);
            }
        },"线程B").start();
    }
}

使用本身是集合安全的Hashtable能够保证多线程操作的安全。

为什么Hashtable是线程安全的?

看源码分析一下

//写操作上加锁
public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }

    addEntry(hash, key, value, index);
    return null;
}
//读操作加锁
public synchronized V get(Object key) {
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            return (V)e.value;
        }
    }
    return null;
}

小结:Hashtable就是将读写方法都进行了加锁,保证了读写的多线程安全性

但是还是仍然存在多线程问题的,因为存在锁资源竞争。

使用Collections.synchronizedMap

package jucTest2;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
 * @author 雷雨
 * @date 2020/12/5 9:17
 * 使用Collections
 */
public class FireCollectionMap2 {
    public static void main(String[] args) {
        Map<Object, Object> map = Collections.synchronizedMap(new HashMap<>());
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                int temp = i;
                map.put(UUID.randomUUID().toString().substring(0,5),temp);
                System.out.println(map);
            }
        },"线层A").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                int temp = i;
                map.put(UUID.randomUUID().toString().substring(0,5),temp);
                System.out.println(map);
            }
        },"线程B").start();
    }
}

Collections.synchronizedMap是线程安全的类。

image-20201205092351428

使用同步Map

使用ConcurrentHashMap

package jucTest2;

import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * @author 雷雨
 * @date 2020/12/5 9:25
 * 使用同步Map
 */
public class FireCollectionMap3 {
    public static void main(String[] args) {
        ConcurrentMap<String,Integer> map = new ConcurrentHashMap<>();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                int temp = i;
                map.put(UUID.randomUUID().toString().substring(0,5),temp);
                System.out.println(map);
            }
        },"线层A").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                int temp = i;
                map.put(UUID.randomUUID().toString().substring(0,5),temp);
                System.out.println(map);
            }
        },"线程B").start();
    }
}

ConcurrentHashMap源码分析放在之后的博客,因为细节比较多。


本文链接: http://www.dtmao.cc/news_show_450125.shtml

附件下载

相关教程

    暂无相关的数据...

共有条评论 网友评论

验证码: 看不清楚?