【牛客网】-【并发详解】-【并发编程基础】-【原子类】

参考书目:
请添加图片描述

并发编程基础

在操作系统中,并发是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
请添加图片描述
堆和方法区中的数据是可以被共享的
堆中的数据是被栈中的变量所持用的,栈是线程隔离的,每个线程私有一个栈,所以栈中的数据不共享
请添加图片描述
调用a方法时,jvm会给a方法创建一块内存区,让其入栈,这块区域被称为a的栈帧,调用b方法、c方法时,同理
如果多个线程访问同一个成员变量,需要加锁,但是如果在方法内定义了一个局部变量,局部变量时是线程私有的,没必要加锁,除非这个局部连量指向了成员变量,即堆中的数据,产生了共享,才需要对局部变量加锁
请添加图片描述
执行n++这条语句,cpu会将高级语言转换成机器语言,在底层执行时,会先将n加载到寄存器并初始化为0,再进行n++,再将值写入内存,执行完(以上三步的)任意一步都可能进行线程切换,这就是原子性问题
请添加图片描述
假如操作系统是单核的,在并发时,先将线程1的n加载到寄存器并初始化为0,此时线程切换了,(执行线程2的n++语句),将线程2的n加载到寄存器并初始化为0,然后执行线程2的n=n+1,然后将线程2的n=1写入内存,此时线程切换了,继续执行线程1,将线程1的n=n+1,因为线程1的n为初始值0,所以此时n=1,接下来将n=1写入内存。问题:执行了两次n++,但结果是n=1。

解决办法:

  1. 加锁
  2. 把i封装为AtomicInteger,调用他的getAndIncrement()方法去加

请添加图片描述
cpu与内存通过缓存交互,内存与硬盘的交互也是类似的,因为内存比硬盘快,缓存比内存快
请添加图片描述
假如操作系统是双核的,有两个cpu,内存中有一个变量x,cpu1的线程a要修改x:将x加载进缓存,线程a在缓存中修改,改完了将x同步回内存,cpu2的线程b要修改x也是这个逻辑,假如两个线程同时修改x,同时同步回内存,会发生冲突,这就是可见性问题
请添加图片描述
以执行下述代码为例:

// 创建一个单例对象
if(instance == null){
	instance = new Singleton();
}

代码执行顺序应该是:判断instance == null,如果为true,则在内存中分配一块空间R,R中存放Singleton实例对象,然后把R的地址给instance变量,但是为了提高性能,编译器和处理器会对指令重排序,可能在内存中分配完空间R,就将R的地址给instance变量了,然后再往R中存放Singleton实例对象,如果是按照这个顺序的话,执行完R的地址给instance变量之后,线程切换了,线程b从头开始执行这段代码,判断结果肯定是false,于是他直接返回instance,但是此时的instance中并没有Singleton实例对象,于是出问题了。
请添加图片描述
请添加图片描述
共享内存:一个线程把数据放在共享的内存中,另一个线程去取
消息传递:一个线程给另一个线程发送消息
请添加图片描述
假如线程a与线程b进行通信,线程a将数据写入本地内存a,然后本地内存a将数据刷到主内存中,线程b将数据读取到本地内存b中,线程b就可以访问数据了,由jmm控制两个线程的读写顺序
请添加图片描述
请添加图片描述
这是内存重排序的例子,执行线程a时,先执行A1,将数据刷到缓冲区A中,假如现在还没有将缓冲区A的内容同步到内存,然后执行A2,因为b是线程b的,所以需要去内存中取(线程A可能将b读到缓冲区A中,然后再去缓冲区A中取,但是不管怎么说都是去内存中取),然后将缓冲区A的内容同步到内存,此时从内存的角度来看,先执行A2再执行A1,因为A3是A1的延续,所以会出现x=0的情况,同理,y=0
请添加图片描述
请添加图片描述
请添加图片描述
请添加图片描述
JMM可以解决内存可见性问题及编译器重排序问题:happens-before,
如果是cpu导致的重排序,通过内存屏障解决,如果是编译器导致的重排序,通过规则、JMM解决
请添加图片描述

总结:

  1. 并发编程的目标:解决通信同步的问题
  2. Java采用的并发编程模型是共享内存模型,该模型被称为JMM
  3. JMM解决了内存可见性问题,内存可见性问题就是两个线程谁先访问谁后访问的问题,即访问顺序问题
  4. 重排序有三种:编译器重排序、cpu重排序、内存系统重排序
  5. 如果是cpu导致的重排序,通过插入内存屏障解决,代码在unsafe类中,编译时插入内存屏障代码
  6. 如果是编译器导致的重排序,通过happens-before规则解决(6点)

请添加图片描述
写内存时,是立刻讲缓存中的数据刷新到主内存,读内存时,是直接从主内存中读
请添加图片描述
volatile只保证可见性不保证原子性,他只保证对单个变量读写的可见性(顺序/原子性)
请添加图片描述
可见性、有序性解决了,原子性也解决了

原子类

请添加图片描述
atomic包中提供了17个类是针对jdk8而言
jdk8之后就不支持32位系统,所以jdk8是支持32位系统的最高版本,会一直维护
long指的不是成员变量的数据类型,是成员变量位于内存中的偏移量
unsafe提供了将成员变量转换成底层偏移量的方法,直接调用即可
期望的值可以理解为旧值,更新的值可以理解为新值
以第一个方法compareAndSwapInt()为例,调用该方法时传入期望的值(旧值),执行该方法时发现成员变量的值和期望的值不一致,他就不会执行该方法
并发少,竞争小的时候用原子类更合适,并发多,竞争激烈的时候加锁

看源码时,点击ctrl+f12可以得到本类的所有方法,查找更方便

请添加图片描述
这三个类的底层实现除了用到前面讲的cas方法外,还用到getIntVolatile()等
请添加图片描述
unsafe.objectFieldOffset()获取成员变量的偏移值
请添加图片描述
获取旧值
请添加图片描述
获取新值
请添加图片描述
加1,相当于i++
请添加图片描述
加1,相当于++i

请添加图片描述
原子替换,将原来的值替换成新的值
请添加图片描述
var5用于保存旧值,更新数据,一次更新可能不成功,所以用do……while,直到更新成功
请添加图片描述
AtomicBoolean就是将boolean类型转换成int类型,再进行cas操作。比较常用的方法:
请添加图片描述
当我们需要将一个boolean值由假改为真或由真改为假,可以调用这个方法,通过原子类来改变
请添加图片描述
ABA问题:假如一个值从1变成了2,又变回了1,这个过程cas是检测不出来的,cas会认为值没变,但实际变了。一般整数的ABA问题不处理,但引用类型的ABA问题得处理,可能对象指向了原来的地址,但是该地址存储的内容变了
解决ABA问题:给值打一个标记,如果标记是整数,则使用第二个方法,如果是是布尔类型,则使用第三个方法
请添加图片描述
请添加图片描述
常用的方法:
请添加图片描述
请添加图片描述
stamp就是标记,原子替换替换整个pair,reference和stamp不变时才能替换掉pair,reference是一个引用
请添加图片描述
请添加图片描述
请添加图片描述
请添加图片描述
与前者一样
请添加图片描述
原子更新属性了解即可,以下源码:
请添加图片描述
请添加图片描述
他的构造器时protected,我们无法使用,所以我们需要使用下述的newUpdate()方法实例化该对象
请添加图片描述
getCallerClass是调用者类,因为可能是他本身在调用,也可能是他的子类在调用,需要区分一下
注解只是一个标识,里面啥也没有,意思是当我用getCallerClass获取调用者类时,把当前类忽略掉,因为需要计算调用的路径
请添加图片描述
tclass:属性属于哪个类
class:调用者是哪个类
请添加图片描述
另一个方法的构造器也是protected,无法使用
请添加图片描述
需要使用下述的newUpdate()方法实例化该对象
请添加图片描述
tclass是目标类型,fieldName是属性名,vclass是属性的类型,因为要原子替换的属性他是引用类型,但是引用类型的范围太广泛了,用vclass标记一下
假如我有一个Person类型,他是别人写好并封装好了的,我没法改源码,我需要改该类的family属性:
请添加图片描述
这样我就能把Person类型的family属性做原子替换
请添加图片描述
请添加图片描述
base:数组的起始偏移量,即数组起始的位置
array:具体要处理的数组
scale:数组中1个元素占多少位,要求scale一定是2的n次方
shift就是n的值,好知道要移多少位
请添加图片描述
将数组的索引换算成偏移量
构造函数:传入一个长度,初始化一个数组
请添加图片描述
原子替换数组中的元素:
请添加图片描述
数组第i个位置的数据加上delta的值,checkedByteOffset(i)表示数组第i个位置的偏移量
请添加图片描述
请添加图片描述
数组第i个位置的数据替换为新值(update的值),expect是旧值
请添加图片描述
4个子类,前两个是处理long类型的,后两个处理double类型
对于高并发的场景,可以加锁,也可以使用这个类,这个类采用分而治之的思想,将long分成多份,每个线程往自己那一段上累加,最后再sum一下。Striped64类的源码如下:
请添加图片描述
transient:序列化时,该变量不会被序列化
base:基础份
cells:剩余份
请添加图片描述
使用原子的方式处理base
请添加图片描述
LongAdder用于累加,源码:
请添加图片描述
先尝试把x原子累加到base上(casBase(b=base, b + x)),如果cells不为null,往某一个cell中(a)原子类加x(a.cas(v = a.value, v + x))。累加完了获取完整值:
请添加图片描述
请添加图片描述
LongAccumulator用于计算,源码:
请添加图片描述
identity:初始值
function是自定义的操作逻辑,你具体要做什么操作,一定是二元的
请添加图片描述
请添加图片描述
与LongAdder的区别在于:使用的是你自定义的操作逻辑
使用上述5类原子类解决问题时,会出现以下问题:
请添加图片描述
自旋:死循环while(true)或for(;😉,cas有失败的可能,失败了不会不管,会在死循环中进行下一次尝试
如果不好合并成一个共享变量,那只能加锁了
原子类的优点:轻量级、不用阻塞别的线程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值