synchronized & volatile & CAS
2020-11-13 18:04:02 3 举报
登录查看完整内容
synchronized & volatile & CAS
作者其他创作
大纲/内容
Mark Word:保存对象的hashcode,分代年龄和锁标志信息00:轻量级锁01:偏向锁10:重量级锁Kclass Point:对象执行它类元数据的指针
lock record地址 00
1,java的内存模型
Thread-0
object body
对象头
Running
HashCode Age Bias 01
CAS
锁膨胀
1)每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word2)让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录3)如果 cas 替换成功,对象头中存储了锁记录地址和状态 00 ,表示由该线程给对象加锁
轻量级锁(000)
实例对象
Blocked
写会内存的操作会使其他cpu中缓存了该内存地址的数据设置为过期或失效
Object
Lock Record
执行完代码后执行monitorExit,进入数减一,为0时才会被其他线程持有
什么是CAS?
是当cpu将数据写到主内存哪一个时刻,会加汇编指定前面加lock,写完数据,则unlock
2. 字节码层面
HashCode Age Bias 00
Thread-1
_EntryList
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
Monitor 地址
Klass Word
偏向锁(101)
1,ABA,在变量前面追加版本号,每操作一次数据版本号加一,这样即使数据没变,它也有可能不是最初的那个版本。类似于数据库的乐观锁实现机制。--------------------------------------------------------------------------------------------------------------------2,多线程环境下如果对共享变量的竞争情况比较激烈,那么使用CAS可能会一直处于失败重试的状态,会给cpu带来不小的执行开销。这个时候就需要用lock来保证原子性。---------------------------------------------------------------------------------------------------------------------3,只能保证一个变量的原子操作,但是可以把多个变量合并成一个共享变量,比如i = 2 和 j = a可以合并成ij = 2a,然后用CAS来操作ij,利用AtomicReference可以保证引用对象之间的原子性,也就是可以把多个变量放在一个对象里来进行原子操作。
1)重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。2)自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。3)在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。4)Java 7 之后不能控制是否开启自旋功能
CAS是比较并交换的一种原子操作,也是实现乐观锁的一种机制。它有三个操作数内存值V,预期值A,要修改的值B。当且仅当内存值与预期值相同时也会才会将内存值修改为B,否则重新读取内存值循环上述过程。
对齐填充8字节背倍数
1)Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态2)BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片3)BLOCKED 线程会在 Owner 线程释放锁时唤醒4) WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争
4)如果 cas 失败,有两种情况如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数5)当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一6)当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头成功,则解锁成功失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
重量级锁(010)
null
诸葛程序员视频:https://www.bilibili.com/video/BV1dE411h7gV?p=3博客:https://blog.csdn.net/nmjhehe/article/details/109523279
利用volatile关键字可以保证线程对某个共享变量的修改对所有线程都是可见的。底层原理:用volatile变量的java代码在转化为汇编代码时会多出来一条lock前缀指令,它是cpu级别的指令,主要完成了以下两个工作。1,将当前处理器缓存中的数据刷新回主内存2,将其他cpu中保存了该变量内存地址的缓存置为无效。如果对声明为volatile的变量进行了写操作,jvm会向处理器发送一条lock前缀指令,将这个变量所在缓存行的数据写回到主内存,然后根据缓存一致性协议,每个处理器会通过在总线上传输的数据来确保自己缓存行的数据是否无效,当发现自己缓存行数据的内存地址被修改了,他就会把当前缓存行的数据设置为无效,下一次对对该变量操作时会从主内存中读取
JVM会向当前处理器发送一条lock前缀指令,将当前处理器缓存行的数据写回到系统内存
3. 锁升级(一般是不可逆的)
有volatile修饰的共享变量进行写操作,会多出一行lock前缀指令的汇编代码
CAS的问题?
问题就在于如果两个线程在主内存中获取到了同一个变量作为自己的变量副本,由于这两个线程对这个变量的操作都是基本自己本地的工作内存,最后在写会主内存的时候势必会有数据不一致的问题。
lock record地址 01
EntryList
Thread-4
自旋锁
WaitSet
一个对象创建时:1)如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的thread、epoch、age 都为 02)偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟3)如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值
多个cpu从主内存读取同一个数据到各自的高速缓冲区,当其中某cpu修改了缓存中的数据,该数据会马上同步回主内存,其他cpu通过总线嗅探机制可以感知到数据的变化而将自己缓存中的数据失效(这里有点像二级缓存中的通知失效机制)历史(最早是通过总线--性能太低)cpu从主内存读取数据到告诉缓冲区,会在总线对这个数据加锁,这样其他cpu就没有办法读和写数据了,直到这个cpu使用完成数据释放锁之后其他cpu才能读取该数据
Owner
1)当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁2)这时 Thread-1 加轻量级锁失败,进入锁膨胀流程即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址然后自己进入 Monitor 的 EntryList BLOCKED3)当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
3,该如何解决
_WaitSet
monitor
底层实现原理?
由缓存一致性协议(MESI)保证;
2,这套内存模型有什么问题?
lock 指令不锁总线,锁缓存
synchronized同步代码块
Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
由总线嗅探机制保证;
Monitor
实例数据
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
Thread-3
volatile
1)撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW)2)访问对象的 hashCode 也会撤销偏向锁3)如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID4)撤销偏向和重偏向都是批量进行的,以类为单位如果撤销偏向到达某个阈值,整个类的所有对象都会变为不可偏向的5)可以主动使用 -XX:-UseBiasedLocking 禁用偏向锁
synchronized原理:1. 每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
volatile可以保证线程可见性和有序性,但不能保证原子性
Thread-2持有monitor锁
无锁(001)
执行字节码指令monitorEnter获得当前对象的所有权,进入数加一
当代码块出现异常后,仍然可以释放锁
1,可以从并发包下的原子类AtomicInteger怎么实现线程安全的i++说起。它内部有一个volatile修饰的value。保证了value变量的修改对所有线程都是可见的。调用getAndIncrement()方法首先去获取这个value,然后调用compareAndSet方法,它里面又调用了unsafe的compareAntSwapInt()方法,它如何去保证比较并替换这个两个操作的原子性呢?因为方法是native修饰的,我们需要去看jdk源码。------------------------------------------------------------------------------------------------------2,native方法都是用c++来实现的,通过unsafe.cpp->atomic.cpp一路找下去的路径为openjdk\\hotspot\\src\\oscpu\\windowsx86\\vm\\ atomicwindowsx86.inline.hpp可以找到对应的Intel x86的处理器源代码发现底层调用的是atomic::cmpxchg()方法-------------------------------------------------------------------------------------------------------3,方法里面首先判断是否为cmpxchg这个指令添加lock前缀,如果是多处理器类型就需要加。lock前缀指令能够确保对内存数据的读写改操作是以原子方式执行的。它的实现原理是锁总线,使得其他处理器无法通过总线来获取内存数据。后来为了减小lock指令的执行开销,采用了锁缓存的一种方式,在指令执行期间,包含该变量的缓存行内存地址无法被其他处理器访问,就达到了原子操作的要求。--------------------------------------------------------------------------------------------------------4,lock前缀指令还能禁止该指令与之前之后读和写指令重排序。--------------------------------------------------------------------------------------------------------5,把缓冲区的数据刷新回主内存
Java其实有自己的一套内存模型,这是java虚拟机规范中定义好的。它套模型有以下三个特点。 1,cpu有自己的主内存,每个线程有自己的工作内存 2,工作内存中保存了从主内存获取的一个变量副本,每个线程不能直接访问其他线程的工作内存。 3,线程对于变量的操作都在自己的工作内存中完成。
Waiting
涉及用户态和内核态的转换,调用内核态里的park()和unpark()方法区加锁和解锁
0 条评论
回复 删除
下一页