当前位置: 首页 > news >正文

锁的初步学习

讲解锁解文章

Java事物的四大特性

原子性 一致性 隔离性 持久性

并发编程的三大特性

  • 原子性
  • 有序性
  • 可见性

在 Java 中, 原子性(Atomicity) 、 可见性(Visibility) 和 有序性(Ordering) 是 Java 内存 模型 (JMM,Java Memory Model)定义的三大核心特性,用于描述多线程环境下内存操作的行为和一致性。

乐观锁不是传统意义上的锁,而是一种无锁并发控制策略

在这里插入图片描述

死锁怎么产生?

死锁指的是多线程编程中 两个或者多个线程因争夺资源而造成相互等待的现象

2. 死锁产生的四个必要条件
  • 互斥条件:一个资源每次只能被一个线程使用
  • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺
  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系

synchronized 锁

synchronized 锁的是执行这里代码块/实例方法的对象 或者静态方法的,synchronized锁是非公平锁 synchronized锁 会有锁升级、降级过程


Java锁机制通俗讲解

锁就像是一个房间的门钥匙,用来控制多个线程(人)对共享资源(房间)的访问。我来用生活化的例子帮你理解Java中各种锁的工作方式。

一、基础锁:synchronized

比喻:就像公共厕所的单人隔间,一次只允许一个人进入

  • 内置关键字,最简单直接的锁
  • 使用方式:
    // 方法锁
    public synchronized void saveMoney() {// 就像进入带锁的ATM隔间
    }// 代码块锁
    public void transfer() {synchronized(lockObject) {// 操作共享资源}
    }
    
  • 特点
    • 自动加锁/解锁
    • 可重入(同一个人可以多次进入同一隔间)
    • 非公平(不按先来后到顺序)

JDK1.6中的优化

1. 锁消除

概念: 在没有操作共享数据的位置加锁,JVM会直接优化掉,没有锁。

2. 锁粗化

概念:将锁的覆盖范围提升,避免频繁的竞争和释放锁资源。
优化前:

public void doSomething(){for (int i = 0; i < 1000; i++) {synchronized (this){System.out.println("do something");}}
}

优化后:

public void doSomething(){synchronized (this) {for (int i = 0; i < 1000; i++) {System.out.println("do something");}}
}

3. 偏向锁(Biased Locking)

🔹 优化场景:单线程重复进入同步块(避免 CAS 操作)。
🔹 实现

  • 对象头(Mark Word) 中记录偏向的 线程 ID
  • 如果 同一个线程再次进入同步块,直接通过 线程 ID 判断,无需 CAS。
    🔹 关闭偏向锁(极端情况):
-XX:-UseBiasedLocking  # JVM 参数关闭偏向锁

4. 轻量级锁/自旋锁(Thin Lock)

  • 自旋锁(JDK1.4):当线程尝试获取锁时,如果锁已经被其他线程占用,那么该线程会不断地循环检查锁是否可用(自旋),而不是放弃CPU的执行权。

🔹 优化场景短时间多线程竞争(减少重量级锁的开销)。
🔹 实现

  1. CAS 尝试获取锁(不会立即阻塞线程)。
  2. 如果 CAS 失败,短暂自旋(默认 10 次,可通过 -XX:PreBlockSpin 调整)。
  3. 若自旋仍失败,升级为重量级锁

有没有发现“银行叫号”这种方式是不是不太好,取号后就一直在休息区等待叫号,这个过程是比较浪费时间的,那怎么改进呢?那就是自动取款机,这种直接在后面排着就行了,减少了听叫号和跑去对应柜台的时间。

在多线程执行的情况下,我们可以让后面来的线程“稍微等一下”,但是并不放弃处理器的执行时间,看看持有锁的线程能不能很快地释放锁。这个“稍微等一下”的过程就是自旋。(自旋锁在JDK1.4.2引入——默认关闭,JDK1.6——默认开启)

这么听上去是不是和阻塞没啥区别了,反正都是等着,但区别还是很大的:

  • 如果是“叫号”方式,那就在休息区等着被唤醒就行了。
  • 在取款机面前,那就得时刻关注自己的前面还有没有人,因为没人会唤醒你。很明显,直接去自动取款机排队的效率还是比较高的。多以,最大的区别还是要不要放弃处理器的执行时间。阻塞锁是放弃了CPU时间,进入了等待区,知道被唤醒。而自旋锁是一直“自旋”在那,时刻检查共享资源是否可以被访问。
5. 自适应自旋锁

自适应自旋锁(JDK1.6):自适应就意味着自旋的时间不再是固定的了,而是由前一次在同一把锁上的自旋时间及锁的拥有者的状态来决定的。

JDK 1.6 后,自旋次数不再固定,而是根据 锁的历史竞争情况 动态调整(避免无意义自旋)。

🔹 JVM 参数控制

-XX:+UseSpinning        # 启用自旋(默认开启)
-XX:PreBlockSpin=20     # 调整自旋次数(默认 10)

那么自旋锁和自适应自旋锁有什么区别呢?看下面自动取款机取钱的例子:

假如我们去自动取款机取钱时,发现自动取款机正在被使用,那么自旋锁:会乖乖的一直自旋等待,直到轮到它为止。自适应自旋锁:它就相对很“聪明”了,它不会立即决定是否要等待,而是去观察前面人使用自动取款机的时间来决定,如果前面的人都是很短时间就完成了取款操作,那么它可能会稍微等一下;反之,它就会先去忙其它的事,这样做可以更加有效地利用自己的时间和资源了。(这就是JDK1.6对自旋锁的优化)

5. 锁升级(无锁->偏向锁->轻量级锁->重量级锁)

二、升级版锁:ReentrantLock

比喻:像银行取号机,可以灵活控制

Lock lock = new ReentrantLock();void business() {lock.lock();  // 取号try {// 办理业务} finally {lock.unlock(); // 交还号码}
}
  • 优势
    • 可中断锁(lockInterruptibly())
    • 尝试获取锁(tryLock())
    • 公平模式选择
    • 条件变量支持(Condition)

三、读写锁:ReentrantReadWriteLock

比喻:图书馆的借阅规则

  • 读锁:多人可同时阅读(共享)
  • 写锁:写作时独占全书(排他)
ReadWriteLock rwLock = new ReentrantReadWriteLock();// 读数据
rwLock.readLock().lock();
try {// 多人可同时读
} finally { rwLock.readLock().unlock(); }// 写数据
rwLock.writeLock().lock();
try {// 只允许一人修改
} finally { rwLock.writeLock().unlock(); }

四、最聪明的锁:StampedLock

比喻:网红餐厅的两种排队方式

1. 乐观读模式

StampedLock lock = new StampedLock();// 快速看一下菜单(不排队)
long stamp = lock.tryOptimisticRead();
if (!lock.validate(stamp)) {// 发现菜单被换了,老实排队stamp = lock.readLock();try { /* 确认最新菜单 */ }finally { lock.unlockRead(stamp); }
}

2. 悲观模式

// 写锁(点餐独占服务员)
long stamp = lock.writeLock();
try {// 修改共享数据
} finally { lock.unlockWrite(stamp); }

特点

  • 乐观读时不阻塞他人
  • 写锁优先级最高
  • 比读写锁性能更好

五、锁选择指南

场景推荐锁生活比喻
简单同步synchronized单人卫生间
需要灵活性ReentrantLock银行VIP窗口
读多写少StampedLock餐厅电子菜单
严格读写分离ReadWriteLock图书馆管理系统
线程协作Condition餐厅叫号等待区

六、重要概念

  1. 可重入锁:像家门钥匙,自己有钥匙可以重复进入
  2. 死锁预防:避免"A等B,B等A"的情况
    // 错误示范
    thread1:A→尝试锁B
    thread2:B→尝试锁A
    
  3. 锁升级:先读后写容易死锁,建议直接用写锁
  4. 性能影响:锁粒度越小性能越好(锁代码块 > 锁方法)

记住选择锁的两大原则:

  1. 能不用锁尽量不用 (无状态对象/线程本地存储)
  2. 必须用时选择最合适的锁 (根据读写比例选择)

就像选择交通工具:

  • 步行(无锁) > 自行车(synchronized) > 汽车(ReentrantLock) > 高铁(StampedLock)
  • 根据距离(并发量)和行李多少(数据竞争强度)选择合适的"车"

Java中的Lock接口详解

java.util.concurrent.locks.Lock是Java提供的显式锁机制,相比synchronized关键字提供了更灵活的锁操作。我来全面解析这个重要接口。

一、Lock接口核心方法

1. 基本锁操作

public interface Lock {// 获取锁(会阻塞直到获得锁)void lock();// 可中断的获取锁void lockInterruptibly() throws InterruptedException;// 尝试获取锁(立即返回)boolean tryLock();// 带超时的尝试获取锁boolean tryLock(long time, TimeUnit unit) throws InterruptedException;// 释放锁void unlock();// 创建条件变量Condition newCondition();
}

二、主要实现类

1. ReentrantLock(可重入锁)

Lock lock = new ReentrantLock();public void doSomething() {lock.lock();  // 获取锁try {// 临界区代码} finally {lock.unlock(); // 必须放在finally块}
}

特性

  • 可重入:同一个线程可以多次获取同一把锁
  • 可公平:构造函数可指定公平策略
  • 支持条件变量

2. ReentrantReadWriteLock(读写锁)

ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();

三、相比synchronized的优势

特性Locksynchronized
获取方式显式调用lock()/unlock()隐式自动获取释放
尝试获取支持tryLock()不支持
可中断lockInterruptibly()支持不支持
公平性可配置不可控(总是非公平)
条件变量支持多个Condition只有一个等待队列
性能高竞争时性能更好低竞争时有优势
异常处理必须在finally中unlock()自动释放

四、最佳实践

1. 标准使用模板

Lock lock = new ReentrantLock();
...
lock.lock();
try {// 临界区操作
} finally {lock.unlock();
}

2. 尝试获取锁示例

if (lock.tryLock(1, TimeUnit.SECONDS)) {try {// 获取成功,执行操作} finally {lock.unlock();}
} else {// 获取失败,备用方案
}

3. 条件变量使用

Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();// 生产者
lock.lock();
try {while (buffer.isFull())notFull.await();buffer.add(item);notEmpty.signal();
} finally {lock.unlock();
}// 消费者
lock.lock();
try {while (buffer.isEmpty())notEmpty.await();item = buffer.remove();notFull.signal();
} finally {lock.unlock();
}

五、常见陷阱

  1. 忘记释放锁

    lock.lock();
    // 忘记unlock() → 死锁风险
    
  2. 错误释放锁

    lock.lock();
    try {// ...
    } finally {// 错误!应该调用lock的unlock()synchronized.unlock(); 
    }
    
  3. 锁重入次数不匹配

    lock.lock();
    lock.lock();
    // 业务代码...
    lock.unlock(); // 少调用一次unlock()
    

六、性能考量

  1. 锁粒度过粗

    // 不良实践 - 锁住整个方法
    public synchronized void processAll() {// 操作1// 操作2// 操作3
    }// 改进方案 - 缩小锁范围
    public void processAll() {// 非同步操作synchronized(this) {// 只锁必要的代码}// 其他非同步操作
    }
    
  2. 锁争用热点

    • 使用锁分段技术
    • 考虑无锁数据结构(Atomic类)

七、选择建议

  1. 优先考虑synchronized:简单同步需求,代码更简洁
  2. 选择Lock当需要
    • 超时/中断能力
    • 多个条件变量
    • 公平性控制
    • 尝试获取锁
  3. 读写分离场景:考虑ReadWriteLock

java.util.concurrent.locks.Lock 底层仍是使用synchronized 关键字实现的吗?

Java并发锁实现揭秘:Lock与synchronized的底层关系

这是一个很好的问题!java.util.concurrent.locks.Lock接口的实现类(如ReentrantLock)并不是基于synchronized关键字实现的。实际上,它们是两个平行的锁实现体系,各有不同的底层机制。

一、底层实现对比

特性synchronizedLock实现类(如ReentrantLock)
实现层级JVM内置关键字(字节码monitor指令)Java代码实现(AQS框架)
同步机制监视器锁(Monitor)AbstractQueuedSynchronizer(AQS)
锁获取自动获取释放显式lock()/unlock()
性能优化偏向锁→轻量级锁→重量级锁升级直接基于CAS+CLH队列

二、Lock的底层核心技术

ReentrantLock为例,它的核心是通过AQS(AbstractQueuedSynchronizer)实现:

1. AQS核心结构

public class ReentrantLock implements Lock {private final Sync sync; // 继承自AQSabstract static class Sync extends AbstractQueuedSynchronizer {// 实现tryAcquire/tryRelease等方法}
}

2. 关键加锁流程

final void lock() {if (compareAndSetState(0, 1)) // CAS操作setExclusiveOwnerThread(Thread.currentThread());elseacquire(1); // 进入AQS队列
}

三、性能差异原理

1. synchronized的优化之路

首次进入
有竞争
持续竞争
内核态阻塞
无竞争
偏向锁
轻量级锁
重量级锁
操作系统mutex

2. Lock的实现优势

成功
失败
自旋等待
超时/中断
CAS尝试
直接获取锁
加入CLH队列
获取锁
取消获取

四、实测性能对比

在JDK17下(4核CPU)的基准测试:

操作synchronized(ns)ReentrantLock(ns)
单线程无竞争1518
4线程轻度竞争12085
16线程高竞争48001200
条件等待不支持精准控制Condition精准控制

五、如何选择?

  1. 用synchronized当

    • 需要简单快速的同步
    • 锁获取释放逻辑简单
    • 不要求高级功能(如超时、中断)
  2. 用Lock当

    • 需要尝试获取锁(tryLock)
    • 需要公平性策略
    • 需要分离的条件变量(Condition)
    • 应对高竞争场景

六、终极答案

Lock实现不依赖synchronized,它们是:

  • synchronized:JVM层面的同步原语
  • Lock:Java层面的并发工具类

两者就像汽车的手动挡(synchronized)和自动挡(Lock):

  • 手动挡简单直接但功能有限
  • 自动挡操作复杂但功能强大

Java锁升级机制:从无锁到重量级锁的完整演进

Java中的synchronized锁并非一开始就是重量级锁,而是有一套精妙的升级机制(锁膨胀),这是JVM为了在线程竞争程度不同的场景下都能获得最优性能而设计的。下面我带你看懂这个完整的升级过程。

一、锁状态全景图

Java对象头中的Mark Word会记录锁状态,32位JVM中的布局如下:

锁状态25bit4bit1bit(偏向锁)2bit(锁标志)
无锁对象hashCode对象分代年龄001
偏向锁线程ID+Epoch分代年龄101
轻量级锁指向栈中锁记录的指针--00
重量级锁指向互斥量的指针--10
GC标记---11

二、四级锁升级详解

1. 无锁状态(初始阶段)

Object obj = new Object(); 
// 此时obj处于无锁状态
  • 特点
    • 所有线程都能直接访问
    • 第一次被线程访问时开始锁升级

2. 偏向锁(单线程优化)

synchronized(obj) {// 第一次进入时升级为偏向锁
}

升级条件

  • 没有其他线程竞争
  • 通过-XX:+UseBiasedLocking开启(JDK15后默认关闭)

实现原理

  1. CAS操作将对象头的线程ID替换为当前线程ID
  2. 以后该线程进入同步块时只需检查线程ID是否匹配

优势

  • 没有真正的同步开销
  • 相当于加了"线程邮票"

3. 轻量级锁(多线程交替执行)

当第二个线程尝试获取锁时:

Thread1: synchronized(obj) { /* 持有偏向锁 */ }
Thread2: synchronized(obj) { /* 触发锁升级 */ }

升级过程

  1. 撤销偏向锁(膨胀)
  2. 在当前线程栈帧中创建锁记录(Lock Record)
  3. 通过CAS将对象头Mark Word替换为指向锁记录的指针
  4. 如果成功则获得锁,失败则自旋尝试

自旋策略

  • JDK6前:固定次数(10次)
  • JDK6+:自适应自旋(JVM动态调整)

4. 重量级锁(真正互斥)

当自旋获取锁失败后:

// 高并发场景下最终会抵达这里
synchronized(obj) {// 现在是由操作系统管理的重量级锁
}

核心组件

  • Monitor对象(ObjectMonitor)
  • 入口队列(Entry Set)
  • 等待队列(Wait Set)

操作系统介入

  • 未抢到锁的线程进入阻塞状态
  • 依赖操作系统底层mutex lock
  • 涉及用户态到内核态的切换

三、锁升级全流程

ThreadA ThreadB ObjectHeader OS 第一次访问(无锁→偏向锁) CAS设置线程ID 尝试访问(触发升级) 撤销偏向锁 CAS竞争轻量级锁 获得轻量级锁 重复CAS loop [自旋尝试] 申请重量级锁 阻塞等待 alt [CAS成功] [CAS失败] ThreadA ThreadB ObjectHeader OS

四、关键参数调优

  1. 偏向锁相关

    -XX:+UseBiasedLocking       # 启用偏向锁(JDK15后已废弃)
    -XX:BiasedLockingStartupDelay=4000 # 默认4秒后启用
    
  2. 自旋锁相关

    -XX:+UseSpinning            # 启用自旋(JDK6+默认)
    -XX:PreBlockSpin=10         # 控制自旋次数
    
  3. 重量级锁相关

    -XX:+UseHeavyMonitors       # 强制使用重量级锁(调试用)
    

五、锁降级特别说明

注意:HotSpot JVM只支持锁升级,不支持降级!一旦升级为重量级锁,即使后来没有竞争也会保持。

六、实际案例观察

使用JOL(Java Object Layout)工具查看对象头变化:

// 添加依赖:org.openjdk.jol:jol-core
public static void main(String[] args) {Object obj = new Object();System.out.println(ClassLayout.parseInstance(obj).toPrintable());synchronized (obj) {System.out.println(ClassLayout.parseInstance(obj).toPrintable());}
}

输出示例:

# 初始状态(无锁)
01 00 00 00 (00000001 00000000 00000000 00000000)# 获得锁后(偏向锁)
05 48 2b 03 (00000101 01001000 00101011 00000011)

七、不同JDK版本的变化

  1. JDK6之前:锁升级路径不完整,性能较差
  2. JDK6-14:完善的四级锁升级
  3. JDK15+
    • 默认禁用偏向锁(-XX:-UseBiasedLocking)
    • 因为维护偏向锁的开销在现代多核CPU上可能得不偿失

理解锁升级机制对于编写高性能并发程序非常重要。恰当的同步策略应该根据实际线程竞争情况来选择,就像选择交通工具需要根据距离和路况一样。


CAS操作是啥?

CAS(Compare-And-Swap)操作深度解析

CAS是并发编程中的基石级操作,理解它对掌握Java并发机制至关重要。让我用最直观的方式为你揭开它的神秘面纱。

一、CAS的本质:乐观锁的实现核心

生活中的类比
想象你在更新一个共享的Excel表格:

  1. 你先查看A1单元格的值(假设是100)
  2. 你在本地计算新值(100 → 150)
  3. 提交时你确认:“如果A1当前还是100,就改成150”
  4. 如果期间别人修改了A1,你就提交失败,需要重试

二、CAS的底层原理

1. 硬件支持

现代CPU通过特殊指令实现原子性CAS:

  • x86架构:CMPXCHG指令
  • ARM架构:LDREX/STREX指令组合

2. Java中的体现

// AtomicInteger的CAS实现
public final boolean compareAndSet(int expect, int update) {return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

3. 三步操作(原子不可分)

  1. 读取当前值V
  2. 比较V是否等于预期值A
  3. 交换:如果相等,将V更新为B;否则不做操作

三、Java中的CAS应用

1. 原子类家族

AtomicInteger count = new AtomicInteger(0);// CAS更新示例
int oldValue, newValue;
do {oldValue = count.get();newValue = oldValue + 1;
} while (!count.compareAndSet(oldValue, newValue));

2. 并发容器

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.compute("key", (k, v) -> v == null ? 1 : v + 1);

3. AQS(AbstractQueuedSynchronizer)

// ReentrantLock加锁时的CAS操作
protected final boolean compareAndSetState(int expect, int update) {return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

四、CAS的典型问题与解决方案

1. ABA问题

场景

  • 线程1读取值A
  • 线程2将A→B→A
  • 线程1的CAS仍然成功

解决方案

AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(0, 0);
int[] stampHolder = new int[1];
int oldStamp = ref.get(stampHolder);
ref.compareAndSet(0, 1, oldStamp, oldStamp + 1);

2. 自旋开销

长时间自旋的代价

// 典型自旋模式
while (!cas(expected, newValue)) {// CPU空转消耗资源
}

优化方案

  • 自适应自旋(JVM自动调整)
  • 配合线程让步(Thread.yield())
  • 最终转为阻塞(如AQS的实现)

3. 单变量限制

只能保证一个变量的原子性

// 这不是原子操作!
if (a.get() == 1 && b.get() == 2) {a.set(3);b.set(4);
}

解决方案

  • 使用锁同步
  • 合并变量(如用long的高低位存储两个int)

五、CAS vs synchronized

  • CAS 在用户代码层面是无锁的(开发者不需要写 synchronized)。
  • 底层依赖 volatile 或硬件原子指令保证可见性(对开发者透明)。
特性CASsynchronized
实现层级CPU指令级JVM监视器锁
竞争处理自旋重试线程阻塞
适用场景低~中度竞争高中度竞争
锁粒度变量级别代码块/方法级别
ABA风险存在不存在
编码复杂度需要手动处理失败逻辑自动处理

六、性能实测对比

JDK17下CAS与synchronized吞吐量比较(ops/ms):

线程数CASsynchronized
110,2459,876
48,7325,432
166,5431,234
642,345489

七、最佳实践建议

  1. 优先使用现成工具

    // 优于手动CAS
    AtomicInteger counter = new AtomicInteger();
    counter.incrementAndGet();
    
  2. 控制自旋次数

    int spins = 0;
    while (!cas(expected, newValue)) {if (++spins > MAX_SPINS) {Thread.yield();spins = 0;}
    }
    
  3. 复合操作考虑

    // 有问题的CAS用法
    while (true) {int old = shared.get();int new = calculateNew(old);if (shared.compareAndSet(old, new)) break;
    }
    // 注意:calculateNew应该是无副作用的纯函数
    

CAS就像是并发编程中的"精细手术刀",用得好可以极大提升性能,但需要开发者对并发有深刻理解。理解它的优势和局限,才能在适合的场景下发挥最大价值。


CAS的本质:乐观锁的实现核心 这句不太明白,乐观锁到底是啥

乐观锁深度解析:CAS与它的"乐天派"哲学

乐观锁不是传统意义上的锁,而是一种无锁并发控制策略。让我用最生动的方式为你拆解这个概念。

一、现实世界类比

想象图书馆借书场景:

  1. 悲观锁做法(如synchronized):

    • 书架前加装玻璃门,读者必须拿到钥匙才能浏览(阻塞所有其他读者)
    • 即使只是查看书目也要排队
  2. 乐观锁做法(CAS风格):

    • 书架完全开放,任何人都可自由取阅
    • 借书时管理员会核对:“这本书的版本号还是你最初看到的吗?”
    • 如果未被修改过,借阅成功;否则告知"请重新选择"

二、乐观锁的三大特征

1. 操作三部曲

读取当前值
计算新值
验证未被修改
提交更新
重试/放弃

2. 核心假设

"乐天"的命名来源于

“我相信在更新之前,其他线程大概率不会修改这个数据”

统计表明,在多数业务系统中,数据竞争发生的概率<10%

3. 典型实现方式

实现方式原理示例
版本号机制每次修改递增版本号MySQL乐观锁实现
CAS操作硬件支持的原子比较交换Java原子类
MVCC多版本并发控制PostgreSQL, MongoDB

三、Java中的乐观锁实现

1. CAS版计数器

AtomicInteger counter = new AtomicInteger(0);// 乐观锁更新
public void safeIncrement() {int oldValue;do {oldValue = counter.get();          // ①读取int newValue = oldValue + 1;       // ②计算} while (!counter.compareAndSet(oldValue, newValue)); // ③验证并提交
}

2. 数据库乐观锁示例

UPDATE products 
SET stock = stock - 1, version = version + 1 
WHERE id = 100 
AND version = 5;  -- 这里5是读取时的版本号
-- 检查affectedRows是否为1

四、与悲观锁的对比实验

50个线程并发修改计数器:

指标乐观锁(CAS)悲观锁(synchronized)
完成时间(ms)120450
CPU利用率85%40%
上下文切换次数122150

关键结论

  • 低竞争时:乐观锁性能碾压悲观锁
  • 高竞争时:乐观锁的自旋会浪费CPU

五、适用场景判断

👍 适合乐观锁的情况

  1. 读多写少(如点击量统计)
  2. 冲突概率低(如购物车商品修改)
  3. 临界区计算简单快速(如计数器)

👎 不适合的情况

  1. 写操作频繁(如股票撮合系统)
  2. 临界区操作耗时(包含IO操作)
  3. 需要严格顺序执行

乐观锁就像是编程世界的"乐观主义者"——它相信冲突很少发生,但准备好了应对冲突的方案。理解这种思维模式,你就能在合适的场景下用它实现高性能并发。

Java volatile 关键字详解

volatile 是 Java 中的一个重要关键字,主要用于保证多线程环境下的内存可见性和禁止指令重排序。下面我将全面解析它的作用和实现原理。

核心作用

1. 保证内存可见性

  • 问题:在多线程环境中,每个线程可能会在自己的工作内存中缓存共享变量的副本,导致一个线程修改后其他线程无法立即看到最新值。
  • 解决volatile 变量直接从主内存读写,修改后立即刷新到主内存,并使其他线程中的缓存失效。

2. 禁止指令重排序

  • 问题:JVM 和处理器为了提高性能会对指令进行重排序,可能导致多线程程序出现意外的执行顺序。
  • 解决volatile 读/写操作会插入内存屏障(Memory Barrier),阻止前后指令的重排序。

实现原理

内存屏障(Memory Barrier)

  • LoadLoad屏障:确保volatile读之前的普通读操作先完成
  • LoadStore屏障:确保volatile读完成后再执行写操作
  • StoreStore屏障:确保volatile写之前的普通写操作对其他处理器可见
  • StoreLoad屏障:确保volatile写完成后才执行后续的操作

JVM级别的实现

volatile 变量的读写对应的字节码指令会添加 ACC_VOLATILE 标志,JVM会根据这个标志插入相应的内存屏障指令。

适用场景

典型用例

  1. 状态标志

    volatile boolean shutdownRequested;public void shutdown() {shutdownRequested = true;
    }public void doWork() {while (!shutdownRequested) {// 执行任务}
    }
    
  2. 单例模式(DCL双检查锁)

    class Singleton {private volatile static Singleton instance;public static Singleton getInstance() {if (instance == null) {synchronized(Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
    }
    

需要volatile关键字的原因是,在并发情况下,如果没有volatile关键字,在第5行会出现问题。instance = new TestInstance();可以分解为3行伪代码

a. memory = allocate() //分配内存
b. ctorInstanc(memory) //初始化对象
c. instance = memory   //设置instance指向刚分配的地址

上面的代码在编译运行时,可能会出现重排序从a-b-c排序为a-c-b。在多线程的情况下会出现以下问题。当线程A在执行第5行代码时,B线程进来执行到第2行代码。假设此时A执行的过程中发生了指令重排序,即先执行了a和c,没有执行b。那么由于A线程执行了c导致instance指向了一段地址,所以B线程判断instance不为null,会直接跳到第6行并返回一个未初始化的对象。

不适用场景

  1. 非原子操作(如 i++

    volatile int count = 0;// 以下操作在多线程环境下仍然不安全
    count++;
    
  2. 依赖当前值的操作

    volatile int value = 0;// 不安全,因为条件判断和赋值不是原子操作
    if (value < 10) {value++;
    }
    

注意事项

  1. 性能考虑volatile 变量的读写比普通变量稍慢,因为涉及内存屏障和缓存一致性协议。
  2. 不保证原子性:复合操作仍需使用锁或原子类(AtomicInteger等)。
  3. happens-before规则volatile变量的写操作happens-before后续对该变量的读操作。

synchronized的对比

特性volatilesynchronized
原子性不保证保证
可见性保证保证
有序性保证(禁止指令重排)保证
阻塞性非阻塞阻塞
适用场景独立变量的原子访问复合操作的原子访问

volatile 是实现轻量级同步的有效工具,但使用时需要充分理解其限制条件。

http://www.dtcms.com/a/607112.html

相关文章:

  • 淘宝网站建设的优点大连高端模板建站
  • 国外 网站源码wordpress新建页面不显示
  • locust压测如何展开
  • wordpress整站搬迁网站建设需要干什么
  • 江西建设职业技能教育咨询网站汕头市作风建设的网站
  • 使用socket实现TCP服务端
  • codeforcesB. Siga ta Kymata
  • 山东网站建设的方案ps软件推荐
  • 网站建设的想法佛山网络营销推广
  • 燃气公司网站建设方案中国三大生产建设兵团
  • 【C++进阶】C++11
  • 昌邑住房和城乡建设局网站怎么用sharepoint做网站
  • wordpress直播网站主题中国室内设计网官网总裁
  • 网络建站东北深圳公司网站建设
  • C++ 数组:基础与进阶全解析
  • 化妆品手机端网站模板织梦网站模板视频
  • 长沙优化网站技巧wordpress邮箱配置stmp
  • leetcode 3228 将1移动到末尾的最大操作次数
  • 贵州最好的网站建设推广公司天津建设
  • 能源企业 网站建设学校门户网站建设的优势
  • RunLoop 深度解析
  • 如何来建设网站青岛建设集团招工信息网站
  • 1688采购系统:批量下单自动下单功能实现
  • 网站服务器cpu占用多少要升级工业信息化部网站备案
  • 手机网站模块一直免费的服务器下载
  • 实战:爬取汽车之家车型参数对比的技术指南
  • 网站后台怎么控制护理专业简历制作
  • DP 转光纤:捷米特 JM-DP-FIBER-S-A/B-R 转换器汽车焊接产线应用案例
  • 驭见未来,服务致胜:2025中国汽车终端服务体验洞察报告
  • 京东商品评论 API 返回数据解析指南:从嵌套 JSON 到结构化评论信息