前言首先,什么是synchronized?我们需要给一个明确的定义——同步锁,没错,就是锁。它可以用来做什么?当然,锁是用于线程之间的同步和临界区资源的保护。我们知道锁是一个很笼统的概念。生活中有指纹锁、密码锁等。synchronized所代表的锁是一种什么样的锁呢?答案是——Java内置锁。在Java中,每个对象中都隐藏着一把锁,synchronized关键字就是激活这把隐式锁的句柄(开关)。让我们先简单看一下同步。我们知道有3种使用方式:使用synchronized修饰静态方法:锁定当前类,作用于该类的所有实例。修改后的非静态方法:只会锁定当前类的实例。修改后的代码块:该方法接受一个对象作为参数,锁定对象为对象。使用方法这里不再赘述,详细用法大家可以自行搜索,不是本文的内容。知道了synchronized的概念,回过头来看题目,它说的lockupgrade到底是什么?不熟悉锁升级的人可能会想:所谓锁,不就是点一下锁就完了吗?升级是什么?这与玩扑克无关。熟悉的人可能会想:这不就是“无锁==>偏向锁==>轻量级锁==>重量级锁”吗?大家可能在很多地方都看到过上面介绍的锁升级过程,也可以直接背下来。但是你真的知道无锁、偏向锁、轻量级锁、重量级锁是什么意思吗?这些锁存放在哪里?而在什么情况下,锁会升级到一个新的等级呢?想要知道答案,我们似乎要先搞清楚Java中内置的锁,它的内部结构是怎样的?内置锁存放在哪里?答案在开头就提到了——在Java对象中。所以现在的问题已经从“内置的锁结构是什么”变成了“Java对象长什么样子”。对象结构从宏观上看,Java对象的结构非常简单,分为三部分:Java对象结构从微观上看,每一部分都可以进一步展开。详见下图:Java对象结构详解接下来,我们将深入探讨这三个部分。从思维导图中可以看出,对象头由三个字段组成:MarkWord、ClassPointer、数组长度。简单来说:MarkWord:主要用来存放自己的运行时数据。类指针:是指向方法区中类的对象的指针。JVM使用这个字段来确定当前对象是哪个类的实例。数组长度:只有当对象是数组时,该字段才会存在。ClassPointer和arraylength没什么好说的。接下来,我们将重点介绍MarkWord。MarkWord所代表的“运行时数据”,主要用来表示当前Java对象的线程锁状态和GC标志。线程锁的状态有无锁、偏向锁、轻量级锁、重量级锁。所以上面说的四种状态其实就是Java内置锁的不同状态。在JDK1.6之前,内置锁都是重量级锁,效率低下。效率低下体现在JDK1.6之后为了提高synchronized的效率引入了偏向锁和轻量级锁。随着锁的竞争越来越激烈,它的状态会逐渐向“无锁==>偏向锁==>轻量级锁==>重量级锁”的方向升级,并且是不可逆的。只能进行锁升级,不能进行锁降级。接下来,我们来思考一个问题。既然MarkWord可以代表4种不同的锁状态,那它们内部是怎么区分的呢?(因为目前主流的JVM都是64位的,我们只讨论64位的MarkWord)那我们通过图片来直观感受一下。(1)Lock-free和lock-free这可以理解为单个线程快乐的运行着,没有其他线程与之竞争。(2)偏向锁偏向锁首先,什么是偏向锁?比如一段同步代码,一直只有线程A访问过,既然没有其他线程竞争,那么每次去获取锁岂不是浪费资源?所以在这种情况下,线程A会自动进入偏向锁状态。当后续线程A再次访问同步代码时,不需要做任何检查,直接执行(对线程的“偏好”),降低了获取锁的成本,提高了效率。看到这里,你会发现无锁和偏向锁的锁标志位是一样的,也就是都是01。这是因为无锁和偏向锁是通过字段biased_lock来区分的。0表示不使用偏向锁,1表示启用偏向锁。你为什么这么做?你可以这样理解,无锁和偏向锁本质上都可以理解为无锁(参考上面提到的线程A的状态),所以锁的标志位为01是没有错的。PS:线程这里的ID是持有当前对象偏向锁的线程(3)LightweightlockLightweightlock但是,一旦有第二个线程参与竞争,它会立即展开为轻量级锁。试图抢占的线程最初将使用自旋:方法来尝试获取锁。如果循环多次,其他线程释放了锁,就没有必要从用户态切换到内核态。即便如此,旋转还是需要大量的CPU资源(理解汽车怠速疯狂踩油门)。如果另一个线程永远不会释放锁,它会一直在这里闲置吗?当然,这是不可能的。JDK1.7之前是普通自旋,会设置最大自旋次数。默认为10次。停止旋转。在JDK1.7之后,引入了自适应自旋。简单的说:如果这次自旋获得了锁,那么自旋的次数就会增加;如果本次自旋没有获取到锁,则自旋次数会减少。(4)重量级锁重量级锁上面说过,当试图抢占的线程的自旋达到阈值后,就会停止自旋,此时锁会膨胀为重量级锁。当它扩展为重量级锁时,其他竞争线程进来时不会自旋,而是直接阻塞等待,MarkWord中的内容会变成一个monitor对象,用来统一管理排队线程。这个监控对象,每个对象都会关联一个。监视器对象本质上是一种同步机制,确保只有一个线程可以同时进入临界区。在HotSpot虚拟机中,它是由C++类ObjectMonitor实现的。那么monitor对象是如何管理线程的呢?接下来我们看一下ObjectMonitor类的几个关键属性:ContentionQueue:是一个队列,所有竞争锁的线程都会先进入这个队列,可以理解为线程的统一入口。线程会阻塞。EntryList:ContentionQueue中符合条件的线程会被移到这里,相当于初步筛选了一轮,进来的线程会被阻塞。Owner:拥有当前监视器对象的线程,即持有锁的线程。OnDeck:对于与Owner线程竞争的线程,同一时间只有一个OnDeck线程在竞争。WaitSet:当Owner线程调用wait()方法被阻塞时,会放在这里。当它被唤醒后,会重新进入EntryList,这个集合的线程会被阻塞。count:用来实现可重入锁,synchronized是可重入的。对象体对象体包含当前对象的字段和值,ul是业务中的核心部分。对齐字节纯粹用于填充,没有其他业务意义。它的目的是保证对象占用的内存大小是8的倍数,因为HotSpotVM的内存管理要求对象的起始地址必须是8的倍数。锁升级了解四种锁状态后,我们可以整体来看一下锁的升级过程。线程A进入synchronized,开始抢锁。JVM会判断当前是否处于偏向锁状态。如果是,则根据MarkWord中保存的线程ID判断当前线程A是否为持有偏向锁的线程。如果是,则忽略检查,线程A直接执行临界区中的代码。但是如果MarkWord中的线程不是线程A,它就会尝试通过自旋来获取锁。如果获取到,则将MarkWord中的线程ID改成自己的;如果竞争失败,则立即撤销偏向锁,扩展为轻量级锁。随后的竞争线程将尝试通过自旋来获取锁。如果自旋成功,锁的状态仍然是轻量级锁。但是,如果竞争失败,锁会膨胀成重量级锁,后续正在等待的竞争线程会被阻塞。在锁升级过程EOF中,其实还有一个偏向锁的撤销过程,这个过程也是有代价的,但是相对于偏向锁的好处来说,是可以接受的。但是这里我们关注的仍然是锁升级的具体逻辑和细节,锁升级的过程就此打住。
