Synchronize a Static Variable Among Different Threads – 在不同线程间同步静态变量

最后修改: 2023年 11月 17日

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

1. Overview

1.概述

In Java, it’s not uncommon to need synchronized access to static variables. In this short tutorial, we’ll look at several ways to synchronize access to static variables among different threads.

在 Java 中,需要同步访问 静态变量的情况并不少见。在本教程中,我们将介绍几种在不同线程间同步访问静态变量的方法。

2. About Static Variables

2.关于静态变量

As a quick refresher, static variables belong to the class rather than an instance of the class. This means all instances of a class have the same state for the variable.

作为快速复习,静态变量属于类而不是类的实例。这意味着类的所有实例都具有相同的变量状态。

For example, let’s consider an Employee class with a static variable:

例如,让我们考虑一个带有 static 变量的 Employee 类:

public class Employee {
    static int count;
    int id;
    String name;
    String title;
}

In this case, the count variable is static and represents the number of total employees that have ever worked at the company. No matter how many Employee instances we create, all of them will share the same value for count.

在这种情况下,count 变量是静态的,代表曾经在公司工作过的员工总数。无论我们创建多少个 Employee 实例,所有实例都将共享相同的 count 值。

We can then add code to the constructor to ensure we track the count with each new employee:

然后,我们就可以在构造函数中添加代码,确保对每个新员工的计数进行跟踪:

public Employee(int id, String name, String title) {
    count = count + 1;
    // ...
}

While this approach is straightforward, it potentially has problems when we want to read the count variable. This is especially true in a multi-threaded environment with multiple instances of the Employee class.

虽然这种方法简单明了,但当我们要读取count变量时,它可能会出现问题。尤其是在多线程环境中使用 Employee 类的多个实例时,情况更是如此。

Below, we’ll see different ways to synchronize access to the count variable.

下面,我们将看到同步访问 count 变量的不同方法。

3. Synchronizing Static Variables With the synchronized Keyword

3.使用 synchronized 关键字同步静态变量

The first way we can synchronize our static variable is by using Java’s synchronized keyword. There are several ways we can utilize this keyword for accessing our static variable.

同步静态变量的第一种方法是使用 Java 的 synchronized 关键字。有几种方法可以利用该关键字访问我们的静态变量。

First, we can create a static method that uses the synchronized keyword as a modifier in its declaration:

首先,我们可以创建一个静态方法,在其声明中使用 synchronized 关键字作为修饰符:

public Employee(int id, String name, String title) {
    incrementCount();
    // ...
}

private static synchronized void incrementCount() {
    count = count + 1;
}

public static synchronized int getCount() {
    return count;
}

In this case, the synchronized keyword locks on the class object because the variable is static. This means no matter how many instances of Employee we create, only one can access the variable at once, as long as they use the two static methods.

在这种情况下,synchronized关键字会锁定类对象,因为变量是静态的。这意味着无论我们创建多少个 Employee 实例,只要它们使用了两个静态方法,那么一次只能有一个实例访问该变量。

Secondly, we can use a synchronized block to explicitly synchronize on the class object:

其次,我们可以使用 synchronized 块显式同步类对象:

private static void incrementCount() {
    synchronized(Employee.class) {
        count = count + 1;
    }
}

public static int getCount() {
    synchronized(Employee.class) {
        return count;
    }
}

Note that this is functionally equivalent to the first example, but the code is a little more explicit.

请注意,这个例子在功能上等同于第一个例子,但代码更清晰一些。

Finally, we can also use a synchronized block with a specific object instance instead of the class:

最后,我们还可以使用带有特定对象实例的 synchronized 块来代替类:

private static final Object lock = new Object();

public Employee(int id, String name, String title) {
    incrementCount();
    // ...
}

private static void incrementCount() {
    synchronized(lock) {
        count = count + 1;
    }
}

public static int getCount() {
    synchronized(lock) {
        return count;
    }
}

The reason this is sometimes preferred is because the lock is private to our class. In the first example, it’s possible for other code outside of our control to also lock on our class. With a private lock, we have full control over how it’s used.

之所以有时首选这种方式,是因为锁对我们的类来说是私有的。在第一个示例中,我们无法控制的其他代码也有可能锁定我们的类。有了私有锁,我们就可以完全控制锁的使用。

The Java synchronized keyword is only one way to synchronize access to a static variable. Below, we’ll look at some Java APIs that can also provide synchronization to static variables.

Java synchronized 关键字只是同步访问静态变量的一种方法。下面,我们将介绍一些也能为静态变量提供同步的 Java API。

4. Java APIs To Synchronize Static Variables

4.同步静态变量的 Java API

The Java programming language offers several APIs that can help with synchronization. Let’s look at two of them.

Java 编程语言提供了多个 API,可以帮助实现同步。让我们来看看其中的两个。

4.1. Atomic Wrappers

4.1.原子封装器

Introduced in Java 1.5, the AtomicInteger class is an alternative way to synchronize access to our static variable. This class provides atomic read and write operations, ensuring a consistent view of the underlying value across all threads.

在 Java 1.5 中引入的 AtomicInteger 类是同步访问静态变量的另一种方法。该类提供原子读写操作,确保所有线程对底层值的一致看法

For example, we could rewrite our Employee class using the AtomicInteger type instead of int:

例如,我们可以使用 AtomicInteger 类型而不是 int 重写 Employee 类:

public class Employee {
    private final static AtomicInteger count = new AtomicInteger(0);

    public Employee(int id, String name, String title) {
        count.incrementAndGet();
    }

    public static int getCount() {
        count.get();
    }
}

In addition to AtomicInteger, Java provides atomic wrappers for long and boolean, as well as reference types. All of these wrapper classes are great tools for synchronizing access to static data.

除了 AtomicInteger 之外,Java 还为 longboolean 以及引用类型提供了原子封装类。所有这些封装类都是同步访问静态数据的绝佳工具。

4.2. Reentrant Locks

4.2.可重入锁

Also introduced in Java 1.5, the ReentrantLock class is another mechanism we can use to synchronize access to static data. It provides the same basic behavior and semantics as the synchronized keyword we used earlier but with additional capabilities.

同样在 Java 1.5 中引入的 ReentrantLock 类是我们可以用来同步静态数据访问的另一种机制。它提供了与我们之前使用的 synchronized 关键字相同的基本行为和语义,但具有额外的功能

Let’s see an example of how our Employee class can use the ReentrantLock instead of int:

让我们举例说明 Employee 类如何使用 ReentrantLock 代替 int

public class Employee {
    private static int count = 0;
    private static final ReentrantLock lock = new ReentrantLock();

    public Employee(int id, String name, String title) {
        lock.lock();
        try {
            count = count + 1;
        }
        finally {
            lock.unlock();
        }

        // set fields
    }

    public static int getCount() {
        lock.lock();
        try {
            return count;
        }
        finally {
            lock.unlock();
        }
    }
}

There are a couple of things to note about this approach. First, it’s much more verbose than the others. Each time we access the shared variable, we have to ensure we lock right before the access and unlock right after. This can lead to programmer errors if we forget to do this sequence in every place we access the shared static variable.

这种方法有几点值得注意。首先,它比其他方法要啰嗦得多。每次访问共享变量时,我们都必须确保在访问前锁定,并在访问后解锁。如果我们在每次访问共享静态变量时都忘记执行此序列,就会导致程序员出错。

Additionally, the documentation for the class suggests using a try/finally block to properly lock and unlock. This adds additional lines of code and verbosity, as well as more potential for programmer error if we forget to do this in all cases.

此外,该类的文档建议使用 try/finally 块来正确锁定和解锁。这不仅增加了代码行数和繁琐程度,而且如果我们忘记在所有情况下都这样做,还可能导致程序员出错。

That said, the ReentrantLock class offers additional behavior beyond the synchronized keyword. Among other things, it allows us to set a fairness flag and query the state of the lock to get a detailed view of how many threads are waiting on it.

也就是说,ReentrantLock 类提供了 synchronized 关键字之外的额外行为。其中,它允许我们设置公平性标志并查询锁的状态,以详细了解有多少线程正在等待该锁

5. Conclusion

5.结论

In this article, we looked at several different ways to synchronize access to a static variable across different instances and threads. We first looked at the Java synchronized keyword and saw examples of how we use it as both a method modifier and a static code block.

在本文中,我们介绍了在不同实例和线程间同步访问静态变量的几种不同方法。我们首先了解了 Java synchronized 关键字,并举例说明了如何将其用作方法修改器和静态代码块。

We then looked at two features of the Java concurrent API: AtomicInteger and ReeantrantLock. Both of these APIs offer ways to synchronize access to shared data with some additional benefits beyond the synchronized keyword.

然后,我们了解了 Java 并发 API 的两个特性:AtomicIntegerReeantrantLock.这两个 API 都提供了同步访问共享数据的方法,并在 synchronized 关键字之外提供了一些额外的优势

All of the examples above can be found over on GitHub.

以上所有示例均可在 GitHub 上找到。