Java并发编程的艺术 第二章-并发机制底层实现原理

volatile的应用

抽象-提取功能,形象-提取外表

内存屏障:是一组处理器命令,用于实现对内存操作的顺序限制

缓存行:高速缓存的最小单位

原子操作:不可中断的一个或一系列操作(不是不可中断,就是被打断了要从头开始)

缓存行填充:从内存中读取操作数是可缓存的,处理器会读取整个缓存行到适当的缓存

缓存:把内存中使用率高的数据存到离使用时比较近的缓存内

缓存命中:cpu先去缓存里面查查数据有没有,有的话就不用去内存找了,而找到就叫缓存命中

写命中:增删改时,如果缓存内有,就直接在缓存增删改,不用对内存进行写操作

写缺失:一个有效的缓存行写到不存在的内存区域,比如有个i在缓存内,但是内存的i已经被删了

缓存淘汰策略

LFU:按照被访问的次数进行排序,新进来的数据插入到尾部,数据满了就把尾巴的踢出去

LRU:按时间排序,新来的数据放头部,被访问的缓存拿出来重新放到头部,如果满了,就把尾部的挤出去

2Q:有两条,一条是FIFO,一条是LRU,第一次缓存数据,把它放到FIFO里,里面跟LRU类似,而第二次访问这个数据,就把数据放到第二条的LRU里面

页面置换算法

一、算法描述

1.先进先出(FIFO)置换算法

(1)描述:FIFO算法是最早出现的置换算法。该算法总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面予以淘汰。

(2)优点:该算法实现简单,只需要把一个进程已调入内存的页面按先后次序链接成一个队列,并设置一个指针,称为替换指针,使它总是指向最老的页面。

(3)缺点:该算法与进程实际运行的规律不相适应,因为在进程中,有些页面经常被访问,比如:含有全局变量、常用函数、例程等的页面,FIFO算法并不能保证这些页面不被淘汰。

2.最佳(Optimal)置换算法

(1)描述:最佳置换算法是由Belady于1966年提出的一种理论上的算法。其所选择的被淘汰的页面将是以后永不使用的,或许是在最长(未来)时间内不再被访问的页面。

(2)优点:采用最佳置换算法通常可以保证获得最低的缺页率。

(3)缺点:人们目前通常还无法预知,一个进程在内存的若干个页面中,哪一个页面是未来最长时间内不再被访问的,因此,该算法是无法实现的,但可以利用该算法去评价其他算法。

3.LRU(Least Recently Used)置换算法

(1)描述:最近最久未使用(LUR)的页面置换算法是根据页面调入内存后使用情况做出决策的。由于无法预测各页面将来的使用情况,只能利用“最近的过去”做“最近的将来”的近似,因此,LUR置换算法是选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间t。当需要淘汰一个页面时,选择现有页面中t值最大的,及最近最久未使用的页面予以淘汰。

(2)优点:考虑程序访问的时间局部性,一般能有较好的性能,实际应用多。

(3)缺点:实现会需要较多的硬件支持,会增加硬件成本。

4、CLCOK算法又称为最近未使用算法(NUR) 每页设置一个访问位,再将内存中的所有页面都通过链接指针链接成一个循环队列;当某个页面被访问时,其访问位置1。淘汰时,检查其访问位,如果是0,就换出;若为1,则重新将它置0;再按FIFO算法检查下一个页面,到队列中的最后一个页面时,若其访问位仍为1,则再返回到队首再去检查第一个页面。

使用场景

volatile是轻量级的synchronized,保证可见性,就是一边修改另一边立刻就能看见

适合做纯赋值操作,不适合做a++之类的操作,因为线程不安全

synchronized的实现和应用

Java对象头

synchronized用的锁存在Java对象头里,如果对象师叔祖,虚拟机用三个自宽存储对象头。如果非数组,就用两个自宽,32位的虚拟机中,一个字宽等于四个字节

对象头里的MarkWord里存储对象的HashCode,分代年龄和锁标记位,锁标记位就是所的状态 - 无锁,偏向锁,轻量级锁,重量级锁

重量级锁,1.6后锁和释放锁带来的性能消耗而引入的片两所和轻量级锁,以及锁的存储结构和升级过程

java中每个对象都可以作为锁

对于铍铜同步方法,锁的是当前实例对象

对于静态同步方法,锁的是class对象

对于同步方法快,锁的是Synchonize括号里的配置的对象

锁的升级和对比

锁一共有四个状态,由低到高时 无锁状态 偏向锁 轻量级锁 重量级锁

锁只能升级不能降级,目的是为了提高获得锁和释放锁的效率

偏向锁,因为很多时候,锁会一直被一个对象调用,来回上锁解锁很浪费性能,偏向锁就是一个线程占用之后,就一直占用,有竞争出现才会释放锁

当偏向锁被来回来修改,那么就会被标记成不适合偏向锁,进行升级成轻量级锁

轻量级锁:当偏向锁来回的修改,那么它就会被标记不适合偏向锁,升级成轻量级锁

JVM会现在当前线程的栈帧中创建用于存储锁记录的空间,将对象头中的mark word复制到锁记录中

当有线程在上锁之后,另一个线程过来,会不断的循环尝试获取(自旋),当获取次数达到了耐心值之后,轻量级锁就会升级为重量级锁

自旋

因为自旋会消耗cpu,为了避免无用自旋,一旦锁升级为重量级,就不会恢复到轻量级,当锁处于这个状态下,其他线程尝试获取锁时,都会被阻塞塞住,当尺有所的线程释放锁后,就会唤醒其他线程,被唤醒的线程进行新的一轮

重量级锁:不会自旋,阻塞线程,追求吞吐量,同步块执行速度较长

为了避免无用的自旋,一旦锁升级为重量级锁,就不会恢复到轻量级状态,当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争

原子操作的实现原理

原子操作就是不可被中断的一个或一系列操作

术语

1. 缓存行: 缓存的最小操作单位

2. 比较并交换: CAS

3. CPU流水线: CPU流水线的工作方式就像工业生产的装备流水线,在cpu中由5-6个不同电子元件

4. 内存顺序冲突: 一般是假共享引起的,假共享就是指多个cpu同时修改一个缓存行的不同部分引起的cpu操作无效,但出现这个内存顺序冲突时,cpu必须清空流水线

处理器如何实现原子操作

基于堆内存枷锁或总线加锁的方式来实现多处理器之间的原子操作

1. 总线锁定:多个处理器同时在各自的缓存中读取操作,比如两个cpu都+1,但是结果之嘉义一次

使用总线锁

使用缓存锁

ENSI

现在的处理器都是多核处理器,并且每个核都带有多个缓存(指令缓存和数据缓存,见下图)。为什么需要缓存呢,这是因为CPU访问内存的速度比较慢,所以在CPU和内存之间加了个缓存以提高访问速度。既然每个核都有缓存,那么假设两个核或者多个核同时访问同一个变量时这些缓存是如何进行同步的呢(缓存细分为一个个缓存行),这就有了MESI协议。

MESI中每个缓存行都有四个状态,分别是E(exclusive)、M(modified)、S(shared)、I(invalid)。下面我们介绍一下这四个状态分别代表什么意思。

M:代表该缓存行中的内容被修改了,并且该缓存行只被缓存在该CPU中。这个状态的缓存行中的数据和内存中的不一样,在未来的某个时刻它会被写入到内存中(当其他CPU要读取该缓存行的内容时。或者其他CPU要修改该缓存对应的内存中的内容时(个人理解CPU要修改该内存时先要读取到缓存中再进行修改),这样的话和读取缓存中的内容其实是一个道理)。

E:E代表该缓存行对应内存中的内容只被该CPU缓存,其他CPU没有缓存该缓存对应内存行中的内容。这个状态的缓存行中的内容和内存中的内容一致。该缓存可以在任何其他CPU读取该缓存对应内存中的内容时变成S状态。或者本地处理器写该缓存就会变成M状态。

S:该状态意味着数据不止存在本地CPU缓存中,还存在别的CPU的缓存中。这个状态的数据和内存中的数据是一致的。当有一个CPU修改该缓存行对应的内存的内容时会使该缓存行变成 I 状态。

I:代表该缓存行中的内容时无效的。

JAVA如何实现原子操作

1. 使用循环cas实现原子操作

CAS原子操作三大问题

1. ABA问题:就是a - b - a,然后cas发现没有变化,就接着执行了,其实是有变化的,解决思路就是加版本号

2. 循环时间长,开销大:

3. 只能保证一个共享变量的原子操作

2. 使用锁机制实现原子操作

只有获得锁的线程才能过操作锁定的内存区域,但是除了偏向锁,其他所都用了循环cas,即当一个线程想要进入同步块的时候使用cas的方式来获取锁,退出的时候使用cas释放锁

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值