JUC-synchorized与锁原理、锁的升级与膨胀

avatar
作者
筋斗云
阅读量:0

syn-ed

是一个可重入、不公平的重量级锁synchronized使用对象锁保证了临界区代码的原子性,无论使用synchorized锁的是代码块还是方法,其本质都是锁住一个对象。

  • 同步代码块,锁住的是括号里的对象
  • 同步方法
    • 普通方法,锁住的是当前实例对象,即this
    • 静态方法,锁住的是当前类对象,即class对象
// 同步代码块 synchorized(锁对象) { }  // 普通方法 class Test{     public synchorized test() {} } // 等价于 class Test{     public void test() {         synchronized(this) {}     } }  // 静态方法 class Test{ 	public synchronized static void test() {} } // 等价于 class Test{     public void test() {         synchronized(Test.class) {} 	} } 

变量线程安全性

  • 成员变量和静态变量
    • 如果没有被共享,那么一定是线程安全的
    • 如果被共享:
      • 只有读操作,一定是线程安全的
      • 有读写操作,存在并发问题
  • 局部变量
    • 局部遍历是线程安全的
    • 但局部变量引用的对象则不一定
      • 如果该对象没有逃离方法的作用访问,它是线程安全的
      • 如果该对象逃离方法的作用范围,存在并发问题

常见的线程安全类

  • String
  • Integer等包装类
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类
    String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的,多个线程调用它们同一个实例的某个方法时,是线程安全的。

锁原理

Monitor

监视器或管程,每一个java对象都可以关联一个Monitor对象,使用synchronized给一个对象加锁(重量级锁),该对象的对象中的Mark Word就被设置指向一个Monitor对象的指针,这其实也就是重量级锁的加锁过程。

Mark Word

java对象的对象头由Mark Word、类型指针、数组长度(如果该对象是一个数组)组成。Mark Word的长度由32bit/64bit。Mark Word里默认存储对象的HashCode、分代年龄和锁标记位

  • 32位的Mark Word
    ![[Pasted image 20240719211851.png]]

  • 64位的Mark Word:
    ![[Pasted image 20240719211811.png]]

Monitor的工作流程

一个对象对应一个Monitor对象。

  • 开始时Monitor中Owner为null
  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner,obj对象的Mark Word指向Monitor,把对象原有的Mark Word存入线程栈中的锁记录
    ![[Pasted image 20240719212720.png]]
  • 在 Thread-2 上锁的过程,Thread-3、Thread-4、Thread-5 也执行 synchronized(obj),就会进入 EntryList BLOCKED(双向链表)
  • Thread-2 执行完同步代码块的内容,根据 obj 对象头中 Monitor 地址寻找,设置 Owner 为空,把线程栈的锁记录中的对象头的值设置回 MarkWord
  • 唤醒 EntryList 中等待的线程来竞争锁,竞争是非公平的,如果这时有新的线程想要获取锁,可能直接就抢占到了,阻塞队列的线程就会继续阻塞
  • WaitSet 中的 Thread-0,是以前获得过锁,但条件不满足进入 WAITING 状态的线程(wait-notify 机制)
    在这里插入图片描述

锁升级

随着竞争的增加,只能锁升级,不能降级

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁	 

偏向锁

在大多数情况下,锁总是由一个线程多次获得,让线程获得锁的代价更低而引入了偏向锁。就和名字一样,是偏向的:

  • 当线程第一次获得锁对象时,其进入偏向状态,该对象的后三位是101,同时使用CAS操作将线程ID记录到Mark Word。如果CAS操作成功,这个线程以后进入这个锁相关的同步块,查看这个线程ID是自己的就表示没有竞争,就不需要再进行任何同步操作。
  • 当另一个线程也尝试获取这个锁对象时,也会使用CAS进行替换,此时一定失败,偏向状态就会结束,撤销偏向后恢复到未锁定或轻量级锁状态。

在java中是默认开启偏向锁的,也就是说在一个对象创建的时候,其Mark Word的后三位是101,其余值均为0;当调用这个对象的hashCode时,就再也无法进入偏向状态了,即后三位是001。这是因为Mark Word会被hashCode占用。

偏向锁的撤销

  • 第一点就是我们前边提过的,调用该对象的hashcode方法。
  • 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
  • 调用 wait/notify,需要申请 Monitor,进入 WaitSet

轻量级锁

一个对象有多个线程要加锁,但加锁的时间是错开的(没有竞争),轻量级锁的使用对我们程序员来说,也是使用syn-ed来完成,只是底层的实现对我们透明的。

加锁过程
当某一个线程对一个对象进行加锁时,会在栈帧中创建一个锁记录(Lock Record),并使用==CAS将对象的Mark Word中的信息保存到该锁记录中,而Mark Word中就记录该锁记录的地址,以及锁标志位(00)。==这样就完成了加锁。但是当CAS失败的时候,此时会有两种情况导致失败:

  • 它线程已经持有了该Object的轻量级锁,我又想去获取,这时表明有竞争,进入锁膨胀过程在膨胀之前还有一个自旋的过程
  • 线程自己执行synchronized锁重入,栈帧中还会存在一条Lock Record作为重入的计数,但是每次重入都会有CAS,所以才引入了偏向锁进行优化

解锁过程(当退出synchronized代码块)

  • 如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减 1
  • 如果锁记录的值不为null,这时使用CAS将 Mark Word的值恢复给对象头
    • 成功,则解锁成功
    • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。

重量级锁

在尝试加轻量级锁的过程中,CAS操作无法成功,可能是其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

  • 当线程1使用CAS对一个对象obj进行加锁时,发现线程1已经持有该轻量锁,此时就会失败并导致锁膨胀
  • 然后就会为obj对象申请Monitor锁,通过obj对象头获取到持锁线程,将Monitor的 Owner置为线程0,将obj的对象头指向重量级锁地址,然后自己进入Monitor的EntryList而BLOCKED。
  • 当线程0释放锁时,使用CAS将Mark Word的值恢复给对象头失败,这时进入重量级解锁流程,即按照 Monitor地址找到Monitor对象,设置Owner为 null,唤醒EntryList中BLOCKED线程
    ![[Pasted image 20240719223237.png]]

![[Pasted image 20240719222445.png]]

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!