关于多线程:synchronized用法原理和锁优化升级过程面试

42次阅读

共计 6407 个字符,预计需要花费 17 分钟才能阅读完成。

简介

多线程始终是面试中的重点和难点,无论你当初处于啥级别段位,对 synchronized 关键字的学习防止不了,这是我的心得体会。上面咱们以面试的思维来对 synchronized 做一个零碎的形容,如果有面试官问你, 说说你对 synchronized 的了解? 你能够从 synchronized 应用层面 synchronized 的 JVM 层面 synchronized 的优化层面 3 个方面做零碎答复,说不定面试官会对你另眼相看哦!文章会有大量的代码是不便了解的,如果你有工夫肯定要入手敲下加深了解和记忆。如果这篇文章能对您能有所帮忙是我创作路上最大快慰。

synchronized 应用层面

大家都晓得 synchronized 是一把锁, 锁到底是什么呢? 举个例子,你能够把锁了解为厕所门上那把锁的惟一钥匙,每个人要进去只能拿着这把钥匙能够去开这个厕所的门,这把钥匙在一时刻只能有一个人领有,有钥匙的人能够重复出入厕所,在程序中咱们叫做这种反复出入厕所行为叫锁的可重入。它能够润饰静态方法,实例办法和代码块,那上面咱们一起来看看 synchronized 用于同步代码锁表白的意思。

  • 对于一般同步办法,锁的是对象实例。
  • 对于动态同步办法,锁的是类的 Class 对象。
  • 对于同步代码块,锁的是括号中的对象。

先说下同步和异步的概念。

  • 同步:交替执行。
  • 异步:同时执行。

举个例子比方吃饭和看电视两件事件,先吃完饭后再去看电视,在工夫维度上这两件事是有先后顺序的,叫同步。能够一边吃饭,一边看刷剧,在工夫维度上是不分先后同时进行的,饭吃完了电视也看了,就能够去学习了,这就是异步,异步的益处是能够提高效率,这样你就能够节省时间去学习了。

上面咱们看看代码,代码中有做了很具体的正文,能够复制到本地进行测试。如果有 synchronized 根底的童鞋,能够跳过锁应用层面的解说。

/**
 * @author:jiaolian
 * @date:Created in 2020-12-17 14:48
 * @description:测试静态方法同步和一般办法同步是不同的锁, 包含 synchronized 润饰的动态代码块用法;
 * @modified By:* 公众号: 叫练
 */
public class SyncTest {public static void main(String[] args) {Service service = new Service();
        /**
         * 启动上面 4 个线程,别离测试 m1-m4 办法。*/
        Thread threadA = new Thread(() -> Service.m1());
        Thread threadB = new Thread(() -> Service.m2());
        Thread threadC = new Thread(() -> service.m3());
        Thread threadD = new Thread(() -> service.m4());
        threadA.start();
        threadB.start();
        threadC.start();
        threadD.start();}

    /**
     * 此案例阐明了 synchronized 润饰的静态方法和一般办法获取的不是同一把锁,因为他们是异步的,相当于是同步执行;
     */
    private static class Service {
        /**
         * m1 办法 synchronized 润饰静态方法,锁示意锁定的是 Service.class
         */
        public synchronized static void m1() {System.out.println("m1 getlock");
            try {Thread.sleep(2000);
            } catch (InterruptedException e) {e.printStackTrace();
            }
            System.out.println("m1 releaselock");
        }

        /**
         * m2 办法 synchronized 润饰静态方法,锁示意锁定的是 Service.class
         * 当线程 AB 同时启动,m1 和 m2 办法是同步的。能够证实 m1 和 m2 是同一把锁。*/
        public synchronized static void m2() {System.out.println("m2 getlock");
            System.out.println("m2 releaselock");
        }

        /**
         * m3 办法 synchronized 润饰的一般办法,锁示意锁定的是 Service service = new Service(); 中的 service 对象;*/
        public synchronized void m3() {System.out.println("m3 getlock");
            try {Thread.sleep(1000);
            } catch (InterruptedException e) {e.printStackTrace();
            }
            System.out.println("m3 releaselock");
        }

        /**
         * 1.m4 办法 synchronized 润饰的同步代码块,锁示意锁定的是以后对象实例, 也就是 Service service = new Service(); 中的 service 对象;和 m3 一样,是同一把锁;* 2. 当线程 CD 同时启动,m3 和 m4 办法是同步的。能够证实 m3 和 m4 是同一把锁。* 3.synchronized 也能够润饰其余对象,比方 synchronized (Service.class), 此时 m4,m1,m2 办法是同步的,启动线程 ABD 能够证实。*/
        public void m4() {synchronized (this) {System.out.println("m4 getlock");
                System.out.println("m4 releaselock");
            }
        }

    }
}

通过下面的测试,你能够能会有疑难, 锁既然是存在的,那它存储在什么中央? 答案:对象外面。上面咱们用代码来证实下。

锁在对象头外面,一个对象包含对象头,实例数据和对齐填充。 对象头包含 MarkWord 和对象指针,对象指针是指向办法区的对象类型的,,实例对象就是属性数据,一个对象可能有很多属性,属性是动静的。对齐填充是为了补齐字节数的,如果对象大小不是 8 字节的整数倍,须要补齐残余的字节数,这是不便计算机来计算的。在 64 位机器外面,一个对象的对象头个别占 12 个本人大小,在 64 位操作系统个别占 4 个字节,所以 MarkWord 就是 8 个字节了。

MarkWord 包含对象 hashcode,偏差锁标记位,线程 id 和锁的标识。为了不便测试对象头的内容,须要引入 maven openjdk 的依赖包。

<dependency>
  <groupId>org.openjdk.jol</groupId>
  <artifactId>jol-core</artifactId>
  <version>0.10</version>
</dependency>
/**
 * @author:duyang
 * @date:Created in 2020-05-14 20:21
 * @description:对象占用内存
 * @modified By:*
 *  Fruit 对象头是 12 字节(markword+class)*  int 占 4 个字节
 *
 *  32 位机器可能占 8 个字节;
 *
 *  Object 对象头 12 对齐填充 4 一共是 16
 */
public class ObjectMemory {public static void main(String[] args) {//System.out.print(ClassLayout.parseClass(Fruit.class).toPrintable());
        System.out.print(ClassLayout.parseInstance(Fruit.class).toPrintable());
    }
}

/**
 *Fruit 测试类
 */
public class Fruit {

    // 占一个字节大小
    private boolean flag;

}

测试后果 :上面画红线的 3 行别离示意对象头,实例数据和对齐填充。对象头是 12 个字节,实例数据 Fruit 对象的一个 boolean 字段 flag 占 1 个字节大小,其余 3 个字节是对齐填充的局部,一共是 16 个字节大小。

咦?你说的锁呢,怎么没有看到呢?小伙,别着急,待会咱们讲到 synchronized 降级优化层面的时候再来详细分析一波。上面咱们先剖析下 synchronized 在 JVM 层面的意思。

最初上图文总结:

synchronized JVM 层面

/**
 * @author:jiaolian
 * @date:Created in 2020-12-20 13:43
 * @description:锁的 jvm 层面应用
 * @modified By:* 公众号: 叫练
 */
public class SyncJvmTest {public static void main(String[] args) {synchronized (SyncJvmTest.class) {System.out.println("jvm 同步测试");
        }
    }
}

下面的案例中,咱们同步代码块中咱们简略输入一句话,咱们次要看看 jvm 中它是怎么实现的。咱们用 Javap -v SyncJvmTest.class 反编译出下面的代码,如下图所示。

上图第一行有一个 monitorenter 和第六行一个 monitorexit,两头的 jvm 指令(2- 5 行)对应的 Java 代码中的 main 办法的代码,synchronized 就是依赖于这两个指令实现。咱们来看看 JVM 标准中 monitorenter 语义

  1. 每个对象都有一把锁,当一个线程进入同步代码块,都会去获取这个对象所持有 monitor 对象锁(C++ 实现),如果以后线程获取锁,会把 monitor 对象进入数自增 1 次。
  2. 如果该线程反复进入,会把 monitor 对象进入数再次自增 1 次。
  3. 当有其余线程进入,会把其余线程放入期待队列排队,直到获取锁的线程将 monitor 对象的进入数设置为 0 开释锁,其余线程才有机会获取锁。

synchronized 的优化层面

synchronized 是一个重量级锁,次要是因为线程竞争锁会引起操作系统用户态和内核态切换,浪费资源效率不高,在 jdk1.5 之前,synchronized 没有做任何优化,但在 jdk1.6 做了性能优化,它会经验偏差锁,轻量级锁,最初才到重量级锁这个过程,在性能方面有了很大的晋升,在 jdk1.7 的 ConcurrentHashMap 是基于 ReentrantLock 的实现了锁,但在 jdk1.8 之后又替换成了 synchronized,就从这一点能够看出 JVM 团队对 synchronized 的性能还是挺有信念的。上面咱们别离来介绍下无锁,偏差锁,轻量级锁,重量级锁。上面咱们我画张图来形容这几个级别锁的在对象头存储状态。如图所示。

  • 无锁。 如果不加 synchronized 关键字,示意无锁,很好了解。
  • 偏差锁。
  • 降级过程:当线程进入同步块时,Markword 会存储偏差线程的 id 并且 cas 将 Markword 锁状态标识为 01,是否偏差用 1 示意以后处于偏差锁(对着上图来看),如果是偏差线程下次进入同步代码只有比拟 Markword 的线程 id 是否和以后线程 id 相等,如果相等不必做任何操作就能够进入同步代码执行,如果不比拟后不相等阐明有其余线程竞争锁,synchronized 会升级成轻量级锁。这个过程中在操作系统层面不必做内核态和用户态的切换,缩小切换线程带来的资源耗费。
  • 收缩过程:当有另外线程进入,偏差锁会升级成轻量级锁。比方线程 A 是偏差锁,这是 B 线程进入,就会成轻量级锁, 只有有两个线程就会升级成轻量级锁

上面咱们代码来看下偏差锁的锁状态。

package com.duyang.base.basic.markword;

import lombok.SneakyThrows;
import org.openjdk.jol.info.ClassLayout;

/**
 * @author:jiaolian
 * @date:Created in 2020-12-19 11:25
 * @description:markword 测试
 * @modified By:* 公众号: 叫练
 */
public class MarkWordTest {private static Fruit fruit = new Fruit();

    public static void main(String[] args) throws InterruptedException {Task task = new Task();
        Thread threadA = new Thread(task);
        Thread threadB = new Thread(task);
        Thread threadC = new Thread(task);
        threadA.start();
        //threadA.join();
        //threadB.start();
        //threadC.start();}

    private static class Task extends Thread {

        @SneakyThrows
        @Override
        public void run() {synchronized (fruit) {System.out.println("==================="+Thread.currentThread().getId()+" ");
                try {Thread.sleep(3000);
                } catch (InterruptedException e) {e.printStackTrace();
                }
                System.out.print(ClassLayout.parseInstance(fruit).toPrintable());
            }
        }
    }
}

下面代码启动线程 A,控制台输入如下图所示,红色标记 3 个 bit 是 101 别离示意,高位的 1 示意是偏差锁,01 是偏差锁标识位。合乎偏差锁标识的状况。

  • 轻量级锁。
  • 降级过程:在线程运行获取锁后,会在栈帧中发明锁记录并将 MarkWord 复制到锁记录,而后将 MarkWord 指向锁记录,如果以后线程持有锁,其余线程再进入,此时其余线程会 cas 自旋,直到获取锁,轻量级锁适宜多线程交替执行,效率高(cas 只耗费 cpu,我在 cas 原理一篇文章中具体讲过。)。
  • 收缩过程:有两种状况会收缩成重量级锁。1 种状况是 cas 自旋 10 次还没获取锁。第 2 种状况其余线程正在 cas 获取锁,第三个线程竞争获取锁,锁也会收缩变成重量级锁。

上面咱们代码来测试下轻量级锁的锁状态。

关上 23 行 -24 行代码,执行线程 A,B,我的目标是程序执行线程 A B,所以我在代码中先执行 threadA.join(),让 A 线程先执行结束,再执行 B 线程,如下图所示 MarkWord 锁状态变动,线程 A 开始是偏差锁用 101 示意,执行线程 B 就变成轻量级锁了,锁状态变成了 00,合乎轻量级锁锁状态。证实结束。

  • 重量级锁 。重量级锁降级后是不可逆的,也就是说分量锁不能够再变为轻量级锁。

关上 25 行代码,执行线程 A,B,C,我的目标是先执行线程 A,在代码中先执行 threadA.join(),让 A 线程先执行结束,而后再同时执行线程 BC,如下图所示看看 MarkWord 锁状态变动,线程 A 开始是偏差锁,到同时执行线程 BC,因为有强烈竞争,属于轻量级锁收缩条件第 2 种状况,当其余线程正在 cas 获取锁,第三个线程竞争获取锁,锁也会收缩变成重量级锁。此时 BC 线程锁状态都变成了 10,这种状况合乎重量级锁锁状态。收缩重量级锁证实结束。

到此为止,咱们曾经把 synchronized 锁降级过程中的锁状态通过代码的模式都证实了一遍,心愿对你有帮忙。下图是本人总结。

总结

多线程 synchronized 始终是个很重要的话题,也是面试中常见的考点。心愿大家都能尽快了解把握,分享给你们心愿你们喜爱!

我是叫练,多叫多练 ,欢送大家和我一起探讨交换,我会尽快回复大家,喜爱点赞和关注哦!公众号【叫练】。

正文完
 0