synchronized(hashmap.get(data)) 线程安全吗?

Is synchronized(hashmap.get(data)) thread safe?

假设我有一个名为 Foo 的 java class,它包含一个名为 h 的 ConcurrentHashMap 属性。

还假设 Foo class 有 2 个这样定义的方法:

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

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

假设现在从 2 个不同的线程首先调用 fooMethod1() 并在 fooMethod2() 之后调用它。

不知道有没有可能在this.h.get("example")调用fooMethod1()和上面get返回的对象同步之间,可以有[=的交错16=] 调用 fooMethod2().

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

在您的代码段中,我假设名为 hConcurrentMap 已经存在并且从未被替换,因此我们没有关于该对象的 CPU 核心缓存可见性问题存在与否。所以不需要将 ConcurrentMap 标记为 volatile.

您的 h 地图是 ConcurrentHashMap, which is a ConcurrentMap。所以多个线程同时调用get方法是安全的。

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

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

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

示例代码

这是一个完整的示例,与您的代码一致。

我们建立您的 Foo 对象,它在其构造函数中实例化并填充一个名为 mapConcurrentMap(而不是您代码中的 h)。

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

我们立即休眠第二个方法,以帮助确保第一个线程继续进行。我们无法确定第一个线程 运行,但长时间睡眠可以帮助它们进入我们为该实验准备的顺序。

当第二个方法在其线程中休眠时,其线程中的第一个方法获取了包含单词“cat”的 String 的内在锁。我们通过在 ConcurrentMap.

上调用 get 以线程安全的方式检索该对象

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

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

这段代码使用了简单的新 try-with-resources 语法和虚拟线程,这些语法和虚拟线程是由 Loom 项目的 Project Loom. I am running with a preliminary build 基于早期访问 Java 16 带给我们的。但是 Loom 的东西是除了这里的重点,这个演示可以使用老式代码。这里的 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

I don't know if it's possible that between this.h.get("example") call in fooMethod1() and the synchronization of object returned by the above get, there can be the interleaving of this.h.get("example") call in fooMethod2().

是的,在您指定的位置可能会出现交错。

synchronized 互斥是针对各自 get 调用的 结果 ,而不是 get 调用本身。

因此,如果第三个线程正在更新地图,则两个 get("example") 调用可能 return 不同的值,并且您不会在同一个地图条目上互斥。

其次,在以下代码段中:

synchronized(this.h.get("example")) {
    ...
}

只有 { ... } 块中的代码得到互斥。

第三点要注意的是 this.h 不能保证是线程安全的,除非 h 被声明为 final.


最后,几乎不可能说这是线程安全的还是非线程安全的。线程安全很难 属性 精确定义,但它非正式地意味着代码将按预期(或指定)运行,而不管线程的数量,以及所有可能的执行交错​​模式。

在您的示例中,您没有提供足够的代码,也没有明确说明您的期望。