i++的操作过程

getfield:把变量i从内存加载到cpu的寄存器
iadd: 把寄存器中执行+1操作
putfield:把结果保存到内存

两个线程执行i++的流程

1.线程1先获得cpu 执行权 在cpu将i=0加载到寄存器中后出现线程切换 cpu把执行权切换给线程2 并保留当前的cpu上下文
2.线程2同样去内存中将i加载到寄存器中进行计算 然后把计算结果写回内存
3.线程2释放cpu资源 线程1重新获得执行权后恢复cpu上下文 而这时的i=0
Dingtalk_20220419140515.jpg

使用

对方法加锁 当多个线程访问方法的时候只有一个线程能成功
锁的是一个对象 不是整个类 有可能这个类有多个实例

  1. public synchronized void m1() {}

类锁

当多个线程调用不同对象实例的同步方法时会产生互斥

  1. public static synchronized void m1() {}
  1. public void m1() {
  2. // UserServiceImpl.class 锁的是 UserServiceImpl一整个类
  3. synchronized (UserServiceImpl.class) {
  4. }
  5. }

对象锁(锁的是对象实例)

  1. public synchronized void m1() {}
  1. public void m1() {
  2. synchronized (this) {
  3. }

Java对象存储结构

1.对象头

Dingtalk_20220419141035.jpg

1.Mark Word(存储锁标记 分带年龄 hashCode等)

2.Klass Pointer(指向方法区的Class信息的指针 表示当前对象是哪个Class实例)

3.Length(表示数组长度 只有对象数组才会有这个属性)

Dingtalk_20220419141513.jpg
Dingtalk_20211026141234.jpg
Dingtalk_20211026141242.jpg

2.对其填充(保证对象的大小是8字节的整数倍)

cpu 访问内存读取数据时 并不是按照逐个字节来访问的 而是以字长为单位访问的 简单来说 字长是指cpu一次能够并行处理的二进制位数 字长总是8字节的整数倍
对其填充使得虽然无效的数据占据了空间 但是减少了访问次数 典型的空间换时间

3.实例数据(对象的所有成员变量)

锁升级的过程

Dingtalk_20220419141654.jpg

过程

线程A先会去尝试判断偏向锁的标记是否为0 如果为0表示没有被获取 如果为1 表示被获取了然后会判断是否是自己获取的 如果线程id 等于自己的线程id 则直接拿到偏向锁
线程B此时会尝试获取偏向锁 发现已经被线程A获取了 抢线程的时候使用cas的方式将LR的指针更新到markword里面,成功刷进去了则表示抢到了锁 ,没抢到的会一直在那自旋 。在老版本中会默认cas 自旋10次如果还抢不到升级成重量级锁 进行阻塞,jdk1.6之后引入自适应自旋 会按照前一次这个锁的自旋次数和拥有者的情况来决定这次要自旋多少次 如果这个锁对象经常获得锁成功则会多自旋几次 反之则少自旋 当到达一定程度之后依然没有获得锁 则升级成重量级锁调用 jvm park 方法进行阻塞等待

无锁

偏向锁(01 表示无锁 00轻量级锁)

线程访问 synchronized 修改的访问时会先判断对象头中的偏向锁指针是否已经被持有 这个过程会通过CAS完成 如果抢占成功则直接修改对象头中是否偏向锁 改成00
比如A线程访问 发现偏向锁的线程ID就是自己就不需要去抢占锁 线程B抢占的时候发现已经被线程A抢占到了 则升级轻量级锁

锁撤销

访问到同一个对象锁会撤销偏向锁
调用撤销偏向锁的方法 尝试撤销lock 锁对象的偏向锁
当到达全局安全点才会执行(当前线程运行到的位置) 会暂停获得偏向锁的线程
把锁对象lock升级成轻量级锁 并指向
当锁撤销达到20次会触发重偏向把锁对象偏向线程2

轻量级锁

当没开启偏向锁会采用轻量级锁来抢占锁资源
当线程抢占偏向锁不成功会升级成轻量级锁 默认自旋10次
jdk1.6之后引入了自适应自旋 自适应自旋,旋的次数不固定 是根据上一次在同一个锁上的自旋次数以及锁的持有者的状态来决定的 如果在同一个锁对象上通过自旋等待成功获得过锁 并且持有锁的线程正在运行中 那么jvm会认为这次自旋很有可能会抢占到锁 因此自旋 时间延长 反之如果一个锁对象通过自旋锁很少成功 那么jvm就会缩短自旋次数

获取轻量级锁原理

1.当线程进入同步代码块之后 jvm会给当前线程分批一个LockRecord 也就是一个BasicObjectLock对象 在它的成员对象BasicLock 中有一个成员属性markOop_displaced_header这个属性专门用来保存锁对象lock的原始MarkWord
2.构建一个无锁状态的MarkWord 把这个MarkWord设置到LockRecord中的 _displaced_header字段中
3.通过CAS将lock锁对象的MarkWord 替换为指向LockRecord的指针 如果替换成功表示获取轻量级锁成功
4.如果失败表示当前lock对象不是无锁状态 会触发锁膨胀 升级到重量级锁

轻量级锁的释放

用CAS把Lock Record中 _displaced_header 存储的lock锁对象的Mark Word 替换到lock 锁对象的Mark Word中
如果成功 轻量级锁释放完成
失败膨胀 完成膨胀之后会调用重量级锁的释放方法 释放锁

重量级锁

轻量级锁通过一定次数的自旋仍然获取不到锁则升级成重量级锁 阻塞等待

实现过程

在获取重量级锁之前会先实现锁膨胀 在锁膨胀的方法中首先创建一个ObjectMonitor对象 然后把ObjectMonitor对象的指针保存到锁对象的MarkWord 中

锁膨胀的情况

当前已经是重量级锁 不需要膨胀
如果有其他线程正在进行锁膨胀那么通过自旋的方式不断重试直到其他线程完成锁膨胀、
如果当前有其他线程获得轻量级锁 那么当前线程会完成锁的膨胀
如果当前是无锁状态 也就是说之前获得锁资源的线程正好释放了锁 那么当前线程完成锁膨胀

_owner:保存当前只有锁的线程
_object 保存锁对象的指针
_xcq 存储没有获得锁的线程的队列 是一个列表结构
_WaitSet 当调用 Object.wait()方法阻塞时 被阻塞的线程会保存到该队列中
_recursions 记录重入次数

获取锁流程

判断当前线程是否是重入 如果是重入增加次数
如果通过自旋竞争锁失败则会把当前下次讷航构建成一个Object.wait节点插入链表的头部在使用park阻塞

释放过程

同步代码块结束之后 把 ObjectMonitor中持有锁的对象的owner设置为null
从_cxq队列中唤醒一个处于阻塞锁状态的线程
被唤醒的线程重新竞争重量级锁 sync是非公平锁因此唤醒不一定能抢到

CAS(Compare And Swap)相当于乐观锁

o:当前的实例对象
offset:表示当前实例对象的内存地址偏移量
expect:表示预期值
update:表示要更新的值
cas 操作会传递三个参数 第一个表示内存地址 第二个参数表示预期值 第三个表示要更新的值
当第一个和第二个值相等的时候会把要更新的值更新否则不更新

  1. protected final boolean compareAndSetState(int expect, int update) {
  2. // See below for intrinsics setup to support this
  3. return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
  4. }

内核态和用户态

运行在用户空间的进程不能直接访问操作系统 内核的指令和程序 运行在内核空间的程序可以直接访问系统内核的数据和程序

死锁

互斥条件:共享资源X和Y只能被一个线程占用
请求和保持条件 线程T1 已经取得共享资源X 在等待共享资源Y的时候 不释放共享资源X
不可抢占条件: 其他线程不能强行抢占线程T1占有的资源
循环条件等待: 线程T1等待线程T2占有的资源 线程T2等待线程T1占有的资源