Bad Practices With Synchronization – 同步化的不良做法

最后修改: 2021年 1月 31日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Synchronization in Java is quite helpful for getting rid of multi-threading issues. However, the principles of synchronization can cause us a lot of trouble when they’re not used thoughtfully.

Java中的同步化对于摆脱多线程问题相当有帮助。然而,当同步的原则没有被深思熟虑地使用时,它们会给我们带来很多麻烦。

In this tutorial, we’ll discuss a few bad practices associated with synchronization and the better approaches for each use case.

在本教程中,我们将讨论与同步有关的一些不良做法,以及每个用例的更好方法。

2. Principle of Synchronization

2.同步化的原则

As a general rule, we should synchronize only on objects that we’re sure no outside code will lock.

一般来说,我们应该只对那些我们确信没有外部代码会锁定的对象进行同步

In other words, it’s a bad practice to use pooled or reusable objects for synchronization. The reason is that a pooled/reusable object is accessible to other processes in the JVM, and any modification to such objects by outside/untrusted code can result in a deadlock and nondeterministic behavior.

换句话说,使用池化或可重用对象进行同步是一种不好的做法。原因是池化/可重用对象可被JVM中的其他进程访问,而外部/不被信任的代码对此类对象的任何修改都会导致死锁和非确定性行为。

Now, let’s discuss synchronization principles based on certain types like String, Boolean, Integer, and Object.

现在,让我们来讨论基于某些类型的同步原则,如StringBooleanIntegerObject

3. String Literal

3.字符串字面意义

3.1. Bad Practices

3.1.不良做法

String literals are pooled and often reused in Java. Therefore, it’s not advised to use the String type with the synchronized keyword for synchronization:

字符串字面是有池的,并且经常在Java中被重复使用。因此,不建议使用String类型与synchronized关键字进行同步

public void stringBadPractice1() {
    String stringLock = "LOCK_STRING";
    synchronized (stringLock) {
        // ...
    }
}

Similarly, if we use the private final String literal, it’s still referenced from a constant pool:

同样地,如果我们使用private final String字面,它仍然是从一个常量池中引用的。

private final String stringLock = "LOCK_STRING";
public void stringBadPractice2() {
    synchronized (stringLock) {
        // ...
    }
}

Additionally, it’s considered bad practice to intern the String for synchronization:

此外,为同步而插入字符串的做法被认为是不好的。

private final String internedStringLock = new String("LOCK_STRING").intern();
public void stringBadPractice3() {
  synchronized (internedStringLock) {
      // ...
  }
}

As per Javadocs, the intern method gets us the canonical representation for the String object. In other words, the intern method returns a String from the pool – and adds it explicitly to the pool, if it’s not there – that has the same contents as this String.

根据Javadocsintern方法为我们获得String对象的规范表示。换句话说,intern方法从池中返回一个String–如果它不在池中,则明确地将它添加到池中–它的内容与这个String相同。

Therefore, the problem of synchronization on the reusable objects persists for the interned String object as well.

因此,对于内部的String对象,可重用对象的同步问题也持续存在。

Note: All String literals and string-valued constant expressions are automatically interned.

注意:所有的String字面意义和以字符串为价值的常量表达式都被自动内化了

3.2. Solution

3.2.解决方案

The recommendation to avoid bad practices with synchronization on the String literal is to create a new instance of String using the new keyword.

为避免在String字面上进行同步的不良做法,建议使用new关键字创建一个新的String实例

Let’s fix the problem in the code we already discussed. First, we’ll create a new String object to have a unique reference (to avoid any reuse) and its own intrinsic lock, which helps synchronization.

让我们在我们已经讨论过的代码中解决这个问题。首先,我们将创建一个新的String对象,以拥有一个唯一的引用(以避免任何重复使用)和它自己的内在锁,这有助于同步。

Then, we keep the object private and final to prevent any outside/untrusted code from accessing it:

然后,我们保持该对象privatefinal,以防止任何外部/不受信任的代码访问它。

private final String stringLock = new String("LOCK_STRING");
public void stringSolution() {
    synchronized (stringLock) {
        // ...
    }
}

4. Boolean Literal

4.Boolean 字面意义

The Boolean type with its two values, true and false, is unsuitable for locking purposes. Similar to String literals in the JVM, boolean literal values also share the unique instances of the Boolean class.

Boolean类型有两个值:truefalse,不适合用于锁定目的。与JVM中的String字头相似,boolean字头值也共享Boolean类的唯一实例。

Let’s look at a bad code example synchronizing on the Boolean lock object:

让我们看看一个在Boolean锁对象上同步的坏代码例子。

private final Boolean booleanLock = Boolean.FALSE;
public void booleanBadPractice() {
    synchronized (booleanLock) {
        // ...
    }
}

Here, a system can become unresponsive or result in a deadlock situation if any outside code also synchronizes on a Boolean literal with the same value.

在这里,如果任何外部代码也在具有相同值的Boolean字面上进行同步,那么系统就会变得没有反应或导致死锁的情况。

Therefore, we don’t recommend using the Boolean objects as a synchronization lock.

因此,我们不建议使用Boolean对象作为同步锁。

5. Boxed Primitive

5.盒装原物

5.1. Bad Practice

5.1.不良做法

Similar to the boolean literals, boxed types may reuse the instance for some values. The reason is that the JVM caches and shares the value that can be represented as a byte.

boolean字面意义相似,盒式类型可能会重复使用某些值的实例。原因是JVM会缓存并共享可以表示为字节的值。

For instance, let’s write a bad code example synchronizing on the boxed type Integer:

例如,让我们写一个在盒装类型Integer上同步的坏代码例子。

private int count = 0;
private final Integer intLock = count; 
public void boxedPrimitiveBadPractice() { 
    synchronized (intLock) {
        count++;
        // ... 
    } 
}

5.2. Solution

5.2.解决办法

However, unlike the boolean literal, the solution for synchronization on the boxed primitive is to create a new instance.

但是,与boolean字面意义不同的是,盒式基元上的同步的解决方案是创建一个新的实例。

Similar to the String object, we should use the new keyword to create a unique instance of the Integer object with its own intrinsic lock and keep it private and final:

String对象类似,我们应该使用new关键字来创建Integer对象的唯一实例,并拥有自己的内在锁,并保持其privatefinal

private int count = 0;
private final Integer intLock = new Integer(count);
public void boxedPrimitiveSolution() {
    synchronized (intLock) {
        count++;
        // ...
    }
}

6. Class Synchronization

6.班级同步化

The JVM uses the object itself as a monitor (its intrinsic lock) when a class implements method synchronization or block synchronization with the this keyword.

当一个类用this关键字实现方法同步或块同步时,JVM将对象本身作为监视器(其固有的锁)。

Untrusted code can obtain and indefinitely hold the intrinsic lock of an accessible class. Consequently, this can result in a deadlock situation.

不受信任的代码可以获得并无限期地持有一个可访问类的内在锁。因此,这可能导致死锁的情况。

6.1. Bad Practice

6.1.不良做法

For example, let’s create the Animal class with a synchronized method setName and a method setOwner with a synchronized block:

例如,让我们创建Animal类,它有一个synchronized方法setName和一个setOwner方法synchronized块。

public class Animal {
    private String name;
    private String owner;
    
    // getters and constructors
    
    public synchronized void setName(String name) {
        this.name = name;
    }

    public void setOwner(String owner) {
        synchronized (this) {
            this.owner = owner;
        }
    }
}

Now, let’s write some bad code that creates an instance of the Animal class and synchronize on it:

现在,让我们写一些不好的代码,创建一个Animal类的实例,并对其进行同步。

Animal animalObj = new Animal("Tommy", "John");
synchronized (animalObj) {
    while(true) {
        Thread.sleep(Integer.MAX_VALUE);
    }
}

Here, the untrusted code example introduces an indefinite delay, preventing the setName and setOwner method implementations from acquiring the same lock.

在这里,不受信任的代码示例引入了一个无限期的延迟,阻止了setNamesetOwner方法的实现获取同一个锁。

6.2. Solution

6.2.解决办法

The solution to prevent this vulnerability is the private lock object.

防止这一漏洞的解决方案是私有锁对象

The idea is to use the intrinsic lock associated with the private final instance of the Object class defined within our class in place of the intrinsic lock of the object itself.

我们的想法是使用与private final类的Object实例相关的内在锁,以取代对象本身的内在锁。

Also, we should use block synchronization in place of method synchronization to add flexibility to keep non-synchronized code out of the block.

另外,我们应该使用块同步来代替方法同步,以增加灵活性,使非同步的代码不在块中。

So, let’s make the required changes to our Animal class:

所以,让我们对我们的Animal类做必要的修改。

public class Animal {
    // ...

    private final Object objLock1 = new Object();
    private final Object objLock2 = new Object();

    public void setName(String name) {
        synchronized (objLock1) {
            this.name = name;
        }
    }

    public void setOwner(String owner) {
        synchronized (objLock2) {
            this.owner = owner;
        }
    }
}

Here, for better concurrency, we’ve granularized the locking scheme by defining multiple private final lock objects to separate our synchronization concerns for both of the methods – setName and setOwner.

在这里,为了提高并发性,我们通过定义多个private final锁对象来细化锁定方案,以分离我们对setNamesetOwner两个方法的同步关注。

Additionally, if a method that implements the synchronized block modifies a static variable, we must synchronize by locking on the static object:

此外,如果实现synchronized块的方法修改了static变量,我们必须通过锁定static对象进行同步。

private static int staticCount = 0;
private static final Object staticObjLock = new Object();
public void staticVariableSolution() {
    synchronized (staticObjLock) {
        count++;
        // ...
    }
}

7. Conclusion

7.结语

In this article, we discussed a few bad practices associated with synchronization on certain types like String, Boolean, Integer, and Object.

在这篇文章中,我们讨论了与StringBooleanIntegerObject等特定类型上的同步相关的一些不良做法。

The most important takeaway from this article is that it’s not recommended to use pooled or reusable objects for synchronization.

这篇文章最重要的启示是,不建议使用池化或可重复使用的对象进行同步。

Also, it’s recommended to synchronize on a private final instance of the Object class. Such an object will be inaccessible to outside/untrusted code that may otherwise interact with our public classes, thus reducing the possibility that such interactions could result in deadlock.

此外,建议在Object类的private final实例上进行同步。这样的对象将无法被外部/不受信任的代码访问,否则这些代码可能会与我们的public类进行交互,从而减少这种交互导致死锁的可能性。

As usual, the source code is available over on GitHub.

像往常一样,源代码可在GitHub上获得。