【Java】锁机制

时间:2019-10-10
本文章向大家介绍【Java】锁机制,主要包括【Java】锁机制使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

参考

https://blog.csdn.net/varyall/article/details/79698145

《深入理解Java虚拟机》

锁状态:无锁、偏向锁、轻量级锁、重量级锁(具体是何种状态,取决于竞争情况

这三个只是代表锁的状态。而非动作。

首先了解为什么锁的是对象

HotSpot虚拟机的对象的内存分布主要分为三个部分:

  • 对象头(分为两部分)

    1. 第一部分用于存储对象自身的运行时数据,如哈希吗,GC分代年龄(又称为Mark Word)

    2. 第二部分用于存储指向方法区对象类型数据的指针。即是对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据。

  • 实例数据(真正存储有效信息)

  • 对齐填充(非必须,仅起着占位符的作用)

    HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,即对象大小必须是8字节的整数倍。而对象头正好是8字节的整数倍,而对象实例数据部分没有对齐,需要通过对齐填充来补全。

虚拟机通过Mark Word的信息来区分锁的状态:

Epoch:偏向时间戳

轻量级锁、重量级锁、偏向锁

这四种状态都不是Java语言中的锁,而是Jvm为了提高锁的获取与释放效率而做的优化

四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。

  • 轻量级锁:本意是在没有多线程竞争的前提下,通过CAS操作而避免了使用互斥量的开销。

    在只有两个线程的情况下,提高性能。

  • 偏向锁:大多数情况下,锁不存在多线程竞争,而是总是由同一线程多次获得时,为了使线程获得锁的代价更低而引入了偏向锁。提高了带有同步,但无竞争的程序性能。

    在只有一个线程的情况下,进一步提高性能。

轻量级锁的原理

  1. 虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间。

  2. 将还没有被锁定的同步对象的对象头中的Mark Word拷贝到锁记录空间中

  3. 拷贝成功后,使用CAS操作将对象头中的Mark Word修改为一个指针(指向此线程的锁记录空间)

  4. 如果CAS操作成功,这个线程就拥有了该对象锁,并将锁标志“01”改为“00”,即表示此对象处于轻量级锁的状态。此时,如果有另一个线程竞争轻量级锁,则会发生自旋。

  5. 如果失败了,说明这个Mark Word已经被别的线程拿走了,如果存在两条以上的线程争夺同一个锁,那么轻量级锁就会膨胀为重量级锁。锁标志则被修改为“10”。膨胀之后,不可逆转。变成重量级锁之后,其他竞争失败的线程不会自旋,而是阻塞。(大量线程自旋,极大浪费CPU)

解锁过程:

  1. 依然是CAS操作,将上面被替换的Mark Word再替换回对象头中。

  2. 替换失败,说明轻量级锁已经膨胀成了重量级锁,所以CAS操作替换失败,此时,一定有其他线程在抢夺锁的过程中被阻塞,这时候,释放锁的同事,唤醒被挂起的线程。

偏向锁

轻量级锁的获取和释放都依赖于CAS原子操作,而偏向锁是在无竞争的情况下,连CAS操作都不做了。只有在需要置换ThreadID的时候,执行一次CAS原子操作。在只有一个线程执行同步块的情况下,进一步提高性能。

偏向锁的获取过程:

  1. 在对象第一次被线程获取的时候,查看Mark Word中偏向锁标志是否为1,锁标志是否为01。是否已经确定Thread ID,如果已经确定,则不需要CAS操作,直接执行同步代码块。

  2. 如果没有确定Thread ID,则通过CAS操作竞争锁,成功:则将Mark Word中的Thread ID设置为当前线程ID,然后执行同步代码块

  3. 如果竞争失败:则说明有线程一同竞争,偏向锁会升级为轻量级锁。竞争失败的线程自旋。

偏向锁的释放:

如果没有竞争,线程不会主动释放偏向锁,只有在有竞争的情况下,偏向锁会被撤销,提升为轻量级锁。

三种锁的转化

三种锁对比

自旋锁

线程的阻塞,挂起,唤醒,都会给系统性能带来压力。

有时候线程只需要等待很短的时间,这个时间将线程挂机,就很不值得。

所以自旋技术:让线程执行一个忙循环(自旋),稍等一下。这就是自旋锁。

-XX:+UseSpinning开启自旋锁。JDK1.6之后,默认开启。

自旋锁的效果:避免了线程切换的开销,但是在自旋过程中要占用CPU。如果锁被占用很短,自旋时间很短,自旋等待的效果就会很好,反之,如果锁占用时间长,自旋就会很浪费资源。

JDK1.6之前自旋默认循环次数为10次。可以使用参数-XX:PreBlockSpin来修改。

JDK1.6之后,次数不在固定,可以自适应,由同一个锁的前一个自旋时间来决定。比如:上一个等待此锁的线程自旋了多久,这次系统就会让此线程自旋多久。

锁消除

对于堆内存中永远不会逃逸,不会被其他线程访问到的变量,就认为是线程私有的,无需同步加锁。

虚拟机即时编译器在运行时,如果检测到不可能存在共享数据竞争的锁,说明这个锁无意义,就会自动进行消除这个锁。这就是锁消除。

// 看起来没有同步的方法
public String concatString(String str1,String str2,String str3){
    return str1 + str2 + str3;
}
// 在JDK1.5之前,会自动转化为StringBuffer进行append()操作
public String concatString(String str1,String str2,String str3){
    StringBuffer sb = new StringBuffer();
    sb.append(str1);
    sb.append(str2);
    sb.append(str3);
    return sb.toString();
}

锁粗化

原则上,将同步块的作用范围限制的尽量小,在多线程中的互斥同步的操作数量就会尽可能的小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。

大部分情况下,这是正确的。但是,如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作在循环体中,即使没有竞争,频繁互斥同步也会导致不必要的性能损耗。

锁粗化:如果虚拟机检测到有这样一连串零碎操作(如上面的代码)都对同一个对象加锁,将会把加锁同步的范围扩大到整个操作序列的外部。

原文地址:https://www.cnblogs.com/mussessein/p/11651148.html