避免在Java中同步(this)?

问题描述 投票:357回答:21

每当有关Java同步的问题出现时,有些人非常渴望指出应该避免使用synchronized(this)。相反,他们声称,首选锁定私人参考。

一些给出的原因是:

其他人,包括我在内,认为synchronized(this)是一种习惯用法,它被广泛使用(也在Java库中),是安全且易于理解的。它不应该被避免,因为你有一个错误,你不知道多线程程序中发生了什么。换句话说:如果适用,则使用它。

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

因此:你应该总是避免使用synchronized(this)并将其替换为私人引用上的锁吗?


一些进一步的信息(更新为答案):

  • 我们正在谈论实例同步
  • 考虑隐式(synchronized方法)和synchronized(this)的显式形式
  • 如果您引用Bloch或其他有关该主题的权限,请不要遗漏您不喜欢的部分(例如,有效的Java,线程安全项目:通常它是实例本身的锁定,但也有例外。)
  • 如果除了synchronized(this)提供的锁定需要粒度,那么synchronized(this)不适用,所以这不是问题
java multithreading synchronization synchronized
21个回答
124
投票

我将分别介绍每一点。

  1. 一些邪恶的代码可能会偷你的锁(这个非常受欢迎,也有一个“意外”变种) 我不小心担心。它相当于使用this是你的类暴露界面的一部分,应该记录在案。有时需要其他代码使用锁的能力。像Collections.synchronizedMap这样的事情也是如此(参见javadoc)。
  2. 同一类中的所有同步方法使用完全相同的锁,这会降低吞吐量 这是过于简单化的思维;只是摆脱synchronized(this)将无法解决问题。适当的吞吐量同步需要更多考虑。
  3. 您(不必要地)暴露了太多信息 这是#1的变体。使用synchronized(this)是您界面的一部分。如果您不希望/需要暴露,请不要这样做。

4
投票

我认为在Brian Goetz的一本名为Java Concurrency In Practice的书中,有一个很好的解释为什么这些都是你的重要技术。他非常明确地指出了一点 - 你必须使用相同的锁“无处不在”来保护对象的状态。同步方法和对象的同步通常是齐头并进的。例如。 Vector同步其所有方法。如果你有一个矢量对象的句柄并且将要“放置如果不存在”那么仅仅Vector同步它自己的各个方法并不会保护你免受状态的破坏。您需要使用synchronized(vectorHandle)进行同步。这将导致每个具有向量句柄的线程获取SAME锁,并保护向量的整体状态。这称为客户端锁定。事实上,我们确实知道向量确实同步(this)/同步其所有方法,因此在对象上进行同步vectorHandle将导致向量对象状态的正确同步。因为你使用线程安全集合而相信你是线程安全的是愚蠢的。这正是ConcurrentHashMap明确引入putIfAbsent方法的原因 - 使这样的操作成为原子。

综上所述

  1. 在方法级别进行同步允许客户端侧锁定。
  2. 如果你有一个私有锁对象 - 它使客户端锁定不可能。如果您知道您的班级没有“put if absence”类型的功能,那么这很好。
  3. 如果您正在设计一个库 - 那么同步或同步该方法通常更明智。因为您很少能够决定如何使用您的课程。
  4. 如果Vector使用了一个私人锁定对象 - 那么就不可能在没有“放置”的情况下使用它。客户端代码永远不会获得私有锁的处理,因此违反了使用EXACT SAME LOCK保护其状态的基本规则。
  5. 正如其他人所指出的那样,对这个或同步方法进行同步确实存在问题 - 有人可能会获得锁定并且永远不会释放它。所有其他线程将继续等待锁被释放。
  6. 所以要知道你在做什么并采用正确的方法。
  7. 有人认为拥有私有锁定对象可以提供更好的粒度 - 例如如果两个操作不相关 - 它们可以被不同的锁保护,从而产生更好的吞吐量。但我认为这是设计气味,而不是代码气味 - 如果两个操作完全不相关,为什么它们是SAME类的一部分?为什么一个班级俱乐部根本不相关的功能呢?可能是一个实用类?嗯 - 一些util通过同一个实例提供字符串操作和日历日期格式? ...至少对我没有任何意义!!

3
投票

不,你不应该永远。但是,当对特定对象存在多个问题时,我倾向于避免它,只需要对自己进行线程安全。例如,您可能有一个具有“label”和“parent”字段的可变数据对象;这些都需要线程安全,但更改一个不需要阻止另一个写入/读取。 (实际上,我会通过声明字段volatile和/或使用java.util.concurrent的AtomicFoo包装器来避免这种情况。

一般来说,同步有点笨拙,因为它会严重限制锁定,而不是准确地考虑如何允许线程相互协作。使用synchronized(this)甚至更笨拙和反社会,因为它说“当我持有锁时,没有人可以在这个班级改变任何东西”。你多久需要这样做?

我宁愿拥有更细粒度的锁;即使你确实想要阻止一切变化(也许你是序列化对象),你可以获得所有的锁来实现同样的事情,而且它更明确。当你使用synchronized(this)时,不清楚为什么你要同步,或副作用可能是什么。如果你使用synchronized(labelMonitor),甚至更好的labelLock.getWriteLock().lock(),你很清楚你正在做什么以及你的关键部分的影响是多少。


3
投票

简短回答:您必须了解差异并根据代码做出选择。

答案很长:一般来说,我宁愿尝试避免同步(这)以减少争用,但私有锁会增加您必须注意的复杂性。因此,使用正确的同步来完成正确的工作。如果您对多线程编程不太熟悉,我宁愿坚持实例锁定并阅读本主题。 (也就是说:只使用synchronize(this)不会自动使你的类完全是线程安全的。)这不是一个简单的话题,但是一旦你习惯了它,是否使用synchronize(this)的答案自然而然。


2
投票

锁用于可见性或用于保护某些数据免于并发修改,这可能导致竞争。

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

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

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

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示例相反,有一个由@Andreas提供的TwoCounters的例子,其中被两个不同的锁保护的状态彼此无关。

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


1
投票

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

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

  1. 什么样的访问受到保护?
  2. 是一锁真的够了,没有人介绍一个bug?

一个例子:

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(...);
        ...
    }
}

如果两个线程在两个不同的longOperation()实例上开始BadObject,它们就会获得锁定;当调用l.onMyEvent(...)的时候,我们有一个死锁,因为这两个线程都没有获取另一个对象的锁。

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


1
投票

正如已经说过的,synchronized块可以使用用户定义的变量作为锁定对象,当同步函数只使用“this”时。当然,您可以使用功能区域进行操作,这些区域应该同步等等。

但是每个人都说同步函数和块之间没有区别,它使用“this”作为锁定对象来覆盖整个函数。事实并非如此,区别在于将在两种情况下生成的字节代码。在同步块使用的情况下,应该分配局部变量,该变量保持对“this”的引用。因此,我们将有一个更大的功能大小(如果您只有很少的功能,则不相关)。

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

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

synchronized关键字在一个区域中非常有限:当退出同步块时,必须取消阻塞等待该锁的所有线程,但只有其中一个线程获取锁;所有其他人都看到锁已被锁定并返回到阻塞状态。这不仅仅是浪费大量的处理周期:通常上下文切换到解锁线程还涉及从磁盘中分页内存,这非常非常昂贵。

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


1
投票

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

@Synchronized
public int foo() {
    return 0;
}

编译成

private final Object $lock = new Object[0];

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

0
投票

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

// 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...
}

正如您在此处所看到的,我们使用同步对此进行了长时间(可能是无限循环的run方法)与一些同步方法的简单协作。

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


0
投票

这取决于你想要做的任务,但我不会使用它。另外,检查你想要完成的线程保存是否可以通过首先同步(this)来完成?还有一些不错的locks in the API可能会帮助你:)


0
投票

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

// 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
}

84
投票

那么,首先应该指出:

public void blah() {
  synchronized (this) {
    // do stuff
  }
}

在语义上等同于:

public synchronized void blah() {
  // do stuff
}

这是不使用synchronized(this)的一个原因。你可能会争辩说你可以在synchronized(this)街区附近做点什么。通常的原因是尝试避免必须进行同步检查,这会导致各种并发问题,特别是double checked-locking problem,它只是表明制作相对简单的检查线程安全是多么困难。

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

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

synchronized(this)真的不给你任何东西。


0
投票

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

   public void foo() {
if(operation = null) {
    synchronized(foo) { 
if (operation == null) {
 // enter your code that this method has to handle...
          }
        }
      }
    }

0
投票

2019年我的两美分,尽管这个问题已经解决了。

锁定'this'并不错,如果你知道你在做什么,但在场景锁定'this'背后(不幸的是,方法定义中的synchronized关键字允许)。

如果你真的希望你的类的用户能够“窃取”你的锁(即阻止其他线程处理它),你实际上希望所有同步的方法在另一个同步方法运行时等待等等。它应该是有意识的并且经过深思熟虑(因此需要记录以帮助您的用户理解它)。

为了进一步说明,反过来你必须知道你正在“获得”(或“失败”),如果你锁定一个无法访问的锁(没有人可以'偷'你的锁,你完全控制等等。 ..)。

对我来说问题是方法定义签名中的synchronized关键字让程序员太容易不去考虑锁定什么,如果你不想在多个问题中遇到问题,那么考虑哪个是一件非常重要的事情。 - 线程程序。

人们不能争辩说“通常”你不希望你班级的用户能够做这些东西或者通常你想要的......这取决于你编码的功能。您无法制作拇指规则,因为您无法预测所有用例。

考虑例如使用内部锁的printwriter但是如果人们不希望他们的输出交错,那么人们很难从多个线程中使用它。

如果您的锁是否可以在课堂外访问,那么您作为程序员的决定是基于该课程的功能。它是api的一部分。例如,您无法从synchronized(this)移动到synchronized(provateObjet),而不会冒使用它的代码中的更改。

注1:我知道你可以通过使用显式锁定对象并暴露它来实现同步(this)'实现'的任何目标,但我认为如果你的行为有充分的记录并且你实际上知道什么锁定'this'意味着它是不必要的。

注2:我不同意这样的论点:如果某些代码意外地窃取了你的锁,那么你就必须解决它。这在某种程度上与我说我可以公开我的所有方法公开,即使它们不是公开的。如果有人'不小心'打电话给我打算私有的方法,那就是一个错误。为什么一开始就发生这起事故!!!如果窃取你的锁是一个问题,你的班级不允许。就如此容易。


-3
投票

我认为第一点(其他人使用你的锁)和两个(所有方法使用相同的锁不必要)可以在任何相当大的应用程序中发生。特别是当开发人员之间没有良好的沟通时。

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


52
投票

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

假设以下代码:

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


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

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

不幸的是,上面的代码不允许这样做,因为我们对锁使用相同的引用;这意味着线程即使它们不处于竞争状态也应该等待,显然代码牺牲了程序的并发性。

解决方案是为两个不同的变量使用2个不同的锁:

public 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),因此结果允许更好的并发性,另一方面它变得比第一个例子更复杂......


14
投票

虽然我同意不盲目地遵守教条规则,但“锁定窃取”场景对您来说是否如此古怪?一个线程确实可以在“外部”(synchronized(theObject) {...})上获取对象的锁定,阻止等待同步实例方法的其他线程。

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

“意外”版本似乎不太可能,但正如他们所说,“做一些傻逼的东西,有人会发明一个更好的白痴”。

所以我同意它取决于什么是课堂的思想学派。


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

我从未经历过锁定窃取问题,但这是一个想象的场景:

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

我是您的客户,并在您的网站上部署我的“好”servlet。碰巧我的代码包含对getAttribute的调用。

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

synchronized (this.getServletConfig().getServletContext()) {
   while (true) {}
}

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

如果getAttribute在私有锁上同步,则无法进行此攻击,因为第三方代码无法获取此锁。

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

所以我会根据安全考虑做出我的设计选择:我是否可以完全控制可以访问实例的代码?线程无限期地锁定一个实例的后果是什么?


11
投票

在C#和Java阵营中似乎有不同的共识。我见过的大多数Java代码都使用:

// apply mutex to this instance
synchronized(this) {
    // do work here
}

而大多数C#代码选择更可靠:

// 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的挖掘,只是反映了我在两种语言上工作的经验。


10
投票

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

查看完整的工作示例here

一个小介绍。

线程和可共享实体 多个线程可以访问同一个实体,例如,多个connectionThread共享一个messageQueue。由于线程同时运行,可能有可能将一个数据覆盖另一个数据,这可能是混乱的情况。 因此,我们需要某种方法来确保一次只能通过一个线程访问可共享实体。 (CONCURRENCY)。

同步块 synchronized()块是一种确保可共享实体并发访问的方法。 首先,一个小小的比喻 假设洗手间内有两人P1,P2(线程)洗脸盆(可分享的实体),还有一扇门(锁)。 现在我们希望一个人一次使用洗脸盆。 一种方法是在门被锁定时通过P1锁定门P2等待直到p1完成他的工作 P1打开门 然后只有p1可以使用脸盆。

句法。

synchronized(this)
{
  SHARED_ENTITY.....
}

“this”提供了与类关联的内部锁(Java开发人员设计的Object类,使得每个对象都可以作为监视器工作)。当只有一个共享实体和多个线程(1:N)时,上述方法可以正常工作。 enter image description here N个可共享实体-M个线程 现在想想洗手间内只有一个洗脸盆而且只有一扇门的情况。如果我们使用之前的方法,只有p1可以一次使用一个洗脸盆,而p2会在外面等待。由于没有人使用B2(洗脸盆),这是资源的浪费。 一个更明智的方法是在洗手间内创建一个较小的房间,并为每个洗脸盆提供一扇门。这样,P1可以访问B1,P2可以访问B2,反之亦然。

washbasin1;  
washbasin2;

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

  synchronized(lock1)
  {
    washbasin1;
  }

  synchronized(lock2)
  {
    washbasin2;
  }

enter image description here enter image description here

在线程上查看更多信息----> here


7
投票

java.util.concurrent包大大降低了我的线程安全代码的复杂性。我只有轶事证据可以继续,但我在synchronized(x)看到的大部分工作似乎都在重新实现Lock,Semaphore或Latch,但是使用了较低级别的监视器。

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


6
投票
  1. 如果可能,使数据不可变(final变量)
  2. 如果无法避免跨多个线程共享数据的突变,请使用高级编程结构[例如颗粒状的Lock API]

锁提供对共享资源的独占访问:一次只有一个线程可以获取锁,并且对共享资源的所有访问都需要首先获取锁。

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

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

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

锁定同步的优点(这)

  1. 同步方法或语句的使用强制所有锁获取和释放以块结构方式发生。
  2. 通过提供,锁实现提供了使用同步方法和语句的附加功能 获取锁的非阻塞尝试(tryLock()) 尝试获取可以中断的锁(lockInterruptibly()) 尝试获取可以超时的锁(tryLock(long, TimeUnit))。
  3. Lock类还可以提供与隐式监视器锁完全不同的行为和语义,例如 保证订购 非重复使用 死锁检测

看看这个关于各种类型的Locks的SE问题:

Synchronization vs Lock

您可以使用高级并发API而不是Synchronied块来实现线程安全。本文档page提供了良好的编程结构来实现线程安全性。

Lock Objects支持锁定成语,简化了许多并发应用程序。

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

Concurrent Collections使管理大量数据更容易,并且可以大大减少同步的需要。

Atomic Variables具有最小化同步和帮助避免内存一致性错误的功能。

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

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


5
投票

如果您已经决定:

  • 你需要做的就是锁定当前对象;和
  • 你想用比整个方法小的粒度锁定它;

然后我没有看到同步的禁忌(这个)。

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

至于你应该使用哪个对象的锁定的问题,我认为在当前对象上进行同步是没有问题的,如果你正在做的事情的逻辑以及你的类通常如何被使用的话。例如,对于集合,逻辑上期望锁定的对象通常是集合本身。

© www.soinside.com 2019 - 2024. All rights reserved.