假设我有一个名为Foo的java类,其中包含一个名称为h的ConcurrentHashMap
属性。
还假设Foo
类有两个这样定义的方法:
public void fooMethod1() {
synchronized(this.h.get("example")) {
...
}
}
public void fooMethod2() {
synchronized(this.h.get("example")) {
...
}
}
现在假设它从两个不同的线程调用第一个fooMethod1()
和第二个fooMethod2()
。
我不知道在fooMethod1()
中的this.h.get("example")
调用和上述get
返回的对象同步之间,是否可能存在fooMethod2()
中this.h.get("example")
调用的交错。
我不知道在
fooMethod1()
中的this.h.get("example")
调用和上述get返回的对象同步之间,fooMethod2()
中是否可能存在this.h.get("example")
调用的交错。
是的,在您指示的点可能存在交错。
synchronized
互斥在各个get
调用的结果上,而不是在get
调用本身上。
因此,如果第三个线程正在更新映射,那么两个get("example")
调用可能会返回不同的值,并且不会在同一映射条目上得到互斥。
其次,在以下片段中:
synchronized(this.h.get("example")) {
...
}
只有CCD_ 18块中的代码得到互斥。
需要注意的第三件事是,除非h
已声明为final
,否则this.h
不能保证是线程安全的。
最后,几乎不可能说这是线程安全的还是线程不安全的。线程安全性是一个很难精确定义的属性,但它非正式地意味着,无论线程数量如何,以及对于所有可能的执行交错模式,代码都将按预期(或指定)运行。
在您的示例中,您没有提供足够的代码,也没有明确说明您的期望是什么。
我不是这方面的专家,但你的代码对我来说确实是线程安全的。
在您的代码片段中,我假设名为h
的ConcurrentMap
已经存在,并且从未被替换,因此对于该对象是否存在,我们不存在CPU核心缓存可见性问题。因此,无需将ConcurrentMap
标记为volatile
。
您的h
映射是ConcurrentHashMap
,也就是ConcurrentMap
。所以多个线程同时调用get
方法是安全的。
我假设我们确信密钥"example"
存在映射。而且ConcurrentHashMap
不允许空值,所以如果您将密钥放入映射中,则必须有一个值供我们检索和锁定。
您的两个方法在从并发映射中检索的任何对象的同一个内在锁上同步。因此,不同线程中的两个方法中,首先访问从映射中检索到的对象的方法获胜,每个synchronized
获得一个锁,而另一个线程则等待该锁释放。当然,我假设"example"
的键的映射条目在线程运行期间不会发生变化。
映射上的get
方法必须返回完全相同的对象才能使两个线程同步。这是我在你的计划中看到的主要弱点。我建议您在协调两个线程时采用不同的方法。但是,从技术上讲,如果所有这些条件都成立,那么您当前的代码应该可以工作。
示例代码
下面是一个完整的代码示例。
我们建立您的Foo
对象,该对象在其构造函数中实例化并填充一个名为map
的ConcurrentMap
(而不是代码中的h
)。
然后我们启动一对线程,每个线程调用两个方法中的一个。
我们立即休眠第二个方法,以帮助确保第一个线程继续进行。我们不能确定哪个线程首先运行,但长时间睡眠可以帮助它们进入我们想要的实验顺序。
当第二个方法在其线程中休眠时,其线程中的第一个方法获取包含单词"的String
的内在锁;猫";。我们通过在ConcurrentMap
上调用get
,以线程安全的方式检索该对象。
然后,第一个方法在持有此锁的同时进入睡眠状态。通过查看控制台上的输出,我们可以推断其线程中的第二个方法必须处于等待状态,等待"cat"
字符串的锁的释放。
最后,第一个方法唤醒,继续,并释放猫锁。通过控制台输出,我们可以看到第二个方法的线程获得了cat锁,并继续其工作。
这段代码使用了ProjectLoom为我们带来的简单的带有资源的新尝试语法和虚拟线程。我正在运行基于早期访问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