JDK8 锁机制深度详解——看完再有面试官要拿锁方面的问题来为难你,能够提醒他当心点,别翻车了。

 本文故事

这是一个面试向的文章,因此我不会讲得太过正统,争取把枯燥的理论知识讲得浅显易懂一点。另外,我会尽可能把知识点整理得全面完整,因此篇幅会比较长。可是只要你耐心看完,保证之后无论遇到什么java锁机制方面的问题,均可以玩玩全全游刃有余。下次再有面试官要拿锁方面的问题来为难你,嘿嘿,让他当心点,别翻车。java

什么是锁?为何要用锁?

只要有资源竞争,就须要有锁。为了引出锁问题,先看下面一段简单的代码:面试

import java.util.concurrent.CountDownLatch;
public class ThreadTest {
    private static final CountDownLatch countDown = new CountDownLatch(1);
    private static int m = 0;
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                try {
                    countDown.await();
                    m++;
                    System.out.println("m = " + m);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },"thread"+i).start();
        }
        countDown.countDown();
    }
}

这段代码执行结果,三个线程都给m进行--操做,咱们期待是没能打印出3,2,1的结果,可是执行后发现,打印出来的结果永远不会是3,2,1.而会是3,2,2或者3,3,2这样的。这就是由于多个线程共同竞争资源m形成的。而为了不这种多线程环境下的资源竞争问题,就须要加入锁。加锁的方式大体有三种,一种是使用Atomic原子类,另外一种是使用可重入锁ReentrantLock,这两种是使用的同一个机制。另外一种是使用Synchronize关键字。下面会依次来整理一下这些锁。算法

这里能够延伸出一个常常考的面试题: 给m 加上 volatile 关键字,能不能解决并发问题?数据库

答案是NO,NO,NO。 volatile只是解决多线程及时可读问题,并不能保证原子性。简单理解就是volatile关键字只适用于少许(一个)线程写,多个线程读的场景。设计模式

Atomic原子操做 和 ReentrantLock

把这两种方式放在一块儿,是由于这两种方式都是经过java代码搭配sun.misc.Unsafe中的本地调用实现的,属于同一种锁机制。数据结构

以原子操做为例,原子操做是使用java.util.concurrent包下的AtomicXXX类进行原子操做。多线程

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadTest {
    private static final CountDownLatch countDown = new CountDownLatch(1);
    private static AtomicInteger m = new AtomicInteger(0);
    private static int i = 0 ;
    public static void main(String[] args) {
        for (i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    countDown.await();
                    m.incrementAndGet();
                    System.out.println("m = " + m);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },"thread"+i).start();
        }
        countDown.countDown();
    }
}

这样能够保证每次执行的结果都是1~10(打印时间有前后,这个不用管)。那是怎么作到的?固然是跟踪代码。一路跟踪incrmentAndGet方法,会跟踪到一个sun.mic.Unsafe类,这个是jdk的基础包rt.jar中包含的一个类。里面有一大堆这样的方法。架构

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

这些方法都是native本地方法,是调用的JVM内的C++语言实现的。实现方法须要查看JDK源码才能看获得。可是这些方法有一个共同点,都是compareAndSwap开头,很显然,这就是他们的共同点, CAS算法。并发

CAS算法与自旋锁

CAS算法就是比较再交换的算法。为了不线程在修改一个值的过程中被其余线程给修改了,在修改一个内存的值时,先把值读出来,为E, 而后计算出值V(计算操做),在把V往原内存写的时候,比较一下原内存地址的值是否和E相同, 相同就往回写,不相同就再次重复。这样能够保证不会有线程共享值冲突。总体以下图:分布式

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk= 这个过程其实就引出了一个重要的概念,自旋锁。即若是线程若是一直更新不成功,那线程就会一直不停去执行红色的流程而不会中止。

这里有两个面试题:

一、ABA问题。就是在一次自旋过程当中,一个值通过屡次修改(值由A变成B,又由B又变成A,最终又变回原来的值,而CAS会认为值没有变,就会发生同步问题。实际上,在绝大部分状况下,虽然中间过程被忽略了,可是只要语义正确,并不会引发太大问题,而若是要完全解决ABA问题,能够加个加版本号(AtomicStampedReference)或者加Boolean标志(AtomicMarkableReference)来解决。

二、CAS算法自己并不能保证线程操做的原子性。在比较完,到更新值V以前,依然能够被其余线程修改。而JAVA的C++底层,最终仍是调用汇编指令lock申请了一个信号锁,最终保持整个线程操做的原子性。因此CAS要保证原子性,仍是须要锁,只是锁已经转移到了汇编级别。

基于这种机制,在JDK中还有不少相关的工具,如RetrantLock、ReadWriteLock等不少工具。 Atomic操做其实更多的细节在底层C++代码中,对于咱们来讲能看到的细节比较少。那别急,下面来仔细看看JDK提供的Synchronize关键字。

Synchronized关键字

Synchronized关键字是经过一对字节码汇编指令monitorenter/monitorexit来实现的,是JDK一开始就有的关键字。自JDK1.6之后,synchronized被进行了大幅度的优化,由重量级锁改成了轻量级锁,性能获得极大的优化。所以,官方也开始建议优先使用Synchronized关键字来执行同步操做。

JOL: Java Object Layout

在了解Synchronized机制前,仍是先来个示例看看。先创建一个Maven工程,引入下面openjdk中的一个依赖来辅助咱们了解Synchronized的实现机制。

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
 </dependency>

这个工具包能够辅助打印出java对象的内存结果。先不用管细节,来一个简单的示例,看看新鲜:

public static void main(String[] args) throws Exception{
        Object o = new Object();
        System.out.println("step 1: new Object");
        System.out.println(ClassLayout.parseInstance(o).toPrintable());

        synchronized(o){
            System.out.println("step 2: add synchronized");
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
       }
    }

下面来分析下执行结果: watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk= 首先看红色部分,这里带出了java对象的一些内存结构。一个对象由16字节构成,前面12个都是object header,最后一个是补充字节,让对象总体长度能够被8整除。三个object header中,第一个叫作markword,相似一个对象标记位。第二个是class point,这个是对象的类指针,指向所属的class类,固然,这是通过压缩过的。第三个是成员变量的指针,固然,成员变量是能够有多个的。

而后咱们看黄色部分,细节部分先不用管,可是经过比对上锁先后的类布局,咱们首先能够获得第一个结论:JVM锁只做用在对象的markword位。简单的说就是上锁并不会改变对象自己,只是影响他的一个标志位。

有了这个示例后,就能够为咱们后面的钻牛角尖打个基础了。

轻量级锁(用户空间锁) VS 重量级锁

其实前面提到了一下,jdk1.6对synchronized的一个重大改进就是将其由重量级锁改成了轻量级锁。那就先来扯扯这两种锁状态。

首先,在操做系统中,CPU有两种运行状态,一种是内核态,一种是用户态。其中,内核态主要是运行操做系统程序的,能够操做硬件,而用户态就是主要用来运行用户的应用程序的,不能够直接操做硬件。有这样的区分就是为了防止用户应用程序直接操做硬件,形成不可控的后果。

能够想象若是有一个应用程序能够把硬盘的引导区给格式化掉,那会是什么情况?传说在早期操做系统中,还真出现过这样的应用程序。

而后,轻量级锁就至关于用户态的锁,由应用程序本身控制,例如以前提到的自旋锁。而重量级锁,就至关于内核态的锁,交由操做系统进行管理,例如咱们将java线程wait()后,实际上就至关因而上了重量级锁。

把用户程序比做一个房子,锁就能够比做房子内的一张门,门上的轻量级的锁就至关于房主能够本身开门关门,而重量级锁就至关于找了小区物业来帮你管理这张门,开门关门都须要找物业申请。这个例子也就能够用来理解为何说jdk1.6后synchronized关键字的性能获得了大幅度的提高。

轻量级锁在必定条件下(锁数量、CPU占用率,能够配置),会升级为重量级锁。由于轻量级锁会一直不断的自旋,而自旋显然是要消耗系统资源的,当消耗到必定程度,固然就会想办法减小自旋,这时就会升级为重量级锁。当锁竞争升级为重量级锁后,就会中止自旋,而另外有机制让线程进入休眠,等待唤醒。

轻量级锁竞争 VS 重量级锁竞争

通过前面的描述,就能够有个大体的概念,轻量级锁是将锁对象的markword部分指向一个用户空间里的对象,而重量级锁就是将锁对象的markword部分指向一个操做系统内的数据结构。那显然,他们竞争锁的过程也是不一样的。

重量级锁竞争过程当中,被锁对象的markwork会经过CAS操做尝试更新为一个包含了操做系统互斥量(mutex)和条件变量(condition variable)的指针。交由操做系统来控制对象的markword信息。

轻量级锁竞争过程才是咱们关注的重点。JVM中线程竞争轻量级锁的过程大体有一下几步

  1. 在代码进入同步块的时候,若是同步对象锁状态为无锁,虚拟机首先会在当前线程的栈帧中创建一个名为锁记录Lock Record的控件,用来存储锁对象目前的MarkWord拷贝。官方称之为Displaced Mark Word
  2. 拷贝对象头中的MarkWord复制到锁记录中,这代表当前线程要竞争锁对象。这时线程堆栈与对象头的状态以下图:

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

  1. 拷贝成功后,虚拟机将使用CAS操做尝试将锁对象的MarkWord更新为指向Lock Rocket的指针,并将Lock Record里的owner指针指向object mark word。若是更新成功,就往下执行,不然就开始自旋,自旋必定次数后,就会往重量级锁升级。
  2. 若是更新动做成功,那么这个线程有拥有了该对象的锁,而且锁对象的MarkWord锁状态值为00(偏向锁,下面会提到),就表示此对象处于轻量级锁定状态。这时候线程堆栈与对象头的状态以下图所示:

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

这个锁竞争过程能够比喻为不少人(线程)一块儿上厕所抢蹲位(锁对象)。每一个人上厕所时,会把本身的名牌贴在厕所门上,上完了厕所再把名牌拿走。后面的人看到门上有人了,就出去转转,等下再来。若是下次回来门上的名牌没了,他就上。可是若是出去转了不少次,厕所仍是没空出来,那他就没办法了,只能去找物业帮忙协调找厕所蹲位了。

JVM锁升级

在JDK1.6后,JDK在实践中发现一种假设,即大多数synchronized竞争状况下,其实只有一个线程在运行。因而,JVM在实际中又加入了另外一种更轻量的锁,叫作偏向锁。因此JVM中的锁机制有以下三种:

偏向锁(Biased Lock)>轻量级锁(Lightweight Lock)>重量级锁(Heavyweight Lock)

偏向锁也是属于一种轻量级锁。这三种机制的切换是根据资源竞争激烈程度进行的。在几乎无竞争的条件下,会使用偏向锁。当竞争渐趋激烈,会升级为轻量级锁。当竞争过于激烈,就会升级为重量级锁。JVM中锁升级的过程以下图: watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk= 看上图,升级的过程大体都已经比较清楚了,那其中有一个奇怪的文字,偏向锁未启动,这是什么意思呢?

这里涉及到一个面试常常要问的问题:打开偏向锁必定可以提高性能吗?为何?

既然这么问,答案确定是否了。为何呢?在某些明知资源竞争很是激烈的状况下(例如应用启动过程,须要加载大量的资源,确定会有很是激烈的资源竞争),若是还要打开偏向锁,那厕所门上贴名牌撕名牌的过程也会至关频繁,这也会消耗至关多的资源。这时,跳过偏向锁,直接升级为轻量级锁,更能节省资源。

实际上, JVM中有两个跟偏向锁有关的启动参数:

-XX:-UseBiasedLocking 启动偏向锁

-XX:BiasedLockingStartupDelay=0 偏向锁启动延迟。默认值是4秒,即JVM启动4秒左右后才会打开偏向锁。

这里首先插入一个小知识,就是HotSpot的JAVA对象内存布局。总体以下图所示。其中红框的标志位部分就是对象的锁状态。如今看不懂不要紧,咱们结合这个表和下面的示例就能看明白这个锁标志了。先看最后两位,若是是01(偏向锁),那就再往前看一位。 watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk= 既然有配置,那就要验证一下。咱们用下面这个简单的例子来演示一下。

public static void main(String[] args) throws Exception{
        Object o = new Object();
        System.out.println("object 1 no biasedLock");
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        Thread.sleep(5000);//休眠时间超过-XX:BiasedLockingStartupDelay默认值4秒
        Object o2 = new Object();
        System.out.println("object 2 biasedLock opened");
        System.out.println(ClassLayout.parseInstance(o2).toPrintable());
    }

咱们来看下执行结果:(看到这里,记得回头再看看上面加了synchronized关键字后的对象布局状况) watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk= JVM的锁机制到这里,大体就已经弄完了。把这些细节搞明白了,至少java面试官就不用怕了吧。

 

最后

  一直想整理出一份完美的面试宝典,可是时间上一直腾不开,这套一千多道面试题宝典,结合今年金三银四各类大厂面试题,以及 GitHub 上 star 数超 30K+ 的文档整理出来的,我上传之后,毫无心外的短短半个小时点赞量就达到了 13k,说实话仍是有点难以想象的。

一千道互联网 Java 工程师面试题

内容涵盖:Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、Redis、MySQL、Spring、SpringBoot、SpringCloud、RabbitMQ、Kafka、Linux等技术栈(485页)

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

《Java核心知识点合集(283页)》

内容涵盖:Java基础、JVM、高并发、多线程、分布式、设计模式、Spring全家桶、Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、MongoDB、Redis、MySQL、RabbitMQ、Kafka、Linux、Netty、Tomcat、数据库、云计算等

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

《Java中高级核心知识点合集(524页)》

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

《Java高级架构知识点整理》

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk= watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

 因为篇幅限制,详解资料太全面,细节内容太多,因此只把部分知识点截图出来粗略的介绍,每一个小节点里面都有更细化的内容!

须要的小伙伴,能够一键三连,点击这里获取免费领取方式

相关文章
相关标签/搜索