对象监视器与线程同步机制

临界区与线程同步基础

临界区定义

在多线程编程中,临界区(Critical Section)指程序中可能因多个线程并发执行而导致结果异常的代码段。这种异常通常源于多个线程对共享资源的并发访问。Java语言本身不提供自动识别临界区的机制,但通过synchronized关键字等构造,允许开发者显式声明临界区并控制线程访问。

典型临界区示例

  • 银行账户余额更新操作
  • 共享缓冲区的读写操作
  • 需要保持原子性的复合操作

Java同步机制

Java通过对象监视器(Monitor)实现线程同步,每个对象都关联一个内置监视器。监视器包含三个核心组件:

  1. 独占锁:控制线程进入临界区
  2. 入口集(Entry Set):等待获取锁的线程集合
  3. 等待集(Wait Set):主动释放锁并等待条件的线程集合
同步声明方式
// 同步方法(实例方法)
public synchronized void updateAccount() {
    // 临界区代码
}

// 同步方法(静态方法)
public static synchronized void audit() {
    // 临界区代码 
}

// 同步代码块
public void transfer() {
    // 非临界区代码
    synchronized(this) {  // 显式指定监视器对象
        // 临界区代码
    }
}

同步类型

互斥同步(Mutual Exclusion)

确保同一时刻只有一个线程能执行临界区代码,通过独占锁实现。例如银行账户余额修改操作:

private int balance = 100;

public synchronized void updateBalance() {
    balance += 10;  // 存款
    balance -= 10;  // 取款
}
条件同步(Conditional Synchronization)

线程间通过条件变量协作,典型场景是生产者-消费者模式:

private Queue buffer = new LinkedList<>();
private final int CAPACITY = 1;

public synchronized void produce(Data data) throws InterruptedException {
    while (buffer.size() == CAPACITY) {
        wait();  // 缓冲区满时等待
    }
    buffer.add(data);
    notifyAll();  // 唤醒消费者
}

public synchronized Data consume() throws InterruptedException {
    while (buffer.isEmpty()) {
        wait();  // 缓冲区空时等待
    }
    Data data = buffer.poll();
    notifyAll();  // 唤醒生产者
    return data;
}

监视器工作流程

  1. 锁获取:线程进入同步代码前必须获得对象监视器锁
  2. 锁重入:已持有锁的线程可重复获取同一锁(锁计数递增)
  3. 条件等待:通过wait()释放锁并进入等待集
  4. 通知机制notify()随机唤醒一个等待线程,notifyAll()唤醒所有等待线程
重要注意事项
  • wait()调用必须放在循环中检查条件,防止虚假唤醒
  • 同步块使用的对象引用必须对所有线程可见(通常声明为final)
  • 构造方法不能声明为synchronized,但内部可使用同步块

典型错误示例

// 错误1:局部变量作为锁对象
public void faultyMethod() {
    Object lock = new Object();  // 每个线程创建新对象
    synchronized(lock) { /* 实际无同步效果 */ }
}

// 错误2:if判断代替循环检查
public synchronized void consume() {
    if (buffer.isEmpty()) {  // 应该使用while
        wait();
    }
    // 可能遇到条件不满足的情况
}

通过合理使用对象监视器机制,可以构建线程安全的高并发程序,但需要注意避免死锁、活锁等同步问题。后续章节将通过生产者-消费者等经典案例展示实际应用场景。

synchronized关键字的实现方式

同步方法实现机制

Java通过对象监视器实现同步方法,分为实例方法和静态方法两种形式:

  1. 实例方法同步:关联当前对象实例的监视器
public class Account {
    private int balance;
    
    // 同步实例方法
    public synchronized void update(int amount) {
        balance += amount;  // 临界区代码
    }
}

线程执行update()方法前必须获取该Account对象实例的监视器锁。

  1. 静态方法同步:关联Class对象的监视器
public class Counter {
    private static int count;
    
    // 同步静态方法  
    public static synchronized void increment() {
        count++;  // 临界区代码
    }
}

线程执行increment()方法前需获取Counter.class对象的监视器锁。

同步代码块实现

同步代码块提供更精细的同步控制,支持任意对象作为监视器:

public class Transaction {
    private final Object lock = new Object();
    private List records;
    
    public void addRecord(Record r) {
        // 非同步代码
        synchronized(lock) {  // 显式指定锁对象
            records.add(r);  // 临界区代码
        }
    }
}

关键优势:

  • 减小同步范围,提升并发性能
  • 支持构造函数内的同步控制
  • 允许不同代码块使用不同锁对象

锁重入机制

Java监视器锁支持可重入特性:

public class ReentrantDemo {
    public synchronized void methodA() {
        methodB();  // 可重入获取同一锁
    }
    
    public synchronized void methodB() {
        // 已持有锁可直接进入
    }
}

实现特点:

  1. 每个锁关联获取计数器
  2. 首次获取时计数器置1
  3. 同一线程每次重入计数器递增
  4. 完全释放需等计数器归零

构造函数的同步限制

构造函数不能声明为synchronized,但可通过同步块实现同步:

public class SafeInit {
    private static volatile SafeInit instance;
    private final Map config;
    
    private SafeInit() {
        // 构造函数同步方案
        synchronized(SafeInit.class) {
            config = loadConfig();  // 线程安全的初始化
        }
    }
    
    public static SafeInit getInstance() {
        if (instance == null) {
            synchronized(SafeInit.class) {
                if (instance == null) {
                    instance = new SafeInit();
                }
            }
        }
        return instance;
    }
}

监视器锁的获取流程

  1. 首次获取
线程进入同步区域
监视器空闲?
获取锁并继续执行
进入入口集等待
  1. 重入场景
public class LockReentry {
    public synchronized void outer() {
        inner();  // 锁计数器+1
    }
    
    public synchronized void inner() {
        // 锁计数器再+1
    }
}  // 方法退出时锁计数器逐级递减

重要实现细节

  1. 对象头结构

    • Mark Word存储锁状态信息
    • 指向监视器对象的指针
  2. 性能优化

    • 偏向锁:单线程访问优化
    • 轻量级锁:多线程交替执行
    • 重量级锁:真实竞争场景
  3. 内存语义

    • 进入同步块触发内存屏障
    • 保证变量可见性
    • 防止指令重排序

正确使用synchronized需要平衡线程安全与性能,避免过度同步导致的性能下降。

监视器的工作模型

入口集与等待集机制

Java对象监视器通过两个关键集合管理线程状态:

  • 入口集(Entry Set):存放等待获取监视器锁的线程,类比医院候诊室的初次挂号患者
  • 等待集(Wait Set):存储调用wait()方法主动释放锁的线程,相当于特殊检查等待区的患者
// 入口集示例
public class EntrySetDemo {
    private final Object lock = new Object();
    
    public void accessResource() {
        synchronized(lock) {  // 未获取锁的线程进入入口集等待
            // 临界区代码
        }
    }
}

// 等待集示例
public class WaitSetDemo {
    private boolean condition = false;
    
    public synchronized void awaitCondition() throws InterruptedException {
        while(!condition) {
            wait();  // 当前线程进入等待集
        }
        // 条件满足后继续执行
    }
}

锁获取流程

JVM自动管理监视器锁的获取与释放,具体流程如下:

  1. 首次获取:当线程进入同步代码块时,若监视器未被占用,立即获得锁
  2. 重入场景:已持有锁的线程可重复获取同一锁(锁计数器递增)
  3. 锁释放:退出同步代码时计数器递减,归零时完全释放
public class LockAcquisition {
    public synchronized void methodA() {
        methodB();  // 锁重入
    }
    
    public synchronized void methodB() {
        // 同一线程可重复获取锁
    }
    
    public void methodC() {
        synchronized(this) {
            // 显式同步块获取锁
        }  // 自动释放锁
    }
}

医生-病人类比模型

通过医疗场景类比可形象理解监视器工作机制:

医疗场景要素 对应监视器概念 技术说明
诊室 监视器 同一时间只允许一个线程访问
初诊候诊区 入口集(Entry Set) 等待获取锁的线程队列
检查等待区 等待集(Wait Set) 调用wait()进入条件等待的线程
医生叫号系统 notify()/notifyAll() 条件满足时的线程唤醒机制
检查结果通知 条件变量 线程恢复执行的前提条件

线程状态转换流程

  1. 新患者挂号(新线程请求锁)

    • 诊室空闲 → 立即就诊(获取锁)
    • 诊室忙碌 → 候诊区等待(进入入口集)
  2. 检查中患者(条件等待线程)

    synchronized(doctor) {
        while(!pupilDilated) {
            doctor.wait();  // 进入特殊等待区
        }
        // 继续检查...
    }
    
  3. 检查完成通知(条件满足通知)

    synchronized(doctor) {
        pupilDilated = true;
        doctor.notifyAll();  // 通知所有等待患者
    }
    

关键实现细节

  1. 等待机制规范

    • 必须持有锁才能调用wait()
    • 典型模式:循环检查条件
    synchronized(lock) {
        while(!condition) {
            lock.wait();
        }
        // 处理业务逻辑
    }
    
  2. 通知机制要点

    • notify()随机唤醒单个等待线程
    • notifyAll()唤醒所有等待线程
    • 通知后线程需重新竞争锁
  3. 错误防范措施

    // 错误示例:使用局部变量作为锁
    public void faultyMethod() {
        Object localLock = new Object();  // 每个线程创建独立实例
        synchronized(localLock) {
            // 实际无同步效果
        }
    }
    
    // 正确做法:使用共享final对象
    private final Object sharedLock = new Object();
    public void correctMethod() {
        synchronized(sharedLock) {
            // 有效的同步控制
        }
    }
    

监视器模型通过这种严谨的线程管理机制,在保证线程安全的同时,实现了高效的资源协调。理解其工作模型是编写正确并发程序的基础。

wait/notify机制详解

核心三要素规范

Java中的wait/notify机制必须遵循三个基本要素:

  1. 同步上下文:必须在synchronized方法或代码块内调用
  2. 锁所有权:调用线程必须持有目标对象的监视器锁
  3. 循环检测:条件检查必须使用while循环而非if判断
// 正确实现模板
public class ConditionWait {
    private final Object lock = new Object();
    private boolean ready = false;
    
    public void await() throws InterruptedException {
        synchronized(lock) {
            while(!ready) {  // 循环检测条件
                lock.wait(); // 释放锁并等待
            }
            // 条件满足后执行操作
        }
    }
    
    public void signal() {
        synchronized(lock) {
            ready = true;
            lock.notifyAll(); // 唤醒所有等待线程
        }
    }
}

通知机制对比

Java提供两种通知方式,具有本质区别:

方法 唤醒范围 适用场景 注意事项
notify() 随机唤醒单个 明确知道只需唤醒一个等待线程 可能产生"信号丢失"问题
notifyAll() 唤醒全部 通用场景,特别是多条件等待情况 可能引起不必要的线程竞争

典型生产者-消费者实现

public class BoundedBuffer {
    private final Queue buffer = new LinkedList<>();
    private final int capacity;
    
    public BoundedBuffer(int capacity) {
        this.capacity = capacity;
    }
    
    public synchronized void produce(int value) throws InterruptedException {
        while(buffer.size() == capacity) {
            wait();  // 缓冲区满时等待
        }
        buffer.add(value);
        notifyAll(); // 唤醒所有消费者
    }
    
    public synchronized int consume() throws InterruptedException {
        while(buffer.isEmpty()) {
            wait();  // 缓冲区空时等待
        }
        int value = buffer.poll();
        notifyAll(); // 唤醒所有生产者
        return value;
    }
}

虚假唤醒防护

虚假唤醒(Spurious Wakeup)指线程可能在没有收到通知的情况下被唤醒。防护措施:

  1. 循环检测模式
while(conditionNotMet) {
    wait();  // 唤醒后重新检查条件
}
  1. 状态变量验证
// 使用原子状态标记
private AtomicBoolean processed = new AtomicBoolean(false);

public void process() throws InterruptedException {
    synchronized(lock) {
        while(!processed.get()) {
            lock.wait();
        }
    }
}

典型应用场景

1. 任务协调器
public class TaskCoordinator {
    private int completedTasks = 0;
    private final int totalTasks;
    
    public TaskCoordinator(int totalTasks) {
        this.totalTasks = totalTasks;
    }
    
    public synchronized void taskCompleted() {
        completedTasks++;
        if(completedTasks == totalTasks) {
            notifyAll(); // 所有任务完成时通知
        }
    }
    
    public synchronized void awaitCompletion() throws InterruptedException {
        while(completedTasks < totalTasks) {
            wait();  // 等待所有任务完成
        }
    }
}
2. 连接池管理
public class ConnectionPool {
    private final List pool = new ArrayList<>();
    private final int maxSize;
    
    public ConnectionPool(int maxSize) {
        this.maxSize = maxSize;
    }
    
    public synchronized Connection getConnection() throws InterruptedException {
        while(pool.isEmpty()) {
            wait();  // 等待可用连接
        }
        return pool.remove(0);
    }
    
    public synchronized void releaseConnection(Connection conn) {
        pool.add(conn);
        notifyAll(); // 通知等待线程
    }
}

重要注意事项

  1. 锁对象一致性
// 错误示例:不同步的锁对象
private Object lock1 = new Object();
private Object lock2 = new Object();

public void faultyMethod() {
    synchronized(lock1) {
        lock2.wait();  // 抛出IllegalMonitorStateException
    }
}
  1. 嵌套调用规范
public class NestedWait {
    private final Object lock = new Object();
    private boolean condition1 = false;
    private boolean condition2 = false;
    
    public void nestedWait() throws InterruptedException {
        synchronized(lock) {
            while(!condition1) {
                lock.wait();
                // 唤醒后需要检查所有相关条件
                if(condition2) {
                    break;
                }
            }
        }
    }
}
  1. 超时等待机制
public synchronized Result getResult(long timeout) throws InterruptedException {
    long endTime = System.currentTimeMillis() + timeout;
    while(!resultReady) {
        long remaining = endTime - System.currentTimeMillis();
        if(remaining <= 0) {
            throw new TimeoutException();
        }
        wait(remaining);  // 带超时的等待
    }
    return result;
}

通过合理运用wait/notify机制,可以构建高效的线程间协作模型,但必须严格遵循使用规范以避免竞态条件和死锁等问题。

线程同步实战案例

BalanceUpdateSynchronized类实现

通过同步方法解决多线程竞争问题,确保余额检查始终一致:

public class BalanceUpdateSynchronized {
    private static int balance = 100;
    
    public static synchronized void updateBalance() {
        balance += 10;  // 存款操作
        balance -= 10;  // 取款操作
    }
    
    public static synchronized void monitorBalance() {
        if (balance != 100) {
            System.out.println("余额异常: " + balance);
            System.exit(1);
        }
    }
}

关键改进点:

  1. 使用static synchronized声明方法,锁定类对象监视器
  2. 确保余额检查与更新操作的原子性
  3. 消除线程交叉执行导致的数据不一致

多锁竞争场景

当线程需要同时获取对象锁和类锁时,可能形成嵌套锁结构:

public class MultiLock {
    public synchronized void instanceMethod() {
        MultiLock.classMethod();  // 需要获取类锁
    }
    
    public static synchronized void classMethod() {
        // 持有类锁时仍可获取实例锁
        new MultiLock().innerMethod();
    }
    
    public synchronized void innerMethod() {
        // 锁重入示例
    }
}

执行特点:

  • 线程需按序获取多个监视器锁
  • 不同锁之间不存在重入关系
  • 可能引发死锁(当锁获取顺序不一致时)

常见同步错误模式

局部变量锁失效问题

public class InvalidLock {
    public void process() {
        Object lock = new Object();  // 每个线程创建独立锁对象
        synchronized(lock) {  // 实际无同步效果
            // 临界区代码
        }
    }
}

修正方案:

public class ValidLock {
    private final Object lock = new Object();  // 共享final锁对象
    
    public void process() {
        synchronized(lock) {  // 有效的同步控制
            // 临界区代码
        }
    }
}

wait()方法超时机制

Java提供三种wait()变体支持条件等待:

public class TimedWait {
    private final Object condition = new Object();
    private boolean ready = false;
    
    public void await(long timeout) throws InterruptedException {
        synchronized(condition) {
            while(!ready) {
                condition.wait(timeout);  // 最大等待指定毫秒
                if(!ready) {
                    throw new TimeoutException();
                }
            }
        }
    }
    
    public void nanosAwait(long nanos) throws InterruptedException {
        synchronized(condition) {
            while(!ready) {
                condition.wait(1000, (int)(nanos%1000000)); // 纳秒精度控制
            }
        }
    }
}

注意事项:

  1. 超时后仍需检查条件状态
  2. 实际等待时间可能超过指定值(受系统调度影响)
  3. 纳秒级控制主要适用于高精度需求场景

线程唤醒策略对比

策略 执行效果 适用场景
notify() 随机唤醒单个等待线程 明确知道只需唤醒一个消费者
notifyAll() 唤醒所有等待线程 多生产者-消费者场景
超时wait() 自动唤醒后检查条件 避免永久阻塞的系统保护

通过合理组合这些同步机制,可以构建健壮的并发程序,在保证线程安全的同时避免死锁和资源竞争问题。

总结

Java线程同步的核心机制建立在对象监视器模型之上,通过synchronized关键字和wait/notify方法实现两种基本同步模式:

互斥同步实现

通过对象内置锁保证临界区原子性访问,JVM自动管理锁获取与释放过程:

// 互斥同步典型实现
public class Counter {
    private int value;
    
    public synchronized void increment() {
        value++;  // 原子性操作
    }
}

条件协作机制

使用wait/notify实现线程间条件协调,必须遵循"循环检测"模式:

// 条件同步标准范式
public class ConditionCoordinator {
    private boolean ready = false;
    
    public synchronized void await() throws InterruptedException {
        while(!ready) {  // 必须使用循环检查
            wait();      // 释放锁并等待
        }
    }
    
    public synchronized void signal() {
        ready = true;
        notifyAll();     // 唤醒所有等待线程
    }
}

同步三要素

  1. 原子性:通过监视器锁保证操作不可分割
  2. 可见性:锁释放前强制刷新工作内存到主内存
  3. 有序性:防止临界区内指令重排序

实践建议

  1. 优先使用java.util.concurrent包中的高级同步工具
  2. 同步范围应尽可能缩小,避免性能损耗
  3. 对共享变量的所有访问路径都必须同步
  4. 避免在持有锁时调用外部方法(防止死锁)

典型错误模式警示:

// 错误示例:不同步的复合操作
public class UnsafeCounter {
    private int value;
    
    public int getAndIncrement() {
        return value++;  // 非原子操作,需要同步
    }
}

在复杂并发场景中,应优先考虑使用ReentrantLockCountDownLatch等JUC组件,它们提供更灵活的同步控制和更好的性能特性。理解底层监视器机制是处理高级并发问题的基础,但实际开发中应当根据具体场景选择最合适的同步策略。

你可能感兴趣的:(Java基础,Java,高质量代码)