Java锁机制学习记录

277 阅读1分钟

学习自寒食君 www.bilibili.com/video/BV1xT…

blog.csdn.net/qicha3705/a… (牛逼)

什么是锁?

这个都会吧

JVM 运行时内存结构

image.png

红色部分是线程私有的,也是安全的,蓝色部分则是线程共享的,java堆存放对象,方法区存放类信息,常量,变量等,需要人工同步

Java 对象头结构

每一个object对象都有一把锁,锁信息是存放在对象头信息中的,记录当前对象被哪个线程占有

对象包括三部分:对象头、实例数据、填充字节

填充字节

他是为了满足Java 原则——每个对象都必须是8bit的倍数,填充的无用字节

对象大小=8 byte * n(hotSpot VM)

对象大小=8 bit * n( JVM)

实例数据

初始化对象时设置的属性、状态等信息

对象头(本次重点

  • 他存放了一些对象本身运行的信息

  • 包括Mark Word和Class Point

  • 它属于是对象额外的存储开销,因此被设计的极小来提高效率

  • Class Point是一个指针,他指向了对象在 方法区 对应的 类元数据 的内存地址 (8字节 64位系统)

  • Mark Word 存储了很多和当前对象运行时相关的数据信息,重中之重 (8字节 64位系统)

image.png

synchronized

synchronized 经过编译后会生成两个字节码指令 monitorenter和monitorexit

写个demo验证下


public class TestSynchronized {

private int num = 0;

public void test(){

for (int i = 0;i < 1000;i ++){

synchronized (this){

System.out.println("thread: " + Thread.currentThread().getId() + " ,num:" + num);

num ++;

}

}

}

}

javac TestSynchronized.java 编译为class文件

javap -c TestSynchronized.class 反编译

image.png

synchronized 的锁机制。

简单了解下monitor?

它被叫做管程或者监视器,可以理解一个monitor就是一个房间,很多线程相当于很多客人,只有一个线程进入后离开,下一个才能进入

image.png

  • 1.Entry Set中有很多想要进入monitor的线程,他们正处于Waiting状态

  • 2.假如线程A成功进入monitor,那么他就处于Active状态,假设此时A的逻辑需要暂时让出执行权,那么他会进入Wait set,状态变为Waiting,

  • 3.此时Entry set中的其他线程就有机会进入monitor,假设线程B进入,并且完成任务,线程B可以通过notify的形式唤醒Wait set中线程A(不一定是A),让A继续进入monitor来完成任务

  • 执行完后,A、B exit退出

synchronized 的缺陷

synchronized可能存在性能问题?(我记得jvm优化了?)

  • synchronized 依赖 monitor ,monitor 依赖 操作系统的 mutex lock 实现的

  • Java 线程实际上是对操作系统线程的映射,所以每当挂起或者唤醒一个线程,都要切换操作系统的内核态,这种操作比较重量级,有些时候,切换线程的时间会超过程序执行时间,这样使用synchronized会对服务性能有很大影响。

  • 从java 6开始,对synchronized进行了优化,引入了偏向锁、轻量级锁,

  • 所以从重量级从低到高:无锁,偏向锁、轻量级锁,重量级锁,这就对应了Mark Word中的四种状态,

  • 锁只能升级,不能降级

锁的四种状态

无锁

  • 情况1:无竞争

  • 情况2:存在竞争,非锁方式同步信息,比如说某一个对象,我同一时间只允许一个线程修改成功,其他的线程不断循环重试,这就是CAS

偏向锁

  • 首先是为何需要偏向锁?

  • 如果只有1个线程竞争对象锁,那么我们最理想的方式,就是不通过线程切换,也并不用通过cas来获取锁,因为CAS也需要耗费一些资源,最好对象锁能认识这个线程,只要这个线程过来,对象就把锁交出去。

  • 如何实现偏向锁?

  • 在Mark Word中,当锁标志位是否是01,如果是判断倒数第三位是否是1,如果是,就是偏向锁,再去读Mark Word前23个bit,也就是线程id,如果是,直接进入

  • 如果不是申请锁的id,也就是对象发现不止有一个线程,而是有多个线程想竞争锁时,那么会升级为轻量级锁

轻量级锁

  • 如何实现轻量级锁?

  • 将Mark Word前30个bit 记录指向栈中锁记录的指针

    1. 当一个线程想要竞争轻量级锁时,假如看到锁标志位为00,就知道他是轻量级锁,这时线程会在自己的JVM虚拟机栈中开辟一块空间(Lock Record,他存放的是对象头中Mark Word副本 以及owner指针)
  • 线程通过cas去尝试获取锁,一旦获得,就会复制该对象头Mark Word 到自己的Lock Record中,并且将自己Lock Record中的owner指针指向该对象,

  • 另一方面,该对象中的Mark Word前30个bit将会生成一个指针,指向线程虚拟机栈中的Lock Record,这样就实现了对象和线程的绑定,他们就互相知道了对方的存在;

  • 这时,该对象已经被线程锁定了,获取了这个对象的线程就可以执行任务了,此时其他线程将会自旋等待(先适应性自旋,尝试获取锁。到达临界值后,阻塞该线程,直到被唤醒),其他线程一直循环查看该对象的锁有没有释放(也就是该对象头的Mark Word前30个bit?),

  • 适应性自旋

  • 自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

如何查看对象头信息?


<!--查看对象头工具-->

<dependency>

<groupId>org.openjdk.jol</groupId>

<artifactId>jol-core</artifactId>

<version>0.9</version>

</dependency>

<dependency>

<groupId>org.openjdk.jol</groupId>

<artifactId>jol-core</artifactId>

<version>0.16</version>

</dependency>

public static void main(String[] args) {

Object dog = new Object();

System.out.println("初始信息:"+ ClassLayout.parseInstance(dog).toPrintable());

System.out.println(dog.hashCode());

System.out.println("hashcode信息:"+ClassLayout.parseInstance(dog).toPrintable());

synchronized (dog) {

System.out.println("加锁后信息:"+ClassLayout.parseInstance(dog).toPrintable());

}

System.out.println("释放锁后信息:"+ClassLayout.parseInstance(dog).toPrintable());

}