并发编程(六)

Java中的锁技术概述

锁是多线程编程中保证线程安全的核心机制,它通过限制对共享资源的并发访问来维护数据一致性。在Java并发编程中,正确使用锁可以避免竞态条件、内存可见性等问题。Java提供了多种锁机制,包括:

  1. 内置锁 :基于synchronized关键字的隐式锁
  2. 显式锁java.util.concurrent.locks包中的ReentrantLock
  3. 读写锁ReentrantReadWriteLock实现读-写分离
  4. 乐观锁StampedLock提供的乐观读模式

不同锁机制适用于不同场景,选择时需考虑并发度、吞吐量要求和业务特点。

synchronized关键字

基本用法

synchronized是Java最基础的同步机制,有两种使用方式:

  1. 同步方法
java 复制代码
public synchronized void method() {
    // 同步代码
}
  1. 同步代码块
java 复制代码
public void method() {
    synchronized(this) {  // 锁对象
        // 同步代码
    }
}

底层实现

synchronized基于Monitor机制实现,每个Java对象都与一个Monitor相关联:

  • 当线程进入同步块时,会尝试获取Monitor的所有权
  • 获取成功则持有锁,其他线程必须等待
  • 退出同步块时释放Monitor

JVM通过对象头中的Mark Word记录锁状态,包含无锁、偏向锁、轻量级锁和重量级锁等状态。

优缺点

优点

  • 语法简单,自动释放锁
  • JVM内置支持,优化空间大
  • 不需要显式创建锁对象

缺点

  • 无法中断等待锁的线程
  • 不支持尝试获取锁(tryLock)
  • 只有一种锁模式(非公平)
  • 粒度较粗,可能影响性能

ReentrantLock

ReentrantLockLock接口的主要实现类,提供比synchronized更灵活的锁操作。

主要特性

  1. 可重入性 :与synchronized相同,允许线程重复获取已持有的锁
  2. 公平性选择
    • 公平锁:按请求顺序分配(构造传入true)
    • 非公平锁:允许插队(默认,吞吐量更高)
  3. 锁绑定:可与多个Condition关联,实现精细等待/通知

高级功能

java 复制代码
Lock lock = new ReentrantLock();

// 1. 可中断获取
lock.lockInterruptibly();

// 2. 尝试获取锁
if(lock.tryLock()) {
    try {
        // 操作共享资源
    } finally {
        lock.unlock();
    }
}

// 3. 超时获取
if(lock.tryLock(5, TimeUnit.SECONDS)) {
    // ...
}

与synchronized对比

特性 synchronized ReentrantLock
实现方式 JVM内置 Java代码实现
锁释放 自动 必须手动unlock
可中断 不支持 支持
尝试获取 不支持 支持
公平锁 非公平 可配置
Condition 单一wait/notify 支持多个
性能 Java6后优化相当 高竞争时可能更优

ReadWriteLock

ReentrantReadWriteLock实现了读写分离,允许多个读操作并发执行,但写操作独占。

锁协作机制

  • 读锁:共享锁,多个线程可同时持有
  • 写锁:独占锁,排斥所有其他锁
  • 锁降级:持有写锁时获取读锁,然后释放写锁(反向不允许)

适用场景

缓存系统示例

java 复制代码
class Cache<K,V> {
    private final Map<K,V> map = new HashMap<>();
    private final ReadWriteLock rwl = new ReentrantReadWriteLock();
    
    public V get(K key) {
        rwl.readLock().lock();
        try {
            return map.get(key);
        } finally {
            rwl.readLock().unlock();
        }
    }
    
    public void put(K key, V value) {
        rwl.writeLock().lock();
        try {
            map.put(key, value);
        } finally {
            rwl.writeLock().unlock();
        }
    }
}

性能优势

  • 读多写少时(如90%读+10%写),吞吐量可提升5-10倍
  • 减少线程竞争,提高并发度

StampedLock

Java 8引入的性能更优的锁,支持三种访问模式:

锁模式

  1. 写锁:独占锁,类似ReentrantLock
  2. 悲观读锁:共享锁,类似ReadWriteLock的读锁
  3. 乐观读:不阻塞写操作,读取后需验证

典型使用

java 复制代码
class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();
    
    void move(double deltaX, double deltaY) {
        long stamp = sl.writeLock(); // 获取写锁
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp); // 释放写锁
        }
    }
    
    double distanceFromOrigin() {
        // 1. 尝试乐观读
        long stamp = sl.tryOptimisticRead(); 
        double currentX = x, currentY = y;
        if (!sl.validate(stamp)) { // 检查是否被修改
            // 2. 升级为悲观读锁
            stamp = sl.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX*currentX + currentY*currentY);
    }
}

适用场景

  • 读操作非常频繁(如1000:1的读写比)
  • 可以容忍短暂的数据不一致
  • 不适合重入场景(StampedLock不可重入)

锁优化技术

JVM层优化

  1. 锁消除:JIT编译器通过逃逸分析,去除不可能存在竞争的锁

    java 复制代码
    public String concat(String s1, String s2) {
        StringBuffer sb = new StringBuffer(); // 局部变量,线程安全
        sb.append(s1);
        sb.append(s2);
        return sb.toString(); // 会自动消除synchronized
    }
  2. 锁粗化:将连续的多个锁操作合并为一个

    java 复制代码
    // 优化前
    for(int i=0; i<100; i++) {
        synchronized(this) {
            // ...
        }
    }
    // 优化后
    synchronized(this) {
        for(int i=0; i<100; i++) {
            // ...
        }
    }
  3. 偏向锁:无竞争时消除同步开销

    • 第一个获取锁的线程ID记录在对象头
    • 该线程再次获取时无需同步操作
  4. 轻量级锁:竞争不激烈时用CAS代替OS互斥

    • 失败后膨胀为重量级锁

应用层优化

  1. 减小锁粒度:如ConcurrentHashMap分段锁
  2. 锁分离:读写锁思想
  3. 无锁编程:使用原子类(AtomicInteger等)

分布式锁扩展

实现方式

  1. 基于Redis

    • SETNX命令实现互斥
    • RedLock算法解决单点问题
    java 复制代码
    // Redisson实现示例
    RLock lock = redisson.getLock("myLock");
    lock.lock();
    try {
        // 业务代码
    } finally {
        lock.unlock();
    }
  2. 基于ZooKeeper

    • 创建临时顺序节点
    • 最小节点获取锁,其他监听前驱节点

CAP权衡

  • Redis:AP系统,高性能但可能不一致
  • ZooKeeper:CP系统,强一致但性能较低

锁的常见问题与解决方案

死锁

条件(全部满足才会发生):

  1. 互斥条件
  2. 请求与保持
  3. 不剥夺条件
  4. 循环等待

检测工具

bash 复制代码
jstack <pid>  # 查看线程堆栈
jconsole      # 图形化监控

预防措施

  • 按固定顺序获取锁
  • 设置锁超时(tryLock)
  • 使用jhat分析线程dump

活锁

表现:线程不断重试却无法取得进展

解决:引入随机退避机制

锁饥饿

原因:低优先级线程始终无法获取锁

方案:使用公平锁(但会降低吞吐量)

最佳实践与性能考量

锁选择原则

  1. 简单场景 :优先使用synchronized
  2. 需要高级功能 :选择ReentrantLock
  3. 读多写少 :考虑ReadWriteLockStampedLock
  4. 超高并发:探索无锁算法(如CAS)

注意事项

  1. 粒度控制

    • 过粗:降低并发度
    • 过细:增加锁开销
  2. 避免嵌套:减少死锁风险

  3. 资源清理:确保finally中释放锁

性能监控

  1. 工具

    • JVisualVM:查看线程状态
    • JProfiler:分析锁竞争
    • Arthas:监控锁热点
  2. 关键指标

    • 锁等待时间
    • 持有时间
    • 竞争频率

通过合理选择和优化锁的使用,可以显著提升Java并发程序的性能和可靠性。

Condition接口详解

Condition接口概述

Condition接口是Java并发包中提供的高级线程协调机制,作为Object监视器方法(wait/notify/notifyAll)的替代方案出现。与基本的Object监视器方法相比,Condition接口提供了更精细的线程控制能力,主要体现在:

  1. 一个Lock可以关联多个Condition实例,允许对不同等待条件进行分组管理
  2. 支持公平/非公平的线程唤醒策略
  3. 提供可中断/不可中断的等待选项
  4. 支持超时等待功能

Condition接口核心方法详解

await()方法

使当前线程进入等待状态,直到以下情况之一发生:

  • 被其他线程调用signal()或signalAll()唤醒
  • 线程被中断(抛出InterruptedException)
  • 发生虚假唤醒(spurious wakeup)

典型使用模式:

java 复制代码
while (!conditionSatisfied) {
    condition.await();
}

signal()方法

精确唤醒一个等待在该Condition上的线程(如果是公平锁则唤醒等待时间最长的线程)。与notify()不同的是,signal()可以确保唤醒的是特定条件队列中的线程。

signalAll()方法

唤醒所有等待在该Condition上的线程,类似于Object.notifyAll(),但只会影响当前Condition关联的等待线程。

Condition与Lock的关系

Condition必须与Lock配合使用的原因:

  1. 线程安全性:Condition操作需要建立在已获取锁的基础上
  2. 状态一致性:await()会自动释放锁,signal()后需要重新获取锁
  3. 精细控制:通过不同Condition实例管理不同的等待条件

创建Condition实例的标准方式:

java 复制代码
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();

使用场景深度分析

生产者-消费者模型增强实现

java 复制代码
class BoundedBuffer {
    final Lock lock = new ReentrantLock();
    final Condition notFull = lock.newCondition(); 
    final Condition notEmpty = lock.newCondition();
    
    final Object[] items = new Object[100];
    int putptr, takeptr, count;
    
    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length)
                notFull.await();
            items[putptr] = x;
            if (++putptr == items.length) putptr = 0;
            ++count;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }
    
    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();
            Object x = items[takeptr];
            if (++takeptr == items.length) takeptr = 0;
            --count;
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}

线程精确交替执行

java 复制代码
class AlternateExecution {
    private Lock lock = new ReentrantLock();
    private Condition conditionA = lock.newCondition();
    private Condition conditionB = lock.newCondition();
    private boolean runA = true;
    
    public void executeA() throws InterruptedException {
        lock.lock();
        try {
            while (!runA) {
                conditionA.await();
            }
            System.out.println("A executing");
            runA = false;
            conditionB.signal();
        } finally {
            lock.unlock();
        }
    }
    
    public void executeB() throws InterruptedException {
        lock.lock();
        try {
            while (runA) {
                conditionB.await();
            }
            System.out.println("B executing");
            runA = true;
            conditionA.signal();
        } finally {
            lock.unlock();
        }
    }
}

注意事项深度解析

虚假唤醒问题

Java语言规范允许await()在没有调用signal()的情况下返回(称为虚假唤醒),因此必须使用循环结构来重新检查等待条件。这是与Object.wait()相同的要求,但Condition接口的设计使这种模式更加自然。

中断处理策略

Condition提供多种等待方法处理中断:

  1. await():可中断等待,响应中断抛出InterruptedException
  2. awaitUninterruptibly():完全忽略中断
  3. awaitNanos()/awaitUntil():支持超时的可中断等待

中断时的状态变化:

  • 被中断的线程会从等待队列转移到同步队列
  • 中断状态会被保留,await()在返回前会清除中断状态

性能优化建议

  1. 在简单同步场景(单个等待条件)下,Object监视器方法性能略优
  2. 在复杂同步场景(多个等待条件)下,Condition接口性能明显更好
  3. 高竞争环境下,Condition的非公平模式(默认)吞吐量更高
  4. 对等待时间有严格要求的场景,应使用公平模式

选择建议:

  • 单个条件:考虑Object.wait/notify
  • 多个条件:必须使用Condition
  • 需要超时/中断控制:优先选择Condition

底层实现原理

Condition在AQS中的实现涉及两个队列:

  1. 条件队列(Condition Queue):单向链表存储等待线程
  2. 同步队列(Sync Queue):CLH队列等待获取锁

关键工作流程:

  1. await()时:

    • 创建节点加入条件队列
    • 完全释放锁
    • 阻塞当前线程
  2. signal()时:

    • 将节点从条件队列转移到同步队列
    • 转移后的节点在同步队列中参与锁竞争
  3. signalAll()时:

    • 将条件队列所有节点转移到同步队列
    • 转移顺序保持FIFO特性

节点状态转换图:

复制代码
[Condition Queue] --signal()--> [Sync Queue] --acquire()--> [Running]

完整示例代码

java 复制代码
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionDemo {
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private boolean ready = false;
    
    public void awaitDemo() throws InterruptedException {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " entering await");
            while (!ready) {
                condition.await();  // 释放锁并等待
            }
            System.out.println(Thread.currentThread().getName() + " resuming execution");
            // 执行条件满足后的操作
        } finally {
            lock.unlock();
        }
    }
    
    public void signalDemo() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " preparing condition");
            ready = true;
            condition.signal();  // 唤醒一个等待线程
            System.out.println(Thread.currentThread().getName() + " signaled condition");
        } finally {
            lock.unlock();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        ConditionDemo demo = new ConditionDemo();
        
        Thread waiter = new Thread(() -> {
            try {
                demo.awaitDemo();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "WaiterThread");
        
        Thread signaller = new Thread(() -> {
            try {
                Thread.sleep(2000);  // 模拟准备工作
                demo.signalDemo();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "SignallerThread");
        
        waiter.start();
        signaller.start();
        
        waiter.join();
        signaller.join();
    }
}

该示例展示了:

  1. 基本await/signal用法
  2. 正确的锁释放/获取模式
  3. 典型的多线程协作流程
  4. 线程安全的条件检查方式
相关推荐
拽着尾巴的鱼儿1 小时前
springboot openfeign 自定义feign 接口重试机制
java·spring boot·后端
白露与泡影1 小时前
2026大厂Java面试题大全!牛客网最新版
java·开发语言
EntyIU2 小时前
JVM内存与GC笔记
java·jvm·笔记
XS0301063 小时前
并发编程 六
java·后端
yaoxin5211233 小时前
419. 现代 Java IO 最佳实践 - 写入文本文件
java·windows·python
雪宫街道3 小时前
synchronized 锁的范围:对象锁、类锁与代码块锁
java·jvm·后端·面试
x***r1513 小时前
linux安装 jdk-8u291-linux-x64.tar.gz 详细步骤(解压配置环境变量)
java
极光代码工作室3 小时前
基于SpringBoot的校园论坛系统
java·springboot·web开发·后端开发
XS0301064 小时前
Spring Bean 作用域 & 生命周期
java·后端·spring