1. 概述

在Java中,对静态变量的同步访问是常见的需求。在这篇简短教程中,我们将探讨几种方法来在不同线程间同步静态变量的访问。

2. 关于静态变量

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

例如,考虑一个带有静态变量的Employee类:

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

在这个例子中,count变量是静态的,它代表公司历史上工作的员工总数。无论创建多少个Employee实例,它们都共享同一个count值。

然后我们可以在构造函数中添加代码,确保每次新员工入职时都能跟踪计数:

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

虽然这种方法直接易懂,但当我们想要读取count变量时,可能会出现问题,尤其是在多线程环境中,有多个Employee实例的情况下

接下来,我们将看到不同方式来同步count变量的访问。

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

我们可以使用Java的synchronized关键字来同步我们的静态变量。有几种方法可以利用这个关键字访问静态变量。

首先,我们可以在声明中使用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;
}

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

其次,我们可以使用synchronized块来显式地锁定在类对象上:

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

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

请注意,这与第一个示例功能相同,但代码更明确。

最后,我们也可以使用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;
    }
}

这样做的原因有时更可取,因为锁是私有的,只对我们类可见。在第一个例子中,外部不受控制的代码也可能锁定在我们的类上。使用私有锁,我们完全控制它的使用方式。

Java的synchronized关键字只是同步访问静态变量的一种方式。下面,我们将看看Java的一些API,它们也可以为静态变量提供同步。

4. 同步静态变量的Java API

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

4.1. 原子包装器

自Java 1.5以来引入的AtomicInteger类是另一种同步访问静态变量的方法。这个类提供了原子的读写操作,确保所有线程对底层值有一个一致的视图

例如,我们可以使用AtomicInteger类型重写我们的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();
    }
}

除了AtomicIntegerJava还提供了对longboolean的原子包装,以及引用类型的包装。所有这些包装类都是同步共享数据的好工具

4.2. 重入锁

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

让我们看看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();
        }
    }
}

关于这种方法,有几个需要注意的地方。首先,它比其他方法冗长得多。每次我们访问共享变量时,都必须确保在访问前锁定并在访问后解锁。如果我们忘记在每个访问共享静态变量的地方执行这个序列,可能会导致编程错误。

此外,类的文档建议使用try/finally/块正确地锁定和解锁。这增加了代码行数和冗余,如果我们在所有情况下都忘记这样做,也增加了编程错误的可能性。

尽管如此,ReentrantLock类提供了synchronized关键字之外的额外行为。其中一些包括设置公平标志和查询锁的状态,以获取详细查看有多少线程在等待它。

5. 总结

在这篇文章中,我们探讨了多种不同的方式来在不同实例和线程之间同步静态变量的访问。我们首先了解了Java的synchronized关键字,并看到了如何作为方法修饰符和静态代码块使用它。

然后我们研究了Java并发API的两个特性:AtomicIntegerReentrantLock这两个API都提供了同步共享数据的方法,而且与synchronized关键字相比,它们还有一些额外的好处

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