为了账号安全,请及时绑定邮箱和手机立即绑定

synchronized底层原理 CAS原理 java对象的布局 对象头 偏向锁 轻量级锁 自旋锁 锁消除 锁粗化 面试

标签:
Java JVM

CAS

CAS概述和作用

CAS的全称是: Compare And Swap(比较相同再交换)。是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。

CAS的作用:

  • CAS可以将比较和交换转换为原子操作,共享变量赋值时的原子操作。

  • CAS操这个原子操作直接由CPU保证。

  • CAS可以保证作依赖3个值:内存中的值V,旧的预估值X,要修改的新值B,如果旧的预估值X等于内存中的值V,就将新的值B保存到内存中。

CAS和volatile实现无锁并发

public class Demo01 {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger();
        Runnable mr = () -> {
            for (int i = 0; i < 1000; i++) {
                atomicInteger.incrementAndGet();
            }
        };
        ArrayList<Thread> ts = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(mr);
            t.start();
            ts.add(t);
        }
        for (Thread t : ts) {
            // join()的作用是:“等待该线程终止”,
            // 这里需要理解的就是该线程是指的主线程等待子线程的终止。
            // 也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。
            t.join();
        }
        System.out.println("number = " + atomicInteger.get());
    }
}

AtomicInteger源码

/**
 * Atomically increments by one the current value.
 *
 * @return the updated value
 */
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

UnSafe.class类

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

CAS原理

通过刚才AtomicInteger的源码我们可以看到,Unsafe类提供了原子操作。

Unsafe类介绍

Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。Unsafe对象不能直接调用,只能通过反射获得。

Unsafe实现CAS

乐观锁和悲观锁

悲观锁从悲观的角度出发:

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这

样别人想拿这个数据就会阻塞。因此synchronized我们也将其称之为悲观锁。JDK中的ReentrantLock

也是一种悲观锁。性能较差!

乐观锁从乐观的角度出发:

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,就算改了也没关系,再重试即可。所

以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去修改这个数据,如何没有人修改则更

新,如果有人修改则重试。

CAS这种机制我们也可以将其称之为乐观锁。综合性能较好!

CAS获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。结合CAS和volatile可以

实现无锁并发,适用于竞争不激烈、多核 CPU 的场景下。

  1. 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一。

  2. 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响。

总结

CAS的作用? Compare And Swap,CAS可以将比较和交换转换为原子操作,这个原子操作直接由处理

器保证。

CAS的原理?CAS需要3个值:

内存地址memoryValue,旧的预期值expectedValue ,要修改的新值newValue,如果内存地址memoryValue和旧的预期值expectedValue 相等就修改内存地址值(memoryValue)为newValue

如果 memoryValue==expectedValue

那么 memoryValue=newValue

synchronized锁升级过程

高效并发是从JDK 5到JDK 6的一个重要改进,HotSpot虛拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,包括偏向锁( Biased Locking )轻量级锁( Lightweight Locking )和如适应性自旋(Adaptive Spinning)锁消除( Lock Elimination)、**锁粗化( Lock Coarsening )**等,这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。

无锁–》偏向锁–》轻量级锁–》重量级锁

升级到重量级锁之前,会进行适应性自旋尝试获取锁,失败后,再升级为重量级锁.

Java对象的布局

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下图所示:
图片描述

对象头
当一个线程尝试访问synchronized修饰的代码块时,它首先要获得锁,那么这个锁到底存在哪里呢?是
存在锁对象的对象头中的。
HotSpot采用**instanceOopDesc(普通对象头)arrayOopDesc(数组对象头)**来描述对象头,arrayOopDesc对象用来描述数组类
型。instanceOopDesc的定义的在Hotspot源码的 instanceOop.hpp 文件中,另外,arrayOopDesc的定义对应 arrayOop.hpp

class instanceOopDesc : public oopDesc {
public:
    // aligned header size.
    static int header_size() {
        return sizeof(instanceOopDesc)/HeapWordSize; }
        // If compressed, the offset of the fields of the instance may not be aligned.
        static int base_offset_in_bytes() {
        // offset computation code breaks if UseCompressedClassPointers
        // only is true 
        return (UseCompressedOops && UseCompressedClassPointers) ? klass_gap_offset_in_bytes() : sizeof(instanceOopDesc); 
    }
    static bool contains_field_offset( int offset, int nonstatic_field_size) { 
        int base_in_bytes = base_offset_in_bytes(); 
        return (offset >= base_in_bytes && (offset-base_in_bytes) < nonstatic_field_size * heapOopSize); 
    } 
};

从instanceOopDesc代码中可以看到 instanceOopDesc继承自oopDesc,oopDesc的定义载Hotspot源码中的 oop.hpp 文件中。

class oopDesc {
    friend class VMStructs;

private:
    volatile markOop _mark;
    union _metadata {
        Klass *_klass;
        narrowKlass _compressed_klass;
    } _metadata;
    // Fast access to barrier set. Must be initialized. 
    static BarrierSet *_bs;
    // 省略其他代码 
};

在普通实例对象中,oopDesc的定义包含两个成员,分别是 _mark 和 _metadata

_mark 表示对象标记、属于markOop类型,也就是接下来要讲解的Mark World,它记录了对象和锁有关的信息

_metadata 表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中Klass表示普通指针、 _compressed_klass 表示压缩类指针。

对象头由两部分组成,一部分用于存储自身的运行时数据,称之为 Mark Word,另外一部分是类型指
针,及对象指向它的类元数据的指针。

//
//  32 bits:
//  --------
//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
//             size:32 ------------------------------------------>| (CMS free block)
//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)
//
//  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
//  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
//  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
//  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
//
//    [JavaThread* | epoch | age | 1 | 01]       lock is biased toward given thread
//    [0           | epoch | age | 1 | 01]       lock is anonymously biased
//
//  - the two lock bits are used to describe three states: locked/unlocked and monitor.
//
//    [ptr             | 00]  locked             ptr points to real header on stack
//    [header      | 0 | 01]  unlocked           regular object header
//    [ptr             | 10]  monitor            inflated lock (header is wapped out)
//    [ptr             | 11]  marked             used by markSweep to mark an object
//                                               not valid at any other time

图片描述

在 64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:
图片描述

在32位虚拟机下,Mark Word是32bit大小的,其存储结构如下:
图片描述

klass pointer

  • 这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。
  • 该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。
  • 如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项-XX:+UseCompressedOops 开启指针压缩,其中,oop即ordinary

object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:

  1. 每个Class的属性指针(即静态变量)
  2. 每个对象的属性指针(即对象变量)
  3. 普通对象数组的每个元素指针

当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。

对象头 = Mark Word + 类型指针(未开启指针压缩的情况下)
在32位系统中,Mark Word = 4 bytes,类型指针 = 4bytes,对象头 = 8 bytes = 64 bits;
在 64位系统中,Mark Word = 8 bytes,类型指针 = 8bytes,对象头 = 16 bytes = 128bits;

实例数据
就是类中定义的成员变量。

对齐填充
对齐填充并不是必然存在的,也没有什么特别的意义,他仅仅起着占位符的作用,由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头正好是8字节的倍数,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

查看Java对象布局

<dependency>
  <groupId>org.openjdk.jol</groupId>
  <artifactId>jol-core</artifactId>
  <version>0.9</version>
</dependency>
public class LockObj {
    private int x;
    private boolean b;
}
import org.openjdk.jol.info.ClassLayout;

public class Demo01 {
    public static void main(String[] args) {
        LockObj obj = new LockObj();

        obj.hashCode();
        System.out.println(obj.hashCode());
        System.out.println(Integer.toHexString(obj.hashCode()));

        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
}
621009875
2503dbd3
com.itheima.demo06_object_layout.LockObj object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           01 d3 db 03 (00000001 11010011 11011011 00000011) (64738049)
      4     4           (object header)                           25 00 00 00 (00100101 00000000 00000000 00000000) (37)
      8     4           (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4       int LockObj.x                                 0
     16     1   boolean LockObj.b                                 false
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total


Process finished with exit code 0

可以看到 布尔类型占一个字节
还有7个字节是是对齐数据,刚好凑成8字节的整数倍

ps
图片描述

Jvm打印出来的是下面 倒着数的 如果你使用代码打印hashcode 会跟日志是反过来的. java api打印出来的16进制的hashcode是正着数的.

小结
Java对象由3部分组成,对象头,实例数据,对齐数据
对象头分成两部分:Mark World + Klass pointer

偏向锁
什么是偏向锁

偏向锁是JDK 6中的重要引进,因为HotSpot作者经过研究实践发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。

偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadID即可。

不过一旦出现多个线程竞争时必须撤销偏向锁,所以撤销偏向锁消耗的性能必须小于之前节省下来的CAS原子操作的性能消耗,不然就得不偿失了。

偏向锁原理
当线程第一次访问同步块并获取锁时,偏向锁处理流程如下:

  1. 虚拟机将会把对象头中的标志位设为“01”,即偏向模式。
  2. 同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中 ,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。

偏向锁的撤销

  1. 偏向锁的撤销动作必须等待全局安全点
  2. 暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
  3. 撤销偏向锁,恢复到无锁(标志位为 01)或轻量级锁(标志位为 00)的状态
    偏向锁在 Java 6之后是默认启用的,但在应用程序启动几秒钟之后才激活,可以使用 -
    XX:BiasedLockingStartupDelay=0 参数关闭延迟,如果确定应用程序中所有锁通常情况下处于竞争
    状态,可以通过 XX: -UseBiasedLocking=false 参数关闭偏向锁。
    偏向锁好处
    偏向锁是在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。偏向
    锁可以提高带有同步但无竞争的程序性能。
    它同样是一个带有效益权衡性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多
    数的锁总是被多个不同的线程访问比如线程池,那偏向模式就是多余的。
    在JDK5中偏向锁默认是关闭的,而到了JDK6中偏向锁已经默认开启。但在应用程序启动几秒钟之后才
    激活,可以使用 - XX:BiasedLockingStartupDelay=0 参数关闭延迟,如果确定应用程序中所有锁通常
    情况下处于竞争状态,可以通过 XX: -UseBiasedLocking=false 参数关闭偏向锁。
    小结
    偏向锁的原理是什么?
    偏向锁的好处是什么?
点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消