java 8 Map.merge是线程安全的吗?

时间:2017-12-05 06:49:43

标签: java java-8

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);
    }

}

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

2 个答案:

答案 0 :(得分:7)

merge界面上Map的Javadoc说:

  

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

虽然HashMap会覆盖默认实现,但它没有关于该实现的并发属性的文档,但确实有这个一般性声明:

  

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

因此它不是线程安全的。

P.S。,你不清楚为什么在启动相应的线程之前调用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);}
}

答案 1 :(得分:6)

要求修改像HashMap这样的数据结构的特定单一方法的线程安全性有点奇怪,这种数据结构通常被证明不是线程安全的

如果要同时修改映射,则必须查找支持并发更新的实现,这通常通过实现ConcurrentMap来显示。对于这些映射,即使default实现也足够了,因为它是在其他接口方法上实现的,这些方法保证是线程安全的。但是,如果您希望保证 atomicity ,则需要使用适当的实现覆盖该方法的实现,例如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() ,这无效。由于您在join()之后没有执行start(),因此您的代码可以在所有线程完成之前打印map,因此它可以打印非零值,即使HashMap 1}}是线程安全的。

在上面的代码中执行join()之后的start()将正确等待完成,但最多允许两次并发更新操作。

要提高并发性,您应该放弃手动创建线程:

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,但由于工作线程可能能够以您的循环可以调度新作业的速度执行此简单任务,因此产生的并发性可能仍然接近于两个。