局部变量表存放了编译器可知的各种 Java 虚拟机基本数据类型、对象引用和 returnAddress 类型(指向了一条字节码指令的地址)
这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示。局部变量表所需的内存空间在编译期完成分配,进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
无论从什么角度看,无论如何划分 Java 堆的内存区域,都不会改变 Java 堆中存储内容的共性,无论是哪个区域,存储的都只能是对象的实例,将 Java 堆细分的目的只是为了更好的回收内存,或者更快的分配内存
在 JDK 1.4 中新加入了 NIO 类,引入了一种基于通道( Channel )与缓冲区( Buffer )的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据
在 HotSpot 虚拟机里,对象在堆内存中的存储布局可以划分为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
HotSpot 虚拟机对象的对象头部分包括两类信息。
对象的实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录下来。
对象的第三部分是对齐填充,这并不是必然存在的,也没有特别的含义,仅仅是起着占位符的作用
主流的访问方式主要有使用 句柄 和 直接指针 两种:
使用句柄来访问的最大好处就是 reference 中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改。
使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在 Java 中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本。
对于 HotSpot 虚拟机而言,主要使用第二种方式进行对象访问。
面试题:说一说 Java 中的内存溢出与栈内存溢出
Java 堆用于存储对象实例,我们只要不断地创建对象,并且保证 GC Roots 到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常,也就是 OutOfMemoryError。
常规的处理方法是首先通过内存映像分析工具对 Dump 出来的堆转储快照进行分析。第一步首先确认内存中导致 OOM 的对象是否是必要的,也就是要先分清楚到底是出现了 内存泄漏 还是 内存溢出 。
如果是内存泄漏,可进一步通过工具查看泄漏对象到 GC Roots 的引用链,找到泄露对象是通过怎样的引用路径、与哪些 GC Roots 相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到 GC Roots 引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找到产生内存泄漏的代码的具体位置。
如果不是内存泄漏,换句话说就是内存中的对象确实都是必须存活的,那就应当检查 Java 虚拟机参数( -Xmx 与 -Xms )设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象声明周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。
如果线程请求的深度大于虚拟机所允许的最大深度(栈帧太大或者虚拟机栈容量太小),将抛出 StackOverflowError 异常
出现 StackOverflowError 异常时,会有明确错误栈帧可供分析,相对而言比较容易定位到问题所在。
可能原因有:
判断对象是否存活有两者算法:
在 Java 领域,主流的 Java 虚拟机一般都使用 可达性分析算法 来判断一个对象是否可以被回收了
Java 中可以作为 GC Roots 的对象:
因为有些对象 “食之无味,弃之可惜”,所以在 JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为 强引用(Strongly Reference)、软引用(Soft Reference)、**弱引用(Weak Reference)**和 虚引用(Weak Reference),这四种引用强度依次逐渐减弱
Object obj = new Object();
这种引用关系。无论在任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。SoftReference
类来实现软引用。WeakReference
类来实现弱引用。PhantomReference
类用以实现虚引用。方法区垃圾收集的 “性价比” 通常是比较低的:在 Java 堆中,尤其是在新生代中,对常规应用进行一次垃圾收集通常可以回收 70% 至 99% 的内存空间,相比之下,方法区回收囿于苛刻的判定条件,其区域垃圾收集的回收成果往往远低于此。
方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。
回收废弃常量与回收 Java 堆中的对象非常类似,当一个常量对象不再任何地方被引用的时候,则被标记为废弃常量,这个常量就可以被回收。
而判断一个类型是否属于 “不再被使用的类” 的条件就比较苛刻了。需要同时满足下面三个条件:
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。Java 虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是 “被允许”,而并不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot 虚拟机提供了 -Xnoclassgc 参数进行控制。
首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。标记的过程就是对象是否属于垃圾的判定过程。
后续的收集算法大多都是以标记-清除算法为基础,对其缺点进行改进而得到的,它的缺点主要有两个:
标记-复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
主要缺点:
更优化的半区复制分代策略–Appel 式回收
Appel 式回收的具体做法是把新生代分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次分配内存只使用 Eden 和其中一块 Survivor 。发生垃圾搜集时,将 Eden 和 Survivor 中仍然存活的对象一次性复制到另外一块 Survivor 空间上,然后清理掉 Eden 和已用过的那块 Survivor 空间。
HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8 : 1,当然这是基本普通场景下 “新生代中的对象有 98% 熬不过第一轮收集”。
当然 Appel 式回收还有一个充当罕见情况的 “逃生门” 的安全设计,当 Survivor 空间不足以容纳一次 Minor GC 之后存活的对象时,就需要依赖其它内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion)。
其中的标记过程仍然与 “标记-清除” 算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后清理掉边界以外的内存
标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策:
和稀泥式的解决方案:让虚拟机平时多数时间采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法,以获得规整的内存空间。
HotSpot 使用一组称为 OopMap 的数据结构根节点的枚举,一旦类加载动作完成的时候,HotSpot 就会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。这样收集器在扫描时就可以直接得知这些信息了,并不需要真正一个不漏地从方法区等 GC Roots 开始查找。
HotSpot 并没有为每条指令都生成 OopMap,因为如果每条指令都生成对应的 OopMap,将会需要大量的额外存储空间,这样垃圾收集伴随而来的空间成本将非常高。所以 HotSpot 只有在特定的位置会生成 OopMap ,这些位置被称为 安全点(SafePoint)。
有了安全点的设定,也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停。适合的安全点例如方法调用、循环跳转、异常跳转等。
对于如何在垃圾收集发生时让所有线程都跑到最近的安全点,有两种方案:
使用安全点可能会带来的问题:当用户线程处于 Sleep 状态或者 Blocked 状态,这时候线程无法响应虚拟机的中断请求,不能再走到安全的地方去中断挂起自己,虚拟机也显然不可能持续等待线程重新被激活分配处理器时间
基于此种情况,引入安全区域(Safe Region)来解决
安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。
当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,那样当这段时间里虚拟机要发起垃圾收集时就不必去管这些已经声明自己在安全区域内的线程了。当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚举,如果完成了,那线程就可以继续执行;否则它就必须一直等待,直到收到可以离开安全区域的信号为止。
记忆集是一种记录从非收集区域指向收集区域的指针集合的抽象数据结构。
在垃圾收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针,所以采用粒度比较大的 卡表 来进行实现
这里的写屏障与解决并发乱序执行问题中的 “内存屏障” 并不是一个概念
在 HotSpot 虚拟机中是通过写屏障( Write Barrier )技术维护卡表状态的。
写屏障可以看作在虚拟机层面对 “引用类型字段赋值” 这个动作的 AOP 切面,在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作,也就是说赋值的前后都在写屏障的覆盖范畴内。在赋值前的部分的写屏障叫做写前屏障,在赋值后的则叫做写后屏障。
除了写屏障的开销外,卡表在高并发场景下还面临着 “伪共享” 问题,伪共享是处理并发底层细节时一种经常需要考虑的问题,现代处理器的缓存系统中是以缓存行(Cache Line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低,这就是伪共享问题。
一个类型从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期将会经历 加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading) 七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)
“加载”(Loading) 阶段是整个 “类加载” (Class Loading)过程中的第一个阶段,在加载阶段,Java 虚拟机需要完成以下三件事情:
验证是连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机自身的安全。
验证阶段大致会完成四个方面的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证
准备阶段是正式为类中定义的变量(即静态变量,被 static 修饰的变量)分配内存并设置类变量初始值的阶段。
要注意,一般情况下变量在准备阶段过后的初始值为该数据类型的默认值(比如 int -> 0,boolean -> false),但是如果该字段是由 final 关键字定义的常量值,那在准备阶段变量值就会被初始化为所指定的初始值。
解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程。
进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。我们也可以从另外一种更直接的形式来表达:初始化阶段就是执行类构造器 () 方法的过程。
Java 虚拟机设计团队有意把类加载阶段中的 “通过一个类的全限定名来获取描述该类的二进制字节流” 这个动作放到了 Java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称作 “类加载器”(Class Loader)。
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都有一个独立的类名称空间。即使两个类来源于同一个 Class 文件,被同一个 Java 虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。
双亲委派模型的 工作过程 是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
使用双亲委派模型来组织类加载之间的关系,一个显而易见的 好处 就是 Java 中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object ,它存放在 rt.jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都能够保证是同一个类。
Java 中泛型的实现方式叫做 “类型擦除式泛型”,它只在程序源码中存在,在编译后的字节码文件中,全部泛型都被替换为原来的裸类型(Raw Type),并且在元素访问时插入了从 Object 到相应类型的强制转型代码,因此对于运行期的 Java 语言来说,ArrayList 与 ArrayList 其实是同一个类型。
Java 中有不少语法糖,泛型、自动装箱、自动拆箱、遍历循环、变长参数、条件编译、内部类、枚举类、断言语句、数值字面量、对枚举和字符串的 switch 支持、try语句中定义和关闭资源、Lambda 表达式等等。
当一个变量被定义成 volatile 之后,它将具备两项特性:
大多数场景下 volatile 的总开销仍然要比锁来的更低。我们在 volatile 与锁中选择的唯一判断依据仅仅是 volatile 的语义能否满足使用场景的需求。
Java 内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这三个特征来建立的。
由 Java 内存模型来直接保证的原子性操作包括read、load、assign、use、store 和 write 这六个,所以我们大致可以认为,基本数据类型的访问、读写都是具备原子性的。
另外如果需要一个更大范围的原子性保证,虚拟机提供了 monitorenter 和 monitorexit 来隐式地使用这两个操作。这两个字节码指令反映到 Java 代码中就是同步块-- synchronized 关键字,因此在 synchronized 块之间的操作也具备原子性。
可见性就是指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。
Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是 volatile 变量都是如此。普通变量与 volatile 变量的区别是,volatile 的特殊规则保证了新值能立即同步到主内存,以及每次使用前j立即从主内存刷新。因此我们可以说 volatile 保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。
除了 volatile 之外,synchronized 和 final 这两个关键字也能实现可见性。
Java 程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指:“线程内似表现为串行的语义”,后半句是指 “指令重排序” 现象和 “工作内存与主内存同步延迟” 现象。
Java 语言提供了volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 关键字本身就包含了禁止指令重排序的语义,而 synchronized 则是由 “一个变量在同一时刻只允许一条线程对其进行 lock 操作” 这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入。
先行发生原则是指在 Java 内存模型中定义的两项操作之间的偏序关系,比如说操作 A 先行发生于操作 B ,其实就是说发生操作 B 之前,操作 A 产生的影响能被操作 B 观察到,“影响” 包括修改了内存中共享变量的值、发送了消息、调用了方法等。
以下是 Java 内存模型下一些 “天然的” 先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来,则它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序。
程序次序规则
管程锁定规则
volatile 变量规则
线程启动规则
线程终止规则
线程中断规则
对象终结规则
传递性
线程调度是指系统为线程分配处理器使用权的过程,调度主要方式有两种,分别是协同式线程调度和抢占式线程调度。
Java 语言定义了 6 种线程状态,在任意一个时间点中,一个线程只能有且只有其中的一种状态,并且可以通过特定的方法在不同状态之间转换。
当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果那就称这个对象是线程安全的。
互斥同步是一种最常见也是最主要的并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条(或者一些,当使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是常见的互斥实现方式。
在 Java 里面,最基本的互斥同步手段就是 synchronized 关键字,这是一种块结构的同步语法。synchronized 关键字经过 Javac 编译之后,会在同步块的前后分别形成 monitorenter 和 monitorexit 这两个字节码指令。这两个字节码指令都需要一个 reference 类型的参数来指明要锁定和解锁的对象。如果 Java 源码中的 synchronized 明确指定了对象参数,那就以这个对象的引用作为 reference ;**如果没有明确指定,那将根据 synchronized 修饰的方法类型(如实例方法或者类方法),来决定是取代码所在的对象实例还是取类型对应的 Class 对象来作为线程要持有的锁。**另外 synchronized 是可重入锁。
除了 synchronized 关键字以外,JDK 5 之后新增的 JUC 下的 Lock 接口是 Java 中另外一种全新的互斥同步手段。基于 Lock 接口,用户能够以非块结构来实现互斥同步,从而摆脱了语言特性的束缚,改为在类库层面去实现同步,这也为日后扩展出不同调度算法、不同特征、不同性能、不同语义的各种锁提供了广阔的空间。
重入锁(ReentrantLock)是 Lock 接口最常见的一种实现,顾名思义,它与 synchronized 一样是可重入的。不过,ReentrantLock 与 synchronized 相比增加了一些高级功能,主要有以下三项:等待可中断、可实现公平锁以及锁可以绑定多个条件。
基于以下理由,推荐在 synchronized 与 ReentrantLock 都可满足需要时优先使用 synchronized:
互斥同步面临的主要问题是进行线程阻塞和唤醒所带来的性能开销,因此这种同步也被称为阻塞同步,它属于一种悲观的并发策略,无论共享的数据是否真的会出现竞争,它都会进行加锁,这将会导致用户态到核心态的转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等开销。
随着硬件指令集的发展,我们有了另外一个选择:基于冲突检测的乐观并发策略,通俗地说就是不管风险,先进性操作,如果没有其他线程争用共享数据,那操作就直接成功了;如果共享的数据的确被争用,产生了冲突,那再进行其他的补偿措施,最常用的补偿措施就是不断地重试,直到出现没有竞争的共享数据为止。这种乐观并发策略的实现不再需要把线程阻塞挂起,因此这种同步操作被称为非阻塞同步,使用这种措施的代码也常称为无锁编程
前面说的互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给 Java 虚拟机的并发性能带来了很大的压力。
如果物理机器上有一个以上的处理器或者处理器核心,能让两个或以上的线程同时执行,我们就可以让后面请求锁的那个线程 “稍等一会儿” ,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只须让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,所以如果锁被占用的时间很短,自旋等待的效果就会非常好,反之如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有价值的工作,这就会带来性能的浪费。因此自旋等待必须有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程。
JDK 6 中对自旋锁进行了优化,引入了自适应的自旋。自适应意味着自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间及锁的拥有者状态来决定的。
锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是被检测到不可能存在共享数据的锁进行消除,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须再进行。
如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能消耗。
如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。
在代码即将进入同步块的时候,如果此同步对象没有被锁定(锁标志位为 “01” 状态),虚拟机首先将当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝。
然后,虚拟机将使用 CAS 操作尝试把对象的 Mark Word 更新为指向 Lock Record 的指针。如果这个更新动作成功了,即代表该线程拥有了这个对象的锁,并且对象 Mark Word 的锁标志位将转变为 “00”,表示此对象处于轻量级锁定状态。
如果这个更新操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,那直接进入同步块继续执行就可以了,否则就说明这个锁对象已经被其他线程抢占了。如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁,锁标志的状态值变为 “10”,此时 Mark Word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也必须进入阻塞状态。
轻量级锁能提升程序同步性能的依据是 “对于绝大部分的锁,在整个同步周期内都是不存在竞争的” 这一经验法则。如果没有竞争,轻量级锁便通过 CAS 操作成功避免了使用互斥量的开销;但如果确实存在锁竞争,除了互斥量的本身开销外,还额外发生了 CAS 操作的开销。因此有竞争的情况下,轻量级锁反而会比传统的重量级锁更慢。
这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。
当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设置为 “01” 、把偏向模式设置为 “1”,表示进入偏向模式。同时使用 CAS 操作把获取到这个锁的线程的 ID 记录在对象的 Mark Word 之中。如果 CAS 操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如加锁、解锁以及对 Mark Word 的更新操作等)。
一旦出现另外一个线程去尝试获取这个锁的情况,偏向模式就马上宣告结束。根据锁对象目前是否处于被锁定的状态决定是否撤销偏向(偏向模式设置为 “0”),撤销后标志位恢复到未锁定(标志位为 “01”)或轻量级锁定(标志位为 “00”)的状态,后续的同步操作如同上面的轻量级锁那样去执行。
开销外,还额外发生了 CAS 操作的开销。因此有竞争的情况下,轻量级锁反而会比传统的重量级锁更慢。
这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。
当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设置为 “01” 、把偏向模式设置为 “1”,表示进入偏向模式。同时使用 CAS 操作把获取到这个锁的线程的 ID 记录在对象的 Mark Word 之中。如果 CAS 操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如加锁、解锁以及对 Mark Word 的更新操作等)。
一旦出现另外一个线程去尝试获取这个锁的情况,偏向模式就马上宣告结束。根据锁对象目前是否处于被锁定的状态决定是否撤销偏向(偏向模式设置为 “0”),撤销后标志位恢复到未锁定(标志位为 “01”)或轻量级锁定(标志位为 “00”)的状态,后续的同步操作如同上面的轻量级锁那样去执行。