我需要在我的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语句将两个资源锁定在calculate方法内?
答案 0 :(得分:3)
否,您不需要使用嵌套的同步语句来将两个资源都锁定在calculate方法内。但是您还需要在get方法中添加synced子句,读取/写入资源都需要同步。
public int getCounter(){
synchronized(mutex){
return counter;
}
}
public int getMux(){
synchronized(mutex){
return multiPlexer;
}
}
答案 1 :(得分:2)
因此,您已经正确了解了互斥锁(和原子性)。但是,Java内存模型还有一个额外的缺陷,那就是必须考虑的 visibility 。
基本上,读取和写入都必须同步,否则不能保证读取可以看到写入。对于您的使用者来说,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 Concurrency in Practice (我强烈推荐),因为他详细介绍了此类内容,并提供了一些简单的示例,这些示例非常清楚地说明您必须进行 同步。总是在读写上都是如此。
规范的相关部分是第17章,尤其是section 17.4 Memory Model.
只引用相关部分:
Java编程语言内存模型的工作方式是检查执行跟踪中的每个读取,并根据某些规则检查该读取所观察到的写入是否有效。
这一点很重要。 检查每次读取。仅检查写入内容,然后假定读取内容可以看到写入内容,该模型就无法工作。
可以通过先发生后关系来命令两个动作。如果一个动作发生在另一个动作之前,则第一个动作对第二个动作可见,并在第二个动作之前排序。
before-before是允许读取看到写入的内容。没有它,JVM可以自由地以可能无法看到写入的方式(例如将值提升到寄存器中)来优化程序。
before-before关系定义何时进行数据争用。
如果一组同步边沿S是最小集,则该边沿就足够了,以使S的传递性闭包与程序顺序决定了执行中所有发生在边沿之前的情况。这组是唯一的。
根据以上定义,它是:
在监视器上进行的每个后续锁定之前,发生监视器上的解锁。
在随后每次对该字段进行读取之前,都会对易失字段(第8.3.1.4节)进行写操作。
因此,before-before定义何时进行(或不进行)数据争用。通过以上描述,我认为volatile
的工作方式显而易见。对于监视器(您的mutex
),请务必注意,先发生是由解锁和随后的锁定建立的,,因此要为读取建立 happens-before ,您确实需要在读取之前再次锁定监视器。
我们说,如果在执行跟踪的部分顺序之前发生,则允许对变量v的读r观察对w的写w:
r在w之前没有排序(即,不是hb(r,w)),并且
没有向v写入w'(即,没有向v写入w'使得hb(w,w')和hb(w',r))
非正式地,如果不发生任何事件,则允许r读取写入w的结果-在下令阻止该读取之前。
“允许观察”表示读取实际上将看到写入。因此,发生-之前是我们需要查看写入的内容,并且锁(程序中的mutex
或volatile
都可以使用。
还有更多(其他原因导致事前发生),并且java.utli.concurrent
中的类也有API,这也将导致内存排序(和可见性)语义。但是程序中有很多细节。
答案 2 :(得分:1)
只使用一个mutex
保护两个字段都可以(甚至更好)。监视对象与字段或保存它们的对象没有任何关系。实际上,它是good practice to use dedicated lock objects(而不是this
)。您只需要确保对所有这些字段的访问最终都使用同一监视器即可。
但是,将setter包装在一个同步块中是不够的,对(非易失性)变量(包括getter)的所有访问都必须位于同一监视器之后。
答案 3 :(得分:1)
由于计数器和 multiPlexer 同时锁定,因此可以将它们视为单个资源。此外,可以将类 CounterMultiplexer 的整个实例视为单个资源。在Java中,将实例视为单一资源是最普遍的方法。对于这种情况,引入了特殊的 synchronozed 方法:
public synchronized void claculate(){
counter ++;
multiPlexer = multiPlexer*counter;
}
public synchronized int getCounter(){
return counter;
}
public synchronized int getMux(){
return multiPlexer;
}
不再需要 mutex 变量。
答案 4 :(得分:0)
解决此类问题的另一种方法是让所有成员变量都是最终变量,然后让calculate方法返回CounterMultiplexer的新实例。这样可以确保CounterMultiplexer的任何实例始终处于一致状态。根据您使用此类的方式,此方法可能需要在此类之外进行同步。
在getter中进行同步仍然允许另一个线程从更改之前读取两个成员变量之一,并从之后读取一个成员。