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