ZetCode

Java synchronized

上次修改时间:2024 年 7 月 16 日

在本文中,我们将展示如何使用 synchronized 关键字来确保 Java 中的线程安全。

Java 中的 synchronized 关键字是确保多线程程序中线程安全的基本工具。它提供了一种机制来控制多个线程对共享资源的访问,从而防止竞争条件和数据不一致。

我们可以通过将 synchronized 应用于方法或代码块来控制同步的范围。

当应用于方法时,它确保一次只有一个线程可以执行该方法。尝试访问 synchronized 方法的其他线程将被阻塞,直到第一个线程完成其执行并释放锁。

当应用于代码块时,它创建一个 synchronized 块。一次只有一个线程可以进入并执行块内的代码。尝试进入 synchronized 块的其他线程将被阻塞,直到第一个线程退出该块。

锁定机制

synchronized 关键字依赖于一个称为监视器(也称为固有锁)的概念。 Java 中的每个对象都有一个关联的监视器。当线程进入 synchronized 块或方法时,它会获取与该块或方法关联的对象上的锁。尝试访问相同 synchronized 块或方法的其他线程将被暂停,直到第一个线程释放该锁。这确保了只有一个线程可以独占访问共享资源。

获取和释放锁可能会引入一些开销。应该谨慎使用 synchronized 关键字,仅用于需要线程安全的关键部分。

用例

当您有多个线程并发访问和修改相同数据时,请使用 synchronized。这可以防止竞争条件,即结果取决于线程执行的不可预测的时序。它适用于多个线程需要更新共享计数器、修改数据结构或访问依赖于一致状态的关键代码段的场景。

Synchronized 方法

以下示例使用 synchronized 方法。

Main.java
import java.util.Scanner;

class Task {

    synchronized void process(int n) {

        System.out.println(Thread.currentThread().getName());

        for (int i = 1; i <= 10; i++) {

            System.out.printf("%d ", n + i);

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        System.out.println();
    }
}


void main() {

    // only one object
    final Task obj = new Task();

    for (int i = 1; i < 5; i++) {
        int e = i;
        String name = String.format("VirThread-%d", e);
        Thread.ofVirtual().name(name).start(() -> obj.process(e * 10));
    }

    waitForKeyPress();
}

void waitForKeyPress() {

    try (var scanner = new Scanner(System.in)) {
        scanner.nextLine();
    }
}

该示例创建了五个虚拟线程,它们都将 Task 作为参数并调用其 process 方法。 synchronized 关键字确保一次只有一个线程启动该方法。

for (int i = 1; i < 5; i++) {
    int e = i;
    String name = String.format("VirThread-%d", e);
    Thread.ofVirtual().name(name).start(() -> obj.process(e * 10));
}

在一个 for 循环中,我们创建并启动五个虚拟线程。

void waitForKeyPress() {

    try (var scanner = new Scanner(System.in)) {
        scanner.nextLine();
    }
}

我们使用 scanner 来防止主线程完成。有一些工具,例如 CountDownLatch,可以完成此类任务,但为了简单起见,我们选择了这种更简单的方法。

$ java Main.java
VirThread-1
11 12 13 14 15 16 17 18 19 20 
VirThread-4
41 42 43 44 45 46 47 48 49 50 
VirThread-3
31 32 33 34 35 36 37 38 39 40 
VirThread-2
21 22 23 24 25 26 27 28 29 30 

Synchronized 计数器

以下示例创建一个 synchronized 计数器。

Main.java
class Counter {

    private int counter = 0;

    public synchronized void inc() {
        counter++;
    }

    public synchronized void dec() {
        counter--;
    }

    public int getCounter() {
        return counter;
    }
}

class Task extends Thread {

    private final String name;
    private final Counter counter;

    public Task(Counter counter, String name) {

        this.counter = counter;
        this.name = name;
    }

    public void run() {

        for (int i = 0; i <= 1000; i++) {

            if (name.contains("inc"))
                counter.inc();
            else
                counter.dec();
        }
    }
}

void main() {

    final var counter = new Counter();

    var taskInc = new Task(counter, "Thread-inc");
    var taskDec = new Task(counter, "Thread-dec");

    taskInc.start();
    taskDec.start();

    try {
        taskInc.join();
        taskDec.join();
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }

    System.out.println(counter.getCounter());
}

在该示例中,我们有两个任务。一个将计数器递增 1000 次,另一个将其递减 1000 次。所以最终输出必须为零。

class Counter {

    private int counter = 0;

    public synchronized void inc() {
        counter++;
    }

    public synchronized void dec() {
        counter--;
    }

    public int getCounter() {
        return counter;
    }
}

为了使该示例起作用,我们必须在 incdec 方法上使用 synchronized 关键字。

try {
    taskInc.join();
    taskDec.join();
} catch (InterruptedException e) {
    throw new RuntimeException(e);
}

join 方法确保主线程等待直到加入的线程完成。

$ java Main.java
0

来源

Java Synchronized 方法 - 教程

在本文中,我们使用了 synchronized Java 关键字。

作者

我叫 Jan Bodnar,是一位热情的程序员,拥有丰富的编程经验。 自 2007 年以来,我一直在撰写编程文章。 迄今为止,我已经撰写了 1,400 多篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出所有Java教程