参考自寒食君:
B站地址:https://www.bilibili.com/video/BV1xT4y1A7kA
锁机制
什么是锁?
并发情况下,多个线程可能会对统一资源产生争抢,那么可能导致数据不一致问题。
为了解决这个问题引入了锁机制,通过一种抽象的锁来对资源进行锁定。
锁机制是怎么设计的?
线程共享的堆和方法区当多个线程同时对其进行修改的时候有可能会有难以预料的情况发生
因此需要锁机制对其进行限制。
在java中,每个Object都有一把锁,存放在对象头中,锁中记录了当前对象被哪个线程占用
对象、对象头的结构?
对象包含三个部分,对象头、实例数据、对齐填充数据
对齐填充字节是为了满足Java对象大小必须满足是8字节的倍数这一条件设计的。为了对象而填充一些无用字节
实例数据就是在初始化数据时设定的属性和状态的内容
对象头,存放了一些对象本身的运行时信息
包含两部分 : Mark Word , Class Pointer
相较于实例数据,对象头属于一些额外的存储开销,所以它被设计得极小来提高效率
Class Pointer就是一个指针,指向了当前对象类型所在方法区中的类型数据
Mark Word存储了很多和当前对象运行时状态有关的数据
通过这张表可以看到,MarkWord只有32bit,包含着hashcode、锁标志位、指向锁的指针等等信息
上面提到的对象都有一把锁,这把锁的信息就存放在对象头的MarkWord中
无锁、偏向锁、轻量级锁、重量级锁
Java中的synchronized关键字用来同步线程,synchronized被编译后会生成monitor enter和monitor exit
这两个字节码指令,依赖这两个字节码指令来进行线程同步。
通过反编译可以看到monitorenter和monitorexit包裹了业务代码
monitor是什么?
monitor:管程/监视器
当有一个线程进入了monitor,那么其他线程必须等待,只有当这个线程退出,其他线程才有机会进入
如上图,Entry Set中聚集了一些想要进入monitor的线程,它们处于waiting状态,
假如某个线程成功进入了monitor那么它处于Active状态,
这时它遇到了一个判断条件需要它让出执行权,那么它将进入Wait Set,状态标记为Waiting
此时entry set中的其他线程就有机会进入monitor,假设这时另外一个线程进入Monitor并完成任务
它可以使用notify来唤醒wait set中的线程,使其进入entry set,并不会唤醒线程。
为什么用Set ,因为它无序不可重复!
Java6开始,synchronized进行了优化,引入了偏向锁、轻量级锁、重量级锁
无锁,没有对资源进行锁定,所有线程都能访问到统一资源
有两种情况:无竞争:没有多线程的情况或者存在多线程但也不会出现竞争的情况
存在竞争:存在竞争但是不想对资源进行锁定,不过还是想通过一些机制来控制多线程
假如有多个线程想要修改同一个值,我们不通过锁定资源的方式,而是通过其他方式来限制,(版本控制)
同时只有一个线程能够修改成功,而其他修改失败的线程将会不断重试直到修改成功,这就是CAS
CAS在操作系统中通过一条指令来实现,所以可以保证其原子性
偏向锁,假如一个对象加锁了,但是在实际运行时只有一个线程会获取这个对象锁,
只要这个线程过来,对象就将锁交出去,我们就可以认为这个对象偏爱这个线程,这就是偏向锁。
偏向锁如何实现的?
在上图的MarkWord中,在最后两bit中是01时,判断倒数第三位是不是1,是的话表示当前对象的锁状态是偏向锁
如果当前锁的状态是偏向锁,于是再去读MarkWord的前23bit的值,这23bit的值就是线程的id,
拿到id和当前想要获取锁的线程是不是老顾客,
假如对象发现有多个线程正在竞争锁,那么偏向锁将会升级成轻量级锁。
如果有一个线程拿到对象的轻量级锁,其他线程就会自旋等待,如果自旋等待的线程超过十个
那么轻量级锁会升级为重量级锁。
什么是自旋?我理解为一种轮询,线程自己在不断的循环尝试着去看一下对象的锁有没有释放,
如果释放了那么获取,如果没有释放那就进行下一次的循环
自旋相当于CPU在空转,如果长时间自旋会浪费CPU资源,于是出现了一种“适应化自旋”的优化
就是自旋的时间不再固定,而是由上一次,在同一个锁上的自旋时间以及锁状态来决定
举个例子,上个线程自旋等待成功,虚拟机就会认为当前自旋也会成功,进而允许它更长的等待时间。
这是适应化自旋。
当锁标志位是00,知道这是一个轻量级锁,这时线程会在自己的虚拟机栈开辟一块名为Lock Record的空间
是线程私有的,Lock Record存放的是对象头中MarkWord的副本以及owner指针
线程通过CAS尝试获得锁,一旦获取将会复制对象头中的MarkWord到Lock Record中
并且将Lock Record中的owner指针指向该对象
另一方面,对象的Mark Word的前30个bit将会生成一个指针,指向虚拟机栈中的LockRecord,
这样就实现了线程和对象的绑定,它们就互相知道了对方的存在