为什么同步块优于同步方法?

107

我已经开始学习线程中的同步。

同步方法:

public class Counter {

   private static int count = 0;

   public static synchronized int getCount() {
      return count;
   }

   public synchronized setCount(int count) {
      this.count = count;
   }

}

同步代码块:

public class Singleton {

   private static volatile Singleton _instance;

   public static Singleton getInstance() {
      if (_instance == null) {
         synchronized(Singleton.class) {
            if (_instance == null)
               _instance = new Singleton();
         }
      }
      return _instance;
   }
}

什么情况下我应该使用synchronized方法和synchronized块?

为什么synchronized块比synchronized方法更好?


1
你还应该阅读http://en.wikipedia.org/wiki/Double-checked_locking。 - René Link
1
在第一种情况下,我会使用AtomicInteger,在第二种情况下,我会使用“枚举”来创建单例。 - Peter Lawrey
哪里说它更好?你的问题基于什么? - user207421
10个回答

130

并不是更好,只是不同。

当你同步一个方法时,实际上是在将其与对象本身同步。对于静态方法,你会将其与对象类同步。因此,以下两段代码执行方式相同:

public synchronized int getCount() {
    // ...
}

这就像是你所写的一样。

public int getCount() {
    synchronized (this) {
        // ...
    }
}

如果您希望控制同步到特定对象,或者只想将方法的一部分同步到对象上,则可以指定一个 synchronized 块。如果在方法声明中使用 synchronized 关键字,则会将整个方法同步到对象或类。


是的,对于类变量和方法也是一样的,只不过获取相应的Class对象的监视器而不是实例(this)的监视器。 - Aniket Thakur
12
这是不同的,但是你永远不希望在“this”上进行同步,原因非常充分,所以我认为这个答案有点偏离重点。 - Voo
9
我强烈不同意你说永远不会想要在“this”上进行同步。实际上,在你想要依次调用一堆同步方法时,这是有益的。你可以在整个过程中对对象进行同步,而不必担心每个方法调用都需要释放并重新获取锁。有许多不同的同步模式可以使用,并且每种模式根据情况都有其优缺点。 - Erick Robertson
2
一个同步方法和一个只有顶层块是synchronized(someObj)的方法之间绝对没有任何区别,实际上编译器生成的同步代码就好像someObj==this一样。因此,这样做没有任何优势,但会向外界暴露内部细节,显然破坏了封装性。嗯,有一个优点:你可以节省大约20个字节。 - Voo
8
@Voo 类的线程行为不是内部细节,它是代码契约的一部分。这经常被省略,但我能否在并发环境中使用类可能是其使用的重要组成部分。synchronized关键字泄漏了*您如何管理线程。我认为这并不是坏事,因为您可以更改线程控制并删除该关键字,而不会破坏任何客户端代码。 - Spina
1
你可以移除它(严格来说这也是不正确的),但是你不能添加它,否则可能会导致破坏性变化。类是否需要在内部锁定与其提供的功能无关。是的,某些东西是否可以并发使用是代码合同的一部分,但它如何实现是一个实现细节。 - Voo

61

区别在于获取锁的位置:

  • 同步方法会获取整个对象的锁。这意味着当一个线程运行该方法时,其他线程无法使用该对象中的任何同步方法。

  • 同步代码块在synchronized关键字后的括号内获取对象锁。这意味着直到同步块退出,其他线程无法获取锁定对象的锁。

因此,如果要锁定整个对象,请使用同步方法。如果要使对象的其他部分对其他线程可访问,请使用同步代码块。

如果谨慎选择锁定对象,则同步代码块会导致较少的竞争,因为不会锁定整个对象/类。

同样适用于静态方法:同步的静态方法将获取整个类对象的锁,而静态方法内的同步代码块将获取括号内对象的锁。


如果我有一个同步块并且一个线程正在其中运行,那么另一个线程可以进入对象并在对象的其他地方运行同步块吗?如果我有一个同步方法并且一个线程正在其中执行,那么没有其他线程可以在该对象中执行或仅在对象的同步区域中执行吗? - Evil Washing Machine
在我看来,这个答案可能会误导新手,因为它谈到了如何锁定“整个对象”。新手经常会认为当一个对象被“锁定”时,那将阻止其他线程使用它。但这完全不是真的。锁定对象唯一能够防止的是,它防止其他线程同时对相同的对象进行加锁。如果线程A锁定了某个对象,线程B仍然可以使用和修改它,只要线程B不试图同时对其进行加锁即可。 - Solomon Slow
另外,如果同步方法锁定了“整个对象”?这是否意味着同步块仅锁定对象的一部分?我不认为这是您想要表达的,但对于新手来说很容易误解您的意思。我认为您想比较使用一个或多个“私有”锁对象进行“细粒度”锁定的类与使用其自己的公开可见实例进行“粗略”锁定的类之间的优点。 - Solomon Slow
@EvilWashingMachine。如果我们正在使用同步块并且一个线程在其中运行,则每个对象只能执行块内代码的一个线程,而且当一个线程在其中运行时,同一对象的其他线程不能执行任何同步块(它们可以进入方法但不能执行块内代码)或方法,因为当前对象被锁定。同步方法的情况也是如此,当一个线程在其中运行时,同一对象的其他线程不能执行任何其他同步块或方法。对象锁定意味着1个线程/对象。 - Debashis

60

虽然通常不是问题,但从安全角度考虑,最好在私有对象上使用 synchronized,而不是在方法上使用。

在方法上使用会导致你使用对象本身的锁来提供线程安全。这种机制可能会使恶意用户获得你的对象锁并永久占用它,有效地阻塞其他线程。即使一个非恶意的用户也可能在无意中做同样的事情。

如果你使用私有数据成员的锁,就可以避免这种情况,因为恶意用户无法获得你的私有对象的锁。

private final Object lockObject = new Object();

public void getCount() {
    synchronized( lockObject ) {
        ...
    }
}

这个技巧提到在Bloch的《Effective Java》(第二版),条目#70中。


3
甚至不必故意为之,使用从其他地方获取的对象进行锁定(比如想同步访问该对象)可能看起来非常容易和无害。最好避免这些问题。 - Voo
9
“恶意用户”指的是使用你的代码进行恶意活动的人。他们会通过篡改代码来破坏自己或其他用户的应用程序,对系统造成潜在的风险和损害。 - Erick Robertson
14
如果您的公共API提供某种服务,并且API公开了一些可变对象,其操作取决于锁定该对象,那么一个恶意客户端可以获取该对象,锁定它,然后无限循环保持锁定状态。这可能会阻止其他客户端能够正确运行,基本上就是一种拒绝服务攻击。因此,他们不只是破坏自己的应用程序。 - wolfcastle
2
这个技巧在《Effective Java(第三版)》的第82项中。 - Rangi Keen
1
@arnobpl,虽然您的评论现在已经“过时”,但我有回复。分为两部分:(A)除非您提供一些明确的手段让恶意用户做到这一点,否则他们无法控制您的代码选择锁定哪个对象;(B)如果一个线程对某个对象O进行同步处理,它对任何其他线程对某个_不同_对象P进行同步处理没有影响。 - Solomon Slow
显示剩余2条评论

14
同步块同步方法的区别如下:
  1. 同步块缩小了锁定范围,但是同步方法的锁定范围是整个方法。
  2. 同步块的性能更好,因为只有关键部分被锁住,但是同步方法的性能不如块。
  3. 同步块提供对锁的细粒度控制,但是同步方法在当前对象上表示这个类级别的锁。
  4. 同步块可能会抛出NullPointerException,但是同步方法不会抛出。
  5. 同步块: synchronized(this){}

    同步方法: public synchronized void fun(){}


同步块可以抛出NullPointerException,而同步方法不会,如果发生在同步方法中,则不会通过日志传播。 - BugsOverflow

7
定义“better”的意思。同步块只是更好的原因是它使您能够:
1.在不同的对象上进行同步
2.限制同步范围
现在您的具体示例是双重检查锁定模式的示例,这是可疑的(在旧的Java版本中它已经失效了,并且很容易出错)。
如果您初始化的成本较低,则最好使用final字段立即进行初始化,而不是在第一次请求时进行初始化,这也会消除同步的需要。

4
synchronized应该只在需要使你的类线程安全时使用。实际上,大多数类都不应该使用synchronized。synchronized方法仅会为该对象提供锁定,并且仅在其执行期间有效。如果你真的想让你的类线程安全,你应该考虑将变量设为volatile同步访问。

使用synchronized方法的问题之一是该类的所有成员都会使用相同的锁定,这会使程序变慢。在你的情况下,同步方法和块不会有任何区别。我的建议是使用专用的锁定,并使用同步块来完成操作,例如:

public class AClass {
private int x;
private final Object lock = new Object();     //it must be final!

 public void setX() {
    synchronized(lock) {
        x++;
    }
 }
}

如果你真的想让你的类线程安全,你应该考虑将变量声明为volatile。这是一个非常专业的工具。在一些特定的情况下它比锁更好用,但是新手往往不理解如何编写多线程代码,经常会错误地使用volatile。尽管它看起来很简单,但是volatile实际上是一个高级主题。 - Solomon Slow

4

同步块和同步方法之间的一个经典区别是,同步方法锁定整个对象,而同步块只锁定代码块内的代码。

同步方法:这两个同步方法基本上都禁用了多线程。因此,一个线程完成method1()后,另一个线程会等待Thread1完成。

class SyncExerciseWithSyncMethod {

    public synchronized void method1() {
        try {
            System.out.println("In Method 1");
            Thread.sleep(5000);
        } catch (Exception e) {
            System.out.println("Catch of method 1");
        } finally {
            System.out.println("Finally of method 1");
        }

    }

    public synchronized void method2() {
        try {
            for (int i = 1; i < 10; i++) {
                System.out.println("Method 2 " + i);
                Thread.sleep(1000);
            }
        } catch (Exception e) {
            System.out.println("Catch of method 2");
        } finally {
            System.out.println("Finally of method 2");
        }
    }
}

Output
-------

In Method 1

Finally of method 1

Method 2 1

Method 2 2

Method 2 3

Method 2 4

Method 2 5

Method 2 6

Method 2 7

Method 2 8

Method 2 9

Finally of method 2

同步块(Synchronized block):允许多个线程在同一时间内访问同一个对象[实现多线程]。
class SyncExerciseWithSyncBlock {

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

    public void method1() {
        synchronized (lock1) {
            try {
                System.out.println("In Method 1");
                Thread.sleep(5000);
            } catch (Exception e) {
                System.out.println("Catch of method 1");
            } finally {
                System.out.println("Finally of method 1");
            }
        }

    }

    public void method2() {

        synchronized (lock2) {
            try {
                for (int i = 1; i < 10; i++) {
                    System.out.println("Method 2 " + i);
                    Thread.sleep(1000);
                }
            } catch (Exception e) {
                System.out.println("Catch of method 2");
            } finally {
                System.out.println("Finally of method 2");
            }
        }
    }

}


Output
-------
In Method 1

Method 2 1

Method 2 2

Method 2 3

Method 2 4

Method 2 5

Finally of method 1

Method 2 6

Method 2 7

Method 2 8

Method 2 9

Finally of method 2

“同步方法锁定整个对象。”这可能会加强新手对Java中“同步”概念的常见误解。许多新手认为,当任何一个线程“锁定了一个对象”时,其他线程将无法访问相同的对象。但是,当然,这不是synchronized的工作方式。无论我们谈论同步方法还是同步块或两者都是,synchronized唯一防止的是,它防止两个或更多线程同时在同一个对象上同步。 - Solomon Slow
“Synchronized block just locks the code within the block.”这句话可能会给新手带来两个完全错误的想法:(1)他们可能会认为代码可以被“锁定”,(2)他们可能会认为synchronized块无法与任何其他synchronized块或方法交互。代码不能被锁定。代码不需要被锁定。代码是不可变的。需要用synchronized保护的是可变数据。只要所有synchronized块或方法都在同一个实例上同步,那么任意两个或更多个synchronized块或方法都可以协同工作。 - Solomon Slow

2

在您的情况下,两者是等价的!

同步静态方法等效于在相应类对象上同步块。

实际上,当您声明同步静态方法时,锁定会在与类对象对应的监视器上获得。

public static synchronized int getCount() {
    // ...
}

相同
public int getCount() {
    synchronized (ClassName.class) {
        // ...
    }
}

如果我同步一个非静态方法,那么会得到什么结果? - Sabapathy
1
那么它是每个实例或您创建的对象。两个线程可以独立地处理两个不同对象中的相同方法。 - Aniket Thakur

1

这并不是关于最佳使用的问题,而是取决于使用情况或场景。

同步方法

整个方法可以标记为同步,从而在 this 引用(实例方法)或类(静态方法)上产生隐式锁。这是一种非常方便的机制来实现同步。

步骤 一个线程访问同步方法。它隐式地获取锁并执行代码。 如果其他线程想要访问上述方法,则必须等待。不能获得锁的线程将被阻塞,并且必须等待直到锁被释放。

同步块

为了在特定的代码块上获取对象锁,同步块是最合适的选择。由于块已足够,使用同步方法将是浪费。

更具体地说,在同步块中,可以定义要获取锁的对象引用。


0

由于锁定操作是昂贵的,因此在使用同步块时,只有当_instance == null时才进行锁定,在_instance最终初始化后,您将不再进行锁定。但是,当您在方法上进行同步时,即使在_instance初始化后,您也会无条件地进行锁定。这就是双重检查锁定优化模式http://en.wikipedia.org/wiki/Double-checked_locking背后的思想。


你说的没错,但这并没有回答 OP 的问题。OP 提供了两个非常不同的代码示例,其中一个展示了使用双重检查锁定来初始化单例对象,但实际问题是关于 Java 中synchronized块与synchronized方法的相对优点。 - Solomon Slow

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