Java基础学习之并发篇:从CAS开始谈起

本文介绍了进程和线程的概念,强调了并发编程的重要性,并详细讲解了Java中的线程状态。通过银行取款的例子,阐述了并发可能导致的数据不一致问题。CAS操作作为解决并发问题的原子操作,确保了数据更新的原子性,但面临ABA问题。最后,文章提到了Java中解决ABA问题的策略和并发控制的其他手段。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

学习目标

  1. 理解进程和线程
  2. 认识并发概念
  3. 什么是原子操作
  4. CAS是啥
  5. CAS引起的ABA问题

进程和多线程

大家如果是计算机专业的人应该对这不陌生,我们都知道计算机的核心是CPU,它承担了所有的计算任务,而操作系统是计算机的管理者,它负责任务的调度,资源的分配和管理,统领整个计算机硬件;应用程序是具有某种功能的程序,程序是运行于操作系统之上的。从专业角度来讲:

进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体

线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间

通俗的来讲就是,进程是应用程序执行的副本,每一个单独的应用运行操作系统为它分配进程,比如我们部署tomcat项目有Java进程。而线程就是执行程序的最小单位,我们敲的代码靠线程执行。
那么对于Java来说,线程有哪些执行状态呢?
Java线程到底有几种状态,其实只要打开一下JDK源码,看一下java.lang.Thread类就知道了,java.lang.Thread定义了一个内部枚举java.lang.Thread.State,共描述了6种状态
在这里插入图片描述

未调用start(),thread线程状态:NEW
执行run(),thread线程状态:RUNNABLE
线程sleep,thread线程状态:TIMED_WAITING
sleep结束,thread线程状态:RUNNABLE
线程synchronized,thread线程状态:BLOCKED
synchronized结束,thread线程状态:RUNNABLE
线程执行完成,thread线程状态:TERMINATED

何为并发

有一个场景,当我们去银行取款机取钱的时候,正常理解情况下大家都是按部就班排队取钱,比如我银行卡总额目前还有100¥,那么第一次取10¥出来,还剩90¥。
我们知道这是正常的,但张三不一样,他手上有两张一模一样的卡,而刚好有两款取款机,张三此时同时插卡取钱,发现每个取款机取出来10¥后,卡余额还剩90¥,张三仿佛发现了财富密码。

其实发生这样的原因,我们可以理解是出现了并发,即多个线程去访问了同一个资源,发生了竞争。

例如我们期望的正常情况是:

  1. 线程T1:读取总额为100
  2. 线程T1:取款机1取款10,总额-10=90
  3. 线程T2:读取总额为90
  4. 线程T2:取款机1取款10,总额-10=80

但发生的是这样的:

  1. 线程T1:读取总额为A1=100
  2. 线程T2:读取总额为A2=100
  3. 线程T1:取款机1取款10,A1-10=90
  4. 线程T2:取款机1取款10,A2-10=90

确实有点操蛋,线程T1和线程T2对于总额发生了竞争。导致了数据的不一致。那么我们如何解决这种竞争,期望得到正常的结果?


CAS操作

什么是CAS操作?

CAS是Java并发编程的基石,它是最基本的原子操作

什么是原子操作?

所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束

CAS(compare and set)的原子操作就是保证了其某个值的原子性。CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。CAS(&V,A,B)其原理是将内存位置的内容V与给定值A进行比较,你必须告诉我当前变量的最新值,如果匹配,则允许修改为B。
那么针对上述的取款问题可以得到解决:

  1. 线程T1:读取总额为100 A1=100
  2. 线程T2:读取总额为100 A2=100
  3. 线程T1:取款机取款10,V=100,A1=100 匹配可以V=100-10=90
  4. 线程T2:取款机取款10,此时V=90,A2=100,发现V!=A2,修改失败

这就是保证了竞争的原子性,防止造成的数据不一致性。取款机通过这种机制解决了这个问题,但是突然有一天张三又发现了漏洞:

张三通过取款50元,同时汇款50发现卡余额本来不变,但最终只有50了,这次被坑了

出现这样的原因原来是取款机机器针对取款操作同时提交了两次,产生了两次线程

  1. 线程T1:读取总额为100 取款50,成功更新为50
  2. 线程T2:读取总额为100 取款50 ,阻塞Blooking
  3. 线程T3:获取当前值50,存款50,更新为100

线程T1和T2同时发生,T1执行完成,T2发生阻塞,T3先于T2完成,使V恢复原来的值。
此时线程T2的A=100,满足V=100,故修改成功,将V=100-50 =50。
这就是著名的ABA问题。

那么如何解决呢?可以通过加个版本号来解决。每次针对V的修改,可以使版本迭代。
比如线程T1修改前初始版本v0,修改后版本为v1。线程T3修改后版本为v2。此时线程T2再想修改比较V=A,但是版本号此时v2!=v0,修改失败!
JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值

但是在Java中,我们有时不仅仅是想保护某个变量,而是想保护多个变量甚至整个代码块的安全性,那么还是不得不采取上锁的机制,在Java中又有哪几种上锁的方式呢,还请听下回分解


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值