java锁升级简述
概念图
流程简述
java 创建出来对象以后先是普通对象,此时没有任何锁,之后需要加锁的时候会先为其添加偏向锁,当竞争激烈一些的时候,会为其添加轻量级锁,如果竞争更加激烈了,会为其添加重量级锁。
CAS
先考虑这样一种情况,当前线程要对内存中的一个值进行修改:比如有一个变量x=0,要对其进行加一操作,线程一般如此操作,先进行读取,进行加一,x1=1,比较此时内存中x的是否为0,如果为0,进行写入x=1,否则重新读取。重新读取,就发生在多个线程同时对x进行修改的情况下。
(至于ABA的问题则是,对x进行写入的时候,验证到x=0,但是我们不知道,这个x是否经历过类似于0->1->0这样的过程)
java对象的内存布局
借助工具:openjdk 我们可以查看到对象在内存中的布局情况。
在64bit JVM中:
对象头分为两部分:
1)Mark Word
2)Class Pointer
java对象在内存中布局分为三块区域:
1)对象头 :固定12字节
2)实例数据:不固定
3)数据对齐:填充数据,保证字节数为8的整数倍
池化思想
以线程池为例子,我们不可能为每一个线程都分配到资源,就需要引入池化的思想。
比如:现在允许最大线程数是5个,<=5个线程来了可以顺利执行,当第六个线程来了以后,则进入等待队列,当上一个线程执行结束以后,才可以执行,当然等待队列也是有大小的,当等待线程数超过等待队列以后则会失败。同时我们应该注意到,第6个线程和第7个线程都处于等待队列时,当允许下一个线程执行时,谁先执行取决于操作系统的调度策略是否为抢占式。
Monitors(管程)
synchronized在JVM中实现是通过Monitor实现的
1)作用: 保证临界资源访问形成互斥,从而确保数据的安全性
2)原理:monitorenter、monitorexit
可重入设计
比如:带锁的A方法和B方法,当A方法访问B方法,B方法也能获得该对象的锁。
每把锁关联一个请求的计数器和一个占有当前锁的线程,如果计数器为0表示该锁未被占有,当线程访问得到该锁时,将占有线程赋值为当前线程,计数器加一,当某个线程再去访问,程序判断是否为当前线程,如果是,就能得到该锁,同时计数器加一,然后释放该锁时,计数器减一,直到为0,就释放锁
monitorenter
指向同步代码块开始位置的指针,当线程获取锁时,计数器加一,同时线程获取这个monitor,能不呢获取到这把锁的前提要么计数器为0,要么当count>0时,必须同一个线程访问,才能拿到锁,也就意味着线程访问临界资源时必须互斥
monitorexit
指向同步代码块退出的位置的指针,释放锁过程就是将计数器减一,同时判断count是否为0,如果为0,那么就设置锁标记为释放状态,然后调用object的notify方法,唤起其他将要获取锁的线程。
为什么会有两个monitorexit ?
第二个monitorexit 是为了防止,第一个释放过程中出现异常,以确保必定释放。
偏向锁
获取偏向锁的三个条件:
1)JVM开启了偏向锁
2)判断锁状态是01
3)当前线程是否等于Mark Word里面存储的Thread ID
偏向锁优势
在于消耗很少的资源去获取锁
缺点
释放锁消耗的资源会比较大
偏向锁是否一定高效率?
不一定
在明确知道多个线程强烈竞争的时候,系统会把资源大量消耗在撤销上。
轻量级锁(自旋锁)
在线程的竞争比较多的情况下会升级到轻量级锁
每个线程在线程栈中生成LockRecord,用CAS方式尝试把自己的指针更新到markword
每一次锁重入,都会有一个LockRecord
轻量级锁优势
竞争比较多情况下,竞争的线程不会阻塞(存在自旋情况),提高程序并发能力
缺点
获取不到锁时,会不停自旋,会大量消好CPU性能
重量级锁
竞争非常激烈时,会升级到重量级锁,它不是自旋,而是获取不到锁的线程全部陷入等待
自旋是要占用CPU的,线程数量过多的时候用自旋不合适
(OLD)10次,PreSpinLock ,cpu/2
Adaptive CAS
优势
等待线程会消耗很少的CPU
缺点
并发性较低
用户空间锁vs重量级锁
偏向锁,自旋锁都是用户空间完成
重量级锁是需要向内核申请