Java并发:锁

目录

一、锁

1、Synchronized关键字(隐式锁)

1.1 用法

1.2 锁的特性与优化

1.3 原理:基于 Monitor 机制,锁升级

1.4 使用注意事项

2、显式锁

2.1 ReentrantLock(可重入锁)

2.1.1 用法

2.1.1.1 公平锁与非公平锁

2.1.1.2 可中断与超时机制

2.1.1.3 多条件变量(Condition)

2.1.1.4 分段锁

2.1.1.5 锁状态查询

2.1.2 锁的特性

2.1.3 原理:AQS

2.1.4 使用注意事项

2.1.5 适用场景

2.1.6 ReentrantLock和synchronized的对比

2.2 ReentrantReadWriteLock(读写锁)

2.2.1 用法

2.2.2 锁的特性

2.2.3 原理:AQS状态分割

2.2.3 使用注意事项

2.3 StampedLock(乐观读锁,不可重入,非公平)

2.3.1 用法

2.3.2 实现原理

2.3.3 锁的特性

2.3.4 使用注意事项

3、乐观锁与无锁编程

3.1 CAS(Compare-And-Swap)

3.1.1 基本原理

3.1.2 优缺点

3.1.3 使用注意事项

3.1.4 典型应用场景

3.2 Atomic原子类

3.2.1 用法

3.2.2 作用及核心类

3.2.3 原理

3.2.4 适用场景

二、监控与分析工具

1、jstack:生成线程转储文件,查看锁持有和等待状态

2、Jconsole:图形化查看线程状态、锁竞争和死锁检测

3、Arthas:第三方工具


一、锁

1、Synchronized关键字(隐式锁)

synchronized 用于实现线程同步,确保多个线程对共享资源的互斥访问

1.1 用法

// 1. 修饰实例方法(锁对象是当前实例this)
public synchronized void method() { ... }

// 2. 修饰静态方法(锁对象是当前类的Class对象)
public static synchronized void staticMethod() { ... }

// 3. 修饰代码块(需显式指定锁对象,通常是 private final 对象,避免暴露)
public void block() {
    synchronized (lockObj) { ... }
}

1.2 锁的特性与优化

  • 可重入性:同一线程可重复获取同一把锁(避免死锁)。

        不可重入的场景,若线程已持有锁,再次尝试获取该锁时会被阻塞,导致线程“自己等待自己”,形成死锁

  • 非公平锁:默认抢占式获取锁,不保证等待线程的获取顺序。wait释放锁,notify唤醒线程后重新竞争锁

  • 自动释放:无论正常执行完毕还是抛出异常,锁都会被释放。

  • 锁消除:JIT编译器通过逃逸分析,若发现锁对象不会逃逸出当前线程,则直接消除锁。

  • 锁粗化:将相邻的多个细粒度锁合并为一个粗粒度锁,减少锁开销

  • 自旋锁与适应性自旋:线程在获取锁失败后,短暂自旋(循环尝试)而非立即阻塞;自适应自旋根据历史成功率动态调整自旋次数

1.3 原理:基于 Monitor 机制,锁升级

synchronized 的底层实现通过 对象头标记、锁升级机制、Monitor 管程 三者结合,平衡了性能与线程安全。

字节码指令层面:

  • monitorenter:进入同步代码块,尝试获取锁。

  • monitorexit:退出同步代码块,释放锁。JVM 确保在代码异常退出时也会执行 monitorexit。

在每个Java对象的对象头中,有Mark Word字段存储了锁的相关信息:

锁标志位(Lock Flag)

指向 Monitor 的指针(重量级锁时),Monitor核心字段:
    _owner:当前持有锁的线程。
    _EntryList:竞争锁的线程队列(未获取锁的线程阻塞在此)。
    _WaitSet:调用 wait() 后进入等待状态的线程队列。notify唤醒线程后,需重新竞争锁
    _recursions:锁的重入次数(支持可重入性)。

线程 ID(偏向锁时)

锁记录指针(轻量级锁时)

锁升级过程(锁膨胀):无锁 → 偏向锁 → 轻量级锁 → 重量级锁

1> 偏向锁:消除无竞争情况下的同步开销(如单线程环境)

实现:

  • 线程首次进入同步代码时,通过 CAS 将 Mark Word 中的线程 ID 设为当前线程 ID。

  • 后续进入同步代码时,直接检查线程 ID 是否匹配,无需 CAS。

触发条件:未发生多线程竞争

升级:当其他线程尝试获取锁时,偏向锁撤销并升级为轻量级锁

2> 轻量级锁:减少多线程轻微竞争时的开销(如交替执行)

实现:

  • 线程在栈帧中创建 锁记录(Lock Record),拷贝对象头的 Mark Word。

  • 通过 CAS 将对象头的 Mark Word 替换为指向锁记录的指针。

        自旋期间若有新线程加入,新线程同样可以参与 CAS 竞争,导致 “后来者先得”(非公平)

  • 成功则获取锁;失败则自旋重试(自适应自旋)。

触发条件:多线程竞争但未达到自旋阈值。

升级:自旋失败后升级为重量级锁。

3> 重量级锁:处理高竞争场景

重量级锁 依赖操作系统的 互斥量(Mutex Lock),涉及用户态到内核态的切换,性能开销较大

实现:

  • 对象头中的 Mark Word 指向 Monitor 的指针。

  • 未获取锁的线程进入 _EntryList 阻塞,依赖操作系统进行线程调度。

触发条件:自旋超过阈值或等待线程数过多

1.4 使用注意事项

1> 避免死锁

预防方法:按固定顺序获取锁;使用 tryLock 超时机制(需配合 Lock 接口);减少锁的持有时间

2> 不要锁不可控对象

错误示例:锁字符串常量、基础类型包装类(如 Integer

3> 锁的范围最小化

4> 避免锁静态方法:静态方法的锁是 Class 对象,可能导致全局性能瓶颈

2、显式锁

2.1 ReentrantLock(可重入锁)

2.1.1 用法
2.1.1.1 公平锁与非公平锁

基本用法

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
    private final ReentrantLock lock = new ReentrantLock();

    public void performTask() {
        lock.lock();  // 获取锁
        try {
            // 临界区代码(线程安全操作)
            System.out.println(Thread.currentThread().getName() + " 持有锁");
        } finally {
            lock.unlock();  // 必须确保释放锁
            System.out.println(Thread.currentThread().getName() + " 释放锁");
        }
    }
}
ReentrantLock fairLock = new ReentrantLock(true);  // 公平锁
ReentrantLock unfairLock = new ReentrantLock();    // 非公平锁(默认)
  • 公平锁:按线程请求锁的顺序分配锁(先到先得,先调用 lock() 的线程先获得锁),减少饥饿现象,但吞吐量较低。

        原理:内部使用 AbstractQueuedSynchronizer(AQS) 的队列机制,每次锁释放时,队列中的第一个等待线程会被唤醒。线程必须检查队列中是否有其他等待线程,若队列非空,则自身加入队列尾部排队。

  • 非公平锁:允许插队(新请求的线程可以直接尝试获取锁,无需立即进入等待队列),吞吐量高,但可能导致线程饥饿。

        原理:锁释放时,新线程可以直接通过 CAS 尝试获取锁,失败后再进入队列等待

2.1.1.2 可中断与超时机制

可中断的锁获取:线程在等待锁的过程中可以响应中断,避免无限阻塞。

使用场景:需要支持任务取消或超时中断的线程协作。

public void performInterruptibleTask() throws InterruptedException {
    lock.lockInterruptibly();  // 可中断的锁获取
    try {
        // 临界区代码
    } finally {
        lock.unlock();
    }
}

超时尝试获取锁:尝试在指定时间内获取锁,失败后执行备选逻辑。

适用场景:避免死锁或高并发下的资源争用

public void tryLockWithTimeout() {
    try {
        if (lock.tryLock(1, TimeUnit.SECONDS)) {  // 尝试1秒内获取锁
            try {
                // 临界区代码
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("获取锁超时,执行其他操作");
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();  // 恢复中断状态
    }
}
2.1.1.3 多条件变量(Condition)

ReentrantLock 可以创建多个 Condition 对象,实现更精细的线程等待与唤醒机制(替代wait()/notify()的更灵活方式)

优势:通过多个 Condition 分离等待条件,避免无效唤醒

案例:生产者消费者模型

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class BoundedBuffer {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();   // 非满条件
    private final Condition notEmpty = lock.newCondition();  // 非空条件
    private final Object[] items = new Object[100];
    private 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();
        }
    }
}

2.1.1.4 分段锁

分段锁(Striped Lock)是一种通过将数据分片(Segment)并为每个分片分配独立锁来减少锁竞争的技术,常用于优化高并发场景下的数据结构(如 ConcurrentHashMap)。

适用场景:

  • 读多写少的高并发数据访问(如缓存、计数器)。

  • 需要分区管理的共享资源(如哈希表、分布式存储分片)。

固定分段锁:先创建固定数量的锁,根据键的哈希值映射到对应锁

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class StripedLock<K> {
    private final Lock[] locks;
    private static final int DEFAULT_STRIPES = 16;  // 默认分段数

    public StripedLock(int stripes) {
        locks = new ReentrantLock[stripes];
        for (int i = 0; i < stripes; i++) {
            locks[i] = new ReentrantLock();
        }
    }

    public StripedLock() {
        this(DEFAULT_STRIPES);
    }

    // 根据键的哈希值获取对应的锁
    public Lock getLock(K key) {
        int hash = key.hashCode();
        int index = (hash & Integer.MAX_VALUE) % locks.length;  // 避免负数
        return locks[index];
    }

    // 执行受锁保护的操作
    public void doWithLock(K key, Runnable task) {
        Lock lock = getLock(key);
        lock.lock();
        try {
            task.run();
        } finally {
            lock.unlock();
        }
    }
}

动态分段锁:根据数据动态调整锁的分段数(如按需创建锁)。实现思路,使用 ConcurrentHashMap 存储键到锁的映射,通过 computeIfAbsent 方法动态创建锁

应用案例:高性能计数器

public class StripedCounter {
    private final StripedLock<String> stripedLock = new StripedLock<>();
    private final Map<String, Integer> counters = new HashMap<>();

    public void increment(String key) {
        stripedLock.doWithLock(key, () -> {
            int count = counters.getOrDefault(key, 0);
            counters.put(key, count + 1);
        });
    }

    public int getCount(String key) {
        stripedLock.doWithLock(key, () -> {
            // 若需要读取一致性,此处也需加锁
        });
        return counters.getOrDefault(key, 0);
    }
}

分段数的选择:通常设置为并发线程数的 2~4 倍(如 16、32、64)。分段数越多,锁竞争越少,但是内存开销越大。分段数应足够大,确保键的哈希分布均匀。

避免死锁:若一个操作涉及多个分段,可能因锁顺序不一致导致死锁

解决方案:

  • 固定锁顺序:按分段编号从小到大加锁。

  • 超时机制:使用 tryLock 尝试获取锁。

2.1.1.5 锁状态查询

ReentrantLock 提供方法查询锁的状态,用于调试或监控

public void lockStatusCheck() {
    System.out.println("锁是否被持有: " + lock.isLocked());
    System.out.println("当前线程是否持有锁: " + lock.isHeldByCurrentThread());
    System.out.println("等待锁的线程数: " + lock.getQueueLength());
}
2.1.2 锁的特性
  • 显式加锁(需手动lock()unlock())。

  • 支持公平锁(通过构造函数设置fair=true)。

  • 可中断等待(lockInterruptibly())。

  • 超时获取锁:支持尝试获取锁并设置超时时间(tryLock(long timeout, TimeUnit unit))。

  • 可重入性:同一线程可多次获取同一把锁(通过计数器实现),避免死锁

  • 条件变量:可通过 newCondition() 创建多个条件变量,实现更精细的线程等待/唤醒机制

2.1.3 原理:AQS

AbstractQueuedSynchronizer        

AQS的核心结构:

  • 同步状态(statestate=0 表示锁未被占用,state>0 表示锁被占用且记录重入次数。

  • 等待队列(CLH队列):双向链表实现的 FIFO 队列,存储等待获取锁的线程。

  • 独占模式ReentrantLock 使用独占模式,同一时刻只有一个线程能持有锁。

核心:通过CLH队列管理等待线程,CAS操作更新状态

加锁流程(以非公平锁为例):

1> 尝试直接获取锁:通过 CAS 操作尝试将 state 从 0 改为 1,成功则设置当前线程为独占线程

2> 获取失败则入队:将线程包装为 Node 加入等待队列,通过自旋或阻塞(LockSupport.park())等待唤醒

3> 锁释放与唤醒:释放锁时,将 state 减 1,若 state=0 则唤醒队列中的下一个线程

公平锁:直接检查等待队列是否有前驱节点,若有则排队

  • 优点:避免线程饥饿;缺点:吞吐量较低。

非公平锁:直接尝试插队获取锁,失败后再排队

  • 优点:减少线程切换,提高吞吐量;缺点:可能导致饥饿。

2.1.4 使用注意事项

1> 必须手动释放锁:unlock() 必须放在 finally 块中,避免异常导致锁无法释放

2> 谨慎适用公平锁:公平锁会显著降低吞吐量,仅在严格要求顺序时使用

3> 条件变量与锁绑定:使用 Condition 前必须持有对应的锁

2.1.5 适用场景

1> 需要可中断或超时控制的锁(如防止死锁)。

2> 需要公平锁机制(如任务调度按顺序执行)。

3> 需要多个条件变量(如生产者-消费者模型中的多个等待队列)。

4> 替代 synchronized 的高竞争场景(如线程池任务队列)。

2.1.6 ReentrantLock和synchronized的对比
特性ReentrantLocksynchronized
锁获取方式显式调用 lock()/unlock()隐式通过代码块或方法
公平性支持公平和非公平锁仅非公平锁
可中断性支持(lockInterruptibly()不支持
超时获取锁支持(tryLock()不支持
条件变量支持多个 Condition单一条件(wait()/notify()
性能高竞争场景下更优低竞争场景优化更好(偏向锁、轻量级锁)
代码灵活性高(可跨方法、控制粒度细)低(基于代码块)

2.2 ReentrantReadWriteLock(读写锁)

2.2.1 用法

1> 创建读写锁,进行加锁和解锁

// 构造方法默认非公平模式,传参true为公平模式
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();   // 获取读锁
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock(); // 获取写锁

// 读锁(共享):允许多个线程同时获取。
readLock.lock();
try {
    // 读操作
} finally {
    readLock.unlock();
}

//写锁(独占):仅允许单个线程获取。只允许无读锁和写锁被持有的情况下获取
writeLock.lock();
try {
    // 写操作
} finally {
    writeLock.unlock();
}

2> 锁降级(允许)

ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.writeLock().lock(); // 1. 获取写锁
try {
    // 2. 修改数据(如更新缓存)
    updateData();
    
    // 3. 获取读锁(锁降级关键)
    rwLock.readLock().lock(); 
} finally {
    rwLock.writeLock().unlock(); // 4. 释放写锁,在释放时,降级为读锁
}

try {
    // 5. 读取数据(其他读线程可并发访问)
    readData();
} finally {
    rwLock.readLock().unlock(); // 6. 释放读锁
}

锁降级的意义:

数据一致性:在写锁降级为读锁的过程中,确保其他线程无法插入写操作,避免数据更新过程中出现中间状态。

高并发性:释放写锁后,允许多个读线程并发访问最新数据,减少写锁独占时间,提升系统吞吐量。

适用场景:缓存更新、配置热加载等需“更新后立即读取”的高并发场景。

3> 锁升级(禁止)

readLock.lock();
try {
    // 读操作
    writeLock.lock();  // 错误:直接尝试升级会导致死锁!
} finally {
    readLock.unlock();
}

当前线程持有读锁,再去获取写锁,会导致阻塞(等待自身读锁释放),从而死锁

2.2.2 锁的特性

1> 读写分离

  • 读锁共享:允许多个线程并发读取。

  • 写锁独占:确保写操作互斥,防止数据不一致。

2> 可重入性:同一线程可重复获取读锁或写锁,释放次数需匹配获取次数。

3> 公平性选项

  • 公平模式:线程按请求顺序获取锁(通过构造函数设置 true)。

  • 非公平模式:允许插队,提高吞吐(默认模式)。

4> 锁降级支持:允许从写锁降级为读锁,确保数据一致性

      

2.2.3 原理:AQS状态分割

1> 基于AQS同步器

  • 状态分割:AQS 的 state 分为两部分:

    • 高 16 位:写锁的重入次数。

    • 低 16 位:读锁的持有线程数。

  • 写锁实现:使用独占模式,记录持有线程及重入次数。

  • 读锁实现:使用共享模式,维护读线程计数。

2> 锁竞争逻辑

  • 写锁获取条件:无读锁和写锁被持有。

  • 读锁获取条件:无写锁被持有,或当前线程持有写锁(锁降级)。

3> 等待队列管理

  • 公平模式下,线程按 FIFO 顺序获取锁。

  • 非公平模式下,新线程可能直接插队尝试获取锁。

2.2.3 使用注意事项

1> 避免锁升级:持有读锁时,不可直接尝试获取写锁,否则导致死锁。需先释放读锁再获取写锁

2> 性能权衡:适用读多写少的情况(如缓存、配置管理)

3> 死锁防范:确保锁的获取和释放成对出现

4> 公平性选择:公平模式减少线程饥饿,但降低吞吐;非公平模式提高吞吐,可能导致读/写线程饥饿

5> 监控与调试

  • 使用 getReadLockCount() 和 isWriteLocked() 监控锁状态。

  • 通过线程转储(jstack)分析锁竞争问题。

2.3 StampedLock(乐观读锁,不可重入,非公平)

StampedLock 是 Java 8 引入的高性能锁,支持三种访问模式(读锁写锁乐观读),适用于读多写少的高并发场景。相比 ReentrantReadWriteLock,它通过乐观读和无锁化设计显著提升性能,但牺牲了部分功能(如可重入性)。

2.3.1 用法

1> 锁的获取与释放:互斥写锁、悲观读锁、乐观读

StampedLock lock = new StampedLock();

// 写锁(独占锁)
long writeStamp = lock.writeLock();  // 阻塞获取写锁
try {
    // 写操作...
} finally {
    lock.unlockWrite(writeStamp);    // 释放写锁
}

// 悲观读锁(共享锁)
long readStamp = lock.readLock();    // 阻塞获取读锁
try {
    // 读操作...
} finally {
    lock.unlockRead(readStamp);      // 释放读锁
}

// 乐观读(无锁,仅验证数据一致性)
long optimisticStamp = lock.tryOptimisticRead();
// 读操作(不阻塞)
if (!lock.validate(optimisticStamp)) {  // 检查期间是否有写操作
    // 数据可能被修改,升级为悲观读锁
    long readStamp = lock.readLock();
    try {
        // 重新读取数据...
    } finally {
        lock.unlockRead(readStamp);
    }
}

2> 锁的尝试获取与超时

// 尝试获取写锁(非阻塞)
long stamp = lock.tryWriteLock();
if (stamp != 0L) {
    try { /* 写操作 */ } finally { lock.unlockWrite(stamp); }
}

// 带超时的读锁获取
long stamp = lock.tryReadLock(1, TimeUnit.SECONDS);
if (stamp != 0L) {
    try { /* 读操作 */ } finally { lock.unlockRead(stamp); }
}

3> 锁转换

// 锁升级(读锁 → 写锁,可能导致死锁!需谨慎)
long readStamp = lock.readLock();
try {
    // 尝试转换为写锁
    long writeStamp = lock.tryConvertToWriteLock(readStamp);
    if (writeStamp == 0L) {
        // 转换失败,需手动释放读锁并重新获取写锁
        lock.unlockRead(readStamp);
        writeStamp = lock.writeLock();
    }
    try { /* 写操作 */ } finally { lock.unlockWrite(writeStamp); }
} finally {
    if (lock.isReadLocked()) lock.unlockRead(readStamp);
}

锁转换支持:提供 tryConvertToReadLocktryConvertToWriteLock 等方法,允许锁模式切换

意义:减少锁竞争开销,避免释放与重新获取锁的竞争窗口和数据不一致问题

2.3.2 实现原理

1> 状态管理

  • state 变量:64 位长整型,分为两部分:

    • 低 7 位:读锁计数(最大支持 126 个读线程)。

    • 高 57 位:写锁标记和版本戳(用于乐观读验证)。

  • 戳记(Stamp):锁状态的版本号,用于验证乐观读的有效性。

2> 锁竞争机制

  • 写锁:独占模式,通过 CAS 设置写锁标记。

  • 读锁:共享模式,通过 CAS 增加读锁计数。

  • 乐观读:仅记录当前版本戳,不修改锁状态。

3> CLH队列

  • 基于 CLH(Craig, Landin, and Hagersten)队列管理等待线程,减少锁竞争开销。

2.3.3 锁的特性

1> 乐观读

  • 无锁化读:仅通过版本戳(Stamp)验证数据一致性,避免阻塞。

  • 轻量高效:若读期间无写操作,性能接近无锁;若检测到写操作,需升级为读锁重新读取。

2> 不可重入性:同一线程重复获取锁会导致死锁,必须严格匹配加锁/解锁次数。不可重入性减少了状态管理开销

3> 锁转换支持:提供 tryConvertToReadLocktryConvertToWriteLock 等方法,允许锁模式切换(需谨慎处理)

4> 非公平策略:采用非公平锁策略,允许新请求插队,提高吞吐量

2.3.4 使用注意事项

1> 乐观读的正确使用

  • 必须验证 Stamp:在乐观读后调用 validate(stamp),确保数据未被修改。

  • 升级锁的时机:若验证失败,需升级为悲观读锁重新读取数据。

2> 避免死锁

  • 不可重入:同一线程不可重复获取同一锁(即使已持有)。

  • 锁转换风险:升级锁(如读锁 → 写锁)可能失败,需处理回退逻辑。

3> 锁释放与资源管理

  • 必须配对释放:每个 lock() 必须对应 unlock(),否则导致资源泄漏。

  • 异常处理:在 finally 块中释放锁,确保异常时仍能释放。

3、乐观锁与无锁编程

3.1 CAS(Compare-And-Swap)

3.1.1 基本原理

CAS 是一种原子操作,用于在多线程环境下无锁(Lock-Free)地更新共享变量。

当变量值为A的情况下,才更改成B,更改成功;否则更改失败

底层实现:

  • 依赖 CPU 的原子指令(如 x86 的 CMPXCHG)。

  • Java 中通过 sun.misc.Unsafe 类或原子类(如 AtomicInteger)封装 CAS 操作。

3.1.2 优缺点

优点:

  • 无锁并发:避免线程阻塞和上下文切换,提高高并发场景的性能。

  • 轻量高效:适合简单原子操作(如计数器、状态标志)。

  • 避免死锁:无锁设计天然规避了锁导致的死锁问题。

缺点:

  • ABA 问题
    变量值从 A → B → A,CAS 无法感知中间变化(需通过版本号/时间戳或时 AtomicStampedReference 解决)。

  • 自旋开销
    竞争激烈时,线程可能长时间自旋(循环尝试),浪费 CPU 资源。

        自旋优化策略:1> 指数退避:竞争激烈时,逐步增加自旋间隔;2> 适应性自旋:JVM根据历史成功率动态调整自旋次数

        替代策略:自旋可能导致性能劣化,可改用锁(如 LongAdder 替代 AtomicLong

  • 单一变量限制
    只能保证一个共享变量的原子性,多变量原子更新需结合锁或 synchronized

3.1.3 使用注意事项

1> 避免过度自旋:设置最大尝试次数或退避策略,防止 CPU 空转。

2> 结合业务验证:即使 CAS 成功,仍需业务逻辑验证(如分布式场景)

3> 性能监控:通过 Profiler 工具(如 JProfilerAsync Profiler)监控 CAS 成功率及耗时

4> 替代方案评估:

  • 低竞争场景:优先使用 CAS。

  • 高竞争场景:考虑锁、LongAdder 或分段锁。

3.1.4 典型应用场景

1> 原子类(java.util.concurrent.atomic

AtomicIntegerAtomicLong 等通过 CAS 实现原子更新

AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();  // CAS 实现自增

2> 无锁数据结构

无锁队列(如 ConcurrentLinkedQueue)、无锁栈、无锁哈希表

// 无锁栈的 push 操作
do {
    Node<T> oldTop = top.get();
    newTop.next = oldTop;
} while (!top.compareAndSet(oldTop, newTop));

3> 乐观锁机制

数据库乐观锁、分布式锁(如 Redis 的 SETNX 命令)

UPDATE table SET val = new_val, version = version + 1 
WHERE id = 1 AND version = old_version; -- 类似 CAS 语义

3.2 Atomic原子类

3.2.1 用法

1> 自增与获取值

低竞争场景:优先使用AtomicIntegerAtomicLong

AtomicInteger atomicInt = new AtomicInteger(0);
int newValue = atomicInt.incrementAndGet(); // 1(原子自增)
int oldValue = atomicInt.getAndIncrement(); // 1(返回旧值,再自增)

2> 原子更新对象引用

AtomicReference<String> ref = new AtomicReference<>("初始值");
ref.compareAndSet("初始值", "新值"); // 成功则更新

3> 解决ABA问题

ABA问题
线程1读取变量值为A,线程2将值改为B后又改回A,此时线程1的CAS操作仍会成功,但期间变量已被修改过

解决方案
使用AtomicStampedReferenceAtomicMarkableReference,通过版本号或标记跟踪变更

AtomicStampedReference<String> stampedRef = 
    new AtomicStampedReference<>("A", 0);
int[] stampHolder = new int[1];
String currentRef = stampedRef.get(stampHolder); // 获取引用和版本号
stampedRef.compareAndSet("A", "B", stampHolder[0], stampHolder[0] + 1); // 更新引用和版本号

4> 高竞争场景

高竞争场景:使用LongAdderDoubleAdder(分段累加,减少竞争)

LongAdder adder = new LongAdder();
adder.add(10);          // 累加
long sum = adder.sum(); // 获取总和(非原子操作)
3.2.2 作用及核心类

原子类的作用:

  • 线程安全操作:无需显式锁(如synchronizedLock),提供无锁的线程安全操作。

  • 避免竞态条件:通过原子操作(如自增、CAS)保证多线程环境下的数据一致性。

  • 高性能:在低/中等竞争场景下,性能优于传统锁机制。

类名用途
AtomicInteger原子操作的整型变量(如计数器)。
AtomicLong原子操作的长整型变量。
AtomicBoolean原子操作的布尔变量(如状态标志位)。
AtomicReference原子操作的对象引用(可用于实现无锁数据结构)。
AtomicStampedReference解决ABA问题,通过版本号(int stamp)跟踪引用变更。
AtomicMarkableReference类似AtomicStampedReference,但版本标记为boolean
LongAdder/DoubleAdder高并发场景下的累加器,性能优于AtomicLong(分段减少竞争)。
AtomicIntegerArray原子操作的整型数组。
AtomicReferenceFieldUpdater原子更新类的指定字段(需volatile修饰)。
3.2.3 原理

通过硬件指令(如cmpxchg)实现无锁并发。操作包含三个值:

  • 内存地址V

  • 预期原值A

  • 新值B
    V的当前值等于A,则将V更新为B,否则放弃操作。

方法:compareAndSet(expectedValue, newValue),返回boolean表示是否成功。

AtomicInteger count = new AtomicInteger(0);
boolean updated = count.compareAndSet(0, 1); // 若当前值为0,则更新为1
3.2.4 适用场景
  • 计数器:如统计请求量、在线人数。

  • 状态标志:如开关状态的原子切换。

  • 无锁数据结构:如实现线程安全的栈、队列。

  • 高并发累加:如使用LongAdder统计点击量。

二、监控与分析工具

详见博客:JVM监控

1、jstack:生成线程转储文件,查看锁持有和等待状态

jstack <pid> > thread_dump.txt
  • 关键信息

    • BLOCKED状态的线程(锁竞争)。

    • waiting on <0x000000076bf62200>(等待的锁对象地址)。

    • locked <0x000000076bf62200>(当前持有的锁对象地址)。

2、Jconsole:图形化查看线程状态、锁竞争和死锁检测

  • 操作

    • 连接目标Java进程。

    • 查看 线程(Threads) 标签页,检测死锁(Deadlock Detection)。

  • 优势:直观展示线程阻塞和锁持有关系。

3、Arthas:第三方工具

  • 核心命令

    • thread -b:快速定位死锁。

    • monitor:监控方法调用耗时和锁竞争。

    • watch:观察方法参数和返回值。

  • 示例

    # 监控某个方法的锁竞争
    monitor -c 5 com.example.MyClass syncMethod
     
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

熙客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值