并发编程笔记
2022-04-13 11:49:46 104 举报
AI智能生成
登录查看完整内容
并发编程是指在同一时间,运行多个任务的能力。在单核CPU中,通过快速切换任务,使每个任务都感觉自己在独占CPU的运行;而在多核CPU中,则可以真正同时运行多个任务。并发编程的目标是最大限度地利用CPU资源,提高程序执行效率。常见的并发编程方法有线程、进程、协程等。线程是最小的程序执行单元,进程则是资源分配的基本单位。协程是一种用户态的轻量级线程,无需内核切换,因此切换开销小。并发编程需要考虑的问题包括数据竞态、死锁、活锁等,需要通过同步机制(如锁、信号量、条件变量)来解决。
作者其他创作
大纲/内容
32位虚拟机中,对于这种64位的操作,可能会有高32位、低32位并发写的问题,volatile是能保证这种数据的原子性的
在有些罕见的条件下,可以保证原子性 (double / floot)
用来解决可见性和有序性的
然后通过总线嗅探机制,保证其他线程的可见性
加了volatile关键字修饰的参数,在读写的时候会强制执行flush和reflush操作
等等,这个不用细扣。各个硬件底层的实现都是不一样的,没有统一的说法
Release
Acquire
Store
Load
内存屏障
添加volatile关键字以后,JVM底层在线程的工作内存计算完数据之后,会向CPU发送一条Lock为前缀的指令,该指令会让线程中工作内存的数据立即刷新到主内存中。通过MESI缓存一致性协议,其他线程同时会嗅探主内存中的数据,一旦发现数据被修改过了,会对工作内存的数据进行失效。这样的话,当其他线程再次要使用同一个变量的数据,发现自己工作内存中的数据已经失效了,此时就会重新从主内存中加载,就可以看到第一个线程更新的数据了。
底层原理
volatile
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作,比如说在代码里有先对一个lock.lock(),lock.unlock(),lock.lock()
volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个volatile变量的读操作,volatile变量写,再是读,必须保证是先写,再读
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作,thread.start(),thread.interrupt()
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
即规定了在某些条件下,不允许编译器、指令器对你写的代码进行指令重排,以此来保证有序性
happen-before原则
导致有null-value这样的数据大量存在,占用内存空间导致内存泄漏
即弱引用,弱引用在gc的时候会被直接清理掉
底层代码ThreadLocalMap -> K-V,其中key是一个内部静态类继承了WeakReference
为什么有内存泄漏问题?
确保不会有很多的null值引用了你的value造成内存的泄漏问题
你在通过ThreadLocal , set、get、remove时,他会自动清理掉map里null为key的
Java团队做了什么优化?
尽量避免在 ThradLocal长时间放入数据,不使用时最好及时进行remove,自己主动把数据删除
平时使用ThreadLocal需要注意什么?
ThreadLocal
javac静态编译器编译成.class文件时指令重排
JIT动态编译.class文件未机器码的时候指令重排
指定重排
指令乱序放入到高速缓存中
处理器的重排序
重排
寄存器
写缓冲器
高速缓存
可能造成可见性问题的组件
数据可能写入,对其他处理器不可见,造成可见性问题
将无效队列中invalid message刷新到高速缓存让数据无效强制从其他处理器的高速缓存/主内存中读取
flush
强制将写缓冲区中数据刷新到高速缓存/主内存中
refresh
处理可见性问题的操作
index确定所在bucket
tag定位cache entry
offset当前缓存变量的偏移量
拉链散列表多个bucket组成
每个bucket挂多个cache entry
tag:当前缓存行指向主存中数据的地址值
cache line:缓存数据
flag:数据状态 s:共享 invalidate:无效 exclusive:独占式 Modify:修改
每个cache entry包含三部分
flag
cache line
tag
...
高速缓存底层的数据结构
示意图
找到了就是缓存命中
给处理器01向总线发送read请求读取数据
总线从主存中读取数据给处理器01
如果数据被多个处理器共享则flag标识为s状态
多个处理器高速缓存通过总线相连(存在问题:多个写操作阻塞 需要等待其他处理器ack)
原理图示意
读写流程、原理
写数据不等待invalidate ack直接写入到写缓冲区中
其他处理器收到invalidate message后直接写入到无效队列中返回ack
处理器01嗅探到invalidate ack消息后从写缓冲区刷新数据到高速缓存中
优化多个写操作阻塞:
优化后
硬件级别MESI协议
线程的创建和销毁的代价很大
有效控制线程数量,避免创建过多线程
为什么要使用线程池
这个就是负责创建、销毁线程池的
线程管理器(ThreadPool)
就是线程池中的一个线程
工作线程(PoolWorker)
就是线程池中某个线程的业务代码实现
工作任务(Task)
这个是扔到线程池里的任务需要排队的队列
任务队列(TaskQueue)
内部组成
里面就一个线程,然后慢慢去消费。
单线程池队列
SingleThreadExecutor
适用于负载比较均衡的情况
比如说,线程池里面固定就100个线程,超过这个线程数就到队列里面去排队等待
根据你设定的线程数量执行,多出来的进入队列排队等待
固定数量线程池
FixedThreadExecutor
适用于存在高峰的情况 | ps : 容易崩掉 ,4核8GB的100个线程就差不多了,cpu负载可能就 70%/80%了
无论多少任务,根据你的需要任意的创建线程,最短的时间满足你
高峰过去后,大量线程处于空闲状态,等待60s就会被销毁掉了
自动回收线程池
CachedThreadExecutor
各种组件源码中常用,比如eureka、rocketmq的心跳等等
线程数量无限制,定时调度执行任务
定时任务线程池
ScheduleThreadExecutor
常见的线程池
代表线程池的接口,有个execute()方法,扔进去一个Runnable类型对象,就可以分配一个线程给你执行
Executor
这是Executor的子接口,相当于是一个线程池的接口,有销毁线程池等方法
ExecutorService
线程池的辅助工具类,辅助入口类,可以根据Executors快速创建你需要的线程池
Executors
一般在Executors里创建线程池的时候,内部都是直接创建一个ThreadPoolExecutor的实例对象返回的,然后同时给设置了各种默认参数
这是ExecutorService的实现类,这才是正儿八经代表一个线程池的类
ThreadPoolExecutor
常用API
分支主题
源码
实现
线程池里的核心线程数量
corePoolSize
线程池里允许的最大线程数量
maximumPoolSize
等待时间,corePoolSize外的线程等待时间大于这个值,则会被清理掉
keepAliveTime
keepAliveTime的单位
unit
工作队列,当前运行的线程数 > corePoolSizes时,多出来的线程进入queue中等待
workQueue
如果有新的线程需要创建时,就是由这个线程池来进行创建的
threadFactory
默认直接报错
线程数超过maximumPoolSize并且queue满了的时候,仍有线程进来所执行的策略
handle
核心参数配置
启动原理示意图
线程池
图解
代码实现
JMM定义了一组规则或规范,该规范定义了一个线程对共享变量写入时,如何确保对另一个线程是可见的。
定义
所有变量存储在主存中。
每个线程都有自己的工作内存,且对变量的操作都是在工作内存中进行的。
不同线程之间无法直接访问彼此工作内存中的变量,要想访问只能通过主存来传递
要点
作用于主存变量。Read操作把一个变量的值从主存传输到工作内存中,以便随后的Load操作使用
主存
Read
作用于工作内存的变量。Load 操作把Read操作从主存中得到的变量值载入工作内存的变量副本中。变量副本可以简单理解为CPU的高速缓存。
工作内存
作用于工作内存的变量。Use操作把工作内存中的一个变量的值传递给执行引擎。 每当JVM遇到一个需要使用变量值的字节码指令时,执行Use操作。
Use
作用于工作内存的变量。执行引擎通过Assign 操作给工作内存的工作内存变量赋值。 每当JVM遇到一个给变量赋值的字节码指令时,执行Assign操作。
Assign
作用于工作内存的变量。Store 操作把工作内存中的一个变量的值传递到主存中,以便随后的Write 操作使用
作用于主存的变量。Write 操作把Store操作从工作内存中得到的变量值放入主存的变量中
Write
作用于主存的变量,把一个变量标识为某个线程独占状态
Lock
作用于主存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
Unlock
图解流程
JMM的8个操作
Java内存模型(JMM)
JMM将所有的变量都存放在公共主存中,当线程使用变量时,会把公共主存中的变量复制到自己的工作内存(或者叫作私有内存)中,线程对变量的读写操作是自己的工作内存中的变量副本。
概要
Mark Word(标记字),用于存储自身运行时的数据,例如GC标志位、哈希码、锁状态等信息。
Mark Word
Class Pointer(类对象指针),用于存放方法区Class对象的地址,虚拟机通过这个指针来确定这个对象是哪个类的实例。
Class Pointer
如果对象是一个Java数组,那么此字段必须有,用于记录数组长度的数据;如果对象不是一个Java数组,那么此字段不存在,所以这是一个可选字段。
Array Length
对象头包含三个字段
Mark Word字段中存放了Java内置锁的信息
Java内置锁的状态总共有4种,级别由低到高依次为:无锁、偏向锁、轻量级锁和重量级锁。并且4种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,也就是说只能进行锁升级(从低级别到高级别)。
锁状态标记位,占两个二进制位,由于希望用尽可能少的二进制位表示尽可能多的信息,因此设置了lock标记。该标记的值不同,整个Mark Word表示的含义就不同。
lock
对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。lock和biased_lock两个标记位组合在一起共同表示Object实例处于什么样的锁状态。
biased_lock
内置锁信息
4位的Java对象分代年龄。在GC中,对象在Survivor区复制一次,年龄就增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,因此最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
age
31位的对象标识HashCode(哈希码)采用延迟加载技术,当调用Object.hashCode()方法或者System.identityHashCode()方法计算对象的HashCode后,其结果将被写到该对象头中。当对象被锁定时,该值会移动到Monitor(监视器)中。
identity_hashcode
54位的线程ID值为持有偏向锁的线程ID。
thread
偏向时间戳。
epoch
占62位,在轻量级锁的状态下指向栈帧中锁记录的指针。
ptr_to_lock_record
占62位,在重量级锁的状态下指向对象监视器的指针。
ptr_to_heavyweight_monitor
其他
关键信息解读
java对象刚创建时,还有任何线程来竞争,说明该对象处于无锁状态,此时这时偏向锁标识位是0,锁状态是01。
无锁状态
偏向锁是指一段同步代码一直被同一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。如果内置锁处于偏向状态,当有一个线程来竞争锁时,先用偏向锁,表示内置锁偏爱这个线程,这个线程要执行该锁关联的同步代码时,不需要再做任何检查和切换。偏向锁在竞争不激烈的情况下效率非常高。偏向锁状态的Mark Word会记录内置锁自己偏爱的线程ID。
加锁场景
如果锁对象时常被多个线程竞争,偏向锁就是多余的,并且其撤销的过程会带来一些性能开销。
缺点
偏向锁
当有两个线程开始竞争这个锁对象时,情况就发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先通过CAS操作占有锁对象,锁对象的Mark Word就指向哪个线程的栈帧中的锁记录。当锁处于偏向锁,又被另一个线程企图抢占时,偏向锁就会升级为轻量级锁。企图抢占的线程会通过自旋的形式尝试获取锁,不会阻塞抢锁线程,以便提高性能。
轻量级锁
如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,就会导致其他争用锁的线程在最大等待时间内还是获取不到锁,自旋不会一直持续下去,这时争用线程会停止自旋进入阻塞状态,该锁膨胀为重量级锁。重量级锁会让其他申请的线程之间进入阻塞,性能降低。重量级锁也叫同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器对象,该监视器对象用集合的形式来登记和管理排队的线程。
此时就又回到了synchronized的底层原理了
过程详解
由于JVM轻量级锁使用CAS进行自旋抢锁,这些CAS操作都处于用户态下,进程不存在用户态和内核态之间的运行切换,因此JVM轻量级锁开销较小。而JVM重量级锁使用了Linux内核态下的互斥锁,这是重量级锁开销很大的原因。
性能开销
重量级锁
锁状态解读
偏向锁,轻量级锁,重量级锁的对比
Java内置锁状态一览
Mark Word解读
对象头
前置知识点
monitorenter
// 代码对应指令
monitorexit
Monitor监视器是一个同步工具,相当于一个许可证,拿到许可证的线程即可进入临界区进行操作,没有拿到则需要阻塞等待。重量级锁通过监视器的方式保障了任何时间只允许一个线程通过受到监视器保护的临界区代码。
监视器所保护的临界区代码是互斥地执行的。一个监视器是一个运行许可,任一线程进入临界区代码都需要获得这个许可,离开时把许可归还。
同步
监视器提供Signal机制,允许正持有许可的线程暂时放弃许可进入阻塞等待状态,等待其他线程发送Signal去唤醒;其他拥有许可的线程可以发送Signal,唤醒正在阻塞等待的线程,让它可以重新获得许可并启动执行。
协作
特点
Monitor监视器
竞争队列(Contention Queue),所有请求锁的线程首先被放在这个竞争队列中。
Cxq
Cxq中那些有资格成为候选资源的线程被移动到EntryList中。
EntryList
某个拥有ObjectMonitor的线程在调用Object.wait()方法之后将被阻塞,然后该线程将被放置在WaitSet链表中。
WaitSet
名词解释
ObjectMonitor的内部抢锁过程
每个类/对象(对象包含Object实例和Class实例。)都有一个关联的monitor,monitor里面有一个计数器,从0开始
如果要对对象加锁,那么必须先这个对象获取关联monitor的lock锁
synchronized(myObject){ // 类的class对象来走的 // 一大堆代码 synchronized(myObject){ // 一大堆代码 }}
代码演示
支持重入锁
如果是0,那么说明没人获取锁,他可以获取锁,然后对计数器 加 1
如果不是0,那么说明有其他线程获取到锁了,那么它就什么事也干不了,只能进入阻塞状态,等着获取锁
如果一个线程要获取monitor的锁,那么就要先看这个计数器是不是0
此时获取锁的线程就会对那个对象的monitor里的计数器减 1,如果有多次重入加锁,那就多次减 1 ,直至减为0
接着如果出了synchronized修饰的代码片段,会执行monitorexit指令
只有一个线程能成功获取锁
此时锁被释放,其他阻塞住的线程可以重新请求获取锁
抢锁步骤
加锁和释放锁,ObjectMonitor
原子性
Load内存屏障
加锁,在进入synchronized代码块时的读操作,都会强制执行refresh
Store内存屏障
释放锁,在出代码块时,代码块内所有的写操作,都会强制执行flush操作
可见性
但是同步代码块内部的指令和外部的指令,是不能重排的
代码块内部不保证有序性
通过加各种内存屏障,保证有序性
有序性
可以保证原子性、有序性、可见性
int b = 0;int c = 0;synchronized(this) { //monitorenter //Load内存屏障 //Acquire内存屏障 int a = b; c = 1; //synchronized代码块里面还是可能会发生指令重排 //Release内存屏障} // monitorexit //Store内存屏障
示例代码
很简单,JDK 1.6之后,对synchronized内的加锁机制做了大量的优化,这里就是优化为CAS加锁的
你在之前把ReentrantLock底层的源码都读懂了,AQS的机制都读懂了之后,那么synchronized底层的实现差不多的
synchronized的ObjectMonitor的地位就跟ReentrantLock里的AQS是差不多的
底层原理示意图
当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以避免无效的资源争夺。
线程间的通信需要借助同步对象(Object)的监视器来完成,Object对象的wait()、notify()方法就如开关信号,用于完成等待方和通知方之间的通信。
wait()和notify()系列方法需要在同步块中使用,否则JVM会抛出异常
通信方法
线程间通信
浅谈synchronized
是一种JVM原生的锁实现方式
synchronized是通过在对象头设置一个标记。上面加一个mark word
并保证对所有线程都能拥有对该int的可见性和原子修改,其本质是基于所谓的AQS框架
ReentrantLock以及所有基于Lock接口的实现类,都是通过一个被volatile修饰的int型变量
其实锁的实现原理都是一个目的,让所有线程看到某种标记
Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现的
而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此需要再finally块中释放锁
synchronized在发生异常时,会自动释放线程粘有的锁,因此不会导致死锁现象的发生
使用synchronized时,等待的线程会一直等待下去,不能响应中断
Lock可以让等待锁的线程响应中断,而synchronized却不行
通过Lock可以知道有没有成功获取锁,而synchronized却无法办到
Lock可以提高多个线程进行读操作的效率
类似zk的羊群效应?Curator对zk锁的优化类似?
而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized
从性能上来说,如果竞争资源不激烈,两者的性能是差不多的
如何选择
synchronized和locks包的锁有什么不同
对比
public synchronized static void init(){}
SynchronizedTest.init();
synchronized (MyService.class){}
类锁就是对jvm中类对应的class对象加锁
类锁
public synchronized void init() {}
SynchronizedTest synchronizedTest = new SynchronizedTest();synchronizedTest.init();
synchronized (synchronizedTest){}
对象锁是对单个对象实例加锁
对象锁
类锁所有对象一把锁,对象锁一个对象一把锁,多个对象多把锁
总结
锁消除是JIT编译器对synchronized锁做的优化,在编译的时候,JIT会通过逃逸分析技术,来分析synchronized锁对象
是不是只有一个线程来加锁,没有其他的线程来竞争加锁,这个时候编译就不用加入monitorenter和monitorexit的指令
毕竟没有多线程并发的情况,加锁也是浪费性能,总体效果就是好像没有加锁一样,锁消除了。
锁消除
JIT编译器如果发现对一个对象中多次调用synchronized修饰的方法,这个时候释放锁和添加锁其实都是针对同一个对象而言的。此时就会干脆在底层将这些方法合并成一个synchronized方法,只会执行一次monitorenter和monitorexit指令,效果就好像锁粗化了一样。
锁粗化
你这个锁里面的代码实际执行的非常快
当其他线程获取锁未成功时,不切换线程,自旋一会,等待这个锁释放,减少上下文切换带来的性能消耗
自旋锁
自适应性锁
1.6以后的锁优化
synchronized
图解AQS
/*** The synchronization state.*/private volatile int state;
AQS使用volatile修饰的int类型的state标示锁的同步状态
AQS是CLH队列的一个变种,是一个虚拟队列,不存在队列实例,仅存在节点之间的前后关系。节点类型通过内部类Node定义
节点之间的结构
AbstractQueueSynchronizer抽象队列同步器
CAS更新 state = 1
线程1
state = 0 -> state = 1
线程2
等待队列
ReentrantLock底层原理
直接尝试获取锁
未获取到锁,看下当前拿到锁的线程是不是自己,是自己则state + 1重入
tryAcquire()尝试获取锁
未获取锁成功,再执行acquire()方法
lock()
NonfairSync ->非公平锁
new ReentrantLock()
看看队列里是否有人排队
没人排队的话再尝试获取锁
acquire()
FairSync -> 公平锁
new ReentrantLock(true)
ReentrantLock源码分析
ReentrantLock与AQS组合关系
ReentrantLock
线程2处于等待队列中,线程1执行完成后,突然来个线程3,直接就执行了
非公平锁
线程2处于等待队列中,线程1执行完成后,突然来个线程3,进入队列排队,线程2开始执行
公平锁
ReentrantLock lock = new ReentrantLock(true); => 默认非公平锁,true即为公平锁
AQS
CopyOnWriteArrayList源码中维护了一个array对象数组用于存储集合的每个元素,并且array数组只能通过getArray和setArray方法来访问。
在调用iterator方法的时候,会通过getArray()方法获取array数组,然后可以基于这个数组进行遍历。
新增一个元素,调用add方法的时候,也是通过getArray()获取到对象数组,然后直接新生成一个数组,最后把旧的数组的值复制到新的数组中,然后直接使用新的数组覆盖实例变量array。
源码解读
CopyOnWriteArrayList实例变量array本质上是一个数组,而数组的各个元素都是一个对象,每个对象内部的状态是可以替换的。因此实例变量并非严格意义上的不可变对象,所以我们称之为等效不可变对象。
特性
通过弱一致性提升读请求并发,适合用在数据读多写少的场景
适用场景
JDBC中的数据库驱动程序列表管理
源码运用
等效不可变对象CopyOnWriteArrayList
即同一时间只有一个线程可以成功执行CAS
先比较再设置
其他线程执行CAS会失败
并发包下AtomicInteger等类天然支持CAS保证原子性
优点
内存值、预期值、新值。当且仅当预期值和内存值相等时才将内存值修改为新值。
这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一样
如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作
否则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存
原理分析
只能保证一个变量的原子操作
长时间自旋,开销大
存在ABA问题
多线程同时读取主内存中的数据,必然会导致并发安全问题
Unsafe类可以像C语言一样使用指针操作内存空间
操作系统层面的CAS是一条CPU的原子指令(cmpxchg指令),正是由于该指令具备原子性,因此使用CAS操作数据时不会造成数据不一致的问题,Unsafe提供的CAS方法直接通过native方式(封装C++代码)调用了底层的CPU指令cmpxchg。
Unsafe的CAS操作会将第一个参数(对象的指针、地址)与第二个参数(字段偏移量)组合在一起,计算出最终的内存操作地址。
Unsafe类中三个关键方法
Unsafe
在执行Unsafe的CAS方法时,这些方法首先将内存位置的值与预期值(旧的值)比较
如果相匹配,那么CPU会自动将该内存位置的值更新为新值,并返回true;
如果不匹配,CPU不做任何操作,并返回false。
原理
即compareAndSet
在争用激烈的场景下,会导致大量的CAS空自旋。比如,在大量线程同时并发修改一个AtomicInteger时,可能有很多线程会不停地自旋,甚至有的线程会进入一个无限重复的循环中。大量的CAS空自旋会浪费大量的CPU资源,大大降低了程序的性能。
CAS操作的性能问题
可以使用LongAdder替代AtomicInteger。Java 8提供了一个新的类LongAdder,以空间换时间的方式提升高并发场景下CAS操作的性能。LongAdder的核心思想是热点分离,与ConcurrentHashMap的设计思想类似:将value值分离成一个数组,当多线程访问时,通过Hash算法将线程映射到数组的一个元素进行操作;而获取最终的value结果时,则将数组的元素求和。最终,通过LongAdder将内部操作对象从单个value值“演变”成一系列的数组元素,从而减小了内部竞争的粒度。
原理图
LongAdder的内部成员包含一个base值和一个cells数组。在最初无竞争时,只操作base的值;当线程执行CAS失败后,才初始化cells数组,并为线程分配所对应的元素。LongAdder中没有类似于AtomicLong中的getAndIncrement()或者incrementAndGet()这样的原子操作,所以只能通过increment()方法和longValue()方法的组合来实现更新和获取的操作。
LongAdder
longAccumulate
LongAdder源码解读
在高并发场景下如何提升CAS操作的性能呢?
CAS
● AtomicInteger:整型原子类。● AtomicLong:长整型原子类。● AtomicBoolean:布尔型原子类。
基本原子类的功能是通过原子方式更新Java基础类型变量的值。
主要通过CAS自旋+volatile的方案实现,既保障了变量操作的线程安全性,又避免了synchronized重量级锁的高开销,使得Java程序的执行效率大为提升。说明CAS用于保障变量操作的原子性,volatile关键字用于保障变量的可见性,二者常常结合使用。
AtomicInteger
基本原子类
● AtomicIntegerArray:整型数组原子类。● AtomicLongArray:长整型数组原子类。● AtomicReferenceArray:引用类型数组原子类。
数组原子类的功能是通过原子方式更数组中的某个元素的值。
数组原子类
● AtomicReference:引用类型原子类。● AtomicMarkableReference:带有更新标记位的原子引用类型。● AtomicStampedReference:带有更新版本号的原子引用类型。● AtomicMarkableReference类将boolean标记与引用关联起来,可以解决使用AtomicBoolean进行原子更新时可能出现的ABA问题。● AtomicStampedReference类将整数值与引用关联起来,可以解决使用AtomicInteger进行原子更新时可能出现的ABA问题
引用原子类
● AtomicIntegerFieldUpdater:原子更新整型字段的更新器。● AtomicLongFieldUpdater:原子更新长整型字段的更新器。● AtomicReferenceFieldUpdater:原子更新引用类型中的字段。
字段更新原子类
JUC并发包中原子类
原子类
并发编程笔记
0 条评论
回复 删除
下一页