Java线程同步详解

110 阅读4分钟

Java线程同步详解

在多线程编程中,线程同步是确保多个线程安全访问共享资源的核心机制。Java提供了多种同步工具和方法,以下是其核心实现方式及适用场景:


一、同步的核心目标

  1. 原子性:确保操作不可分割,避免中间状态被其他线程干扰。
  2. 可见性:确保共享变量的修改对其他线程立即可见。
  3. 有序性:防止指令重排序破坏程序逻辑。

二、同步的实现方式

1. 内置锁(synchronized

原理:通过对象头的Monitor锁机制实现。 • 使用方式

  // 同步代码块
  synchronized (lockObject) {
      // 临界区代码
  }
​
  // 同步方法
  public synchronized void method() {
      // 临界区代码
  }

特点: • 自动释放锁:代码块或方法执行完毕自动释放锁。 • 可重入性:同一线程可多次获取同一把锁。 • 性能开销:在JDK 6后优化(偏向锁、轻量级锁),适用于低竞争场景。

2. 显式锁(ReentrantLock

原理:基于AQS(AbstractQueuedSynchronizer)实现,提供更灵活的锁控制。 • 使用方式

Lock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区代码
} finally {
    lock.unlock(); // 必须手动释放锁
}

特点: • 可中断锁lockInterruptibly()允许线程响应中断。 • 公平性:支持公平锁(按等待顺序获取锁)。 • 条件变量:通过Condition实现多条件等待。

3. volatile关键字

原理:通过内存屏障保证变量的可见性,禁止指令重排序。 • 使用方式

private volatile boolean flag = false;

适用场景: • 单一变量的原子操作(如状态标志位)。 • 配合CAS操作实现无锁编程。

4. 原子类(AtomicInteger等)

原理:基于CAS(Compare-And-Swap)实现无锁原子操作。 • 使用方式

AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // 原子递增

适用场景: • 计数器、累加器等简单原子操作。 • 替代synchronized,减少锁竞争。

5. 同步工具类

CountDownLatch:等待一组操作完成。

CountDownLatch latch = new CountDownLatch(3);
latch.await(); // 等待计数器归零
latch.countDown(); // 计数器减1

CyclicBarrier:多个线程到达屏障后同时继续。

CyclicBarrier barrier = new CyclicBarrier(3);
barrier.await(); // 等待其他线程到达屏障

Semaphore:控制并发线程数。

Semaphore semaphore = new Semaphore(5);
semaphore.acquire(); // 获取许可
semaphore.release(); // 释放许可

三、同步的核心问题与解决方案

1. 死锁

条件:互斥、占有且等待、不可抢占、循环等待。 • 预防: • 避免嵌套锁。 • 使用tryLock()设置超时时间。 • 按固定顺序获取锁。

2. 活锁

场景:线程不断重试失败的操作(如CAS自旋)。 • 解决:引入随机退避机制。

3. 饥饿

场景:低优先级线程长期无法获取资源。 • 解决:使用公平锁或调整线程优先级。


四、同步的性能优化

  1. 减小锁粒度 • 使用分段锁(如ConcurrentHashMap的分段锁机制)。 • 避免锁住整个方法,仅同步必要代码块。

  2. 无锁编程 • 使用原子类(AtomicInteger)或CAS操作。 • 示例:无锁计数器。

    public class NonblockingCounter {
        private AtomicInteger value = new AtomicInteger(0);
        public int getValue() { return value.get(); }
        public int increment() { return value.incrementAndGet(); }
    }
    
  3. 读写分离 • 使用ReentrantReadWriteLock,允许多个读线程共享锁。

    ReadWriteLock rwLock = new ReentrantReadWriteLock();
    rwLock.readLock().lock();    // 读锁(共享)
    rwLock.writeLock().lock();   // 写锁(独占)
    
  4. 锁粗化与锁消除锁粗化:合并多个连续锁操作,减少锁开销。 • 锁消除:JVM通过逃逸分析移除不必要的锁。


五、同步的最佳实践

  1. 优先使用高层工具 • 使用ConcurrentHashMap替代手动同步的HashMap。 • 使用BlockingQueue实现生产者-消费者模型。

  2. 避免嵌套锁 • 若必须使用多个锁,按固定顺序获取。

  3. 使用tryLock()控制超时

    if (lock.tryLock(1, TimeUnit.SECONDS)) {
        try { /* 临界区 */ }
        finally { lock.unlock(); }
    } else { /* 处理超时 */ }
    
  4. 优先使用不可变对象 • 不可变对象(如String)无需同步。


六、示例场景

1. 线程安全的单例模式
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;
    }
}
2. 生产者-消费者模型(BlockingQueue
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);

// 生产者
new Thread(() -> {
    while (true) {
        queue.put(1); // 队列满时自动阻塞
    }
}).start();

// 消费者
new Thread(() -> {
    while (true) {
        Integer data = queue.take(); // 队列空时自动阻塞
    }
}).start();

七、总结

同步方式适用场景优势劣势
synchronized简单同步需求、低竞争环境自动管理锁、代码简洁灵活性较低
ReentrantLock复杂锁需求(如超时、公平性)支持条件变量、可中断锁需手动释放锁
volatile单一状态标志、可见性保证无锁、高性能仅保证可见性,不保证复合操作原子性
原子类计数器、累加器等简单原子操作无锁、高性能仅适用于单一变量
同步工具类多线程协作(如任务分阶段)简化代码逻辑功能特定

选择建议: • 简单场景:优先使用synchronized或原子类。 • 复杂需求:使用ReentrantLock或并发工具类。 • 性能敏感:采用无锁编程(CAS、不可变对象)。

通过合理选择同步机制,可以构建高效、安全的并发程序,同时避免死锁、活锁等常见问题。