synchronized(this) 只锁定同步块还是所有“this”代码?

5
public class ObjectCounter {
    private static long numOfInstances = 0;
    public ObjectCounter(){
        synchronized(this){
        numOfInstances++;
        }
    }
    **public static synchronized long getCount(){
        return numOfInstances;
    }**
//vs//
    **public static long getCount(){
        return numOfInstances;
    }**
}

如果我运行几个线程,其中一些调用静态函数getCount(),而另一些创建新实例。我希望在每次调用getCount()时获得实时的实例数量。

  1. 代码中的这两个选项有什么区别吗?
  2. 如果我锁定“this”,那么难道不应该意味着在构造函数退出同步块之前无法调用getCount()(假设我没有在getCount()上写同步)。
  3. 如果我在代码的某个地方执行同步块,它是否只锁定同步块或所有“this”代码?
  4. 从这里开始编辑:谢谢大家,非常有帮助,但根据你们的答案我还有一些问题。
  5. 如果我理解正确,synchronized(this)块不会影响(或连接到)静态同步函数(在锁定术语中不是numOfInstances增量)吗?
  6. 是否有更好的选择使增量和getCount()函数线程安全?(例如打开一个静态对象并执行synchronized(obj)而不是synchronized(this) - 朋友建议)。
  7. 如果ObjectCounter类中有一个f1()方法(非静态),当一个线程在synchronized(this)中时,其他线程能进入f1()块(不是同步类或具有同步块内部)吗?
  8. 如果ObjectCounter中有一个f1()方法(非静态)和f2()方法(非静态),在f1()中我有一个synchronized(this)块。当一个线程在synchronized(this)块中时,另一个线程能进入f1()块(不是同步类或具有同步块内部)吗?(假设这两个线程“工作”在同一实例上)

在实例构造函数返回之前,绝不能允许调用任何对象的实例方法(例如 getCount())。这甚至只有在构造函数将 this 发布到另一个线程时才可能发生(也称为“从构造函数中泄漏 this”)。如果您认为必须在构造函数内编写 synchronized(this),那么您可能正在犯一个巨大的错误。 - Solomon Slow
4
请勿编辑您的问题以添加更多问题(或更糟糕的是替换您现有的问题),在您得到正确完整的答案后。请另开一个新问题或在正确答案下评论您的后续问题。 - Buurman
所有的答案都非常好而且有用,然而我只能选择一个对我有帮助的,而且我认为只选一个不太公平。如果必须要选一个,那我就选一个吧。 - Oriel Cochavi
4个回答

9
使用synchronized意味着为了让一个线程执行该块或方法,它必须获取该块或方法引用(显式或隐式)的锁。对于static synchronized方法,该锁是类对象上的监视器。对于synchronized(this)块,使用的锁是当前实例上的监视器。多个方法或块之间共享锁是强制原子性和内存可见性更新的机制,同时,共享锁提供了共享通信路径,通过该路径可以进行等待和通知。

由于静态同步块使用与构造函数中块所使用的不同锁,进入静态同步块不会被另一个线程访问需要获取当前实例锁的块所阻塞,而构造函数中的同步块对任何内容都没有影响,锁获取将始终不受争议。更重要的是,一个线程在构造函数中所做的更改可能不会被使用getter的其他线程看到。同步影响锁定和内存可见性。

这个更改后的版本会起作用:

public class ObjectCounter {
    private static long numOfInstances = 0;
    public ObjectCounter(){
        synchronized(ObjectCounter.class){
            numOfInstances++;
        }
    }
    public static synchronized long getCount(){
        return numOfInstances;
    }
}

因为getter和递增块正在使用相同的锁。使不同的线程获取相同的监视器,确保计数器的更改得到安全发布,以便访问getter的另一个线程可以看到更新后的值。
synchronized关键字表示:“在进入之前必须获取锁定”,其中对于该方法,假定锁定:对于方法上的static关键字,它是类上的监视器,没有静态关键字则是当前实例上的监视器。为了使锁定正常工作,不同的块和方法需要使用相同的锁。在Java的设计中,有太多的语法糖和方便性:允许隐式选择锁,并将监视器放在java.lang.Object上可能会导致混淆。
关于你的问题#6:对于你在这里做的事情,最好使用AtomicLong。使用同步块协调需要进行多个更改,而不受其他线程干扰。
问题#3、#7和#8似乎非常相似:如果一个方法/块没有尝试获取锁,那么没有什么可以阻止线程执行该方法/块。整个对象不会得到任何保护,使用同步方法或块来强制锁定才是保护的关键。少考虑“使用synchronized关键字”,更多地考虑线程需要获取哪些锁。

2
坏狗!新手需要明白锁与块、方法和变量无关。我已经数不清有多少次他们问:“两个线程如何可能同时进入同步的块/方法?”或者,“两个线程如何可能同时在count上同步?”他们需要非常清楚地说明synchronized唯一防止的是两个线程同时锁定同一个实例。 - Solomon Slow
@james:我重新措辞以使其更清晰。欢迎提出改进或编辑建议。顺便说一下,我的 gravatar 是一只猫,虽然是粗略渲染的。 - Nathan Hughes
哦,天啊!我一直以为它是某种雪纳瑞。对于那个不太恰当的评论,我感到很抱歉,但正是你提到“与块相关联的锁”这部分让我有些激动。我可能有时会对我的同行说同样的话,但我的同行都知道锁和代码之间的关联是我的程序设计的一个方面,而不是语言本身的工作方式。 - Solomon Slow
@james:没问题,我认为措辞得到了改善,谢谢。 - Nathan Hughes
@NathanHughes,如果在不同的锁上同步,可视性是否得到保证?(我知道这不是线程安全的,但对于在不同锁中的线程,更改是否可见) - Mohammad Karmi

2
  1. 选项之间是有区别的。在上面的选项中,两个线程不能同时调用getCount(),而在下面的选项中可以。

  2. 是的,这是正确的。同一时间只能有一个线程持有对象的锁。

  3. 每个对象都有自己的锁。因此它会锁定该对象的所有synchronized (this)块。

但请注意,每个对象都有自己的锁,每个类也都有自己的锁。在构造函数中,您使用对象锁来访问静态(类)变量,而在getCount()中,您使用类锁。这意味着您的代码不是线程安全的!


0

synchronized 步骤:

  1. 检查对象锁是否已经被占用。如果是,则进入 synchronized 块/方法。
  2. 尝试获取锁。如果锁已经被另一个线程占用,则该线程将等待锁被释放,然后再次执行步骤 (2.)。

-2
代码中的两个选项有区别吗?
是的,它们之间有明显的区别。在第一个选项中,您正在同步线程访问 ObjectCounter 类的类对象上的 getCount() 方法。而在第二个选项中,您没有这样做。
如果我锁定“this”,那么是否意味着在构造函数退出同步块之前(假设我没有在 getCount() 上写同步块),我不能调用 getCount()?
由于对象只有一个锁(使用 static 关键字和 synchronized 保持的类锁不同),因此如果其他线程正在获取该锁,无论是因为 synchronized(this){ 还是因为 synchronized long getCount(){,则尝试获取锁的新线程都必须等待前一个线程释放锁。

现在,由于在您的情况下,您正在执行 static synchronized long getCount(){,因此它的锁定与 synchronized(this){ 不同。意思是,如果某些线程正在获取由于 synchronized(this){ 而获得的锁定,并且其他一些线程正在尝试调用 getCount(),那么该线程将不会被阻塞。

如果我在代码的某个地方执行同步块,是否只锁定同步块或所有 "this" 代码?

  1. 非静态同步: 如果您在代码的某个地方执行同步块,并且它是非静态的 public synchronized long getCount(){,则对象的锁也将保持不变,因此尝试获取锁的新线程必须等待以前的线程释放锁。

  2. 静态同步: 如果您在代码的某个地方执行同步块,并且它是静态的 public static synchronized long getCount(){,则它对非静态同步的锁定没有影响。


底线:

  • 每个对象只有一个锁,如果某个线程获取了该锁,则其他线程必须等待直到该锁被释放。

  • 然后还有一个类锁,如果使用static关键字和synchronized关键字,则会保持该锁。


@jameslarge 哦,好的。我深表歉意。 - hagrawal7777
@CKing 我使用了OP的代码进行解释,并忘记将其中的static删除,后来意识到并进行了更正。我想我因疏忽而付出了代价。谢谢你的提示。尽管我同意您对于投票者的立场,但我想我已经涵盖了OP所需的所有内容,虽然在几次编辑之后才实现的。 - hagrawal7777
@hagrawal 在你的第一个版本中,这个语句怎么样:首先,在ObjectCounter对象上同步线程访问getCount()。而在第二个版本中,你没有这样做。 这也是一个打字错误吗?我非常怀疑,因为这个语句没有打字错误的余地。底线是,你因为一个极其不正确的答案而被踩了。这被记录为downvotes有意义的情况之一。黑点是合格的。当你纠正你的答案时,可能已经太晚了,因为downvotes只能在有限的时间内撤回。 - Chetan Kinger
@CKing 是的,那不是打错了。那是为了解释清楚,以免 OP 感到困惑。 - hagrawal7777
@hagrawal 这个解释是不正确的。getCount 方法的两种形式都不会在 ObjectCounter 对象上获取锁。这实际上会让 OP 更加困惑,而不是避免混淆。这里是有关投票反对的文档。公平起见,用户可能无法撤回他们的反对票,因为撤回反对票的时间限制已经过期。 - Chetan Kinger
显示剩余6条评论

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