前言

在Java开发中,集合是最常用的API之一,JDK提供的集合也是非常强大,在实际的开发中能很方便的解决很多需求问题。但是经常会听到“集合线程安全”,那么什么是集合线程安全?不安全又是什么情况?


一、什么是线程安全?

凡事谈到线程安全,必然是多线程环境,如果是单线程应用,一般不会有线程安全的说法。另外就是线程安全的主角应该是共享数据。

如果用一句简单的话来总结,线程安全就是:在多线程的情况下,有一个共享数据能够按照我们自己设定的规则去变化。如果这个共享数据的变化没有按照我们既定的规则去执行,那线程就不安全了。

二、线程不安全的示例

今天我们主要讨论集合的线程安全,所以我们先给出一个集合的示例:

package com.test.collects;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ThreadSafeTest {
    public static void main(String[] args) {
        Set<Integer> set = new HashSet<>();
        ExecutorService executorService = Executors.newFixedThreadPool(32);
 
        for (int i = 0; i < 1000; i++) {
            final int number = i;
            executorService.submit(() -> {
                set.add(number);
            });
        }
        executorService.shutdown();
        try {
            executorService.awaitTermination(5, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
 
        System.out.println("Set size: " + set.size()); // 通常集合大小会等于1000,说明集合是线程安全的
    }
}

我们直接运行上面这段中

  • 我们模拟一个多线程环境,都向集合中添加数据,总共需要添加1000个
  • 正常情况下,最后集合的总数应该是1000,如果不是1000那么我们整个代码就存在问题,也就是线程竞争问题。由于竞争就造成了线程不安全。

直接运行会发现:

Set size: 969

多次运行,数值会不一样,有可能某一次刚好1000,这没有达到我们的预期。

三、解决集合线程不安全的方案

Java中处理线程安全的方式有很多,这里我给大家列出一些常用的方式。

1、synchronized关键字

我们改造一下上面的代码:

for (int i = 0; i < 1000; i++) {
    final int number = i;
    executorService.submit(() -> {
        synchronized (String.class){
            set.add(number);
        }
    });
}

再次运行多次,发现最终结果始终是1000

2、lock机制

private static final Lock lock=new ReentrantReadWriteLock().writeLock();
for (int i = 0; i < 1000; i++) {
    final int number = i;
    executorService.submit(() -> {
        lock.lock();
        try{
            set.add(number);
        }finally {
        	//记得释放
            lock.unlock();
        }
    });
}

效果如上

3、java.util.Collections工具

java.util.Collections工具提供了创建线程安全的集合。这里我们只需要替换集合的创建方式即可。

Set<Integer> set = Collections.synchronizedSet(new HashSet<>());

for (int i = 0; i < 1000; i++) {
    final int number = i;
    executorService.submit(() -> {
        set.add(number);
    });
}

4、commons-collections工具

使用commons-collections工具包需要加入maven依赖:

<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.2</version>
</dependency>

然后也只需要修改创建集合的方式即可:

Set<Integer> set = SynchronizedSet.decorate(new HashSet<>());


for (int i = 0; i < 1000; i++) {
    final int number = i;
    executorService.submit(() -> {
        set.add(number);
    });
}

5、guava工具

guava提供了很多集合的封装以及扩展,方式和commons一样,加入maven依赖:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>

然后修改代码:

Set<Integer> set = Sets.newConcurrentHashSet();


for (int i = 0; i < 1000; i++) {
    final int number = i;
    executorService.submit(() -> {
        set.add(number);
    });
}


总结

最后大家可能会关系上面几种处理线程安全的方式中执行的效率问题。经测试对比,没有发现哪一种方式会有显著的提升,所有的差距在执行时间上也就是几十毫秒,而且各自的表现也不一定,也许我的测试方式不对。

在不要求极致的效率问题时,我感觉大家可以根据实际情况来选择

03-15 07:30