Loading [MathJax]/jax/input/TeX/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Java并发编程深度解析:掌握CAS、Synchronized与Callable的高效并发之道

Java并发编程深度解析:掌握CAS、Synchronized与Callable的高效并发之道

作者头像
小皮侠
发布于 2024-10-23 00:18:03
发布于 2024-10-23 00:18:03
12900
代码可运行
举报
运行总次数:0
代码可运行

Java并发编程

本篇文章主要带大家深入了解Java并发编程,了解CAS,Synchronized原理以及Callable接口的概念及使用。

1.CAS

CAS: 全称 Compare and swap ,字面意思 :” 比较并交换 “ ,一个 CAS 涉及到以下操作:

我们假设内存中的原数据 V ,旧的预期值 A ,需要修改的新值 B 。 1. 比较 A 与 V 是否相等。(比较) 2. 如果比较相等,将 B 写入 V 。(交换) 3. 返回操作是否成功。

CAS是一个具有原子性的硬件指令集,当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。

针对不同的操作系统,JVM用到了不同的原理在Java中也实现了CAS。

CAS的具体应用
实现原子类

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
AtomicInteger atomicInteger = new AtomicInteger(0);
// 相当于 i++
atomicInteger.getAndIncrement();

假设两个线程同时调用 getAndIncrement。有以下几个步骤:

  1. 两个线程都读取 value 的值到 oldValue 中. (oldValue 是一个局部变量, 在栈上. 每个线程有自己的栈)
  2. 线程1 先执行 CAS 操作. 由于 oldValue 和 value 的值相同, 直接进行对 value 赋值.
  3. 线程2 再执行 CAS 操作, 第一次 CAS 的时候发现 oldValue 和 value 不相等, 不能进行赋值. 因此需要进入循环. 在循环里重新读取 value 的值赋给 oldValue.
  4. 线程2 接下来第二次执行 CAS, 此时 oldValue 和 value 相同, 于是直接执行赋值操作.
  5. 线程1 和 线程2 返回各自的 oldValue 的值即可.

通过形如上述代码就可以实现一个原子类. 不需要使用重量级锁, 就可以高效的完成多线程的自增操作.

实现自旋锁

基于 CAS 实现更灵活的锁 , 获取到更多的控制权 .

自旋锁伪代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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;
   }
}
CAS的ABA问题
什么是ABA问题

ABA 的问题 :

假设存在两个线程 t1 和 t2. 有一个共享变量 num, 初始值为 A.

接下来 , 线程 t1 想使用 CAS 把 num 值改成 Z, 那么就需要:

  • 先读取 num 的值, 记录到 oldNum 变量中.
  • 使用 CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z.

但是 , 在 t1 执行这两个操作之间 , t2 线程可能把 num 的值从 A 改成了 B, 又从 B 改成了 A. t1 线程无法区分当前这个变量始终是 A, 还是经历了一个变化过程 .如下图所示:

ABA问题引起的bug以及解决方案

上述的ABA问题就有可能在一些对业务准确性要求较高的领域引起难以接受的bug。

比如:笔友有100元存款,滑稽想从 ATM 取 50 块钱. 取款机创建了两个线程, 并发的来执行 -50操作. 我们期望一个线程执行 -50 成功, 另一个线程 -50 失败. 如果使用 CAS 的方式来完成这个扣款过程就可能出现问题. 正常的过程 1) 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更新为 50. 2) 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中. 3) 轮到线程2 执行了, 发现当前存款为 50, 和之前读到的 100 不相同, 执行失败. 异常的过程 1) 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更新为 50. 2) 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中. 3) 在线程2 执行之前, 滑稽的朋友正好给滑稽转账 50, 账户余额变成 100 !! 4) 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 再次执行扣款操作. 这个时候, 扣款操作被执行了两次!!!

解决方案:

给要修改的值 , 引入版本号 . 在 CAS 比较数据当前值和旧值的同时 , 也要比较版本号是否符合预期 .

CAS 操作在读取旧值的同时 , 也要读取版本号,真正修改的时候:

  • 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
  • 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了).

2.Synchronized原理

上篇文章中,我分享了锁策略,里面提到了synchronized具有的特性(在JDK1.8中),如下:

1. 开始时是乐观锁 , 如果锁冲突频繁 , 就转换为悲观锁 .

2. 开始是轻量级锁实现 , 如果锁被持有的时间较长 , 就转换成重量级锁 .

3. 实现轻量级锁的时候大概率用到的自旋锁策略

4. 是一种不公平锁

5. 是一种可重入锁

6. 不是读写锁

JVM在运行synchronized时采用了非常多的优化操作。JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依升级。

什么是偏向锁?

偏向锁不是真的 "加锁", 只是给对象头中做一个 "偏向锁的标记", 记录这个锁属于哪个线程. 如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态。偏向锁本质上相当于 "延迟加锁" 。能不加锁就不加锁, 尽量来避免不必要的加锁开销。但是该做的标记还是得做的, 否则无法区分何时需要真正加锁。

什么是轻量级锁?

随着其他线程进入竞争 , 偏向锁状态被消除 , 进入轻量级锁状态 ( 自适应的自旋锁 ). 此处的轻量级锁就是通过 CAS 来实现:

  • 通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
  • 如果更新成功, 则认为加锁成功
  • 如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).

什么是重量级锁?

如果竞争进一步激烈 , 自旋不能快速获取到锁状态 , 就会膨胀为重量级锁 此处的重量级锁就是指用到内核提供的 mutex。

  • 执行加锁操作, 先进入内核态.
  • 在内核态判定当前锁是否已经被占用
  • 如果该锁没有占用, 则加锁成功, 并切换回用户态.
  • 如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒.
  • 经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒这个线程, 尝试重新获取锁.

锁消除

编译器 +JVM会 判断锁是否可消除 . 如果可以 , 就直接消除。

有些应用程序的代码中, 用到了 synchronized, 但其实没有在多线程环境下. (例如 StringBuffer)

此时每个 append 的调用都会涉及加锁和解锁. 但如果只是在单线程中执行这个代码, 那么这些加锁解锁操作是没有必要的, 白白浪费了一些资源开销.

锁粗化

一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化。如下图所示:

3.Callable接口

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

例如:我现在要创建一个线程计算1 + 2 + 3 + ... + 1000。

不使用Callable接口,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
static class Result {
    public int sum = 0;
    public Object lock = new Object();
}
public static void main(String[] args) throws InterruptedException {
    Result result = new Result();
    Thread t = new Thread() {
        @Override
        public void run() {
            int sum = 0;
            for (int i = 1; i <= 1000; i++) {
                sum += i;
           }
            synchronized (result.lock) {
                result.sum = sum;
                result.lock.notify();
           }
       }
   };
    t.start();
    synchronized (result.lock) {
        while (result.sum == 0) {
            result.lock.wait();
       }
        System.out.println(result.sum);
   }
}

可以看到, 上述代码需要一个辅助类 Result, 还需要使用一系列的加锁和 wait notify 操作, 代码复杂, 容易出错.

下面使用Callable来解决这个问题:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Callable<Integer> callable = new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 1000; i++) {
            sum += i;
       }
        return sum;
   }
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
int result = futureTask.get();
System.out.println(result);

可以看到, 使用 Callable 和 FutureTask 之后, 代码简化了很多, 也不必手动写线程同步代码了.

Callable 和 Runnable 相对 , 都是描述一个 " 任务 ". Callable 描述的是带有返回值的任务 , Runnable 描述的是不带返回值的任务 . Callable 通常需要搭配 FutureTask 来使用 . FutureTask 用来保存 Callable 的返回结果 . 因为Callable 往往是在另一个线程中执行的 , 啥时候执行完并不确定 . FutureTask 就可以负责这个等待结果出来的工作 .

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

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

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

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
JAVA并发编程系列(3)JUC包之CAS原理
首先,Atomic包,原子操作类,提供了用法简单、性能高效、最重要是线程安全的更新一个变量。支持整型、长整型、布尔、double、数组、以及对象的属性原子修改,支持种类非常丰富。
拉丁解牛说技术
2024/09/06
1451
JavaEE 【知识改变命运】06 多线程进阶(1)
执行流程:address里面的值和expectValue比较,如果相等,九江要设置的新值跟内存里面的值更新,如果不相等,就进行下一次比较
用户11319080
2025/01/03
610
JavaEE 【知识改变命运】06 多线程进阶(1)
【JavaEE初阶】多线程重点知识以及常考的面试题-多线程进阶(一)
悲观锁: 总是以最坏的情况考虑, 每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样一来如果其他线程想拿到这个数据就会阻塞等待直到拿到锁为止. 乐观锁: 假设数据⼀般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
用户11369350
2025/04/15
1200
【JavaEE初阶】多线程重点知识以及常考的面试题-多线程进阶(一)
【JavaEE初阶】多线程进阶(五)常见锁策略 CAS synchronized优化原理
互斥锁:一个线程加锁了,另一个线程尝试加锁时,就会阻塞等待。(例如synchronized,提供了加锁和解锁的操作。) 读写锁:提供了三种操作
xxxflower
2023/05/10
1760
【JavaEE初阶】多线程进阶(五)常见锁策略 CAS synchronized优化原理
Java多线程八股(二),CAS详解,ReentrantLock和Synchronized的区别
CAS: 全称Compare and swap,字⾯意思:”比较并交换“,⼀个 CAS 涉及到以下操作。
用户11305962
2024/11/21
950
Java多线程八股(二),CAS详解,ReentrantLock和Synchronized的区别
《Java-SE-第二十九章》之Synchronized原理与JUC常用类
Synchronized即是轻量级锁又是重量级锁,它会根据实际情况自适应加锁。
用户10517932
2023/10/07
1720
《Java-SE-第二十九章》之Synchronized原理与JUC常用类
锁策略、原子编程CAS 和 synchronized 优化过程
synchronized初始使用乐观锁策略,当发现锁竞争比较频繁时,就会自动切换成悲观锁策略。
用户10788736
2023/10/16
1860
锁策略、原子编程CAS 和 synchronized 优化过程
Java并发编程之synchronized底层原理
对象头包含两部分:运行时元数据(Mark Word)和类型指针 (Klass Word)
冬天vs不冷
2025/01/21
1270
Java并发编程之synchronized底层原理
【JavaEE初阶】多线程重点知识以及常考的面试题-多线程进阶(二)
synchronized的基本特点(只考虑JDK1.8): 1. 一开始还是乐观锁,如果锁冲突频繁,就转换为悲观锁. 2. 开始是轻量级锁,如果锁被持有时间较长,就转换成重量级锁. 3. 实现轻量级锁时大概率用自旋锁策略. 4. 是一种不公平锁. 5. 是一种可重入锁. 6. 不是读写锁.
用户11369350
2025/04/17
410
【JavaEE初阶】多线程重点知识以及常考的面试题-多线程进阶(二)
cas原理【java并发编程】
在我们的表结构中,添加一个字段,版本字段version,多个字段,对同一行数据进行操作,提前查询最新的版本号码,最为update条件查询,如果version变化,就修改失败,否则就不断重试。
高大北
2022/06/14
3210
Java 并发编程·Java 并发
可能正在运行,也可能正在等待 CPU 时间片。包含了操作系统线程状态中的 Running 和 Ready。
数媒派
2022/12/01
2.8K0
《Java-SE-第二十八章》之CAS
  CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:把内存中的某个值和CPU寄存器A中的值,进行比较,如果两个值相同,就把另一个寄存器B中的值个内存的值进行交换,也就是把内存的值放到寄存器B,同时把寄存器B的值写给内存。
用户10517932
2023/10/07
1600
《Java-SE-第二十八章》之CAS
Java 多线程系列Ⅵ
CAS就是compare and swap(比较交换),是一种很出名的无锁的算法,就是可以不使用锁机制实现线程间的同步。使用CAS线程是不会被阻塞的,所以又称为非阻塞同步。CAS算法涉及到三个操作:三个操作数——内存位置、预期原值及新值
终有救赎
2024/02/25
1470
Java 多线程系列Ⅵ
并发编程(一)| Volatile 与 Synchronized 深度解析
今天这篇是我的好朋友 evil say的投稿,这小伙现在大四,客观来说,大四有这个实力,我觉得很不错。他目前正在找实习,如果看了本文觉得他可以,有公司有坑位、愿意抛出橄榄枝的话。请联系他:hack7458@outlook.com
JavaFish
2020/02/18
5550
【Java】CAS及其缺点和解决方案梳理
CAS 英文就是 compare and swap ,也就是比较并交换,首先它是一个原子操作,可以避免被其他线程打断。在Java并发中,最初接触的应该就是Synchronized关键字了,但是Synchronized属于重量级锁,很多时候会引起性能问题,虽然在新的 JDK 中对其已经进行了优化。volatile也是个不错的选择,但是volatile不能保证原子性,只能在某些场合下使用。那么问题来了,这个 CAS 机制是怎么在不加锁的情况下来保证共享资源的互斥呢?
后端码匠
2023/09/02
4510
【Java】CAS及其缺点和解决方案梳理
【javaEE】多线程(进阶)
重量级锁和轻量级锁: 重量级锁:该锁开销比较大(锁冲突比较多),一个悲观锁,通常是重量级锁(不绝对) 轻量级锁:加锁开销比较小(锁冲突比较少),一个乐观锁通常是轻量级锁(不绝对)
E绵绵
2025/03/11
880
【javaEE】多线程(进阶)
Java并发编程之CAS
在Java并发编程的世界里,synchronized 和 Lock 是控制多线程并发环境下对共享资源同步访问的两大手段。其中 Lock 是 JDK 层面的锁机制,是轻量级锁,底层使用大量的自旋+CAS操作实现的。
编程大道
2020/08/27
4300
锁策略相关问题(面试常考)
🍖乐观锁最重要的就是检测出是否发生线程冲突,这里引入一个版本号(version)解决:
终有救赎
2023/10/16
1820
锁策略相关问题(面试常考)
synchronized连环问-java并发编程基础知识储备
在Java中,synchronized关键字是用来控制线程同步的。就是在多线程的环境下,控制synchronized代码段不被多个线程同时执行。
行百里er
2020/12/02
3620
synchronized连环问-java并发编程基础知识储备
Java并发编程的艺术[2]
2.1-volatile的应用(wall la tai l 还是 wall lei tai l)
疯狂的KK
2020/07/31
7453
Java并发编程的艺术[2]
推荐阅读
相关推荐
JAVA并发编程系列(3)JUC包之CAS原理
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档