synchronized关键字

synchronized关键字解决的是多个线程访问资源的同步性。synchronized可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

实现原理

synchronized修饰同步代码块,javac在编译时,在synchronized同步块的进入的指令前和退出的指令后,会分别生成对应的monitorenter和monitorexit指令进行对应,代表尝试获取锁和释放锁。(为了保证抛异常的情况下也能释放锁,所以javac为同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit命令释放锁。)

方法同步通过调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。

锁对象:

1、修饰代码块

可以指定对象作为锁

private static Integer resources = 1;

public static void main(String[] args) {
    TestCodeBlockSynchronized obj = new TestCodeBlockSynchronized();
    new Thread(()->{
        System.out.println("Thread1 before");
        synchronized (resources){
            System.out.println("Thread1 synchronized start job1");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread1 synchronized end job1");
        }
        System.out.println("Thread1 after");
    }).start();
    new Thread(()->{
        System.out.println("Thread2 before");
        synchronized (resources){
            System.out.println("Thread2 synchronized start job2");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread2 synchronized end job2");
        }
        System.out.println("Thread2 after");
    }).start();
}

synchronized代码块执行结果

持有锁资源的线程才可以执行,未持有锁资源的线程只能等待

2、修饰实例方法

以当前对象作为锁

3、修饰静态方法

以当前类对象作为锁

public class TestStaticSynchronized {

    public static void main(String[] args) {
        new Thread(()->{
            System.out.println("method01 before");
            method01();
            System.out.println("method01 after");
        }).start();
        new Thread(()->{
            System.out.println("method02 before");
            method02();
            System.out.println("method02 after");
        }).start();
    }

    static synchronized void method01(){
        System.out.println("I'm a static synchronized code block method01");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("method01 execution complete");
    }
    static synchronized void method02(){
        System.out.println("I'm a static synchronized code block method02");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("method02 execution complete");
    }
}

//当method01执行完毕method02才开始执行

静态synchromized执行过程

由图可以看出虽然启动了多个线程调用的是不同的方法,但是当第一个方法执行完成后才开始执行第二个方法。

  • java对象

    • 对象头
      • Mark Word(长度32位的JVM中32bit、64位的JVM中64bit)
        • 对象的Hashcode、分代年龄、GC标记、锁的标记
      • 指向类对象的指针(长度32位的JVM中32bit、64位的JVM中64bit)
      • 数组长度(只有数组对象才有)(都是32bit)
    • 实例数据
    • 对齐填充字符
  • 锁的状态

    • 无锁状态

      • 对象的Hashcode、分代年龄、是否偏向锁、锁的标记
    • 偏向锁状态

      • 线程ID、Epoch、分代年龄、是否偏向锁、锁标志位
    • 轻量级锁状态

      • 指向栈中锁记录的指针、锁标志位
    • 重量级锁状态

      • 指向重量级锁的指针、锁标志位

Mark Work结构,32位HotSopt为例:

markWord

​ 图片来源于网络侵删

偏向锁

HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低从而引入偏向锁。偏向锁在获取资源的时候会在锁对象头上记录当前线程ID,偏向锁并不会主动释放,这样每次偏向锁进入的时候都会判断锁对象头中线程ID是否为自己,如果是则不需要进行额外的操作,直接进入同步操作。

偏向锁的获取过程

I:判断是否为可偏向状态--MarkWord中锁标志是否为‘01’,是否偏向锁是否为‘1’

II:如果是可偏向状态,则查看线程ID是否为当前线程,如果是,则进入步骤'V',否则进入步骤‘III’

III:通过CAS操作竞争锁,如果竞争成功,则将MarkWord中线程ID设置为当前线程ID,然后执行‘V’;竞争失败,则执行‘IV’

IV:CAS获取偏向锁失败表示有竞争。当达到safepoint时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块

V:执行同步代码

轻量级锁

轻量级锁是相对于重量级锁需要阻塞/唤醒涉及上下文切换而言,主要针对多个线程在不同时间请求同一把锁的场景

轻量级锁获取过程:

I:进行加锁操作时,jvm会判断是否已经时重量级锁,如果不是,则会在当前线程栈帧中划出一块空间,作为该锁的锁记录,并且将锁对象头的MarkWord复制到该锁记录中

II:复制成功之后,JVM使用CAS操作将对象头的MarkWord更新为指向锁记录的指针,并将锁记录里的owner指针指向加锁对象。如果成功,则执行‘III’,否则执行‘IV’

III:更新成功,则当前线程持有该对象锁,并且对象MarkWord锁标志设置为‘00’,即表示此对象处于轻量级锁状态

IV:更新失败(说明有其他线程正在执行同步方法/代码块),jvm先检查当前线程栈帧中的锁记录(Lock Record)与锁对象MarkWord是否匹配,如果是则执行‘V’,否则执行‘VI’

V:表示锁重入;然后当前线程栈帧中增加一个锁记录,并使其的owner指针指向锁对象,起到一个重入计数器的作用。

VI:表示该锁对象已经被其他线程抢占,则进行自旋等待(默认10次),等待次数达到阈值仍未获取到锁,则升级为重量级锁

偏向锁可重入

重量级锁

是基于操作系统的互斥量(mutex)实现的、当获取锁是需要从用户态切换到内核态

面试题1:JDK1.6对synchronized的优化

JDK1.6之前是重量级锁,它加锁是依赖操作系统的mutex相关指令实现,所以会有用户态和内核态之间的切换,性能损耗十分明显。

JDK1.6以后引入偏向锁和轻量级锁,在JVM层面实现了加锁的逻辑,不依赖底层操作系统,就没有切换的消耗

对象头的Mark Word对锁状态的记录一共有四种:无锁、偏向锁、轻量级锁和重量级锁

面试题2:简单说说你对偏向锁、轻量级锁中重量级锁的理解
偏向锁:

实际情况下很多场景获得锁的总是那同一个线程,二频繁的获取释放锁又会带来严重的性能浪费,为了减少不必要的开销,JVM的设计者引入了偏向锁的概念。在mark word中记录持有锁的线程ID,只要是线程来执行代码了,都会比较线程ID是否相等;相等则当前线程为锁的持有者,就不在获取锁了,直接执行同步代码块的代码;如果不相等,则采用CAS来修改Mark Word中的线程ID为当前线程ID,修改成功表示没有竞争,则使锁偏向于自己。如果CAS失败,说明有竞争,由于偏向锁不会自己释放,当有竞争的时候,持有锁的线程代码执行到一个安全点(同步代码执行完毕后的某一个点)释放锁,此时会对偏向锁进行撤销,升级为轻量级锁。

偏向锁

轻量级锁:

当偏向锁有竞争时就会升级为轻量级锁,轻量级锁获取的时候,首先会以CAS的方式先在线程栈帧中先开辟一块空间作为Lock Record,并将锁对象对象头中的Mark word复制进去,且在Lock Record中有一个Owner指针指向锁对象。若操作成功则获得锁对象,执行同步代码。失败,分两种情况,一、持有锁资源的是当前线程;二、持有锁资源的是其他线程。只需判断Lock Record中Owner指针指向的对象是否为锁对象,若是则在当前线程栈帧中新创建一个Lock Record,使其displaced mark word为null,owner指向锁对象,以此来实现锁的可重入;若不是则说明持有锁资源的是其他线程,通过CAS自旋(默认10次)来等待其他线程释放资源,等到了就获得锁,执行同步代码块,等不到则升级/膨胀为重量级锁。

重量级锁:

重量级锁的获取与释放依赖操作系统的互斥量(mutex),涉及到从用户态到内核态以及上下文的切换故而很消耗性能。

总结
  • 锁只有升级没有降级
  • 偏向锁不会自己释放锁,当有竞争时代码执行到安全点(同步代码执行完毕后的某个点)时释放锁
  • 偏向锁和轻量级锁是JVM层面的锁获取与释放,重量级锁是操作系统层面的锁获取与释放
  • 只有一个线程进入临界区,偏向锁
  • 有少量线程(每个线程都很快)交替进入临界区,轻量级锁
  • 大量线程同时进入临界区,重量级锁
posted @ 2021-03-26 17:15  岸北  阅读(78)  评论(0编辑  收藏  举报