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

时间:2018-11-02 14:23:30

标签: java multithreading

我需要在我的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方法内?

5 个答案:

答案 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的结果-在下令阻止该读取之前。

“允许观察”表示读取实际上将看到写入。因此,发生-之前是我们需要查看写入的内容,并且锁(程序中的mutexvolatile都可以使用。

还有更多(其他原因导致事前发生),并且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中进行同步仍然允许另一个线程从更改之前读取两个成员变量之一,并从之后读取一个成员。