首页 文章

在Java中避免同步(this)?

提问于
浏览
342

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

一些给出的原因是:

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

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

因此: should you always avoid synchronized(this) and replace it with a lock on a private reference?


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

  • 我们正在谈论实例同步

  • 考虑隐式( synchronized 方法)和显式形式的 synchronized(this)

  • 如果你引用Bloch或其他有关该主题的权威,请不要这样做(例如,有效的Java,关于线程安全的项目:通常它是实例本身的锁定,但也有例外 . )

  • 如果除了 synchronized(this) 提供的锁定需要粒度,则 synchronized(this) 不适用,因此不是问题

20 回答

  • 2

    虽然我同意不盲目地遵守教条规则,但是对于你来说,情景是否如此古怪?线程确实可以获取对象的锁定"externally"( synchronized(theObject) {...} ),阻止等待同步实例方法的其他线程 .

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

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

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


    Edit following eljenso's first 3 comments:

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

    让's say your system is a servlet container, and the object we'重新考虑的是 ServletContext 实现 . 它的 getAttribute 方法必须是线程安全的,因为上下文属性是共享数据;所以你把它声明为 synchronized . 我们还假设您根据容器实现提供公共托管服务 .

    我是您的客户,并在您的网站上部署我的"good" servlet . 碰巧我的代码包含对 getAttribute 的调用 .

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

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

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

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

    我承认这个例子是人为的,并且过于简单地看待servlet容器是如何工作的,但恕我直言,它证明了这一点 .

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

  • 11

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

    综上所述

    • 在方法级别进行同步允许客户端侧锁定 .

    • 如果您有私有锁对象 - 它会使客户端锁定无法进行 . 如果您知道您的类没有"put if absent"类型的功能,那么这很好 .

    • 如果您正在设计一个库 - 然后在此或同步同步方法通常更明智 . 因为您很少能够决定如何使用您的课程 .

    • 如果Vector使用了私有锁对象 - 那么就不可能得到"put if absent" . 客户端代码永远不会获得私有锁的处理,因此违反了使用EXACT SAME LOCK来保护其状态的基本规则 .

    • 同步此方法或同步方法确实存在问题,正如其他人指出的那样 - 有人可能会获得锁定并且永远不会释放它 . 所有其他线程将继续等待锁被释放 .

    • 所以要知道你在做什么并采用正确的方法 .

    • 有人认为拥有私人锁定对象可以提供更好的粒度 - 例如如果两个操作不相关 - 它们可以被不同的锁保护,从而产生更好的吞吐量 . 但我认为这是设计气味,而不是代码气味 - 如果两个操作完全不相关,为什么它们是SAME类的一部分?为什么一个 class 俱乐部根本不相关的功能呢?可能是一个实用类?嗯 - 一些util通过同一个实例提供字符串操作和日历日期格式? ...至少对我没有任何意义!!

  • 1

    Short answer :您必须了解差异并根据代码做出选择 .

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

  • 83
    • 如果可能,使数据不可变( final 变量)

    • 如果无法避免跨多个线程共享数据的突变,请使用高级编程结构[例如粒状 Lock API]

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

    使用 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()
         }
       }
     }
    

    Advantages of Lock over Synchronized(this)

    • 使用同步方法或语句会强制所有锁获取和释放以块结构方式发生 .

    • 通过提供,锁实现提供了使用同步方法和语句的附加功能

    • 获取锁定的非阻塞尝试( tryLock()

    • 尝试获取可以中断的锁( lockInterruptibly()

    • 尝试获取可能超时的锁( tryLock(long, TimeUnit) ) .

    • 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包 .

  • 1

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

    See full working example here

    一个小介绍 .

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

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

    句法 .

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

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

    washbasin1;  
    washbasin2;
    
    Object lock1=new Object();
    Object lock2=new Object();
    
      synchronized(lock1)
      {
        washbasin1;
      }
    
      synchronized(lock2)
      {
        washbasin2;
      }
    

    See more on Threads ----> here

  • 3

    这取决于你想要做的任务,但我不会首先通过同步(this)来完成它?还有一些不错的locks in the API可能会帮助你:)

  • -3

    使用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)可能是唯一的解决方案) .

  • 6

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

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

    • 什么样的访问受到保护?

    • 真的够锁,没有人介绍一个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(...);
            ...
        }
    }
    

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

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

  • 0

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

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

    编译成

    private final Object $lock = new Object[0];
    
    public int foo() {
        synchronized($lock) {
            return 0;
        }
    }
    
  • 3

    那么,首先应该指出:

    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) 只是真的不给你任何东西 .

  • 9

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

    当您需要将原始类型操作设置为原子时,可以使用 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 那么它就不会是线程安全的 .

    这个例子只是为了展示而不一定是它应该实现的方式 . 做到这一点的最好方法是使它成为 IMMUTABLE .

    现在与 Point 示例相反,有一个由@Andreas提供的 TwoCounters 示例,其中受两个不同锁保护的状态彼此无关 .

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

  • 7

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

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

    我宁愿拥有更细粒度的锁;即使你确实想要阻止所有事情发生变化(也许你更明确地这样做 . 当你使用 synchronized(this) 时,它会重新同步,或副作用可能是什么 . 如果你使用 synchronized(labelMonitor) ,甚至更好 labelLock.getWriteLock().lock() ,它很明显您正在做什么以及您的关键部分的影响仅限于此 .

  • 50

    我只想在没有依赖关系的代码的原子部分中提到唯一私有引用的可能解决方案 . 您可以使用带锁的静态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
    }
    
  • 5

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

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

  • 119

    如果你已经决定:

    • 你需要做的就是锁定当前对象;和

    • 您希望以小于整个方法的粒度锁定它;

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

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

    至于哪个对象's lock you should use, I think there'在当前对象 if this would be expected by the logic of what you're doing and how your class would typically be used 上同步没什么问题 . 例如,对于集合,逻辑上期望锁定的对象通常是集合本身 .

  • 0

    There seems a different consensus in the C# and Java camps on this. 我见过的大多数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代码也存在这种风险, but it seems that the Java community has gravitated over time to the slightly less safe, but slightly more terse version.

    这并不意味着对Java的挖掘,只是反映了我在两种语言上工作的经验 .

  • 14

    在使用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个不同的锁 .

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

  • 1

    我将分别介绍每一点 .

    • 一些邪恶的代码可能会偷你的锁(这个非常受欢迎,也有一个“意外”变种)

    我不小心担心 . 它相当于 this 的使用是你的类暴露界面的一部分,应该记录在案 . 有时需要其他代码使用锁的能力 . 对于像 Collections.synchronizedMap 这样的事情也是如此(参见javadoc) .

    • 同一类中的所有同步方法使用完全相同的锁,这会降低吞吐量

    这是过于单纯的思维;只是摆脱 synchronized(this) 将无法解决问题 . 适当的吞吐量同步需要更多考虑 .

    • 您(不必要地)暴露了太多信息

    这是#1的变体 . 使用 synchronized(this) 是您界面的一部分 . 如果你不这样做的话 .

  • 0

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

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

    您可以在此处找到有关差异的更详细说明:http://www.artima.com/insidejvm/ed2/threadsynchP.html

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

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

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

  • 4

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

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

相关问题