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

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

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

  1. public void fooMethod1() {
  2. synchronized(this.h.get("example")) {
  3. ...
  4. }
  5. }
  6. public void fooMethod2() {
  7. synchronized(this.h.get("example")) {
  8. ...
  9. }
  10. }

假设现在它被称为第一 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的初步构建。但是织布机的东西在这里是无关紧要的,这个演示可以与旧的学校代码。这里的项目代码更简单、更干净。

  1. package work.basil.example;
  2. import java.time.Duration;
  3. import java.time.Instant;
  4. import java.util.concurrent.ConcurrentHashMap;
  5. import java.util.concurrent.ConcurrentMap;
  6. import java.util.concurrent.ExecutorService;
  7. import java.util.concurrent.Executors;
  8. public class Foo
  9. {
  10. private ConcurrentMap < Integer, String > map = null;
  11. public Foo ( )
  12. {
  13. this.map = new ConcurrentHashMap <>();
  14. this.map.put( 7 , "dog" );
  15. this.map.put( 42 , "cat" );
  16. }
  17. public void fooMethod1 ( )
  18. {
  19. System.out.println( "Starting fooMethod1 at " + Instant.now() );
  20. synchronized ( this.map.get( 42 ) )
  21. {
  22. System.out.println( "fooMethod1 got the intrinsic lock on cat string. " + Instant.now() );
  23. // Pause a while to show that the other thread must be waiting on on the intrinsic `synchronized` lock of the String "cat".
  24. try { Thread.sleep( Duration.ofSeconds( 5 ) ); } catch ( InterruptedException e ) { e.printStackTrace(); }
  25. System.out.println( "Continuing fooMethod1 at " + Instant.now() );
  26. }
  27. }
  28. public void fooMethod2 ( )
  29. {
  30. System.out.println( "Starting fooMethod2 at " + Instant.now() ); // Sleep to make it more likely that the other thread gets a chance to run.
  31. try { Thread.sleep( Duration.ofSeconds( 2 ) ); } catch ( InterruptedException e ) { e.printStackTrace(); }
  32. synchronized ( this.map.get( 42 ) )
  33. {
  34. System.out.println( "fooMethod2 got the intrinsic lock on cat string. " + Instant.now() );
  35. System.out.println( "Continuing fooMethod2 at " + Instant.now() );
  36. }
  37. }
  38. public static void main ( String[] args )
  39. {
  40. System.out.println( "INFO - Starting run of `main`. " + Instant.now() );
  41. Foo app = new Foo();
  42. try (
  43. ExecutorService executorService = Executors.newVirtualThreadExecutor() ;
  44. )
  45. {
  46. executorService.submit( ( ) -> app.fooMethod1() );
  47. executorService.submit( ( ) -> app.fooMethod2() );
  48. }
  49. // At this point, flow-of-control blocks until submitted tasks are done. Then executor service is automatically shutdown as an `AutoCloseable` in Project Loom.
  50. System.out.println( "INFO - Done running `main`. " + Instant.now() );
  51. }
  52. }

当你跑的时候。

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

注意:文本发送到 System.out 并不总是按预期顺序在控制台上打印。验证时间戳以确定何时运行了什么。在本例中,第三行 Starting fooMethod2 实际上发生在第二行之前 fooMethod1 got the intrinsic lock .
所以我会手动将它们按时间顺序重新排列。

  1. INFO - Starting run of `main`. 2021-01-05T23:35:25.804193Z
  2. Starting fooMethod1 at 2021-01-05T23:35:25.871971Z
  3. Starting fooMethod2 at 2021-01-05T23:35:25.875959Z
  4. fooMethod1 got the intrinsic lock on cat string. 2021-01-05T23:35:25.888092Z
  5. Continuing fooMethod1 at 2021-01-05T23:35:30.893112Z
  6. fooMethod2 got the intrinsic lock on cat string. 2021-01-05T23:35:30.893476Z
  7. Continuing fooMethod2 at 2021-01-05T23:35:30.893784Z
  8. 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条目上得到互斥。
其次,在下面的片段中:

  1. synchronized(this.h.get("example")) {
  2. ...
  3. }

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

相关问题