Java 8中的Map.merge方法是否线程安全?

3
import java.util.HashMap;
import java.util.Map;

public class MainTest
{
    static Map<String, Integer> map = new HashMap();

    public static void incr()
    {
        map.merge("counter", 1, Integer::sum);
    }

    public static void decr()
    {
        map.merge("counter", -1, Integer::sum);
    }

    public static void main(String[] args) throws Exception
    {
        map.put("counter", 0);
        for (int i = 0; i < 10000; i++)
        {
            Thread t1 = new Thread(new Runnable()
            {
                @Override
                public void run()
                {
                    incr();
                }
            });
            t1.join();
            t1.start();

            Thread t2 = new Thread(new Runnable()
            {
                @Override
                public void run()
                {
                    decr();
                }
            });
            t2.join();
            t2.start();
        }
      System.out.println(map);
    }

}

在运行主方法时,结果为{counter=-2}。为什么不是0呢?

9
不会的。为什么呢?因为HashMap不是线程安全的,所以HashMap上的任何方法都不是线程安全的。 - Boris the Spider
2个回答

7
Map接口上merge方法的Javadoc文档如下所示:

默认实现不保证此方法的同步或原子性属性。提供原子性保证的任何实现都必须重写此方法并记录其并发性质。

HashMap覆盖了默认实现,但没有关于该实现的并发性质的文档说明,但它有这样一个通用声明:

请注意,此实现未同步。如果多个线程同时访问哈希映射,并且其中至少一个线程在结构上修改了映射,则必须在外部进行同步。

因此,它不是线程安全的。

补充说明,不清楚为什么您在启动相应的线程之前调用t1.join()t2.join()

如果您颠倒这些调用

    t1.join();
    t1.start();

为了

    t1.start();
    t1.join();

并且。
    t2.join();
    t2.start();

为了

    t2.start();
    t2.join();

你将会得到输出为0。当然,如果你这样做的话,就不会出现任何并发修改,因为每个线程都会在前一个线程结束后才开始执行。
另一种选择是在外部同步map.merge调用。
public static void incr()
{
    synchronized(map) {map.merge("counter", 1, Integer::sum);}
}

public static void decr()
{
    synchronized(map) {map.merge("counter", -1, Integer::sum);}
}

6

对于像 HashMap 这样的数据结构,通常情况下是不支持线程安全的,因此询问一个特定的单个方法是否线程安全有些奇怪。

如果你需要同时修改一个map,你需要寻找一个支持并发更新的实现,通常这样的实现都是通过实现 ConcurrentMap 接口来展现出来的。对于这些类,即使使用 default 实现也足够了,因为它是在其他接口方法的基础上实现的,这些接口方法保证是线程安全的。但是,如果你需要保证 原子性 ,你需要找到一个适当地重写了方法的实现,例如 ConcurrentHashMap:

merge

… 整个方法调用是原子性的。一些尝试更新此映射表的操作可能会被阻塞,直到计算完成,因此计算应该是短小简单的,并且不能尝试更新此 Map 的任何其他映射。

要修复和简化您的示例:

Map<String, Integer> map = new ConcurrentHashMap<>();
for (int i = 0; i < 10000; i++) {
    Thread t1 = new Thread(() -> map.merge("counter", -1, Integer::sum));
    Thread t2 = new Thread(() -> map.merge("counter",  1, Integer::sum));
    t1.start();
    t2.start();
    t1.join();
    t2.join();
}
System.out.println(map);

请注意,在您的原始代码中,您在开始之前调用了join(),这没有任何效果。由于您没有在start()之后执行join(),因此您的代码可能会在所有线程完成之前打印map,因此即使HashMap是线程安全的,它也可能打印出非零值。
像上面的代码一样在start()之后执行join()将正确地等待完成,但最多允许两个并发更新操作。
为提高并发性,您应该放弃手动创建线程:
ExecutorService threadPool
    = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
Map<String, Integer> map = new ConcurrentHashMap<>();
for (int i = 0; i < 10000; i++) {
    threadPool.execute(() -> map.merge("counter", -1, Integer::sum));
    threadPool.execute(() -> map.merge("counter",  1, Integer::sum));
}
threadPool.shutdown();
threadPool.awaitTermination(1, TimeUnit.DAYS);
System.out.println(map);

这样可以实现高于2的并发,但由于工作线程可能能够像循环调度新任务一样快速执行此简单任务,因此导致的并发仍可能接近2。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接