首页 > 解决方案 > synchronized(hashmap.get(data)) 线程安全吗?

问题描述

假设我有一个名为 Foo 的 java 类,其中包含一个ConcurrentHashMap名为 h 的属性。

假设Foo该类还定义了 2 个方法,如下所示:

public void fooMethod1() {
    synchronized(this.h.get("example")) {
        ...
    }
}

public void fooMethod2() {
    synchronized(this.h.get("example")) {
        ...
    }
}

现在假设它是从 2 个不同的线程中调用fooMethod1()的。fooMethod2()

不知道this.h.get("example")call infooMethod1()和上面返回的object的同步之间是否有可能存在call inget的交错。this.h.get("example")fooMethod2()

标签: javaconcurrencysynchronizationsynchronizedconcurrenthashmap

解决方案


我不是这方面的专家,但你的代码对我来说确实是线程安全的。

在您的代码段中,我假设ConcurrentMapnamedh已经存在并且永远不会被替换,因此对于该对象是否存在,我们没有 CPU 核心缓存可见性问题。所以不需要标记ConcurrentMapas volatile

你的h地图是一个ConcurrentHashMap,这是一个ConcurrentMap。所以多个线程同时调用get方法是安全的。

我假设我们确定 key 存在映射"example"。并且ConcurrentHashMap不允许空值,因此如果您将键放入映射中,则必须有一个值供我们检索和锁定。

您的两种方法都在从并发映射中检索到的任何对象的相同内在锁上同步。因此,不同线程中的两个方法中的任何一个首先获得对从映射中检索到的对象的访问权,都会获得一个锁,synchronized而另一个线程等待直到该锁被释放。当然,我假设 key 的映射条目"example"在我们的线程运行期间没有改变。

get为了使两个线程同步,映射上的方法必须返回完全相同的对象。这是我在您的计划中看到的主要弱点。我建议您采用不同的方法来协调您的两个线程。但是,从技术上讲,如果所有这些条件都成立,那么您当前的代码应该可以工作。

示例代码

这是您的代码行中的完整示例。

我们建立您的Foo对象,该对象在其构造函数中实例化并填充ConcurrentMap命名map(而不是h在您的代码中)。

然后我们启动一对线程,每个线程调用两个方法中的一个。

我们立即休眠第二种方法,以帮助确保第一个线程继续进行。我们无法确定哪个线程首先运行,但长时间的睡眠可以帮助它们进入我们打算用于此实验的顺序。

当第二种方法在其线程中休眠时,其线程中的第一种方法获取String包含单词“cat”的内在锁。get我们通过调用a以线程安全的方式检索该对象ConcurrentMap

然后第一种方法在持有这个锁的同时进入睡眠状态。通过查看控制台上的输出,我们可以推断其线程中的第二个方法必须处于等待状态,等待释放"cat"字符串的锁。

最终,第一个方法唤醒、继续并释放猫锁。通过控制台输出,我们可以看到第二种方法的线程获得了猫锁并继续其工作。

这段代码使用了Project Loom提供给我们的简单的新 try-with-resources 语法和虚拟线程。我正在运行基于早期访问 Java 16 的 Project Loom 的初步构建。但是 Loom 的东西在这里无关紧要,这个演示可以使用老式代码。这里的这个 Project Loom 代码更简单、更干净。

package work.basil.example;

import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Foo
{
    private ConcurrentMap < Integer, String > map = null;

    public Foo ( )
    {
        this.map = new ConcurrentHashMap <>();
        this.map.put( 7 , "dog" );
        this.map.put( 42 , "cat" );
    }

    public void fooMethod1 ( )
    {
        System.out.println( "Starting fooMethod1 at " + Instant.now() );
        synchronized ( this.map.get( 42 ) )
        {
            System.out.println( "fooMethod1 got the intrinsic lock on cat string. " + Instant.now() );
            // Pause a while to show that the other thread must be waiting on on the intrinsic `synchronized` lock of the String "cat".
            try { Thread.sleep( Duration.ofSeconds( 5 ) ); } catch ( InterruptedException e ) { e.printStackTrace(); }
            System.out.println( "Continuing fooMethod1 at " + Instant.now() );
        }
    }

    public void fooMethod2 ( )
    {
        System.out.println( "Starting fooMethod2 at " + Instant.now() ); // Sleep to make it more likely that the other thread gets a chance to run.
        try { Thread.sleep( Duration.ofSeconds( 2 ) ); } catch ( InterruptedException e ) { e.printStackTrace(); }
        synchronized ( this.map.get( 42 ) )
        {
            System.out.println( "fooMethod2 got the intrinsic lock on cat string. " + Instant.now() );
            System.out.println( "Continuing fooMethod2 at " + Instant.now() );
        }
    }

    public static void main ( String[] args )
    {
        System.out.println( "INFO - Starting run of  `main`. " + Instant.now() );
        Foo app = new Foo();
        try (
                ExecutorService executorService = Executors.newVirtualThreadExecutor() ;
        )
        {
            executorService.submit( ( ) -> app.fooMethod1() );
            executorService.submit( ( ) -> app.fooMethod2() );
        }
        // At this point, flow-of-control blocks until submitted tasks are done. Then executor service is automatically shutdown as an `AutoCloseable` in Project Loom.
        System.out.println( "INFO - Done running `main`. " + Instant.now() );
    }
}

跑的时候。

INFO - Starting run of  `main`. 2021-01-05T23:35:25.804193Z
Starting fooMethod1 at 2021-01-05T23:35:25.871971Z
fooMethod1 got the intrinsic lock on cat string. 2021-01-05T23:35:25.888092Z
Starting fooMethod2 at 2021-01-05T23:35:25.875959Z
Continuing fooMethod1 at 2021-01-05T23:35:30.893112Z
fooMethod2 got the intrinsic lock on cat string. 2021-01-05T23:35:30.893476Z
Continuing fooMethod2 at 2021-01-05T23:35:30.893784Z
INFO - Done running `main`. 2021-01-05T23:35:30.894273Z

注意:发送到的文本System.out并不总是按预期顺序在控制台上打印出来。验证时间戳以确保运行时间。在这个示例运行中,第三行Starting fooMethod2实际上发生在第二行之前fooMethod1 got the intrinsic lock

所以我会手动将它们重新排列成时间顺序。

INFO - Starting run of  `main`. 2021-01-05T23:35:25.804193Z
Starting fooMethod1 at 2021-01-05T23:35:25.871971Z
Starting fooMethod2 at 2021-01-05T23:35:25.875959Z
fooMethod1 got the intrinsic lock on cat string. 2021-01-05T23:35:25.888092Z
Continuing fooMethod1 at 2021-01-05T23:35:30.893112Z
fooMethod2 got the intrinsic lock on cat string. 2021-01-05T23:35:30.893476Z
Continuing fooMethod2 at 2021-01-05T23:35:30.893784Z
INFO - Done running `main`. 2021-01-05T23:35:30.894273Z

推荐阅读