微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

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

如何解决synchronized(hashmap.get(data)) 线程安全吗?

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

还假设 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")中的fooMethod1()调用和上面get返回的对象同步之间,可能存在{{1}的交错} 调用this.h.get("example")

解决方法

我不是这方面的专家,但您的代码在我看来确实是线程安全的。

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

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

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

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

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

示例代码

这里有一个完整的示例,沿着您的代码行。

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

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

我们立即休眠第二个方法以帮助确保第一个线程继续进行。我们无法确定哪个线程最先运行,但长时间的睡眠可以帮助它们按照我们在此实验中计划的顺序进行。

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

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

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

此代码使用简单的新 try-with-resources 语法和 Project Loom 带给我们的虚拟线程。我正在运行基于早期访问 Java 16 的 Loom 项目的 preliminary build。但是 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
,

不知道有没有可能在this.h.get("example")中的fooMethod1()调用和上面get返回的对象同步之间,this.h.get("example")调用fooMethod2()之间可能存在交错{1}}。

是的,在您指定的点可能存在交错。

synchronized 互斥在各个 get 调用的结果上,而不是在 get 调用本身上。

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

其次,在以下代码段中:

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

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

要注意的第三件事是,除非 this.h 已声明为 h,否则不能保证 final 是线程安全的。


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

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。