如何在java多线程中锁定多个资源



我需要在java类的一个方法中锁定多个对象。例如,查看以下类:

public class CounterMultiplexer {
private int counter =0;
private int multiPlexer =5;
private Object mutex = new Object();
public void calculate(){
synchronized(mutex){
counter ++;
multiPlexer = multiPlexer*counter;
}
}
public int getCounter(){
return counter;
}
public int getMux(){
return multiPlexer;
}
}

在上面的代码中,我有两个可以由多个线程访问的资源。这两种资源是counter和multiPlexer属性。正如您在上面的代码中看到的,我已经使用互斥锁锁定了这两个资源。

这种锁定方式正确吗?我需要使用嵌套的Synchronized语句来锁定计算方法中的两个资源吗?

所以您对互斥(和原子性)的想法是正确的。然而,Java内存模型中还有一个额外的问题,即必须考虑可见性

基本上,读取和写入都必须同步,否则读取不能保证看到写入。对于getter,JIT很容易将这些值提升到寄存器中,并且永远不会重新读取它们,这意味着写入的值永远不会被看到。这被称为数据竞赛,因为写入和读取的顺序无法保证。

为了打破数据竞争,您必须使用内存排序语义。这可以归结为同步读取和写入。每次需要在任何地方使用同步时,都必须这样做,而不仅仅是在上面的特定情况下。

您可以使用几乎任何方法(如AtomicInteger),但最简单的方法可能是重用现有的mutex,或者使两个基元值成为volatile。两者都有效,但必须至少使用一个。

public class CounterMultiplexer {
private int counter =0;
private int multiPlexer =5;
private Object mutex = new Object();
public void claculate(){
synchronized(mutex){
counter ++;
multiPlexer = multiPlexer*counter;
}
}
public int getCounter(){
synchronized(mutex){
return counter;
}
}
public int getMux(){
synchronized(mutex){
return multiPlexer;
}
}
}

因此,要深入了解这一点,我们必须阅读规范。您还可以获得Brian Goetz的《实践中的Java并发》,我强烈推荐他,因为他详细介绍了这类内容,并用简单的例子表明,您必须始终在读写上同步。

规范的相关章节是第17章,特别是第17.4节内存模型。

仅引用相关部分:

Java编程语言内存模型的工作原理是检查执行跟踪中的每个读取,并根据某些规则检查该读取所观察到的写入是否有效。

这个位很重要检查每个读数模型不能通过单独检查写入,然后假设读取可以看到写入来工作。

两个操作可以按先发生后发生的关系排序。如果一个动作发生在另一个动作之前,那么第一个动作对第二个动作可见并在第二个之前排序。

之前发生的事情允许读取看到写入。如果没有它,JVM就可以自由地优化程序,以避免看到写入(比如将值提升到寄存器中)。

先发生后关系定义了数据竞争发生的时间。

如果一组同步边S是最小集,使得S与程序顺序的传递闭包决定了执行中所有发生在边之前的情况,那么它就足够了。这套是独一无二的。

根据以上定义可知:

显示器上的解锁发生在该显示器上的每次后续锁定之前。

对易失性字段(§8.3.1.4)的写入发生在该字段的每次后续读取之前。

在定义数据竞争何时发生(或不发生)之前发生这种情况。我认为volatile的工作原理从上面的描述中是显而易见的。对于监视器(您的mutex),需要注意的是,发生在通过解锁和随后的锁定建立之前,因此,要在读取之前建立,您确实需要在读取之前再次锁定监视器。

我们说,如果在执行跟踪的部分顺序之前发生,则允许变量v的读取r观察到对v的写入w:

r不在w之前排序(即,不是hb(r,w)的情况),并且

不存在对v的中间写入w’(即,不将w’写入v,使得hb(w,w’)和hb(w',r))。

非正式地,如果在命令阻止读取之前没有发生任何事情,则允许读取r查看写入w的结果。

"允许观察";意思是读实际会看到写。所以之前发生的事情就是我们需要看到的写入,并且锁定(程序中的mutex)或volatile都可以工作。

还有很多(其他事情会导致之前发生的事情),还有API,java.utli.concurrent中的类也会导致内存排序(和可见性)语义。但你的节目中有血腥的细节。

不需要使用嵌套的同步语句来锁定计算方法中的两个资源。但是,您需要在get方法中添加synchronized子句。此外,读取/写入资源都需要同步。

public int getCounter(){
synchronized(mutex){
return counter;
}
}
public int getMux(){
synchronized(mutex){
return multiPlexer;
}
}

只使用一个mutex来保护两个字段是可以的(甚至更好)。监视器对象实际上与字段或包含字段的对象无关。事实上,使用专用锁对象(而不是this)是一种很好的做法。您只需要确保所有对这些字段的访问最终都使用同一个监视器。

然而,仅仅将setter封装在同步块中是不够的,对(非易失性)变量(包括getter)的所有访问都必须在同一个监视器后面。

由于计数器multiPlexer同时被锁定,因此它们可以被视为单个资源。此外,类CounterMultiplexer的整个实例可以被视为单个资源。在Java中,将实例视为单个资源是最普遍的方法。对于这种情况,引入了特殊的同步方法:

public synchronized void claculate(){
counter ++;
multiPlexer = multiPlexer*counter;
}
public synchronized int getCounter(){
return counter;
}
public synchronized int getMux(){
return multiPlexer;
}

不再需要互斥变量。

解决这类问题的另一种方法是让所有成员变量都是最终变量,并让计算方法返回CounterMultiplexer的新实例。这保证了CounterMultiplexer的任何实例始终处于一致状态。根据您使用此类的方式,这种方法可能需要此类之外的同步。

getter内部的同步仍然允许另一个线程从更改之前读取两个成员变量中的一个,从更改之后读取一个。

最新更新