首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Java EE(8)——线程安全——锁策略&CAS

Java EE(8)——线程安全——锁策略&CAS

作者头像
用户11873138
发布2026-01-13 21:25:54
发布2026-01-13 21:25:54
350
举报

本文内容

1.锁策略:乐观/悲观,轻量/重量,自旋/挂起等待,读写,公平/非公平,可重入/不可重入,其他锁策略,Callable 2.CAS:原理,应用(原子类,自旋锁),ABA 3.JUC(java.util.concurrent) 的常见类:ReentrantLock,原子类,Semaphore,CountDownLatch 4.线程安全的集合类:多线程环境使用 ArrayList/队列/哈希表

1.锁策略

1.1各种锁策略介绍

乐观锁&悲观锁

1.1.1乐观锁&悲观锁

乐观锁和悲观锁只是锁的一种策略,并不是具体实现

乐观锁: 假设冲突概率低,先操作,更新时检查数据有没有被修改过,比如用版本号机制。更新数据时带上版本号,如果版本号不匹配,说明数据被修改过,这时候需要处理冲突,可以重试或者报错

悲观锁: 总是假设最坏的情况,每次在处理数据的时候都认为别人会修改,所以每次处理数据之前都会上锁,防止干扰

synchronized初始使用乐观锁策略,当发现锁竞争比较频繁的时候,,就会自动切换成悲观锁策略

1.1.2轻量级锁&重量级锁

锁的核心特性 “原子性”,这样的机制追根溯源是CPU这样的硬件设备提供的

在这里插入图片描述
在这里插入图片描述
代码语言:javascript
复制
 重量级锁:严重依赖操作系统内核提供的互斥机制(mutex)
代码语言:javascript
复制
 轻量级锁:尽量不依赖mutex,能在用户态解决就不切换内核态

当线程遇到轻量级锁时,会使用CAS指令快速获取锁,当获取成功后,该线程继续执行,如果获取失败,线程不会直接陷入阻塞,而是进入自旋状态,持续探测锁是否被释放了。但不会无限制的自旋下去,达到一定自旋次数时,停止自旋,进入阻塞状态。所以,轻量级锁和重量级锁是可以相互切换的。 重量级锁开销大的原因

代码语言:javascript
复制
 上下文切换开销:线程被挂起和唤醒时,需要进行上下文切换,这涉及到保存和恢复线程的寄存器状态,程序计数器等,开销较大
代码语言:javascript
复制
 操作系统内核介入:重量级锁的实现依赖于操作系统的内核函数调用,这会增加系统的调用开销。
1.1.3自旋锁&挂起等待锁

自旋锁是轻量级锁的具体实现,挂起等待锁是重量级锁的具体实现 自旋锁:自旋锁是一种忙等待的锁,当某线程尝试获取自旋锁时,如果该锁已经被其他线程持有,该线程不会陷入阻塞,而是会在一个循环中不断地检查锁是否被释放,这个过程不涉及内核态和用户态的切换,是轻量级锁的实现

挂起等待锁(也叫阻塞等待锁):当一个线程尝试获取一个已经被其他线程持有的锁时,该线程会被挂起,这个操作在内核态进行。这个过程涉及用户态和内核态的切换,线程的阻塞和唤醒,并且要保存该线程的上下文信息,会消耗性能,所以挂起等待锁是重量级锁的实现

1.1.4读写锁

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗,所以读写锁因此而产生 读写锁就是把读操作和写操作区分对待. Java 标准库提供了ReentrantReadWriteLock 类, 实现了读写锁.

代码语言:javascript
复制
 ReentrantReadWriteLock.ReadLock类表示一个读锁,这个对象提供了 lock / unlock 方法进行加锁解锁
代码语言:javascript
复制
 ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock /unlock 方法进行加锁解锁

其中: 读加锁和读加锁之间, 不互斥 写加锁和写加锁之间, 互斥 读加锁和写加锁之间, 互斥

1.1.5公平锁/非公平锁

公平锁: 线程陷入阻塞后进入阻塞队列(这需要额外的数据结构来实现,比如记录线程阻塞的时间,放进优先级队列中),在锁释放后按照先来后到的顺序获取锁 非公平锁:锁释放后操作系统会唤醒一个或者多个线程,这些线程同时竞争锁,不关心是哪个线程能获取锁

1.1.6可重入锁/不可重入锁

可重入锁:线程已经持有某个对象的锁,那么它可以再次获取该对象的锁,不会被阻塞。可重入锁通常会维护一个计数器,记录当前线程获取锁的次数。每次获取锁时,计数器加一;释放锁时,计数器减一。当计数器为零时,锁才真正被释放

不可重入锁:线程已经持有某个对象的锁,那么它可以再次获取该对象的锁,会被阻塞

1.2其他锁策略

锁消除 编译器+JVM 判断锁是否可消除.。如果可以,,就直接消除 什么是 “锁消除” StringBuffer的代码中, 用到了 synchronized, 但单线程环境下不存在锁竞争,JVM就会自动消除synchronized 锁粗化 一段逻辑中如果出现多次加锁解锁,编译器 + JVM 会自动进行锁的粗化,例如

在这里插入图片描述
在这里插入图片描述

1.3Callable

Callable 是一个interface,相当于把线程封装了一个 “返回值”,方便程序员借助多线程的方式计算结果

代码语言:javascript
复制
public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = () -> {
            int sum = 0;
            for (int i = 1; i <= 1000; i++) {
                sum += i;
            }
            return sum;
        };
        //FutureTask中的run方法会调用Callable中的call方法
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread thread = new Thread(futureTask);
        thread.start();
        //将call()方法的返回值保存起来,以便后续通过Future接口的get()方法获取
        System.out.println(futureTask.get());//futureTask.get() = 500500
    }
}

2.CAS

2.1什么是CAS?

CAS是Compare And Swap的缩写,意思是比较并交换。 CAS操作通过比较内存中的值(address)与预期值(expectedValue)是否相同,如果相同,则将内存中的值更新为新值;否则,不进行更新 下面是CAS的伪代码

代码语言:javascript
复制
boolean CAS(address, expectValue, swapValue) {
 if (address == expectedValue) {
   address = swapValue;
        return true;
   }
    return false;
}

当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线 程只会收到操作失败的信号

2.2CAS应用

2.2.1实现原子类

标准库中提供了java.util.concurrent.atomic包,里面的类都是基于这种方式来实现的 典型的就是 AtomicInteger 类,其中的 getAndIncrement相当于 i++ 操作

代码语言:javascript
复制
AtomicInteger atomicInteger = new AtomicInteger(0);
// 相当于 i++
atomicInteger.getAndIncrement();

伪代码实现原子类

代码语言:javascript
复制
class AtomicInteger {
	//内存中的值
    private int value;
    public int getAndIncrement() {
        int oldValue = value;
        //当value==oldValue时,oldValue+1并且结束循环
        //当value!=oldValue时,将value的值赋给oldValue,再次判断value是否等于oldValue
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
       }
        return oldValue;
   }
}   
2.2.2实现自旋锁
代码语言:javascript
复制
public class SpinLock {
    private Thread owner = null;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有
        // 如果这个锁已经被别的线程持有,那么就自旋等待
        // 如果这个锁没有被别的线程持有,那么就把 owner 设为当前尝试加锁的线程
        while(!CAS(this.owner, null, Thread.currentThread())){
       }
   }
    public void unlock (){
        this.owner = null;
   }
}

2.3ABA问题

它指的是在多线程环境下,一个变量在某个时间点被修改为另一个值,然后又修改回原来的值。尽管变量的值最终没有变化,但这个过程中变量的状态可能已经发生了实质性的改变,而CAS操作无法检测到这种变化。 例如: 假设滑稽老哥有100存款,滑稽想从ATM取50 块钱,取款机创建了两个线程,并发的来执行-50 操作 我们期望一个线程执行-50成功,另一个线程-50失败 如果使用CAS的方式来完成这个扣款过程就可能出现问题 取款过程如下:

代码语言:javascript
复制
 存款100:线程1获取到当前存款值为100, 期望更新为50;线程2获取到当前存款值为100,期望更新为 50
代码语言:javascript
复制
 线程1执行扣款成功,存款被改成50;线程2阻塞等待中
代码语言:javascript
复制
 在线程2执行之前,滑稽的朋友给他转账50,余额又变回100
代码语言:javascript
复制
 轮到线程2执行了,发现当前存款为100,和之前读到的100相同,再次执行扣款操作

解决方案:引入版本号 版本1: 存款100:线程1获取到当前存款值为100, 期望更新为50;线程2获取到当前存款值为100,期望更新为 50 版本2: 线程1执行扣款成功,存款被改成50;线程2阻塞等待中 版本3: 在线程2执行之前,滑稽的朋友给他转账50,余额又变回100

轮到线程2 执行了, 发现当前存款为100,和之前读到的 100 相同。但是当前版本号为3, 之前读 到的版本号为1,版本小于当前版本,认为操作失败

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-05-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 本文内容
  • 1.锁策略
    • 1.1各种锁策略介绍
      • 1.1.1乐观锁&悲观锁
      • 1.1.2轻量级锁&重量级锁
      • 1.1.3自旋锁&挂起等待锁
      • 1.1.4读写锁
      • 1.1.5公平锁/非公平锁
      • 1.1.6可重入锁/不可重入锁
    • 1.2其他锁策略
    • 1.3Callable
  • 2.CAS
    • 2.1什么是CAS?
    • 2.2CAS应用
      • 2.2.1实现原子类
      • 2.2.2实现自旋锁
    • 2.3ABA问题
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档