使用同步方法而不是同步块有什么优势吗?

有人能告诉我同步方法比同步块的优势与一个例子吗?

295554 次浏览

使用同步块,您可以有多个同步器,因此多个同时但不冲突的事情可以同时进行。

唯一真正的区别是同步块可以选择在哪个对象上同步。同步方法只能使用'this'(或同步类方法的对应Class实例)。例如,它们在语义上是等价的:

synchronized void foo() {
...
}


void foo() {
synchronized (this) {
...
}
}

后者更灵活,因为它可以竞争任何对象的关联锁,通常是成员变量。它也更细粒度,因为您可以在块之前和块之后执行并发代码,但仍然在方法中。当然,您也可以通过将并发代码重构为单独的非同步方法来轻松地使用同步方法。使用任何使代码更容易理解的方法。

注意:静态同步方法和块工作在Class对象上。

public class MyClass {
// locks MyClass.class
public static synchronized void foo() {
// do something
}


// similar
public static void foo() {
synchronized(MyClass.class) {
// do something
}
}
}

主要的区别是,如果你使用同步块,你可以锁定一个对象,而不是,这允许更灵活。

假设您有一个消息队列和多个消息生产者和消费者。我们不希望生产者相互干扰,但是消费者应该能够检索消息,而不必等待生产者。 我们只需要创建一个对象

Object writeLock = new Object();

从现在开始,每当制作人想要添加一条新信息时,我们就会锁定它:

synchronized(writeLock){
// do something
}

因此,消费者可能仍会阅读,而生产者将被锁定。

谁能告诉我同步方法比同步块的优势与一个例子?谢谢。

与块相比,使用同步方法并没有明显的优势。

也许唯一的一个(但我不认为它是一个优势)是你不需要包括对象引用this

方法:

public synchronized void method() { // blocks "this" from here....
...
...
...
} // to here

布洛克:

public void method() {
synchronized( this ) { // blocks "this" from here ....
....
....
....
}  // to here...
}

看到了吗?一点好处都没有。

比方法有优势,主要是灵活性,因为你可以使用另一个对象作为锁,而同步方法将锁定整个对象。

比较:

// locks the whole object
...
private synchronized void someInputRelatedWork() {
...
}
private synchronized void someOutputRelatedWork() {
...
}

vs。

// Using specific locks
Object inputLock = new Object();
Object outputLock = new Object();


private void someInputRelatedWork() {
synchronized(inputLock) {
...
}
}
private void someOutputRelatedWork() {
synchronized(outputLock) {
...
}
}

另外,如果方法增长了,你仍然可以保持同步段的分离:

 private void method() {
... code here
... code here
... code here
synchronized( lock ) {
... very few lines of code here
}
... code here
... code here
... code here
... code here
}

大多数情况下,我使用它来同步对列表或映射的访问,但我不想阻止对对象的所有方法的访问。

在下面的代码中,修改列表的线程不会阻塞等待正在修改映射的线程。如果方法在对象上是同步的,那么每个方法都必须等待,即使它们所做的修改不会冲突。

private List<Foo> myList = new ArrayList<Foo>();
private Map<String,Bar) myMap = new HashMap<String,Bar>();


public void put( String s, Bar b ) {
synchronized( myMap ) {
myMap.put( s,b );
// then some thing that may take a while like a database access or RPC or notifying listeners
}
}


public void hasKey( String s, ) {
synchronized( myMap ) {
myMap.hasKey( s );
}
}


public void add( Foo f ) {
synchronized( myList ) {
myList.add( f );
// then some thing that may take a while like a database access or RPC or notifying listeners
}
}


public Thing getMedianFoo() {
Foo med = null;
synchronized( myList ) {
Collections.sort(myList);
med = myList.get(myList.size()/2);
}
return med;
}

同步的方法

优点:

  • 您的IDE可以指示同步方法。
  • 语法更加紧凑。
  • 强制将同步块分割为单独的方法。

缺点:

  • 与此同步,因此外部人员也可以与之同步。
  • 将代码移到同步块之外更加困难。

同步块

优点:

  • 允许为锁使用私有变量,从而将锁强制留在类内部。
  • 同步块可以通过搜索变量的引用来找到。

缺点:

  • 语法更复杂,因此使代码更难阅读。

就我个人而言,我更喜欢使用同步方法,类只关注需要同步的东西。这样的类应该尽可能小,所以应该很容易检查同步。其他人不需要关心同步。

一般来说,除了显式显示对象的监视器和隐式this对象之外,这些基本是相同的。同步方法的一个缺点,我认为有时被忽视的是,在使用“this”引用进行同步时,你会打开外部对象锁定同一对象的可能性。如果你遇到了,这可能是一个非常微妙的错误。在内部显式Object或其他现有字段上同步可以避免这个问题,完全封装了同步。

Synchronized方法用于锁定所有对象 Synchronized块用于锁定特定对象

同步的方法

同步方法有两个作用 首先,当一个线程正在为一个对象执行同步方法时,所有为同一对象调用同步方法的其他线程将阻塞(暂停执行),直到第一个线程处理完该对象。< / p >

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

注意,构造函数不能同步——在构造函数中使用synchronized关键字是一个语法错误。同步构造函数没有意义,因为只有创建对象的线程才能在构造对象时访问它。

同步语句

与同步方法不同,同步语句必须指定提供内在锁的对象:大多数情况下,我使用它来同步对列表或映射的访问,但我不想阻塞对对象的所有方法的访问。

Q:内在锁和同步 同步是围绕一个称为内在锁或监视器锁的内部实体构建的。(API规范通常将此实体简单地称为“监视器”)内在锁在同步的两个方面都发挥作用:强制独占访问对象的状态,并建立对可见性至关重要的happens-before关系。< / p >

每个对象都有一个与之相关的内在锁。按照惯例,需要独占和一致访问对象字段的线程必须在访问对象字段之前获得对象的内在锁,然后在使用完对象字段时释放内在锁。线程在获得锁和释放锁之间拥有内在锁。只要一个线程拥有一个内在锁,其他线程就不能获得相同的锁。另一个线程在试图获取锁时将阻塞。

package test;


public class SynchTest implements Runnable {
private int c = 0;


public static void main(String[] args) {
new SynchTest().test();
}


public void test() {
// Create the object with the run() method
Runnable runnable = new SynchTest();
Runnable runnable2 = new SynchTest();
// Create the thread supplying it with the runnable object
Thread thread = new Thread(runnable,"thread-1");
Thread thread2 = new Thread(runnable,"thread-2");
//      Here the key point is passing same object, if you pass runnable2 for thread2,
//      then its not applicable for synchronization test and that wont give expected
//      output Synchronization method means "it is not possible for two invocations
//      of synchronized methods on the same object to interleave"


// Start the thread
thread.start();
thread2.start();
}


public synchronized  void increment() {
System.out.println("Begin thread " + Thread.currentThread().getName());
System.out.println(this.hashCode() + "Value of C = " + c);
//      If we uncomment this for synchronized block, then the result would be different
//      synchronized(this) {
for (int i = 0; i < 9999999; i++) {
c += i;
}
//      }
System.out.println("End thread " + Thread.currentThread().getName());
}


//    public synchronized void decrement() {
//        System.out.println("Decrement " + Thread.currentThread().getName());
//    }


public int value() {
return c;
}


@Override
public void run() {
this.increment();
}
}

用同步方法,块和不同步交叉检查不同的输出。

同步方法可以使用反射API进行检查。这对于测试一些合约很有用,比如模型中的所有方法都是同步的

下面的代码段打印哈希表的所有同步方法:

for (Method m : Hashtable.class.getMethods()) {
if (Modifier.isSynchronized(m.getModifiers())) {
System.out.println(m);
}
}

同步线程。 1)永远不要在线程中使用synchronized(this),它不起作用。与(this)同步使用当前线程作为锁定线程对象。由于每个线程都独立于其他线程,因此不存在同步协调。 2)代码测试表明,在Mac上的Java 1.6中,方法同步不起作用。 3) synchronized(lockObj),其中lockObj是所有线程同步的公共共享对象。 4) ReenterantLock.lock()和.unlock()工作。

下面的代码显示了这些点。它还包含线程安全的Vector,它可以代替ArrayList,以表明添加到Vector的许多线程不会丢失任何信息,而添加到ArrayList的许多线程可能会丢失信息。 0)当前代码显示由于竞态条件导致的信息丢失 A)注释当前标记的A行,并取消注释它上面的A行,然后运行,方法丢失数据,但它不应该。 B)反向步骤A,取消注释B和//结束块}。然后运行以查看结果是否有数据丢失 C)注释掉B,取消注释C,运行,看到同步(this)丢失数据,正如预期的那样。 没有时间完成所有的变化,希望这有助于。 如果同步(此),或方法同步工作,请说明您测试的Java和操作系统的版本。谢谢你。< / p >
import java.util.*;


/** RaceCondition - Shows that when multiple threads compete for resources
thread one may grab the resource expecting to update a particular
area but is removed from the CPU before finishing.  Thread one still
points to that resource.  Then thread two grabs that resource and
completes the update.  Then thread one gets to complete the update,
which over writes thread two's work.
DEMO:  1) Run as is - see missing counts from race condition, Run severa times, values change
2) Uncomment "synchronized(countLock){ }" - see counts work
Synchronized creates a lock on that block of code, no other threads can
execute code within a block that another thread has a lock.
3) Comment ArrayList, unComment Vector - See no loss in collection
Vectors work like ArrayList, but Vectors are "Thread Safe"
May use this code as long as attribution to the author remains intact.
/mf
*/


public class RaceCondition {
private ArrayList<Integer> raceList = new ArrayList<Integer>(); // simple add(#)
//  private Vector<Integer> raceList = new Vector<Integer>(); // simple add(#)


private String countLock="lock";    // Object use for locking the raceCount
private int raceCount = 0;        // simple add 1 to this counter
private int MAX = 10000;        // Do this 10,000 times
private int NUM_THREADS = 100;    // Create 100 threads


public static void main(String [] args) {
new RaceCondition();
}


public RaceCondition() {
ArrayList<Thread> arT = new ArrayList<Thread>();


// Create thread objects, add them to an array list
for( int i=0; i<NUM_THREADS; i++){
Thread rt = new RaceThread( ); // i );
arT.add( rt );
}


// Start all object at once.
for( Thread rt : arT ){
rt.start();
}


// Wait for all threads to finish before we can print totals created by threads
for( int i=0; i<NUM_THREADS; i++){
try { arT.get(i).join(); }
catch( InterruptedException ie ) { System.out.println("Interrupted thread "+i); }
}


// All threads finished, print the summary information.
// (Try to print this informaiton without the join loop above)
System.out.printf("\nRace condition, should have %,d. Really have %,d in array, and count of %,d.\n",
MAX*NUM_THREADS, raceList.size(), raceCount );
System.out.printf("Array lost %,d. Count lost %,d\n",
MAX*NUM_THREADS-raceList.size(), MAX*NUM_THREADS-raceCount );
}   // end RaceCondition constructor






class RaceThread extends Thread {
public void run() {
for ( int i=0; i<MAX; i++){
try {
update( i );
}    // These  catches show when one thread steps on another's values
catch( ArrayIndexOutOfBoundsException ai ){ System.out.print("A"); }
catch( OutOfMemoryError oome ) { System.out.print("O"); }
}
}


// so we don't lose counts, need to synchronize on some object, not primitive
// Created "countLock" to show how this can work.
// Comment out the synchronized and ending {, see that we lose counts.


//    public synchronized void update(int i){   // use A
public void update(int i){                  // remove this when adding A
//      synchronized(countLock){            // or B
//      synchronized(this){             // or C
raceCount = raceCount + 1;
raceList.add( i );      // use Vector
//          }           // end block for B or C
}   // end update


}   // end RaceThread inner class




} // end RaceCondition outter class
这里已经说过,同步块可以使用用户定义的变量作为锁对象,当同步函数只使用“this”时。当然,你也可以对函数中需要同步的部分进行操作。 但是每个人都说synchronized函数和block之间没有区别,block覆盖了使用“this”作为锁对象的整个函数。这是不对的,不同的是字节码,将在这两种情况下产生。在同步块使用的情况下,应该分配本地变量,其中包含引用“this”。因此,函数的大小会稍微大一点(如果你只有几个函数,这就无关紧要了)

你可以在这里找到更详细的解释: http://www.artima.com/insidejvm/ed2/threadsynchP.html < / p >

关于使用同步块的重要提示:小心你使用的锁对象!

上面user2277816的代码片段说明了这一点,其中对字符串字面值的引用被用作锁定对象。 意识到字符串字面值在Java中是自动互缩的,您应该开始看到问题所在:在字面值“锁”上同步的每段代码都共享同一个锁!这很容易导致完全不相关的代码段发生死锁

您需要注意的不仅仅是String对象。装箱的原语也是一种危险,因为autoboxing和valueOf方法可以重用相同的对象,这取决于值。

更多信息见: https://www.securecoding.cert.org/confluence/display/java/LCK01-J.+Do+not+synchronize+on+objects+that+may+be+reused < / p >

通常在方法级别上使用锁太粗鲁了。为什么要通过锁定整个方法来锁定一段不能访问任何共享资源的代码呢?因为每个对象都有一个锁,所以可以创建虚拟对象来实现块级同步。 块级别更有效,因为它不锁定整个方法

这里有一些例子

方法级

class MethodLevel {


//shared among threads
SharedResource x, y ;


public void synchronized method1() {
//multiple threads can't access
}
public void synchronized method2() {
//multiple threads can't access
}


public void method3() {
//not synchronized
//multiple threads can access
}
}

块级别

class BlockLevel {
//shared among threads
SharedResource x, y ;


//dummy objects for locking
Object xLock = new Object();
Object yLock = new Object();


public void method1() {
synchronized(xLock){
//access x here. thread safe
}


//do something here but don't use SharedResource x, y
// because will not be thread-safe
synchronized(xLock) {
synchronized(yLock) {
//access x,y here. thread safe
}
}


//do something here but don't use SharedResource x, y
//because will not be thread-safe
}//end of method1
}

(编辑)

对于Collection,比如VectorHashtable,当ArrayListHashMap不同步时,它们是同步的,你需要设置synchronized关键字或调用Collections synchronized方法:

Map myMap = Collections.synchronizedMap (myMap); // single lock for the entire map
List myList = Collections.synchronizedList (myList); // single lock for the entire list

来自Java规范摘要: http://www.cs.cornell.edu/andru/javaspec/17.doc.html < / p >

同步语句(§14.17)计算对对象的引用; 然后,它尝试对该对象执行锁定操作,但没有执行 继续操作,直到锁定操作成功完成. ...

同步方法(§8.4.3.5)自动执行锁定动作 当它被调用时;直到锁定动作结束,才执行它的主体 成功完成。如果方法是实例方法,它 锁定与它被调用的实例关联的锁 (也就是说,在执行过程中将被称为this的对象 方法的主体)。如果方法是静态的,它锁定 中表示类的Class对象关联的锁 哪个方法被定义. ...

基于这些描述,我想说以前的大多数答案都是正确的,同步方法可能对静态方法特别有用,否则你必须弄清楚如何获得“Class对象”,该对象表示定义方法的类。

编辑:我原本以为这些是对实际Java规范的引用。澄清一下,本页只是对规范的总结/解释

当java编译器将源代码转换为字节码时,它处理同步方法和同步块的方式非常不同。

当JVM执行一个同步方法时,执行线程识别出该方法的method_info结构体设置了ACC_SYNCHRONIZED标志,然后它自动获取对象的锁,调用该方法,并释放锁。如果发生异常,线程自动释放锁。

另一方面,同步方法块绕过了JVM对获取对象锁和异常处理的内置支持,并要求显式地用字节代码编写功能。如果读取带有同步块的方法的字节代码,您将看到十多个额外的操作来管理此功能。

这显示了生成同步方法和同步块的调用:

public class SynchronizationExample {
private int i;


public synchronized int synchronizedMethodGet() {
return i;
}


public int synchronizedBlockGet() {
synchronized( this ) {
return i;
}
}
}

synchronizedMethodGet()方法生成以下字节代码:

0:  aload_0
1:  getfield
2:  nop
3:  iconst_m1
4:  ireturn

下面是来自synchronizedBlockGet()方法的字节代码:

0:  aload_0
1:  dup
2:  astore_1
3:  monitorenter
4:  aload_0
5:  getfield
6:  nop
7:  iconst_m1
8:  aload_1
9:  monitorexit
10: ireturn
11: astore_2
12: aload_1
13: monitorexit
14: aload_2
15: athrow

synchronized方法和block之间的一个显著区别是,synchronized block通常会减小锁的作用域。由于锁定的范围与性能成反比,因此只锁定关键部分的代码总是更好的。使用同步块的一个最好的例子是单例模式中的双重检查锁定,其中不锁定整个getInstance()方法,我们只锁定用于创建单例实例的代码的关键部分。这极大地提高了性能,因为只需要锁定一次或两次。

在使用同步方法时,如果混合使用静态同步方法和非静态同步方法,则需要格外小心。

在实际应用中,同步方法相对于同步块的优势在于它们更能抵抗白痴;因为您不能选择任意对象来锁定,所以您不能滥用synchronized方法语法来做一些愚蠢的事情,比如锁定字符串文字或锁定从线程下面更改的可变字段的内容。

另一方面,使用同步方法,您无法保护锁不被任何可以获得对象引用的线程获取。

因此,在方法上使用synchronized作为修饰符可以更好地保护你的奶牛免受伤害,而将synchronized块与私有final锁对象结合使用则可以更好地保护你自己的代码免受奶牛的伤害。

在同步方法的情况下,锁将在对象上获得。但是如果你使用同步块,你可以选择指定一个对象来获取锁。

例子:

    Class Example {
String test = "abc";
// lock will be acquired on String  test object.
synchronized (test) {
// do something
}


lock will be acquired on Example Object
public synchronized void testMethod() {
// do some thing
}


}

唯一的区别是:与同步方法不同,同步块允许颗粒状锁定

基本上,synchronized块或方法被用于通过避免内存不一致错误来编写线程安全代码。

这个问题很老了,在过去的7年里,很多事情都发生了变化。 为了线程安全,引入了新的编程结构

你可以通过使用高级并发API而不是synchronied块来实现线程安全。这个文档页面提供了良好的编程结构来实现线程安全。

< <强> < em >锁对象/ em > < / >强支持简化许多并发应用程序的锁定习惯用法。

< >强执行人< / >强定义了用于启动和管理线程的高级API。concurrent提供的执行器实现提供了适合大型应用程序的线程池管理。

< >强并发集合< / >强使它更容易管理大量的数据集合,并可以大大减少同步的需要。

< >强原子变量< / >强具有最小化同步和帮助避免内存一致性错误的特性。

ThreadLocalRandom(在JDK 7中)提供了从多个线程有效地生成伪随机数。

synchronized更好的替代品是ReentrantLock,它使用Lock API

一个可重入互斥锁,其基本行为和语义与使用同步方法和语句访问的隐式监视锁相同,但具有扩展功能。

锁的例子:

class X {
private final ReentrantLock lock = new ReentrantLock();
// ...


public void m() {
lock.lock();  // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}

其他编程结构也可以参考java . util . concurrentjava.util.concurrent.atomic包。

参考这个相关的问题:

同步vs锁定

我知道这是一个老问题,但通过我快速阅读这里的回答,我没有看到任何人提到有时synchronized方法可能是错误的锁 从Java并发实践(第72页):

public class ListHelper<E> {
public List<E> list = Collections.syncrhonizedList(new ArrayList<>());
...


public syncrhonized boolean putIfAbsent(E x) {
boolean absent = !list.contains(x);
if(absent) {
list.add(x);
}
return absent;
}

上面的代码具有线程安全的外观。然而,现实并非如此。在这种情况下,锁在类的实例上获得。然而,列表有可能被另一个不使用该方法的线程修改。正确的方法是使用

public boolean putIfAbsent(E x) {
synchronized(list) {
boolean absent = !list.contains(x);
if(absent) {
list.add(x);
}
return absent;
}
}

上面的代码将阻止所有线程试图修改列表修改列表,直到同步块完成。

TLDR;既不使用synchronized修饰符,也不使用synchronized(this){...}表达式,而是使用synchronized(myLock){...},其中myLock是保存私有对象的最终实例字段。


在方法声明中使用synchronized修饰符与在方法体中使用synchronized(..){ }表达式的区别如下:

  • 方法签名上指定的synchronized修饰符
    1. 在生成的JavaDoc中可见,
    2. 在测试方法的修饰符修饰符。同步时,可以通过反射编程确定,
    3. synchronized(this) { .... }相比,需要更少的类型和缩进
    4. (取决于你的IDE)在类大纲和代码完成中可见,
    5. 在非静态方法上声明时使用this对象作为锁,在静态方法上声明时使用外围类。
    6. 李< / ol > < / >
    7. synchronized(...){...}表达式允许
      1. 为了只同步方法主体部分的执行,
      2. 在构造函数或(静态)初始化块中使用,
      3. 选择控制同步访问的锁对象。
      4. 李< / ol > < / >

然而,使用synchronized修饰符或带有this作为锁定对象的synchronized(...) {...}(如synchronized(this) {...})也有同样的缺点。两者都使用它自己的实例作为锁对象进行同步。这很危险,因为不仅对象本身,而且其他持有该对象引用的外部对象/代码也可以将其用作同步锁,这可能会产生严重的副作用(性能下降和死锁)。

因此,最佳实践是既不使用synchronized修饰符,也不使用synchronized(...)表达式与this一起作为锁对象,而是使用该对象的私有锁对象。例如:

public class MyService {
private final lock = new Object();


public void doThis() {
synchronized(lock) {
// do code that requires synchronous execution
}
}


public void doThat() {
synchronized(lock) {
// do code that requires synchronous execution
}
}
}

您也可以使用多个锁对象,但是需要特别注意,以确保在嵌套使用时不会导致死锁。

public class MyService {
private final lock1 = new Object();
private final lock2 = new Object();


public void doThis() {
synchronized(lock1) {
synchronized(lock2) {
// code here is guaranteed not to be executes at the same time
// as the synchronized code in doThat() and doMore().
}
}


public void doThat() {
synchronized(lock1) {
// code here is guaranteed not to be executes at the same time
// as the synchronized code in doThis().
// doMore() may execute concurrently
}
}


public void doMore() {
synchronized(lock2) {
// code here is guaranteed not to be executes at the same time
// as the synchronized code in doThis().
// doThat() may execute concurrently
}
}
}

我想这个问题是关于线程安全单例带有双重检查锁定的惰性初始化之间的区别。当我需要实现某些特定的单例时,我总是会参考这篇文章。

这是一个线程安全单例:

// Java program to create Thread Safe
// Singleton class
public class GFG
{
// private instance, so that it can be
// accessed by only by getInstance() method
private static GFG instance;


private GFG()
{
// private constructor
}


//synchronized method to control simultaneous access
synchronized public static GFG getInstance()
{
if (instance == null)
{
// if instance is null, initialize
instance = new GFG();
}
return instance;
}
}

优点:

  1. 延迟初始化是可能的。

  2. 它是线程安全的。

缺点:

  1. getInstance()方法是同步的,因此它会导致性能变慢,因为多个线程不能同时访问它。

这是一个带有双重检查锁定的惰性初始化:

// Java code to explain double check locking
public class GFG
{
// private instance, so that it can be
// accessed by only by getInstance() method
private static GFG instance;


private GFG()
{
// private constructor
}


public static GFG getInstance()
{
if (instance == null)
{
//synchronized block to remove overhead
synchronized (GFG.class)
{
if(instance==null)
{
// if instance is null, initialize
instance = new GFG();
}


}
}
return instance;
}
}

优点:

  1. 延迟初始化是可能的。

  2. 它也是线程安全的。

  3. 克服了synchronized关键字导致的性能下降。

缺点:

  1. 第一次,它会影响性能。

  2. 由于双重检查锁定方法是可承受的,所以可以 用于高性能多线程应用。

详情请参考这篇文章:

https://www.geeksforgeeks.org/java-singleton-design-pattern-practices-examples/