synchronized底层原理、偏向锁、轻量级锁、自旋锁详解

文章目录

  • 1.Java对象头
  • 2.Monitor工作原理
  • 3.synchronized原理
  • 4.synchronized原理进阶
  • 锁演变
  • 5.轻量级锁
  • 6.锁膨胀
  • 7.自旋优化
  • 8.偏向锁
    • 偏向锁状态
    • 偏向锁撤销
    • 批量重偏向
    • 批量撤销偏向锁
  • 9.锁消除
  • 10.锁粗化
  • 11.常见的锁类别(死锁,活锁,饿死)

1.Java对象头

对象头包含两部分:运行时元数据(Mark Word)和类型指针 (Klass Word)

Mark Word
Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致。

类型指针
类型指针指向对象的类元数据,虚拟机通过这个指针确定该对象是哪个类的实例。

以32位虚拟机为例:
synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第1张图片
synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第2张图片
synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第3张图片

  • HashCode:保存对象的哈希码
  • age:Java GC标记位对象年龄,4位的表示范围为0-15,因此对象经过了15次垃圾回收后如果还存在,则肯定会移动到老年代中。
  • biased_lock: 对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁
  • lock:锁状态标记位,该标记的值不同,整个mark word表示的含义不同。
  • lock和biased_lock共同表示对象处于什么锁状态。

不同的锁状态,存储着不同的数据:
synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第4张图片

  • thread:持有偏向锁的线程ID。
  • epoch:偏向时间戳。
  • ptr_to_lock_record:指向栈中锁记录的指针。
  • ptr_to_heavyweight_monitor:指向管程Monitor的指针。

2.Monitor工作原理

synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第5张图片
synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第6张图片

3.synchronized原理

synchronized修饰代码块

当我们用synchronized修饰代码块时字节码层面上是通过monitorenter和monitorexit指令来实现的锁的获取与释放动作。

代码举例:

/**
 * 当我们使用synchronized关键字来修饰代码块时,
 * 字节码层面上是通过monitorenter和monitorexit指令来实现的锁的获取与释放动作。
 * monitorenter跟monitorexit 是一对多的关系
 *
 * 当线程进入到monitorenter指令后,线程将会持有Monitor对象,执行monitorexit指令,
 * 线程将会释放Monitor对象
 */
@Slf4j
public class Test {

    static int counter = 0;
    static final Object lock = new Object();

    public static void main(String[] args)  {

        synchronized (lock) {
            counter++;
        }

    }
}

对应的字节码:
synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第7张图片
synchronized修饰方法

对于synchronized关键字修饰方法说,并没有出现monitorenter与monitorexit指令,而是出现一个ACC_SYNCHRONIZED标志。

/**
 * 对于synchronized关键字修饰方法说,并没有出现monitorenter与monitorexit指令,而是出现一个ACC_SYNCHRONIZED标志。
 * 

* JVM使用了ACC_SYNCHRONIZED访问标志来区分一个方法是否为同步方法;当方法被调用时,调用指令会检查该方法是否拥有ACC——SYNCHRONIZED标志 * 如果有,那么执行线程将会先持有方法所在对象的Monitor对象,然后再去执行方法体;在改方法执行期间,其他任何线程均无法在获取到这个 * monitor,当线程执行完该方法后,它会释放掉这个Monitor对象。 */ public class MyTest2 { public synchronized void method() { System.out.println("hello world"); } } /* 0 getstatic #2 3 ldc #3 5 invokevirtual #4 8 return */

synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第8张图片
此时就没有通过monitorebter和moniterexit 来获取锁而是通过ACC_SYNCHRONIZED标识符来尝试获取锁

synchronized修饰静态方法

/**
 * 当synchronized修饰静态方法其实跟修饰成员方法一样 只不过方法标识符多了个ACC_STATIC
 * 其次锁的是 类锁
 */
public class MyTest3 {
    /**
     * static静态方法不存在this局部变量
     * 原因直接类名.就能调用
     */
    public static synchronized void method() {
        System.out.println("hello world!");
    }

}
/*
0 getstatic #2 
3 ldc #3 
5 invokevirtual #4 
8 return

 */

synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第9张图片

4.synchronized原理进阶

synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第10张图片
synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第11张图片

从JDK1.6开始,synchronized锁的实现发生了很大的变化;JVM引入了相应的优化手段来提升synchronized锁的性能,这种提升涉及到偏向锁,轻量级锁及重量级锁等,从而减少锁的竞争所带来的的用户态(如程序执行业务代码在用户端)与内核态(Monitor是依赖于底层操作系统 此时阻塞就是内核执行)之间的切换;这种锁的优化实际上是通过Java对象头中的一些标志位来实现的;对应锁的访问与改变,实际上都与Java对象头息息相关。

锁演变

对于synchronized锁来说,锁的升级主要都是通过Mark Word中的锁标志位与是否偏向锁标志位来达成的;synchronized关键字所对应的锁都是先从偏向锁开始的, 随着锁的竞争不断升级,逐步演化至轻量级锁,最后则变成了重量级锁。

对于锁的演化来说,它会经历如下阶段:

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

无锁:当前对象没有线程访问

偏向锁
只针对单个线程同步: 针对于一个线程来说的,它的主要作用就是优化同一个线程多次获取一个锁的情况;如果一个synchronized方法被一个线程访问,那么这个方法所在的对象 就会在其Mark word中的将偏向锁进行标记,同时还会有一个字段来存储该线程的ID;当这个线程再次访问同一个对象锁的synchronized方法时,它会检查这个对象 的Mark Word的偏向锁标记以及是否指向了其线程ID,如果是的话,那么该线程就无需再去进行管程(Monitor)了,而是直接进入到该方法体中。

如果是另一个线程访问这个synchronized方法,那么实际情况会如何了?
偏向锁会被取消掉

轻量级锁
多个线程同步,若第一个线程已经获取到了当前对象的锁,这时第二个线程又开始尝试争抢该对象的锁,由于该对象的锁已经被第一个线程获取到,因此它是偏向锁, 而第二个线程在争抢时,会发现改对象的对象头中的Mark Word已经是偏向锁了,但里面存储的线程ID并不是自己(第一个线程),那么会进行CAS(Compare and Swap),从而获取到锁这里面存在两种情况:

  • 获取锁成功:那么它会直接将Mark Word中的线程ID由第一个线程变成自己(偏向锁标记位保持不变),这样该对象依然会保持偏向锁的状态。
  • 获取锁失败:则表示这时可能会有多个线程同时在尝试争抢该对象的锁,那么这时偏向锁就会进行升级,升级为轻量级锁。

自旋锁
若自旋失败(依然无法获取到锁),那么锁就会转化为重量级锁,在这种情况下,无法获取到锁的线程都会进入到Monitor(即内核态)
自旋最大的一个特点就是避免了线程冲用户态进入带内核态。

重量级锁
线程最终从用户态进入到内核态.

5.轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞 争),那么可以使用轻量级锁来优化。

轻量级锁对使用者是透明的,即语法仍然是synchronized

假设有两个方法同步块,利用同一个对象加锁

static final Object obj = new Object();
public static void method1() {
     synchronized( obj ) {
         // 同步块 A
         method2();
     }
}
public static void method2() {
     synchronized( obj ) {
         // 同步块 B
     }
}

创建锁记录 (Lock Record) 对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对 象的 Mark Word
synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第12张图片
让锁记录中Object reference指向锁对象,并尝试用cas替换Object的 Mark Word, 将 Mark Word的值存入 锁记录
synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第13张图片
如果cas替换成功,对象头中存储了锁记录地址和状态 00,表示由该线程给对象加锁,这时图示如下
synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第14张图片
如果cas失败,有两种情况
如果是其它线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程 。
如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record作为重入的计数。
synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第15张图片

当退出 synchronized 代码块(解锁时)如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示 重入计数减一
synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第16张图片
当退出 synchronized 代码块(解锁时)锁记录的值不为null, 这时使用cas将 Mark Word的值恢复给对象 头。
成功则解锁成功。
失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。

6.锁膨胀

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

@Slf4j
public class Test {
    static int counter = 0;
    static final Object lock = new Object();

    public static void main(String[] args)  {

        synchronized (lock) {
            counter++;
        }

    }
}

当 Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁
synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第17张图片
这时Thread-1 加轻量级锁失败,进入锁膨胀流程
即为Object对象申请Monitor锁,让 Object指向重量级锁地址
然后自己进入Monitor 的 EntryList BLOCKED

synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第18张图片
当 Thread-0 退出同步块解锁时,使用cas将 Mark Word 的值恢复给对象头,失败.这时会进入重量级解锁 流程,即按照Monitor 地址找到Monitor对象,设置Owner 为 null, 唤醒 EntryList 中 BLOCKED线程

7.自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了 同步块,释放了锁),这时当前线程就可以避免阻塞。

不会马上让当前线程进入等待状态,而是进行自旋操作,如果自旋成功,就不需要进行上下文切换,优化了性能

自旋重试成功的情况
synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第19张图片
自旋重试失败的情况
synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第20张图片

  • 在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性 会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。
  • Java7之后不能控制是否开启自旋功能

8.偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作。

Java6 中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的 Mark Word 头,之后 发现这个线程ID是自己的就表示没有竞争,不用重新 CAS。 以后只要不发生竞争,这个对象就归该线程所有

只针对单个线程同步: 针对于一个线程来说的,它的主要作用就是优化同一个线程多次获取一个锁的情况;如果一个synchronized方法被一个线程访问,那么这个方法所在的对象 就会在其Mark word中的将偏向锁进行标记,同时还会有一个字段来存储该线程的ID;当这个线程再次访问同一个对象锁的synchronized方法时,它会检查这个对象 的Mark Word的偏向锁标记以及是否指向了其线程ID,如果是的话,那么该线程就无需再去进行管程(Monitor)了,而是直接进入到该方法体中。

例如:

public class Test {
    static final Object obj = new Object();

    public static void m1() {
        synchronized (obj) {
            // 同步块A
            m2();
        }
    }

    public static void m2() {
        synchronized (obj) {
            // 同步块B
            m3();
        }
    }

    public static void m3() {
        synchronized (obj) {
            // 同步块C
        }
    }
}

synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第21张图片
synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第22张图片

偏向锁状态

synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第23张图片

偏向锁撤销

synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第24张图片
其它线程使用对象
当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁

撤销偏向锁 - 调用 wait/notify (只有重量级锁才支持这两个方法)

批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重 置对象的 Thread ID
当撤销偏向锁阈值超过20次后,jvm会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏 向至加锁线程

批量撤销偏向锁

当撤销偏向锁阈值超过40次后,jvm会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有 对象都会变为不可偏向的,新建的对象也是不可偏向的

9.锁消除

编译器对于锁的优化措施: (锁消除和锁粗化都是针对运行期的 且针对代码块)

JIT编译器(Just In Time编译器)可以在动态编译同步代码时,使用一种叫做逃逸分析的技术,来通过该项技术判别程序中所使用的锁对象是否只被 一个线程所使用,而没有散布到其他线程当中;如果情况就是这样的话,那么JIT编辑器在编译这个同步代码时就不会生成synchronized关键字所标识 的锁的申请与释放机器码,从而消除了锁的使用流程。

/**
 * 锁消除技术
 */
public class MyTest4 {

    public void method() {
        /*
        属于栈被每个线程所独有object 不共享 此时synchronized 被消除是不执行的
        但字节码还是会生成monitorenter和monitorexit
         */
        Object object = new Object();

        synchronized (object) {
            System.out.println("hello world");
        }
    }
}

10.锁粗化

JIT编译器在执行动态编译时,若发现前后相邻的synchronized块使用的是同一个对象的锁(monitor),那么它就会把这几个synchronized块给合并为一个较大的同步块,这样做的好处在于线程在执行这些代码时,就无需频繁申请与释放锁了,从而达到申请与释放锁各一次,就可以执行完全部的同步代码块,从而提升了性能。

/**
 * 锁粗化

 */
public class MyTest5 {
    Object o = new Object();

    public void method() {

        synchronized (o) {
            System.out.println("hello world");
        }

        synchronized (o) {
            System.out.println("hello ");
        }

        synchronized (o) {
            System.out.println(" world");
        }
    }
}

11.常见的锁类别(死锁,活锁,饿死)

  • 死锁: 线程1等待线程2互斥持有的资源,而线程2也在等待线程1互斥持有的资源,两个线程都无法继续执行。
  • 活锁: 线程持续重试一个总是失败的操作,导致无法继续执行.
  • 饿死: 线程一直被调度器延迟访问其赖以执行的资源,也许是调度器先于低优先级的线程而执行高优先级的线程,同时总是会有一个高优先级的线程可以执行,饿死也叫做无限延迟.

死锁案例

/*
 * jps -l
 * jstack 进程id
 */
public class MyTest6 {
    //生成两个不同的对象每个对象都有一个monitor
    private Object lock1 = new Object();
    private Object lock2 = new Object();

    public void myMethod1() {
        synchronized (lock1) {//这里锁的是成员变量对象1
            synchronized (lock2) {//这里锁的是成员变量对象2
                System.out.println("myMethod1 invoked");
            }
        }
    }

    public void myMethod2() {
        synchronized (lock2) {
            synchronized (lock1) {
                System.out.println("myMethod2 invoked");
            }
        }
    }

    public static void main(String[] args) {
        MyTest6 myTest6 = new MyTest6();

        Runnable run1 = () -> {
            while (true) {
                myTest6.myMethod1();
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread myThread1 = new Thread(run1, "myThread1");

        Runnable run2 = () -> {
            while (true) {
                myTest6.myMethod2();
                try {
                    Thread.sleep(250);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread myThread2 = new Thread(run2, "myThread2");


        myThread1.start();
        myThread2.start();
    }
}

通过jvisualvm查看
synchronized底层原理、偏向锁、轻量级锁、自旋锁详解_第25张图片

jstack

D:\workspace\jvm\jvm>jps -l
7536 sun.tools.jps.Jps
10708 org.jetbrains.jps.cmdline.Launcher
10472 org/netbeans/Main
7976
8076 org.jetbrains.jps.cmdline.Launcher
8316 com.example.demo.com.concurrecy.concurrency3.MyTest6
9468 org.jetbrains.jps.cmdline.Launcher

D:\workspace\jvm\jvm>jstack 8316

Found one Java-level deadlock:
=============================
"myThread2":
  waiting to lock monitor 0x000000001c789108 (object 0x000000076ba72f40, a java.lang.Object),
  which is held by "myThread1"
"myThread1":
  waiting to lock monitor 0x000000001c789058 (object 0x000000076ba72f50, a java.lang.Object),
  which is held by "myThread2"

Java stack information for the threads listed above:
===================================================
"myThread2":
        at com.example.demo.com.concurrecy.concurrency3.MyTest6.myMethod2(MyTest6.java:29)
        - waiting to lock <0x000000076ba72f40> (a java.lang.Object)
        - locked <0x000000076ba72f50> (a java.lang.Object)
        at com.example.demo.com.concurrecy.concurrency3.MyTest6.lambda$main$1(MyTest6.java:52)
        at com.example.demo.com.concurrecy.concurrency3.MyTest6$$Lambda$2/708049632.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
"myThread1":
        at com.example.demo.com.concurrecy.concurrency3.MyTest6.myMethod1(MyTest6.java:21)
        - waiting to lock <0x000000076ba72f50> (a java.lang.Object)
        - locked <0x000000076ba72f40> (a java.lang.Object)
        at com.example.demo.com.concurrecy.concurrency3.MyTest6.lambda$main$0(MyTest6.java:39)
        at com.example.demo.com.concurrecy.concurrency3.MyTest6$$Lambda$1/2085857771.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

你可能感兴趣的:(Java并发编程,java)