What is Thread-Safety and How to Achieve it? – 什么是线程安全以及如何实现它?

最后修改: 2019年 1月 12日

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

1. Overview

1.概述

Java supports multithreading out of the box. This means that by running bytecode concurrently in separate worker threads, the JVM is capable of improving application performance.

Java支持开箱即用的多线程。这意味着,通过在不同的工作线程中并发运行字节码,JVM能够提高应用程序的性能。

Although multithreading is a powerful feature, it comes at a price. In multithreaded environments, we need to write implementations in a thread-safe way. This means that different threads can access the same resources without exposing erroneous behavior or producing unpredictable results. This programming methodology is known as “thread-safety.”

尽管多线程是一个强大的功能,但它是有代价的。在多线程环境中,我们需要以线程安全的方式编写实现。这意味着不同的线程可以访问相同的资源,而不会暴露出错误的行为或产生不可预知的结果。这种编程方法被称为 “线程安全”

In this tutorial, we’ll look at different approaches to achieve it.

在本教程中,我们将看一下实现它的不同方法。

2. Stateless Implementations

2.无状态的实现

In most cases, errors in multithreaded applications are the result of incorrectly sharing state between several threads.

在大多数情况下,多线程应用程序中的错误是由于几个线程之间不正确地共享状态造成的。

So, the first approach that we’ll look at is to achieve thread-safety using stateless implementations.

因此,我们要看的第一个方法是使用无状态实现来实现线程安全

To better understand this approach, let’s consider a simple utility class with a static method that calculates the factorial of a number:

为了更好地理解这种方法,让我们考虑一个简单的实用类,它有一个静态方法来计算一个数字的阶乘。

public class MathUtils {
    
    public static BigInteger factorial(int number) {
        BigInteger f = new BigInteger("1");
        for (int i = 2; i <= number; i++) {
            f = f.multiply(BigInteger.valueOf(i));
        }
        return f;
    }
}

The factorial() method is a stateless deterministic function. Given a specific input, it always produces the same output.

factorial()方法是一个无状态的确定性函数。鉴于一个特定的输入,它总是产生相同的输出。

The method neither relies on external state nor maintains state at all. So, it’s considered to be thread-safe and can be safely called by multiple threads at the same time.

该方法既不依赖外部状态,也根本不维护状态。因此,它被认为是线程安全的,可以被多个线程同时安全调用。

All threads can safely call the factorial() method and will get the expected result without interfering with each other and without altering the output that the method generates for other threads.

所有线程都可以安全地调用factorial()方法,并将获得预期的结果,而不会相互干扰,也不会改变该方法为其他线程产生的输出。

Therefore, stateless implementations are the simplest way to achieve thread-safety.

因此,无状态实现是实现线程安全的最简单方法。

3. Immutable Implementations

3.不可变的实现

If we need to share state between different threads, we can create thread-safe classes by making them immutable.

如果我们需要在不同的线程之间共享状态,我们可以通过使其不可变来创建线程安全的类。

Immutability is a powerful, language-agnostic concept, and it’s fairly easy to achieve in Java.

不变性是一个强大的、与语言无关的概念,而且在Java中相当容易实现。

To put it simply, a class instance is immutable when its internal state can’t be modified after it has been constructed.

简单地说,当一个类的内部状态在构建后不能被修改时,该类实例就是不可变的。

The easiest way to create an immutable class in Java is by declaring all the fields private and final and not providing setters:

在Java中创建不可变类的最简单的方法是声明所有字段privatefinal,并且不提供设置器。

public class MessageService {
    
    private final String message;

    public MessageService(String message) {
        this.message = message;
    }
    
    // standard getter
    
}

A MessageService object is effectively immutable since its state can’t change after its construction. So, it’s thread-safe.

一个MessageService对象实际上是不可变的,因为它的状态在构建后不会改变。因此,它是线程安全的。

Moreover, if MessageService were actually mutable, but multiple threads only have read-only access to it, it’s thread-safe as well.

此外,如果MessageService实际上是可变的,但多个线程对它只有只读访问权,那么它也是线程安全的。

As we can see, immutability is just another way to achieve thread-safety.

正如我们所看到的,immutability只是实现线程安全的另一种方式。

4. Thread-Local Fields

4.线程-本地字段

In Object-Oriented Programming (OOP), objects actually need to maintain state through fields and implement behavior through one or more methods.

在面向对象的编程(OOP)中,对象实际上需要通过字段维持状态,并通过一个或多个方法实现行为。

If we actually need to maintain state, we can create thread-safe classes that don’t share state between threads by making their fields thread-local.

如果我们确实需要维护状态,我们可以创建线程安全的类,通过使它们的字段成为线程本地的而不在线程之间共享状态。

We can easily create classes whose fields are thread-local by simply defining private fields in Thread classes.

我们可以通过简单地在Thread类中定义私有字段,轻松创建字段为线程本地的类。

We could define, for instance, a Thread class that stores an array of integers:

例如,我们可以定义一个Thread类,用来存储arrayintegers

public class ThreadA extends Thread {
    
    private final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
    
    @Override
    public void run() {
        numbers.forEach(System.out::println);
    }
}

Meanwhile, another one might hold an array of strings:

同时,另一个可能持有一个arraystrings

public class ThreadB extends Thread {
    
    private final List<String> letters = Arrays.asList("a", "b", "c", "d", "e", "f");
    
    @Override
    public void run() {
        letters.forEach(System.out::println);
    }
}

In both implementations, the classes have their own state, but it’s not shared with other threads. So, the classes are thread-safe.

在这两种实现中,类都有自己的状态,但它不与其他线程共享。因此,这些类是线程安全的。

Similarly, we can create thread-local fields by assigning ThreadLocal instances to a field.

同样地,我们可以通过为一个字段分配ThreadLocal实例来创建线程本地字段。

Let’s consider the following StateHolder class:

让我们考虑以下StateHolder类。

public class StateHolder {
    
    private final String state;

    // standard constructors / getter
}

We can easily make it a thread-local variable:

我们可以很容易地把它变成一个线程本地变量。

public class ThreadState {
    
    public static final ThreadLocal<StateHolder> statePerThread = new ThreadLocal<StateHolder>() {
        
        @Override
        protected StateHolder initialValue() {
            return new StateHolder("active");  
        }
    };

    public static StateHolder getState() {
        return statePerThread.get();
    }
}

Thread-local fields are pretty much like normal class fields, except that each thread that accesses them via a setter/getter gets an independently initialized copy of the field so that each thread has its own state.

线程本地字段和普通的类字段差不多,只是每个通过setter/getter访问它们的线程都会得到一个独立初始化的字段副本,这样每个线程都有自己的状态。

5. Synchronized Collections

5.同步的集合

We can easily create thread-safe collections by using the set of synchronization wrappers included within the collections framework.

通过使用集合框架中包含的一组同步包装器,我们可以轻松创建线程安全的集合。

We can use, for instance, one of these synchronization wrappers to create a thread-safe collection:

例如,我们可以使用这些同步包装器中的一种来创建一个线程安全的集合。

Collection<Integer> syncCollection = Collections.synchronizedCollection(new ArrayList<>());
Thread thread1 = new Thread(() -> syncCollection.addAll(Arrays.asList(1, 2, 3, 4, 5, 6)));
Thread thread2 = new Thread(() -> syncCollection.addAll(Arrays.asList(7, 8, 9, 10, 11, 12)));
thread1.start();
thread2.start();

Let’s keep in mind that synchronized collections use intrinsic locking in each method (we’ll look at intrinsic locking later).

让我们记住,同步集合在每个方法中都使用内在锁(我们将在后面看内在锁)。

This means that the methods can be accessed by only one thread at a time, while other threads will be blocked until the method is unlocked by the first thread.

这意味着每次只有一个线程可以访问这些方法,而其他线程将被阻断,直到该方法被第一个线程解锁。

Thus, synchronization has a penalty in performance, due to the underlying logic of synchronized access.

因此,由于同步访问的基本逻辑,同步化在性能上有一定的惩罚。

6. Concurrent Collections

6.并发收集

Alternatively to synchronized collections, we can use concurrent collections to create thread-safe collections.

作为同步集合的替代,我们可以使用并发集合来创建线程安全的集合。

Java provides the java.util.concurrent package, which contains several concurrent collections, such as ConcurrentHashMap:

Java提供了java.util.concurrent包,它包含了几个并发的集合,例如ConcurrentHashMap

Map<String,String> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("1", "one");
concurrentMap.put("2", "two");
concurrentMap.put("3", "three");

Unlike their synchronized counterparts, concurrent collections achieve thread-safety by dividing their data into segments. In a ConcurrentHashMap, for example, several threads can acquire locks on different map segments, so multiple threads can access the Map at the same time.

并发集合与同步集合不同,它通过将数据划分为若干段来实现线程安全。例如,在ConcurrentHashMap中,多个线程可以获得不同图段的锁,因此多个线程可以同时访问Map

Concurrent collections are much more performant than synchronized collections, due to the inherent advantages of concurrent thread access.

并发集合是比同步集合的性能高得多,这是由于并发线程访问的固有优势。

It’s worth mentioning that synchronized and concurrent collections only make the collection itself thread-safe and not the contents.

值得一提的是,同步和并发集合只使集合本身成为线程安全的,而不是内容。

7. Atomic Objects

7.原子对象

It’s also possible to achieve thread-safety using the set of atomic classes that Java provides, including AtomicInteger, AtomicLong, AtomicBoolean and AtomicReference.

使用 Java 提供的一组 原子类也可以实现线程安全,包括 AtomicInteger, AtomicLong, AtomicBooleanAtomicReference

Atomic classes allow us to perform atomic operations, which are thread-safe, without using synchronization. An atomic operation is executed in one single machine-level operation.

原子类允许我们执行原子操作,这些操作是线程安全的,无需使用同步。一个原子操作在一个单一的机器级操作中执行。

To understand the problem this solves, let’s look at the following Counter class:

为了理解这个问题的解决,让我们看一下下面的Counter类。

public class Counter {
    
    private int counter = 0;
    
    public void incrementCounter() {
        counter += 1;
    }
    
    public int getCounter() {
        return counter;
    }
}

Let’s suppose that in a race condition, two threads access the incrementCounter() method at the same time.

我们假设在race condition中,两个线程同时访问incrementCounter()方法。

In theory, the final value of the counter field will be 2. But we just can’t be sure about the result because the threads are executing the same code block at the same time and incrementation is not atomic.

理论上,counter字段的最终值将是2。但我们就是不能确定结果,因为线程在同一时间执行同一个代码块,而且增量不是原子性的。

Let’s create a thread-safe implementation of the Counter class by using an AtomicInteger object:

让我们通过使用AtomicInteger对象来创建一个Counter类的线程安全实现。

public class AtomicCounter {
    
    private final AtomicInteger counter = new AtomicInteger();
    
    public void incrementCounter() {
        counter.incrementAndGet();
    }
    
    public int getCounter() {
        return counter.get();
    }
}

This is thread-safe because while incrementation, ++, takes more than one operation, incrementAndGet is atomic.

这是线程安全的,因为虽然增量,++,需要一个以上的操作,但incrementAndGet是原子的。

8. Synchronized Methods

8.同步的方法

The earlier approaches are very good for collections and primitives, but we’ll sometimes need greater control than that.

早期的方法对于集合和基元来说非常好,但我们有时需要比这更大的控制。

So, another common approach that we can use for achieving thread-safety is implementing synchronized methods.

因此,我们实现线程安全的另一个常用方法是实现同步方法。

Simply put, only one thread can access a synchronized method at a time, while blocking access to this method from other threads. Other threads will remain blocked until the first thread finishes or the method throws an exception.

简单地说,每次只有一个线程可以访问一个同步方法,同时阻断其他线程对这个方法的访问。其他线程将一直被阻断,直到第一个线程完成或该方法抛出一个异常。

We can create a thread-safe version of incrementCounter() in another way by making it a synchronized method:

我们可以通过另一种方式创建incrementCounter()的线程安全版本,把它变成一个同步方法。

public synchronized void incrementCounter() {
    counter += 1;
}

We’ve created a synchronized method by prefixing the method signature with the synchronized keyword.

我们通过在方法签名前加上synchronized关键字来创建一个同步的方法。

Since one thread at a time can access a synchronized method, one thread will execute the incrementCounter() method, and in turn, others will do the same. No overlapping execution will occur whatsoever.

由于每次只有一个线程可以访问同步方法,一个线程将执行incrementCounter()方法,反过来,其他线程也会做同样的事情。没有任何重叠执行的情况会发生。

Synchronized methods rely on the use of “intrinsic locks” or “monitor locks.” An intrinsic lock is an implicit internal entity associated with a particular class instance.

同步方法依赖于 “内在锁 “或 “监控锁 “的使用。内在锁是一个与特定类实例相关的隐性内部实体。

In a multithreaded context, the term monitor is just a reference to the role that the lock performs on the associated object, as it enforces exclusive access to a set of specified methods or statements.

在多线程上下文中,术语monitor只是指锁在相关对象上执行的作用,因为它强制执行对一组指定方法或语句的独占访问。

When a thread calls a synchronized method, it acquires the intrinsic lock. After the thread finishes executing the method, it releases the lock, which allows other threads to acquire the lock and get access to the method.

当一个线程调用一个同步方法时,它获得了内在的锁。在该线程执行完该方法后,它将释放该锁,这允许其他线程获得该锁,并获得对该方法的访问。

We can implement synchronization in instance methods, static methods and statements (synchronized statements).

我们可以在实例方法、静态方法和语句(同步语句)中实现同步化。

9. Synchronized Statements

9.同步报表

Sometimes, synchronizing an entire method might be overkill if we just need to make a segment of the method thread-safe.

有时,如果我们只需要让某个方法的某个部分成为线程安全的,那么同步整个方法可能是多余的。

To exemplify this use case, let’s refactor the incrementCounter() method:

为了举例说明这个用例,让我们重构incrementCounter()方法。

public void incrementCounter() {
    // additional unsynced operations
    synchronized(this) {
        counter += 1; 
    }
}

The example is trivial, but it shows how to create a synchronized statement. Assuming that the method now performs a few additional operations, which don’t require synchronization, we only synchronized the relevant state-modifying section by wrapping it within a synchronized block.

这个例子很微不足道,但它展示了如何创建一个同步语句。假设该方法现在执行了一些额外的操作,这些操作不需要同步,我们只通过将其包裹在synchronized块中来同步相关的状态修改部分。

Unlike synchronized methods, synchronized statements must specify the object that provides the intrinsic lock, usually the this reference.

与同步方法不同,同步语句必须指定提供内在锁的对象,通常是this引用。

Synchronization is expensive, so with this option, we are able to only synchronize the relevant parts of a method.

同步是很昂贵的,所以通过这个选项,我们能够只同步一个方法的相关部分。

9.1. Other Objects as a Lock

9.1.其他对象作为锁

We can slightly improve the thread-safe implementation of the Counter class by exploiting another object as a monitor lock, instead of this.

我们可以通过利用另一个对象作为监控锁,而不是this,来稍微改善Counter类的线程安全实现。

Not only does this provide coordinated access to a shared resource in a multithreaded environment, but it also uses an external entity to enforce exclusive access to the resource:

这不仅提供了在多线程环境中对共享资源的协调访问,而且还使用了一个外部实体来强制执行对资源的独占访问

public class ObjectLockCounter {

    private int counter = 0;
    private final Object lock = new Object();
    
    public void incrementCounter() {
        synchronized(lock) {
            counter += 1;
        }
    }
    
    // standard getter
}

We use a plain Object instance to enforce mutual exclusion. This implementation is slightly better, as it promotes security at the lock level.

我们使用一个普通的Object实例来执行互斥。这个实现稍微好一点,因为它在锁级别上促进了安全。

When using this for intrinsic locking, an attacker could cause a deadlock by acquiring the intrinsic lock and triggering a denial of service (DoS) condition.

当使用这个进行内在锁定时,攻击者可以通过获取内在锁来导致deadlock,并触发拒绝服务(DoS)条件。

On the contrary, when using other objects, that private entity is not accessible from the outside. This makes it harder for an attacker to acquire the lock and cause a deadlock.

相反,当使用其他对象时,该私有实体不能从外部访问。这使得攻击者更难获得锁并导致死锁。

9.2. Caveats

9.2.注意事项

Even though we can use any Java object as an intrinsic lock, we should avoid using Strings for locking purposes:

尽管我们可以使用任何Java对象作为内在的锁,但我们应该避免使用字符串作为锁的目的。

public class Class1 {
    private static final String LOCK  = "Lock";

    // uses the LOCK as the intrinsic lock
}

public class Class2 {
    private static final String LOCK  = "Lock";

    // uses the LOCK as the intrinsic lock
}

At first glance, it seems that these two classes are using two different objects as their lock. However, because of string interning, these two “Lock” values may actually refer to the same object on the string pool. That is, the Class1 and Class2 are sharing the same lock!

乍一看,这两个类似乎在使用两个不同的对象作为它们的锁。然而,由于string interning,这两个 “Lock “值实际上可能指的是string pool上的同一个对象。也就是说,Class1Class2是共享同一个锁的

This, in turn, may cause some unexpected behaviors in concurrent contexts.

这反过来可能会在并发的情况下造成一些意想不到的行为。

In addition to Stringswe should avoid using any cacheable or reusable objects as intrinsic locks. For example, the Integer.valueOf() method caches small numbers. Therefore, calling Integer.valueOf(1) returns the same object even in different classes.

除了字符串我们应该避免使用任何可缓存或可重用对象作为内在锁。例如,Integer.valueOf() 方法会缓存小数字。因此,调用Integer.valueOf(1)会返回相同的对象,即使在不同的类中。

10. Volatile Fields

10.挥发性领域

Synchronized methods and blocks are handy for addressing variable visibility problems among threads. Even so, the values of regular class fields might be cached by the CPU. Hence, consequent updates to a particular field, even if they’re synchronized, might not be visible to other threads.

同步方法和块对于解决线程间的变量可见性问题很方便。即便如此,常规类字段的值可能会被CPU缓存起来。因此,对一个特定字段的后续更新,即使是同步的,也可能对其他线程不可见。

To prevent this situation, we can use volatile class fields:

为了防止这种情况,我们可以使用volatile类字段。

public class Counter {

    private volatile int counter;

    // standard constructors / getter
    
}

With the volatile keyword, we instruct the JVM and the compiler to store the counter variable in the main memory. That way, we make sure that every time the JVM reads the value of the counter variable, it will actually read it from the main memory, instead of from the CPU cache. Likewise, every time the JVM writes to the counter variable, the value will be written to the main memory.

通过volatile关键字,我们指示JVM和编译器将counter变量存储在主内存中。这样,我们确保每次JVM读取counter变量的值时,它实际上是从主内存中读取,而不是从CPU缓存中读取。同样地,每次JVM向counter变量写入时,该值将被写入主内存。

Moreover, the use of a volatile variable ensures that all variables that are visible to a given thread will be read from the main memory as well.

此外,使用volatile变量可以确保所有对某一线程可见的变量也将从主内存中读取。

Let’s consider the following example:

让我们考虑下面的例子。

public class User {

    private String name;
    private volatile int age;

    // standard constructors / getters
    
}

In this case, each time the JVM writes the age volatile variable to the main memory, it will write the non-volatile name variable to the main memory as well. This assures that the latest values of both variables are stored in the main memory, so consequent updates to the variables will automatically be visible to other threads.

在这种情况下,每次JVM将agevolatile变量写入主内存时,它也会将非volatilename变量写入主内存。这就保证了两个变量的最新值都存储在主内存中,因此对变量的后续更新将自动对其他线程可见。

Similarly, if a thread reads the value of a volatile variable, all the variables visible to the thread will be read from the main memory too.

同样地,如果一个线程读取一个volatile变量的值,所有对线程可见的变量也将从主内存中读取。

This extended guarantee that volatile variables provide is known as the full volatile visibility guarantee.

易失性变量提供的这种扩展保证被称为全易失性可见性保证

11. Reentrant Locks

11.可重入的锁

Java provides an improved set of Lock implementations whose behavior is slightly more sophisticated than the intrinsic locks discussed above.

Java提供了一套改进的Lock实现,其行为比上面讨论的内在锁略为复杂。

With intrinsic locks, the lock acquisition model is rather rigid: One thread acquires the lock, then executes a method or code block, and finally releases the lock so other threads can acquire it and access the method.

对于内在锁,锁的获取模式是相当僵化的。一个线程获取锁,然后执行一个方法或代码块,最后释放锁,以便其他线程可以获取锁并访问该方法。

There’s no underlying mechanism that checks the queued threads and gives priority access to the longest waiting threads.

没有底层机制来检查排队的线程,并将优先权交给等待时间最长的线程。

ReentrantLock instances allow us to do exactly that, preventing queued threads from suffering some types of resource starvation:

ReentrantLock 实例正是允许我们这样做,防止排队的线程遭受某些类型的resource starvation

public class ReentrantLockCounter {

    private int counter;
    private final ReentrantLock reLock = new ReentrantLock(true);
    
    public void incrementCounter() {
        reLock.lock();
        try {
            counter += 1;
        } finally {
            reLock.unlock();
        }
    }
    
    // standard constructors / getter
    
}

The ReentrantLock constructor takes an optional fairness boolean parameter. When set to true, and multiple threads are trying to acquire a lock, the JVM will give priority to the longest waiting thread and grant access to the lock.

ReentrantLock构造函数需要一个可选的fairness boolean参数。当设置为true时,如果多个线程试图获取一个锁,JVM将优先考虑等待时间最长的线程,并授予对该锁的访问。

12. Read/Write Locks

12.读/写锁

Another powerful mechanism that we can use for achieving thread-safety is the use of ReadWriteLock implementations.

我们可以用来实现线程安全的另一个强大机制是使用ReadWriteLock的实现。

A ReadWriteLock lock actually uses a pair of associated locks, one for read-only operations and the other for writing operations.

一个ReadWriteLock锁实际上使用一对相关的锁,一个用于只读操作,另一个用于写操作。

As a result, it’s possible to have many threads reading a resource, as long as there’s no thread writing to it. Moreover, the thread writing to the resource will prevent other threads from reading it.

因此,只要没有线程向其写入资源,就有可能有许多线程读取资源。此外,向该资源写入的线程将阻止其他线程读取该资源。

Here’s how we can use a ReadWriteLock lock:

下面是我们如何使用ReadWriteLock锁。

public class ReentrantReadWriteLockCounter {
    
    private int counter;
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();
    
    public void incrementCounter() {
        writeLock.lock();
        try {
            counter += 1;
        } finally {
            writeLock.unlock();
        }
    }
    
    public int getCounter() {
        readLock.lock();
        try {
            return counter;
        } finally {
            readLock.unlock();
        }
    }

   // standard constructors
   
}

13. Conclusion

13.结论

In this article, we learned what thread-safety is in Java, and we took an in-depth look at different approaches for achieving it.

在这篇文章中,我们了解了什么是Java中的线程安全,并深入研究了实现线程安全的不同方法。

As usual, all the code samples shown in this article are available over on GitHub.

像往常一样,本文中显示的所有代码样本都可以在GitHub上找到