CodeGym /课程 /JAVA 25 SELF /synchronized, volatile:语法与用法

synchronized, volatile:语法与用法

JAVA 25 SELF
第 52 级 , 课程 1
可用

1. 关键字 synchronized:为什么以及如何使用

在 Java 中,关键字 synchronized 就像洗手间门上的“占用中!”牌子:当一个线程位于“临界区”时,其他线程会礼貌地等待轮到自己。只有当第一个离开后,下一个才能进入并执行其代码。

语法:代码块与方法

同步代码块

synchronized (object) {
    // 临界区
}
  • object — 任意你想“上锁”的对象。只要有一个线程在执行该代码块,其他也想进入使用相同对象作为锁的线程就会等待。

同步方法

public synchronized void increment() {
    // 临界区
}
  • 这里“锁”加在对象本身(this)上。也就是说,同一时间只有一个线程可以执行该对象的任意同步方法。

静态同步方法

public static synchronized void foo() {
    // 临界区
}
  • 这里在类级别(ClassName.class)进行加锁,而不是具体对象。

底层如何工作

当线程进入同步代码块或方法时,它会获取该对象的“监视器”(对于静态方法则是类的监视器)。如果监视器已被占用——线程就等待。一旦监视器释放,下一个线程即可进入。

2. 示例:有无同步的计数器自增

未同步

public class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}
public class CounterDemo {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("最终值: " + counter.getCount());
    }
}

期望值: 2000
实际值: 可能更小(例如,19951987……),而且每次运行都会有自己的“惊喜”。

为什么?因为操作 count++ 不是原子的:它分成三个步骤——读取值、递增、写回。如果两个线程同时进行,就可能互相“覆盖”结果。

解决方案:synchronized

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

现在同一时间只有一个线程可以执行 increment() 方法。最终值将始终是 2000

替代方案:同步代码块

public class Counter {
    private int count = 0;

    public void increment() {
        synchronized (this) {
            count++;
        }
    }
}

结果相同。你可以只同步需要的那一部分,而不是整个方法。

3. 对象监视器简介

监视器是 Java 中内置于每个对象的“锁”。当你写 synchronized(object) 时,线程会尝试“锁住”这个对象。如果锁是空闲的——线程获得锁;如果不是——就等待。一旦线程退出该代码块,锁便释放。

重要! 如果你在不同的对象上进行同步——线程之间不会互相等待。因此选择正确的同步对象非常重要。

静态同步方法

有时共享资源并不是某个对象,而是对类的所有实例都通用的东西(例如静态变量)。这种情况下,应在类级别进行同步。

public class StaticCounter {
    private static int count = 0;

    public static synchronized void increment() {
        count++;
    }

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

这等价于:

public static void increment() {
    synchronized (StaticCounter.class) {
        count++;
    }
}

监视器加在类对象(Class)上,而不是具体实例。

4. 关键字 volatile:是什么,有何作用

线程间可见性问题

在 Java 中,每个线程都可能为了加速而缓存变量的值。这意味着如果一个线程修改了变量,另一个线程可能“察觉不到”,仍然从自己的本地缓存中读取。这对线程间用来传递信号的标志尤其关键。

volatile 如何工作

如果变量被声明为 volatile,意味着:

  • 所有线程总是只从主内存读取、并写回主内存,绕过缓存。
  • 对该变量的任何修改会立刻对所有线程可见。

但是!volatile 变量的操作本身并不都是原子的(除了对 booleanint 等这类简单原始类型的读/写)。如果你做的事情比简单赋值更复杂——就需要同步。

示例:停止标志

public class Worker extends Thread {
    private volatile boolean running = true;

    public void run() {
        while (running) {
            // 做一些有用的事情
        }
        System.out.println("线程已结束");
    }

    public void shutdown() {
        running = false;
    }
}
Worker w = new Worker();
w.start();
// ... 过一段时间
w.shutdown();

如果没有 volatile,线程可能“注意不到”标志的变化而陷入无限循环(尤其在多核系统上)。使用 volatile 时——一切会按预期工作。

5. volatile 的限制:非原子性

很多新手会想:“如果把 int 声明为 volatile,就可以写 count++ 而不操心了”。很遗憾,并非如此:

private volatile int count = 0;

public void increment() {
    count++;
}

错误! 操作 count++ 仍然不是原子的——它是三步:(1) 读取,(2) 增加,(3) 写回。如果两个线程同时读取到相同的值,它们都会将其增加并写回相同的结果——就会“丢失”一次自增。

结论: volatile 只提供可见性,但不能在复杂操作中防止竞态条件。

6. 何时使用 synchronized,何时使用 volatile

  • volatile —— 当你有一个简单的标志(例如 boolean),由一个线程写、另一个线程读。示例:线程结束信号、事件通知。
  • synchronized —— 当需要保证复杂操作的原子性(例如自增、修改多个变量、操作数据结构)。

记忆表

场景 volatile synchronized
在线程之间传递信号
原子操作(自增)
临界区中的多步操作
仅保证可见性

7. 使用 synchronizedvolatile 时的常见错误

错误 1:在错误的对象上进行同步。 如果你在局部变量上同步,或每个线程使用不同的对象进行同步——就起不到任何保护作用。

Object lock = new Object();
synchronized (lock) {
    // ...
}

如果每个线程各自创建自己的 lock —— 等于没有锁。需要一个统一的同步点,即所有线程共享的锁对象。

错误 2:指望 volatile 提供原子性。 volatile 只保证可见性,不保证原子性。像 count++ 这样的操作在没有同步的情况下依然不安全。

错误 3:同步的代码范围过大。 如果你同步了整个方法,而实际上只需要同步一行代码,你就在无谓地阻塞其它线程,损失性能。尽量缩小“临界区”。

错误 4:对静态数据忘了进行类级别的同步。 如果变量是静态的,而你在 this 上同步,这是无效的。静态数据需要在类级别同步:synchronized(ClassName.class)。

错误 5:在字符串字面量上同步。 在字符串上同步是危险的,因为相同的字面量会被 JVM 驻留(intern)。程序的不同部分可能意外地共享同一把锁。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION