------------------------每个关节都是武器,浑身上下都是才艺
目录
《深入理解Java虚拟机》读书笔记--第十二章 Java内存模型与线程_时空恋旅人的博客-CSDN博客
2.3 RenntrantLock与synchronized的区别?
3.3 synchronized是如何优化的?锁的这四种状态是如何变化的?
1.对象的内存布局
2.MarkWord里面内容
计算机世界的快速发展离不开 CPU、内存和 I/O 设备的高速发展,但是这三者一直存在速度差异性问题,我们可以从存储器的层次结构可以看出 , 随着摩尔定律的失效,人们需要开发出更高效的软件,他的核心思想是拆分计算机处理的任务发布到多个廉价的CPU上同时执行,这样就能底层本的提高软件执行的效率。当多个任务想要进行读写同一块内存值的时候,难题就出现了。
单核处理器的缓存结构
L1大概几十K,L2大概几百K
多核处理器的缓存:
缓存一致性协议作用的地方:
缓存的最小存储的单元称为缓存行
CPU中每个缓存行使用四种状态进行标记
M:Modified 修改
指的是该缓存行只被缓存在该CPU缓存中,并且是被修改过的,因此他与主存的数据是不一致的,该缓存行中的数据需要在未来的某个时间点(允许其他CPU读取主存相应的内容之前)写回主存,然后状态变成E(独享)。
E :Exclusive 独享
缓存行只被缓存在该CPU的缓存中,是未被修改过的,与主存的数据是一致的,可以在任何时刻当有其他CPU读取该内存时,变成S(共享)状态,当CPU修改缓存行的内容时,变成M(修改)的状态。
S :Share 共享
意味着该缓存行可能被多个CPU进行缓存,并且该缓存中的数据与主存数据是一致的,当有一个CPU修改该缓存行时,其他CPU是可以被作废的,变成I(无效的)。
I :Invalid 无效的
代表这个缓存是无效的,可能是有其他CPU修改了该缓存行。一个无效(Invalid)的缓存行必须从主存中读取(变成Share或者Exclusive状态)来满足该CPU的读请求。
现在我们来模拟一个两核处理器,他的读写操作是怎样的
假设CPUa去读取一个数据x,那么首先他会检查自己的缓存中是否有这个数据,如果有缓存中那么就要去判断一下他的状态是什么样子的。如果是E,S,M这三个状态,那么都表示他是整个处理器中处于最新的一个值,直接读取就OK了。假设x的状态是I,那么CPUa就要通过总线通知发送一个读的请求,如果其他CPU收到了这个请求并且在缓存中的这个x的状态是E或者S,那么通过总线通知A去获取一下就行了。然后B中x的状态改成S,如果是B中的X是M态,那么B要先把X写到主存中去,然后再分享给A。
如果CPUa要去写这个数据,那么如果x在缓存中是M或者E,那么这个表示CPU对x是独占的,直接去写就完事儿了。如果x的状态是S,说明其他CPU也有这个变量的拷贝。那么就需要通过总线通知其他的CPUj将自己缓存中的值改为I,如果说状态是I,那么就需要从主内存中去读取,然后改为M,并且通过总线通知其他的CPU都改为I。
两个方面,一个是代码编写如何实现线程安全?虚拟机如何实现同步与锁
互斥同步(临界区,互斥量,信号量都是主要的互斥方式) 这是一种悲观的并发策略,互斥是方法同步是目的, 在Java中最基本的互斥同步手段就是synchronized关键字。
非阻塞同步 基于冲突检测的一种乐观的并发策略,例如CAS(比较并交换)就是一种乐观并发策略,说他是乐观锁的实现其实不准确,因为他是无锁编程,没有涉及到锁。
涉及到用户态和内核态的切换,
不仅仅功能上RenntrantLock比synchronized更强大,吞吐量RenntrantLock要比synchronized高,即便是在synchronized被优化过后。
2.4 什么是CAS算法?
CAS指令需要三个操作数,分别是内存位置V,旧的预期值A, 新的预期值B,
当线程要修改资源对象的值的时候,他会先读取当前资源对象的值存在A中,此时该线程如果得到时间片他就继续修改资源对象的值,修改之前会先根据资源对象的内存地址V找到资源对象的值,然后做一个对比,如果资源对象的值等于旧值A,那么将其更新为新值B。另外一个线程比较失败后会进入一个自旋的状态。
CAS为什么要原子性?如何保证CAS的原子性?
当然CAS比较交换的这个动作需要同步的才行,那怎么保证同步呢?加锁吗?当然不是CAS是无锁编程,加锁不是转着转着给转回去了吗?我们来看看,以AtomicInteger为例,他的CAS方法是调用的Unsafe类的方法,而这些方法都是native的,说明他依赖操作系统,而如今的CPU指令中恰好有CAS的指令,具有原子性。
2.5什么是ABA问题?
如果一个变量在初次读取的时候就是A值,并且在准备赋值的时候检查他仍然是A值,它可能在这段时间的值被改成了B,然后又改成了A。
锁优化都是为了解决线程之间更高效的共享数据以及解决竞争问题
锁也分不同状态,JDK6之前只有两个状态:无锁、有锁(重量级锁),而在JDK6之后对synchronized进行了优化,新增了两种状态,总共就是四个状态:无锁状态、偏向锁、轻量级锁、重量级锁,其中无锁就是一种状态了。锁的类型和状态在对象头Mark Word
中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word
数据。
每一个锁都对应一个monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的(C++实现)。每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个monitor被某个线程持有后,它便处于锁定状态。
monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因
ObjectMonitor中有两个队列_WaitSet和_EntryList,用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入_EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。
无锁:
首先资源不存在竞争是无锁的,或者以非锁的方式竞争(CAS),这个时候是无锁的状态。
偏向锁:
无锁并不能替代有锁的情况,偏向锁的意思就是锁会偏向于第一个获得他的线程,如在在接下来的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程将永不再进行同步,偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不用做了。
偏向锁的实现:先看对象的对象头的mark word里面的锁标志位是不是01,如果是,则看看是否有偏向,如有偏向,再去读mark word的前23个bit 这就是偏向的线程ID
轻量级锁
当有另外的线程想要获取这个锁的时候,偏向锁的模式宣告结束,会升级成为轻量级锁,
当锁为轻量级锁的时候,线程对象是如何与锁绑定的呢?当一个线程看到锁标志位为00的时候,就知道他是一个轻量级的锁,这时线程会在自己的虚拟机栈中建立一个Lock Record的空间,用于存放对象头部mark word的拷贝,线程使用CAS去尝试获取锁,一旦获取成功,则就会复制mark word的内容去Lock Record。并且将Lock Record中的owner指针指向该对象。对象mark word中前三十个bit将会生成一个指针指向线程虚拟栈中的Lock Record.
这个时候这个线程已经获取了这个对象的锁,如果此时另外的线程想要获取这个锁,这个时候他就会进入一个忙循环,也就是自旋等待的时候。自旋锁不会放弃处理器的执行时间
重量级锁
如果在自旋状态的线程超过一个,那么锁就会膨胀成重量级锁,那么就需要使用Monitor来对线程进行控制。此时将会完全锁定资源,对线程的管控也最为严格。