关于多线程:避免在Java中同步(this)?

Avoid synchronized(this) in Java?

每当一个问题出现在Java同步上时,有些人非常急切地指出应该避免EDCOX1 0。相反,他们声称,最好锁定一个私有引用。

其中一些原因是:

  • 一些邪恶的密码可能会偷走你的锁(这个很流行,也有一个"意外"的变体)
  • 同一类中的所有同步方法都使用完全相同的锁,这会降低吞吐量
  • 你(不必要地)暴露了太多的信息

其他人,包括我,认为EDOCX1 0是一个惯用的习惯用法(也在爪哇的图书馆里),是安全和很好理解的。这是不应该避免的,因为你有一个错误,你不知道在你的多线程程序中发生了什么。换句话说:如果它适用,那么就使用它。

我有兴趣看到一些现实世界中的例子(没有foobar的东西),当synchronized(this)也能完成这项工作时,避免锁定this是更好的选择。

因此:您是否应该总是避免使用synchronized(this)并用私有引用上的锁替换它?

更多信息(更新为答案):

  • 我们讨论的是实例同步
  • 同时考虑了隐式(synchronized方法)和显式形式的synchronized(this)
  • 如果你引用布洛赫或其他有关这个问题的权威,不要漏掉你不喜欢的部分(例如有效的Java,线程安全的项目:通常它是实例本身的锁,但也有例外)。
  • 如果您的锁定中需要粒度而不是synchronized(this)提供,那么synchronized(this)不适用,因此这不是问题所在。


我将分别讨论每一点。

  • Some evil code may steal your lock (very popular this one, also has an
    "accidentally" variant)

    我更担心意外。这意味着使用this是类的公开接口的一部分,应该加以记录。有时需要其他代码使用您的锁的能力。这一点在像Collections.synchronizedMap这样的事情上是正确的(见javadoc)。

  • All synchronized methods within the same class use the exact same
    lock, which reduces throughput

    这是过于简单化的想法;仅仅摆脱synchronized(this)并不能解决问题。适当的吞吐量同步需要更多的考虑。

  • You are (unnecessarily) exposing too much information

    这是1的变体。使用synchronized(this)是接口的一部分。如果你不想/不需要曝光,就不要这样做。


  • 首先应该指出:

    1
    2
    3
    4
    5
    public void blah() {
      synchronized (this) {
        // do stuff
      }
    }

    在语义上等价于:

    1
    2
    3
    public synchronized void blah() {
      // do stuff
    }

    这是不使用synchronized(this)的原因之一。你可能会说你可以在synchronized(this)块周围做些事情。通常的原因是尽量避免执行同步检查,这会导致各种并发性问题,特别是双重检查锁定问题,这只是为了说明制作一个相对简单的检查线程安全是多么困难。

    私有锁是一种防御机制,这绝不是一个坏主意。

    另外,正如您所提到的,私有锁可以控制粒度。一个对象上的一组操作可能与另一个完全无关,但synchronized(this)将相互排斥对所有操作的访问。

    synchronized(this)只是没有给你任何东西。


    使用synchronized(this)时,将类实例用作锁本身。这意味着当线程1获取锁时,线程2应该等待

    假设以下代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public void method1() {
        do something ...
        synchronized(this) {
            a ++;      
        }
        ................
    }


    public void method2() {
        do something ...
        synchronized(this) {
            b ++;      
        }
        ................
    }

    方法1修改变量A,方法2修改变量B,避免两个线程同时修改同一个变量。但是当thread1修改a和thread2修改b时,可以在没有任何竞争条件的情况下执行。

    不幸的是,上面的代码不允许这样做,因为我们对一个锁使用相同的引用;这意味着线程即使不在争用条件下也应该等待,显然代码会牺牲程序的并发性。

    解决方案是对两个不同的变量使用两个不同的锁。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
      class Test {
            private Object lockA = new Object();
            private Object lockB = new Object();

    public void method1() {
        do something ...
        synchronized(lockA) {
            a ++;      
        }
        ................
    }


    public void method2() {
        do something ...
        synchronized(lockB) {
            b ++;      
        }
        ................
     }

    上面的示例使用了更多的细粒度锁(2个锁而不是一个(变量a和b分别使用locka和lockb),因此允许更好的并发性,另一方面,它变得比第一个示例更复杂…


    虽然我同意不要盲目地遵守教条,但"偷锁"的情况对你来说是否如此古怪?线程确实可以"从外部"获取对象的锁(synchronized(theObject) {...}),从而阻塞等待同步实例方法的其他线程。

    如果您不相信恶意代码,请考虑该代码可能来自第三方(例如,如果您开发某种应用程序服务器)。

    "偶然"的版本似乎不太可能,但正如他们所说,"做一些白痴证明,有人会发明一个更好的白痴"。

    所以我同意这一点,这取决于学校的思想是什么。

    编辑以下Eljenso的前3条评论:

    我从来没有遇到过偷锁的问题,但这里有一个假想的场景:

    假设您的系统是一个servlet容器,我们考虑的对象是ServletContext实现。它的getAttribute方法必须是线程安全的,因为上下文属性是共享数据;所以您将其声明为synchronized。我们还假设您提供了一个基于容器实现的公共托管服务。

    我是你的客户,在你的网站上部署我的"好"servlet。碰巧我的代码包含一个对getAttribute的调用。

    一个黑客伪装成另一个客户,在你的网站上部署他的恶意servlet。它在init方法中包含以下代码:

    1
    2
    3
    synchronized (this.getServletConfig().getServletContext()) {
       while (true) {}
    }

    假设我们共享相同的servlet上下文(只要两个servlet在同一虚拟主机上,规范就允许),那么我对getAttribute的调用将永远被锁定。黑客在我的servlet上实现了一个DoS。

    如果getAttribute在私有锁上同步,则不可能发生这种攻击,因为第三方代码无法获取该锁。

    我承认这个例子是人为的,对servlet容器的工作方式有一个过于简单的观点,但我认为它证明了这一点。

    因此,我将根据安全性考虑做出设计选择:我是否可以完全控制可以访问实例的代码?线程无限期地持有实例上的锁会导致什么后果?


    在C和JAVA营地上似乎有不同的共识。我见过的大多数Java代码使用:

    1
    2
    3
    4
    // apply mutex to this instance
    synchronized(this) {
        // do work here
    }

    尽管大多数C代码选择了可以说更安全的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // instance level lock object
    private readonly object _syncObj = new object();

    ...

    // apply mutex to private instance level field (a System.Object usually)
    lock(_syncObj)
    {
        // do work here
    }

    C习语当然更安全。如前所述,不能从实例外部恶意/意外地访问锁。Java代码也有这样的风险,但是Java社区似乎随着时间的推移而变得更加不安全,但略显简洁。

    这并不意味着对Java的挖潜,只是我对这两种语言的体验的反映。


    这取决于情况。如果只有一个或多个共享实体。

    请参阅这里的完整工作示例

    小小的介绍。

    线程和可共享实体多个线程可以访问同一个实体,例如共享单个消息队列的多个连接线程。由于线程同时运行,可能会有机会用另一个线程覆盖一个人的数据,这可能是一个混乱的情况。因此,我们需要某种方法来确保可共享实体一次只能由一个线程访问。(并发)。

    同步块synchronized()块是确保可共享实体并发访问的一种方法。首先,一个小的类比假设有两个人,p1,p2(线程),一个洗脸盆(共享实体),在一个卫生间里,有一扇门(锁)。现在我们要一个人一次使用脸盆。一种方法是在门被锁定时通过p1锁定门,p2等待p1完成他的工作。P1打开门只有P1可以使用洗脸盆。

    语法。

    1
    2
    3
    4
    synchronized(this)
    {
      SHARED_ENTITY.....
    }

    "这个"提供了与类相关联的内在锁(Java开发人员设计的对象类,这样每个对象可以作为监视器工作)。当只有一个共享实体和多个线程(1:n)时,上述方法可以很好地工作。enter image description heren个可共享实体-m个线程现在想想这样一种情况:一个洗手间里有两个洗脸盆,只有一扇门。如果我们使用前面的方法,一次只有p1可以使用一个洗脸盆,而p2将在外面等待。这是资源浪费,因为没有人使用B2(洗脸盆)。一个更明智的方法是在洗手间内创建一个较小的房间,并为每个洗脸盆提供一个门。这样,p1可以访问b1,p2可以访问b2,反之亦然。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    washbasin1;  
    washbasin2;

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

      synchronized(lock1)
      {
        washbasin1;
      }

      synchronized(lock2)
      {
        washbasin2;
      }

    enter image description hereenter image description here

    查看更多关于线程的信息---->这里


    java.util.concurrent包大大降低了线程安全代码的复杂性。我只有轶事证据可以继续,但我在synchronized(x)上看到的大多数工作似乎是重新实现锁、信号灯或闩锁,而是使用较低级别的监视器。

    考虑到这一点,使用这些机制中的任何一种进行同步都类似于在内部对象上进行同步,而不是泄漏锁。这是有益的,因为您可以绝对确定通过两个或多个线程控制进入监视器的条目。


  • 如果可能,使数据不可变(final变量)
  • 如果无法避免跨多个线程的共享数据突变,请使用高级编程结构[例如,粒度LockAPI]
  • A Lock provides exclusive access to a shared resource: only one thread at a time can acquire the lock and all access to the shared resource requires that the lock be acquired first.

    使用ReentrantLock实现Lock接口的示例代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
     class X {
       private final ReentrantLock lock = new ReentrantLock();
       // ...

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

    同步锁定的优点(此)

  • 使用同步方法或语句会强制以块结构方式进行所有锁的获取和释放。

  • 锁实现通过提供

  • 获取锁的非阻塞尝试(tryLock())
  • 试图获取可中断的锁(lockInterruptibly())
  • 试图获取可以超时的锁(tryLock(long, TimeUnit))。
  • 锁类还可以提供与隐式监视器锁截然不同的行为和语义,例如

  • 保证订货
  • 不可重复使用
  • 死锁检测
  • 请看一下关于各种类型的Locks的SE问题:

    同步与锁定

    您可以通过使用高级并发API而不是同步块来实现线程安全。此文档页提供了良好的编程结构,以实现线程安全。

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

    执行器定义用于启动和管理线程的高级API。由java.util.concurrent提供的执行器实现提供了适用于大型应用程序的线程池管理。

    并发集合使管理大型数据集合变得更容易,并且可以大大减少同步的需要。

    原子变量具有最小化同步和帮助避免内存一致性错误的功能。

    threadLocalRandom(在JDK7中)提供了从多个线程高效生成伪随机数的功能。

    有关其他编程构造,请参阅java.util.concurrent和java.util.concurrent.atomic包。


    如果你决定:

    • 你需要做的是锁定当前对象;以及
    • 你想锁定粒度小于整体方法;

    那么,我不认为同步化的禁忌(这个)。

    有些人故意在一个方法的整个内容中使用synchronized(this)(而不是将方法标记为synchronized),因为他们认为它"对读者来说更清楚"哪个对象实际上正在被同步。只要人们做出一个明智的选择(例如,通过这样做,他们实际上是在向方法中插入额外的字节码,这可能会对潜在的优化产生连锁反应),我不会特别看到这个问题。您应该始终记录程序的并发行为,所以我不认为"synchronized"发布行为"参数如此引人注目。

    关于您应该使用哪个对象的锁的问题,我认为在当前对象上进行同步没有什么错,如果您正在做什么以及类通常如何使用的逻辑会期望这样做。例如,对于集合,逻辑上期望锁定的对象通常是集合本身。


    我认为有一个很好的解释,为什么在Brian Goetz的实践中,这些都是你的腰带中的关键技术。他有一点非常清楚——你必须在"任何地方"使用相同的锁来保护你的物体的状态。同步方法和对象上的同步经常是同时进行的。例如,矢量同步其所有方法。如果你有一个向量对象的处理方法,并且打算做"如果不存在就放",那么仅仅同步向量本身的方法并不能保护你免受状态的破坏。您需要使用同步(vectorhandle)进行同步。这将导致具有向量句柄的每个线程获取相同的锁,并保护向量的整体状态。这称为客户端锁定。我们知道,事实上,矢量是同步的(这个)/同步所有的方法,因此在对象矢量手柄上同步将导致矢量对象状态的适当同步。仅仅因为使用的是线程安全集合,就认为您是线程安全的,这是愚蠢的。这正是Concurrenthashmap明确引入putifastent方法的原因——使这种操作具有原子性。

    综上所述

  • 方法级别的同步允许客户端锁定。
  • 如果您有一个私有锁对象,它会使客户端无法锁定。如果您知道您的类没有"如果不存在则放入"类型的功能,那么这很好。
  • 如果您正在设计一个库,那么在这个库上同步或者同步方法通常更明智。因为你很少能决定如何使用你的课程。
  • 如果Vector使用了一个私有锁对象,就不可能得到"如果没有"的正确值。客户机代码永远无法获得私有锁的句柄,因此违反了使用完全相同的锁来保护其状态的基本规则。
  • 在这种方法或同步方法上进行同步确实存在一个问题,正如其他人指出的那样-有人可能会得到一个锁,但永远不会释放它。所有其他线程都会一直等待释放锁。
  • 所以知道你在做什么,采用正确的方法。
  • 有人认为拥有一个私有锁对象可以给您提供更好的粒度——例如,如果两个操作不相关——它们可以由不同的锁进行保护,从而获得更好的吞吐量。但我认为这是设计气味而不是代码气味——如果两个操作完全无关,为什么它们是同一类的一部分?为什么一个与班级俱乐部无关的职能?可能是实用类?hmmmm-通过同一个实例提供字符串操作和日历日期格式的一些实用工具?……至少对我来说没有任何意义!!

  • 简短的回答:你必须了解区别,并根据代码做出选择。

    长话短说:一般来说,我宁愿尝试避免同步(这个)来减少争用,但是私有锁增加了您必须注意的复杂性。因此,对正确的作业使用正确的同步。如果您对多线程编程没有那么丰富的经验,我宁愿坚持实例锁定并阅读本主题。(这就是说:仅仅使用synchronize(this)并不能自动使类完全线程安全。)这不是一个简单的主题,但是一旦您习惯了它,是否使用synchronize(this)的答案就会自然出现。


    不,你不应该总是这样。但是,当一个特定对象上存在多个关注点时,我倾向于避免这种情况,因为这些关注点只需要对其自身进行线程安全即可。例如,您可能有一个具有"label"和"parent"字段的可变数据对象;这些字段必须是threadsafe,但是更改一个字段不需要阻止另一个对象被写入/读取。(在实践中,我会通过声明字段不稳定和/或使用java. U.L.Read的AtomicFoo包装器来避免这种情况)。

    一般来说,同步有点笨拙,因为它触发了一个很大的锁,而不是精确地考虑线程如何被允许在彼此之间工作。使用EDOCX1[0]甚至更加笨拙和反社会,因为它的意思是"当我持有锁的时候,没有人可以改变这个班上的任何东西"。你需要多久做一次?

    我更愿意拥有更细粒度的锁;即使你真的想阻止所有的事情发生变化(也许你正在序列化对象),你也可以获取所有的锁来实现相同的事情,而且这样做更为明确。当你使用synchronized(this)的时候,不清楚你为什么要同步,或者副作用是什么。如果你使用synchronized(labelMonitor),或者更好的labelLock.getWriteLock().lock(),很明显你在做什么,你的关键部分的影响是有限的。


    锁用于可见性或保护某些数据不受可能导致争用的并发修改的影响。

    当您只需要将原始类型操作设置为原子操作时,可以使用诸如AtomicInteger之类的选项。

    但是假设你有两个相互关联的整数,比如xy坐标,它们相互关联,应该以原子的方式改变。然后你用同样的锁保护它们。

    锁应该只保护彼此相关的状态。不少也不多。如果在每个方法中使用synchronized(this),那么即使类的状态是不相关的,即使更新不相关的状态,所有线程也将面临争用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Point{
       private int x;
       private int y;

       public Point(int x, int y){
           this.x = x;
           this.y = y;
       }

       //mutating methods should be guarded by same lock
       public synchronized void changeCoordinates(int x, int y){
           this.x = x;
           this.y = y;
       }
    }

    在上面的示例中,我只有一种方法可以同时对xy进行变异,而没有两种不同的方法,如xy是相关的,如果我分别对xy进行了两种不同的变异,那么它就不会是线程安全的。

    这个例子只是为了演示而不一定是应该实现的方式。最好的方法是使它不可变。

    现在,与Point的例子相反,有一个例子,TwoCounters已经由@andreas提供,其中受两个不同锁保护的状态,因为状态彼此不相关。

    使用不同的锁来保护不相关状态的过程称为锁条带化或锁拆分。


    不同步的原因是有时需要一个以上的锁(第二个锁经常在经过一些额外的思考后被移除,但是您仍然需要处于中间状态)。如果你锁定了它,你总是要记住这两个锁中的哪一个是这个;如果你锁定了一个私有对象,变量名会告诉你这一点。

    从读者的角度来看,如果你看到锁定这一点,你总是需要回答以下两个问题:

  • 哪种访问受此保护?
  • 一个锁真的足够吗,有人没有引入一个bug?
  • 一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class BadObject {
        private Something mStuff;
        synchronized setStuff(Something stuff) {
            mStuff = stuff;
        }
        synchronized getStuff(Something stuff) {
            return mStuff;
        }
        private MyListener myListener = new MyListener() {
            public void onMyEvent(...) {
                setStuff(...);
            }
        }
        synchronized void longOperation(MyListener l) {
            ...
            l.onMyEvent(...);
            ...
        }
    }

    如果两个线程在两个不同的BadObject实例上开始longOperation(),则它们获取它们的锁;当需要调用l.onMyEvent(...)时,我们会遇到死锁,因为两个线程都不能获取另一个对象的锁。

    在这个例子中,我们可以使用两个锁来消除死锁,一个用于短操作,另一个用于长操作。


    如前所述,当同步函数只使用"this"时,同步块可以使用用户定义的变量作为锁对象。当然,您可以对应该同步的函数区域进行操作,等等。

    但大家都说,使用"this"作为锁对象覆盖整个函数的synchronized函数和block没有区别。这不是真的,差异在于两种情况下都会生成的字节代码。在同步块的情况下,应分配局部变量,该变量保存对"this"的引用。因此,我们将有一个稍微大一点的函数大小(如果您只有几个函数,则与此无关)。

    您可以在这里找到关于差异的更详细的解释:http://www.artima.com/insidejvm/ed2/threadsynchp.html

    同时,由于以下观点,同步块的使用也不好:

    The synchronized keyword is very limited in one area: when exiting a synchronized block, all threads that are waiting for that lock must be unblocked, but only one of those threads gets to take the lock; all the others see that the lock is taken and go back to the blocked state. That's not just a lot of wasted processing cycles: often the context switch to unblock a thread also involves paging memory off the disk, and that's very, very, expensive.

    有关此领域的更多详细信息,我建议您阅读本文:http://java.dzone.com/articles/synchronized-considered


    这实际上只是对其他答案的补充,但是如果您对使用私有对象进行锁定的主要反对意见是它使类中的字段与业务逻辑无关,那么项目lombok让@Synchronized在编译时生成样板文件:

    1
    2
    3
    4
    @Synchronized
    public int foo() {
        return 0;
    }

    编译到

    1
    2
    3
    4
    5
    6
    7
    private final Object $lock = new Object[0];

    public int foo() {
        synchronized($lock) {
            return 0;
        }
    }

    避免使用synchronized(this)作为锁定机制:这会锁定整个类实例,并可能导致死锁。在这种情况下,重构代码以只锁定特定的方法或变量,这样整个类就不会被锁定。方法级别内可以使用Synchronised。下面的代码不是使用synchronized(this),而是说明如何锁定一个方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
       public void foo() {
    if(operation = null) {
        synchronized(foo) {
    if (operation == null) {
     // enter your code that this method has to handle...
              }
            }
          }
        }

    这取决于你想做的任务,但我不会用它。另外,首先通过同步(这个)来检查您想要伴随的线程保存是否不能完成?API中还有一些很好的锁可以帮助您:)


    我只想提一个在没有依赖关系的代码的原子部分中唯一私有引用的可能解决方案。可以使用带有锁的静态哈希映射和名为atomic()的简单静态方法,该方法使用堆栈信息(完整类名和行号)自动创建所需的引用。然后,可以在同步语句中使用此方法,而无需编写新的锁对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    // Synchronization objects (locks)
    private static HashMap<String, Object> locks = new HashMap<String, Object>();
    // Simple method
    private static Object atomic() {
        StackTraceElement [] stack = Thread.currentThread().getStackTrace(); // get execution point
        StackTraceElement exepoint = stack[2];
        // creates unique key from class name and line number using execution point
        String key = String.format("%s#%d", exepoint.getClassName(), exepoint.getLineNumber());
        Object lock = locks.get(key); // use old or create new lock
        if (lock == null) {
            lock = new Object();
            locks.put(key, lock);
        }
        return lock; // return reference to lock
    }
    // Synchronized code
    void dosomething1() {
        // start commands
        synchronized (atomic()) {
            // atomic commands 1
            ...
        }
        // other command
    }
    // Synchronized code
    void dosomething2() {
        // start commands
        synchronized (atomic()) {
            // atomic commands 2
            ...
        }
        // other command
    }


    使用synchronized(this)的一个好例子。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // add listener
    public final synchronized void addListener(IListener l) {listeners.add(l);}
    // remove listener
    public final synchronized void removeListener(IListener l) {listeners.remove(l);}
    // routine that raise events
    public void run() {
       // some code here...
       Set ls;
       synchronized(this) {
          ls = listeners.clone();
       }
       for (IListener l : ls) { l.processEvent(event); }
       // some code here...
    }

    正如您在这里看到的,我们在这里使用synchronize,以便与那里的一些同步方法进行长时间的合作(可能是无限循环的run方法)。

    当然,在私有字段上使用synchronized可以很容易地重写它。但有时,当我们已经有了一些使用同步方法的设计(例如,遗留类,我们从中派生,同步(这个)可能是唯一的解决方案)。


    我认为,在任何相当大的应用程序中,都可能出现一个(其他人使用您的锁)和两个(所有方法都不必要地使用同一个锁)。尤其是当开发人员之间没有良好的沟通时。

    它不是石头铸成的,它主要是一个良好实践和防止错误的问题。