引言:什么是线程安全问题?
当多个线程同时访问共享资源时,如果不进行适当的同步控制,可能导致数据不一致、竞态条件等问题。
线程安全类比
想象一个共享厕所:
不安全情况:
- 两个人同时推门进去
- 看到对方在里面
- 尴尬了!(数据冲突)
安全情况:
- 门上有个锁(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
| 特性 | volatile | synchronized |
|---|---|---|
| 可见性 | ✓ 保证 | ✓ 保证 |
| 原子性 | ✗ 不保证 | ✓ 保证 |
| 性能 | 轻量级 | 重量级(需获取锁) |
| 使用场景 | 状态标志、配置更新 | 复合操作、保护共享资源 |
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*)在某些场景下性能更好
- 注意死锁问题:固定加锁顺序、避免嵌套锁
- 单例模式推荐使用双重检查锁定或静态内部类