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

qco9c6ql  于 2021-06-27  发布在  Java
关注(0)|答案(2)|浏览(521)

假设我有一个名为foo的java类,其中包含一个 ConcurrentHashMap 叫h。
也假设 Foo 类有两个定义如下的方法:

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

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

假设现在它被称为第一 fooMethod1() 之后呢 fooMethod2() 从两个不同的线程。
我不知道有没有可能 this.h.get("example") 呼叫 fooMethod1() 以及上面返回的对象的同步 get ,可以有 this.h.get("example") 呼叫 fooMethod2() .

ff29svar

ff29svar1#

我不是这方面的Maven,但你的代码在我看来是线程安全的。
在你的片段中,我假设 ConcurrentMap 命名 h 已经存在并且永远不会被替换,因此对于该对象是否存在,我们没有cpu核心缓存可见性问题。所以不需要标记 ConcurrentMap 作为 volatile .
你的 h Map是一个 ConcurrentHashMap ,这是一个 ConcurrentMap . 所以多线程调用 get 方法是安全的。
我假设我们确定键存在Map "example" . 以及 ConcurrentHashMap 不允许空值,因此如果您将密钥放入Map中,则必须有一个值供我们检索和锁定。
两个方法在从并发Map中检索的对象的同一个内在锁上同步。因此,不同线程中的两个方法中的任何一个首先访问从Map中检索到的对象的方法都将获得一个锁 synchronized 而另一个线程则等待该锁被释放。当然,我假设 "example" 在线程运行过程中没有变化。
这个 get 方法必须返回完全相同的对象才能使两个线程同步。这是我在你的计划中看到的主要弱点。我建议您在协调两个线程时采用不同的方法。但是,从技术上讲,如果所有这些条件都成立,那么您当前的代码应该可以工作。

示例代码

下面是一个完整的代码示例。
我们建立您的 Foo 对象,它在其构造函数中示例化并填充 ConcurrentMap 命名 map (而不是 h 在代码中)。
然后我们启动一对线程,每个线程调用两个方法中的一个。
我们立即休眠第二个方法,以帮助确保第一个线程继续前进。我们不能确定哪个线程先运行,但是长时间的睡眠可以帮助它们进入我们为这个实验准备的顺序。
当第二个方法在其线程中休眠时,其线程中的第一个方法获取 String 包含“猫”字。我们通过调用 getConcurrentMap .
然后,第一个方法在持有此锁时进入睡眠状态。通过查看控制台上的输出,我们可以推断其线程中的第二个方法必须处于等待状态,等待 "cat" 弦的锁。
最终第一个方法会唤醒、继续并释放cat锁。通过控制台输出,我们可以看到第二个方法的线程获得cat锁并继续其工作。
这段代码使用了projectloom提供的简单的try-with-resources语法和虚拟线程。我正在运行基于早期访问Java16的ProjectLoom的初步构建。但是织布机的东西在这里是无关紧要的,这个演示可以与旧的学校代码。这里的项目代码更简单、更干净。

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
pprl5pva

pprl5pva2#

我不知道有没有可能 this.h.get("example") 呼叫 fooMethod1() 而上述get返回的对象的同步,可以有 this.h.get("example") 呼叫 fooMethod2() .
是的,在你指定的点上可能有交错。
这个 synchronized 互斥是基于各自的结果 get 电话,不是电话 get 他们自称。
因此,如果第三个线程正在更新Map get("example") 调用可以返回不同的值,并且不会在同一Map条目上得到互斥。
其次,在下面的片段中:

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

只有 { ... } 块得到互斥。
第三件要注意的是 this.h 不保证线程安全,除非 h 已声明为 final .
最后,几乎不可能判断这是否是线程安全的。线程安全性是一个很难精确定义的属性,但非正式地说,它意味着无论线程数多少,代码都将按预期(或指定)运行,并且对于所有可能的执行交织模式都是如此。
在您的示例中,您没有提供足够的代码,也没有清楚地说明您的期望是什么。

相关问题