Java同步方法锁定对象还是方法?

230
如果我在同一个类中有2个同步方法,但每个方法访问不同的变量,那么2个线程是否可以同时访问这2个方法?锁定是发生在对象上,还是会针对同步方法内部的变量变得更具体?
示例:
class X {

    private int a;
    private int b;

    public synchronized void addA(){
        a++;
    }

    public synchronized void addB(){
        b++;
    }

}

2个线程可以同时访问类X的同一个实例,执行x.addA()x.addB()吗?

11个回答

231
如果你声明了一个方法为 synchronized(就像你通过输入public synchronized void addA()那样做),你会对整个对象进行同步,因此两个访问同一对象的不同变量的线程将彼此阻塞。
如果您想一次只对一个变量进行同步,这样两个线程在访问不同的变量时不会相互阻塞,您需要在synchronized ()块中分别对它们进行同步。 如果ab是对象引用,您将使用:
public void addA() {
    synchronized( a ) {
        a++;
    }
}

public void addB() {
    synchronized( b ) {
        b++;
    }
}

但是由于它们是原始数据类型,所以你无法这样做。

我建议你使用 AtomicInteger 替代:

import java.util.concurrent.atomic.AtomicInteger;

class X {

    AtomicInteger a;
    AtomicInteger b;

    public void addA(){
        a.incrementAndGet();
    }

    public void addB(){ 
        b.incrementAndGet();
    }
}

231
如果你在方法上同步,那么你会锁住整个对象,因此两个线程访问这个相同对象的不同变量也会阻塞彼此。这有点误导人。在方法上同步在功能上等同于在方法体周围使用synchronized (this)语句块。对象"this"并没有被锁定,而是将对象"this"用作互斥锁,并且防止该方法与其他也在"this"上同步的代码段同时执行。它对未同步的其他"this"字段/方法没有影响。 - Mark Peters
15
是的,这确实很误导人。举个例子,看看这个链接:https://dev59.com/n2Yq5IYBdhLWcg3weQky - 总结:锁定只能在同步方法级别进行,对象的实例变量可以被其他线程访问。 - mac
7
第一个示例基本上是有问题的。如果'a'和'b'是对象,例如'Integer',则在应用'++'运算符时,你正在同步替换为不同对象的实例。请注意保持原文意思,使语言更加通俗易懂。 - Holger
1
修正你的答案并初始化AtomicInteger:AtomicInteger a = new AtomicInteger(0); - Mehdi
也许这个答案应该更新一下,使用这个关于在对象本身上同步的解释:https://dev59.com/HWkv5IYBdhLWcg3w1kIJ#10324280 - lucasvc
为什么“在方法上同步,那么你就同步整个对象”? - LookIntoEast

82

在方法声明中使用synchronized是语法糖,相当于这个:

 public void addA() {
     synchronized (this) {
          a++;
     }
  }

静态方法中,它是以下语法糖:

 ClassA {
     public static void addA() {
          synchronized(ClassA.class) {
              a++;
          }
 }

我认为如果当时Java的设计者了解现在对于同步的理解,他们就不会添加这种语法糖了,因为它往往导致并发实现出现问题。


5
不正确。同步方法会生成不同于同步(object)的字节码。虽然功能等效,但它不仅是语法糖。 - Steve Kuo
14
我认为“语法糖”并没有严格定义为等效于字节码的东西。重点在于它具有功能上的等效性。 - Yishai
2
如果Java的设计者已经知道了关于监视器的已知知识,他们本应该以不同的方式进行操作,而不是基本上模拟Unix的内部结构。Per Brinch Hansen在看到Java并发原语时说:“显然我徒劳无功” - user207421
这是真的。OP给出的示例似乎会锁定每个方法,但实际上它们都在同一个对象上锁定。非常具有欺骗性的语法。使用Java十多年后,我不知道这一点。因此,出于这个原因,我会避免使用同步方法。我一直以为对于每个定义为同步的方法,都会创建一个不可见的对象。 - Peter Quiring

33

来自synchronized方法的Java™教程:

首先,对于同一对象上的两次调用同步方法不可能交错。当一个线程执行一个对象的同步方法时,所有调用该对象的同步方法的其他线程都会阻塞(暂停执行),直到第一个线程完成该对象。

来自同步块的Java™教程:

同步语句还可以用于通过细粒度同步来提高并发性。例如,假设MsLunch类有两个实例字段c1和c2,它们永远不会同时使用。这些字段的所有更新必须同步,但是没有理由防止c1的更新与c2的更新交替进行,这样做会通过创建不必要的阻塞来降低并发性。相反,我们创建两个对象仅提供锁,而不是使用同步方法或以其他方式使用与此关联的锁。(强调在原文中)

假设您有两个互不干扰的变量。因此,您希望同时从不同的线程访问每个变量。您需要将锁定定义在类Object而不是对象类本身上,如下所示(来自第二个Oracle链接的示例):

public class MsLunch {

    private long c1 = 0;
    private long c2 = 0;

    private Object lock1 = new Object();
    private Object lock2 = new Object();

    public void inc1() {
        synchronized(lock1) {
            c1++;
        }
    }

    public void inc2() {
        synchronized(lock2) {
            c2++;
        }
    }
}

这是可行的,因为线程有自己的堆栈,但共享实例成员,包括任何声明的锁定对象。 - theRiley
这应该是被接受的答案。 - stackoverflowed

14

锁定的是对象,而不是方法。方法内访问哪些变量是无关紧要的。

将 "synchronized" 添加到方法中意味着运行代码的线程必须在继续之前获取对象上的锁。 将 "static synchronized" 添加到方法中意味着运行代码的线程必须在继续之前获取类对象上的锁。 或者您可以像这样包装代码块:

public void addA() {
    synchronized(this) {
        a++;
    }
}

以便您可以指定必须获取锁的对象。

如果您想避免在包含对象上锁定,您可以选择以下方法之一:


7

来自 Oracle 文档链接

将方法设为 synchronized 有两个效果:

首先,不可能在同一对象上调用两个同步方法时交错执行。当一个线程在执行对象的同步方法时,调用该对象的所有其他同步方法的线程都会阻塞(暂停执行),直到第一个线程完成对象的操作为止。

其次,当同步方法退出时,它自动与同一对象的任何后续同步方法的调用建立 happens-before 关系。这保证了对对象状态的更改可见于所有线程。

请参阅此文档页面以了解内置锁和锁行为。

这将回答你的问题:在同一对象 x 上,在一个同步方法执行期间,不能同时调用 x.addA() 和 x.addB()。


5

如果您有一些未同步的方法,并且正在访问和更改实例变量。在您的示例中:

 private int a;
 private int b;

当其他线程在同一对象的同步方法中时,任意数量的线程可以同时访问这些非同步方法,并且可以对实例变量进行更改。

例如:
 public void changeState() {
      a++;
      b++;
    }

你需要避免非同步方法访问实例变量并更改它的情况,否则使用同步方法就没有意义。
在下面的场景中:
class X {

        private int a;
        private int b;

        public synchronized void addA(){
            a++;
        }

        public synchronized void addB(){
            b++;
        }
     public void changeState() {
          a++;
          b++;
        }
    }

同时只能有一个线程进入addA或addB方法,但任意数量的线程可以进入changeState方法。由于对象级别锁定,没有两个线程可以同时进入addA和addB,但是任意数量的线程可以同时进入changeState。


4

这个例子(虽然不太好看)可以更深入地了解锁定机制。如果incrementA同步的,而incrementB没有同步,则incrementB将立即执行,但如果incrementB也被同步,则它必须“等待”incrementA完成,然后incrementB才能做它的工作。

这两个方法都被调用到单个实例-对象上,在这个例子中是:job,竞争的线程是aThreadmain

尝试在incrementB中使用'同步的'和不使用,您将看到不同的结果。如果incrementB也是'同步的',那么它必须等待incrementA()完成。每种情况运行多次。

class LockTest implements Runnable {
    int a = 0;
    int b = 0;

    public synchronized void incrementA() {
        for (int i = 0; i < 100; i++) {
            this.a++;
            System.out.println("Thread: " + Thread.currentThread().getName() + "; a: " + this.a);
        }
    }

    // Try with 'synchronized' and without it and you will see different results
    // if incrementB is 'synchronized' as well then it has to wait for incrementA() to finish

    // public void incrementB() {
    public synchronized void incrementB() {
        this.b++;
        System.out.println("*************** incrementB ********************");
        System.out.println("Thread: " + Thread.currentThread().getName() + "; b: " + this.b);
        System.out.println("*************** incrementB ********************");
    }

    @Override
    public void run() {
        incrementA();
        System.out.println("************ incrementA completed *************");
    }
}

class LockTestMain {
    public static void main(String[] args) throws InterruptedException {
        LockTest job = new LockTest();
        Thread aThread = new Thread(job);
        aThread.setName("aThread");
        aThread.start();
        Thread.sleep(1);
        System.out.println("*************** 'main' calling metod: incrementB **********************");
        job.incrementB();
    }
}

3
您可以像以下这样做。在这种情况下,您将使用a和b上的锁来进行同步,而不是使用"this"上的锁。我们不能使用int,因为原始值没有锁,因此我们使用Integer。
class x{
   private Integer a;
   private Integer b;
   public void addA(){
      synchronized(a) {
         a++;
      }
   }
   public synchronized void addB(){
      synchronized(b) {
         b++;
      }
   }
}

3
在Java中的同步机制中,如果一个线程想要进入一个synchronized方法/块,它将会获取以下锁:
  • 该对象的所有同步非静态方法
    而不仅仅是该线程正在使用的一个同步方法
  • 该类的所有同步静态方法
因此,调用同步方法addA()的线程将会在addA()addB()上获取锁,因为两者都是同步的。
所以,具有相同对象的其他线程无法执行addB()

3

是的,它会阻塞另一个方法,因为同步方法适用于整个类对象,正如指出的那样...但无论如何,在执行addA或addB方法时,它只会阻塞另一个线程执行,因为当它完成任务后...一个线程将释放对象,另一个线程将访问其他方法,以此类推,完美地工作。

我的意思是,“同步”恰好是为了阻止其他线程在特定代码执行时访问另一个线程。因此,最终这段代码将正常工作。

最后需要注意的是,如果存在'a'和'b'变量,而不仅仅是唯一变量'a'或其他名称,则无需同步这些方法,因为访问其他变量(其他内存位置)是完全安全的。

class X {

private int a;
private int b;

public void addA(){
    a++;
}

public void addB(){
    b++;
}}

同样有效


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接