教程中心 / Java教程 / 线程同步

线程同步

18分钟 多线程

引言:什么是线程安全问题?

多个线程同时访问共享资源时,如果不进行适当的同步控制,可能导致数据不一致竞态条件等问题。

线程安全类比

想象一个共享厕所:

不安全情况:
- 两个人同时推门进去
- 看到对方在里面
- 尴尬了!(数据冲突)

安全情况:
- 门上有个锁(synchronized)
- 一个人进去,锁门
- 另一个人只能在外面等
- 里面的人出来,开锁
- 下一个人才能进去

这就是线程同步的核心:锁机制

问题演示:多线程抢票

不安全的代码

class UnsafeTicketSeller implements Runnable {
    private int tickets = 10;

    @Override
    public void run() {
        while (tickets > 0) {
            // 问题:下面的操作不是原子性的!
            System.out.println(Thread.currentThread().getName() +
                "卖出第" + tickets + "张票");
            tickets--;  // 读取->减1->写回,三步操作
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {}
        }
    }
}

// 测试
UnsafeTicketSeller seller = new UnsafeTicketSeller();
new Thread(seller, "窗口1").start();
new Thread(seller, "窗口2").start();
new Thread(seller, "窗口3").start();

// 可能出现的问题:
// 1. 同一张票被多次卖出
// 2. 票数变成负数
// 3. 打印顺序混乱

synchronized关键字

synchronized是Java提供的内置锁机制,可以保证原子性可见性

同步方法

class SafeTicketSeller implements Runnable {
    private int tickets = 10;

    @Override
    public synchronized void run() {
        while (tickets > 0) {
            System.out.println(Thread.currentThread().getName() +
                "卖出第" + tickets + "张票");
            tickets--;
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {}
        }
    }
}

同步代码块(推荐)

class SafeTicketSeller implements Runnable {
    private int tickets = 10;
    private Object lock = new Object();  // 锁对象

    @Override
    public void run() {
        while (true) {
            synchronized (lock) {  // 只同步必要的部分
                if (tickets > 0) {
                    System.out.println(Thread.currentThread().getName() +
                        "卖出第" + tickets + "张票");
                    tickets--;
                } else {
                    break;
                }
            }
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {}
        }
    }
}

synchronized的三种用法

用法锁对象说明
synchronized实例方法this(当前对象)锁定整个实例
synchronized static方法Class对象锁定整个类
synchronized(object)指定对象细粒度锁定

锁定实例 vs 锁定类

class MyClass {
    private int count = 0;

    // 锁定当前实例,不同实例之间不互斥
    public synchronized void instanceMethod() {
        count++;
    }

    // 锁定类,所有实例共享同一把锁
    public static synchronized void staticMethod() {
        count++;
    }

    // 代码块锁定指定对象
    public void blockMethod() {
        synchronized (this) {  // 锁定当前实例
            count++;
        }
    }
}

synchronized的原理

Monitor(监视器锁)机制:每个Java对象都有一把关联的monitor锁。线程进入synchronized代码块时,需要获取对象的monitor,执行完毕后自动释放。

工作流程

线程A执行synchronized方法:
1. 检查monitor是否被占用
2. 如果没有被占用,线程A获得锁,进入方法
3. 执行方法体
4. 方法结束,释放锁

线程B执行同一个synchronized方法:
1. 检查monitor是否被占用
2. 如果被线程A占用,线程B阻塞等待
3. 线程A释放锁
4. 线程B获得锁,进入方法

volatile关键字

volatile保证变量的可见性,即一个线程修改了volatile变量的值,其他线程能立即看到。

volatile的作用

作用说明
可见性保证写操作立即刷新到主内存,读操作从主内存读取
禁止指令重排序防止编译器和CPU优化导致的重排序

volatile使用示例

class VolatileDemo extends Thread {
    private volatile boolean running = true;  // 关键:volatile

    @Override
    public void run() {
        int count = 0;
        while (running) {  // 每次循环都会从主内存读取
            count++;
        }
        System.out.println("线程停止,count=" + count);
    }

    public void stopThread() {
        running = false;  // 修改会立即刷新到主内存
    }
}

// 测试
VolatileDemo demo = new VolatileDemo();
demo.start();
Thread.sleep(100);
demo.stopThread();  // 线程会正确停止

volatile vs synchronized

特性volatilesynchronized
可见性✓ 保证✓ 保证
原子性✗ 不保证✓ 保证
性能轻量级重量级(需获取锁)
使用场景状态标志、配置更新复合操作、保护共享资源

volatile的局限性

// volatile不保证原子性!
private volatile int count = 0;

// 这个操作不是线程安全的!
// 即使count是volatile,count++也不是原子性操作
public void increment() {
    count++;  // 问题:读取->加1->写回,三步操作
}

// 正确做法:使用synchronized或AtomicInteger
private int count = 0;

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

// 或者使用原子变量
private AtomicInteger count = new AtomicInteger();

public void increment() {
    count.incrementAndGet();
}

原子变量

java.util.concurrent.atomic包提供了原子变量,在某些场景下可以替代synchronized,提供更高的并发性能。

常用原子类

说明
AtomicInteger原子整数
AtomicLong原子长整数
AtomicBoolean原子布尔
AtomicReference<T>原子引用
AtomicIntegerArray原子整数数组

AtomicInteger示例

import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();  // 原子操作
    }

    public int get() {
        return count.get();
    }

    public void decrement() {
        count.decrementAndGet();
    }

    public int getAndIncrement() {
        return count.getAndIncrement();
    }
}

// 使用
AtomicCounter counter = new AtomicCounter();
counter.increment();
counter.increment();
System.out.println(counter.get());  // 2

死锁

死锁是指两个或多个线程相互等待对方释放锁,导致程序无法继续执行。

死锁示例

class DeadLockDemo {
    private Object lock1 = new Object();
    private Object lock2 = new Object();

    public void method1() {
        synchronized (lock1) {
            System.out.println("线程A获得锁1");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {}
            // 尝试获取锁2,但锁2被线程B持有
            synchronized (lock2) {
                System.out.println("线程A获得锁2");
            }
        }
    }

    public void method2() {
        synchronized (lock2) {
            System.out.println("线程B获得锁2");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {}
            // 尝试获取锁1,但锁1被线程A持有
            synchronized (lock1) {
                System.out.println("线程B获得锁1");
            }
        }
    }
}

// 线程A执行method1,线程B执行method2
// 线程A获得锁1,等待锁2
// 线程B获得锁2,等待锁1
// 死锁!

避免死锁的方法

固定加锁顺序:所有线程按相同顺序获取锁
使用超时:tryLock()设置超时时间
减少锁的使用:尽量缩小同步范围
避免嵌套锁:尽量不要在持有一个锁时获取另一个锁

固定加锁顺序解决死锁

// 错误:不同线程按不同顺序获取锁
public void method1() {
    synchronized (lock1) {  // 线程A先锁1
        synchronized (lock2) {
            // 操作
        }
    }
}

public void method2() {
    synchronized (lock2) {  // 线程B先锁2
        synchronized (lock1) {
            // 操作
        }
    }
}

// 正确:所有线程按相同顺序获取锁
public void method1() {
    synchronized (lock1) {  // 所有线程都先锁1
        synchronized (lock2) {
            // 操作
        }
    }
}

public void method2() {
    synchronized (lock1) {  // 所有线程都先锁1
        synchronized (lock2) {
            // 操作
        }
    }
}

线程安全的单例模式

双重检查锁定(推荐)

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {  // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) {  // 第二次检查
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

静态内部类(更简洁)

public class Singleton {
    private Singleton() {}

    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

实战:线程安全的计数器

import java.util.concurrent.atomic.AtomicInteger;

class ThreadSafeCounter {
    private AtomicInteger value = new AtomicInteger(0);

    public void increment() {
        value.incrementAndGet();
    }

    public void decrement() {
        value.decrementAndGet();
    }

    public int get() {
        return value.get();
    }

    public int getAndSet(int newValue) {
        return value.getAndSet(newValue);
    }
}

// 测试
public class CounterTest {
    public static void main(String[] args) throws InterruptedException {
        ThreadSafeCounter counter = new ThreadSafeCounter();
        int threadCount = 100;
        Thread[] threads = new Thread[threadCount];

        for (int i = 0; i < threadCount; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter.increment();
                }
            });
            threads[i].start();
        }

        for (Thread t : threads) {
            t.join();
        }

        System.out.println("最终计数: " + counter.get());
        // 输出:100000(正确)
    }
}

小结

  • synchronized保证原子性和可见性,是阻塞式同步
  • volatile只保证可见性,不保证原子性
  • 应尽量缩小同步范围(使用同步代码块而非同步方法)
  • 原子变量(Atomic*)在某些场景下性能更好
  • 注意死锁问题:固定加锁顺序、避免嵌套锁
  • 单例模式推荐使用双重检查锁定或静态内部类
☕ Java 在线代码编辑器
📝 运行结果