13. 线程安全类型-集合

java 多线程系列文章列表, 请查看目录: 《java 多线程学习笔记》

Java 中常见的集合类型都是不安全的, 比如说ArrayList, LinkedList, HashSet, TreeSet, HashMap, TreeMap 等, 这些都是线程不安全的. 也就是说当多线程访问这些数据时, 便可能会产生结果紊乱.
Java5 之后, 在java.util.concurrent 包下新增了大量的支持高并发的集合接口和实现类, 同时Collections 工具类中也提供了一些将不安全的集合类型转换为安全的集合类型的方法.

1. 线程安全的集合类型

java.util.concurrent 下线程安全的集合类型大概可以分为两类:

  • 以Concurrent 开头的集合类: ConcurrentHashMap, ConcurrentLinkedQueue, ConcurrentSkipListSet 等
  • 以CopyOnWrite 开头的集合类: CopyOnWriteArrayList, CopyOnWriteArraySet 等

1.1 ConcurrentXXX 系列API

  • 以Concurrent 开头的集合类, 支持多个线程并发写入, 写入线程的操作都是线程安全的, 但是读取操作不必锁定.
  • 以Concurrent 开头的集合类, 采用了更复杂的算法保证永远不会锁住整个集合, 因此在并发写入时有更好的性能
  • ConcurrentLinkedQuue: 当多个线程共享访问一个公共集合时, ConcurrentLinkedQueue 是一个更好的选择. 性能较高, 多线程访问时无须等待, 但需要注意不允许添加null 元素
  • ConcurrentHashMap:
    • 默认情况支持16个线程的并发写入, 超过16个线程之后, 线程需要等待. 可以通过设置concurrencyLevel 构造参数调整并发性能.
    • java8 扩展了30多个新方法, 如forEach 系列, search系列, reduce 系列等.
public static void main(String[] args) {

    // 1. 线程不安全的集合类型
    ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();
    queue.addAll(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20));

    // 3. 多线程清空数组
    for (int i = 0; i < 3; i++) {
        new Thread(){
            @Override
            public void run() {
                while (!queue.isEmpty()) {
                    Integer remove = queue.poll();
                    System.out.println(Thread.currentThread().getName() + " delete: " + remove);
                    ThreadUtil.sleep(1);
                }
            }
        }.start();
    }

    ThreadUtil.sleep(10);
    System.out.println("剩余数量:" + queue.size());
}

1.2 CopyOnWriteXXX 系列API

  • 顾名思议, CopyOnWriteXX 集合, 采用复制底层数组的方式来实现写操作.
    • 读取操作时, 线程会直接读取集合本身, 无须加锁和阻塞, 因此读性能非常高
    • 写操作时, 集合会先在底层复制一份新的数组, 接下来对新的输入进行写操作, 写完之后替换原来的数组. 以保证线程安全. 写曹组底层是加了锁的.
  • 由于CopyOnWriteXXX API 在写操作时需要频繁复制底层数组, 因此性能很慢.
  • 常用类型有: CopyOnWriteArrayList, CopyOnWriteArraySet 等
  • CopyWriteXXX 的API 和List, Set 并无太大差异, 因此笔者这里就不赘述了.
// CopyOnWriteArrayList 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();
    }
}

2. Java 包装线程不安全的集合

对于常见的线程不安全的集合类型, java 也提供了非常便捷的方法进行转换. 转换的相关代码均在java.util.Collections 类中.

2.1 Collections 的API

  • java 提供了一组集合的工具方法, 可以将线程不安全的集合类型包装成线程安全的集合类型.
方法签名 方法描述
public static Collection synchronizedCollection(Collection c) 返回线程安全的集合类型, 源码返回: SynchronizedCollection对象
public static List synchronizedList(List list) 返回线程安全的List类型, 源码返回:SynchronizedList或SynchronizedRandomAccessList
public static Set synchronizedSet(Set s) 返回线程安全的Set类型, 源码返回: SynchronizedSet
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) 返回线程安全的Map类型, 源码返回: SynchronizedMap
public static SortedSet synchronizedSortedSet(SortedSet s) 返回线程安全的有序Set类型, 源码返回: SynchronizedSortedSet
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m) 返回线程安全的有序Map类型, 源码返回: SynchronizedSortedMap

2.2 源码分析

  • 底层实现源码时, 为每一种集合类型返回一个对应的包装类型 SychronizedXXX 类型
  • SynchronizedXXX 类型全部位于类java.util.Collections中, 使用synchronized 同步代码块儿方式为原来的方法加锁, 以这种方式保证线程安全
# 返回线程安全的包装类型
public static <T> Collection<T> synchronizedCollection(Collection<T> c) {
    return new SynchronizedCollection<>(c);
}

# 内部静态类, 使用synchronized 修饰方法
static class SynchronizedCollection<E> implements Collection<E>, Serializable {
    private static final long serialVersionUID = 3053995032091335093L;

    final Collection<E> c;  // Backing Collection
    final Object mutex;     // Object on which to synchronize

    SynchronizedCollection(Collection<E> c) {
        this.c = Objects.requireNonNull(c);
        mutex = this;
    }

    public int size() {
        synchronized (mutex) {return c.size();}
    }
    public boolean isEmpty() {
        synchronized (mutex) {return c.isEmpty();}
    }
    public boolean contains(Object o) {
        synchronized (mutex) {return c.contains(o);}
    }
    public Object[] toArray() {
        synchronized (mutex) {return c.toArray();}
    }
}

2.3 用法示例

  • 测试线程不安全的类型时, 将2. 处代码修改为 list = intList 即可. 多测试两次, 就会发现线程不安全问题
  • 转换为线程安全的类型之后, 测试结果正常, 没有任何问题.
public static void main(String[] args) {

    // 1. 线程不安全的集合类型
    List<Integer> intList = new ArrayList<>();
    intList.addAll(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20));

    // 2. 转换为线程安全的类型
    List<Integer> list = Collections.synchronizedList(intList);

    // 3. 多线程清空数组
    for (int i = 0; i < 3; i++) {
        new Thread(){
            @Override
            public void run() {
                while (!list.isEmpty()) {
                    Integer remove = list.remove(0);
                    System.out.println(Thread.currentThread().getName() + " delete: " + remove);
                    ThreadUtil.sleep(1);
                }
            }
        }.start();
    }
}
发布了321 篇原创文章 · 获赞 676 · 访问量 147万+

猜你喜欢

转载自blog.csdn.net/zongf0504/article/details/100186171