二、详解 synchronize 锁的升级

synchronized 锁定的是一个对象,执行某段代码的时候必须锁定一个对象,不锁定就无法执行

一、概念介绍

1.1 用户态与内核态

  • 内核态(kener):内核/操作系统可以做的一些操作。
  • 用户态(APP):用户的程序可以做的一些操作。
  • 用户态的程序要访问一些比较危险的操作的时候,比如格式化硬盘或直接访问内存网卡等,必须经过操作系统即内核的允许,这样可以保证安全性。
  • 从指令来讲,用户态只能执行某些指令,而内核态可以执行所有指令。
  • 对于 JVM 虚拟机来说就是一个普通程序,即属于用户态。
  • 早期的 synchronized 叫重量级锁,因为早期使用 synchronized 加锁的时候要结果内核态的允许,即要经过操作系统线程的调度才能拿到锁,所以称为重量级锁。
  • 后期经过了优化在某些特定情况下不需要结果操作系统,在用户态就可以解决,即使轻量级锁,比如 CAS 只是一个对比和交换,不需要经过操作系统是轻量级锁(锁的升级)。

1.2 CAS

CAS :compare and swap/compare and exchange

  • 举个例子:
    1. A 线程获取变量 a 的值此时 a = 1,然后 A 线程对变量 a 进行 a++ 操作,操作完成要写回内存。
    2. 此时会再次获取当前时间下变量 a 的值,如果此时 a 依旧为0,就认为没有线程操作过 a,就正常将 a=1 写入。
    3. 如果发现 a 的值已近变了比如 a = 3了,说明有线程对 a 做了操作,那就不写入。
    4. 此时重新获取 a 的值,在进行 ++ 操作,操作完在判断当前 a 的值和 ++ 前的值是否一致。这样一致循环下去。
    5. 上面说的这种情况不用上锁, CAS 也称为自旋锁/无锁。无锁不是没有锁,是没有内核状态的锁。
      CAS
      对图中的 ABA 问题做一下解释:
      还是上面的例子,A 线程执行完 a++ 操作后,要将新的 a 值写入内存,此时会再次获取当前时间下变量 a 的值,如果此时 a 依旧为0,就认为没有线程操作过 a,就正常将 a=1 写入。但是可能存在这种情况,就是 B 线程将变量 a 改为3,然后 C 线程又将变量 a 改为了0,实际上此时变量 a 已经发生了变化。这就是 ABA 问题。
      解决方法:可以给变量 a 增加一个版本号

再举个例子:

    public static void main(String[] args) throws InterruptedException {
        AtomicInteger integer = new AtomicInteger(0);

        Thread[] threads = new Thread[10];
        // 等待线程结束
        CountDownLatch downLatch = new CountDownLatch(threads.length);

        for (int i = 0; i < threads.length; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                    for (int j = 0; j < 5; j++) {
                        // 如果是 integer++ 的话就要加锁
                        integer.incrementAndGet();
                    }
                    downLatch.countDown();
                }
            });
            threads[i] = thread;
        }

        Arrays.stream(threads).forEach(f -> f.start());

        downLatch.await();

        System.out.println(Thread.currentThread().getName() + "	" + integer);


    }

上面的代码中如果采用 integer++ 这种方式就要进行加锁,采用 integer.incrementAndGet() 就不需要加锁,因为 incrementAndGet 方法底层就是采用的 CAS 实现的,是汇编的一条指令lock cmpxchg 指令。cmpxchg 指令不是原子的,所以需要 lock 指令给 CPU 加锁,只让一个线程操作。

1.3 对象在内存中的分布

对象在内存中的分布

二、锁的升级

锁的升级
偏向锁、自旋锁都是在用户空间完成
重量级锁都需要向内核空间申请
Hotspot的实现

偏向锁:

  • 向 markword 上记录自己的线程指针,实际上没有上锁,只是标记,此时只有一个线程执行,没有竞争的概念。
  • 为何会有偏向锁:因为经过统计大多数情况下 synchronized 方法只有一个线程在执行(如:stringbuffer的一些sync方法,vector的一些sync方法),此时没必要申请锁,节约资源
  • JVM 中偏向锁是默认打开的,但是有延迟 4S,可以设置参数修改 1.-XX:BiasedLockingStartupDelay=0。对应的就是锁升级图中 new 一个对象后会有两种情况。
  • 偏向锁默认打开原因是:JVM 虚拟机自己有一些默认启动的线程,里面有好多 sync 代码,这些 sync 代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。
  • 偏向锁是否一定比自旋锁效率高:不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁不涉及锁撤销,效果高。

自旋锁/轻量级锁:

  • 有偏向锁升级而来,当有多个线程执行(>= 2)的时候,此时就会有竞争不能在采用偏向锁了。
  • 多个线程通过竞争,某一个线程会将自己的线程指针写入 markword,标记自己占有,其他线程只能等待。
  • 怎么等待呢,就是采用 CAS 的方式,不停的去获取 markword 上记录的指针信息,看是不是被占有,如果没有被占有就把自己的指针写进去。这种方式下等待的线程会占用 CPU 资源
  • 所以自旋锁也没有经过内核态的操作,是轻量级锁。
  • 每个线程有自己的 LockRecord 在自己的线程栈上,用 CAS 去争用 markword 的 LR 的指针,指针指向哪个线程的 LR,哪个线程就拥有锁。

重量级锁:

  • 可以是自旋锁升级而来,自旋是消耗 CPU 资源的,如果锁的时间长,或者自旋线程多,CPU 会被大量消耗。
  • 重量级锁有等待队列,竞争队列,所有拿不到锁的进入等待队列,不需要消耗 CPU 资源。
  • JDK6之前,一个线程自旋超过10次,或者等待的线程数超过 CPU 核数的1/2,升级为重量级锁,如果太多线程自旋 CPU 消耗过大,不如升级为重量级锁,进入等待队列(不消耗CPU)。自旋次数和等待的线程数都可以通过参数控制。-XX:PreBlockSpin。
  • 自旋锁在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 来开启。JDK 6 中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。
  • 自适应自旋锁意味着自旋的时间(次数)不再固定,根据历史情况由 JVM 来管理。
  • 偏向锁耗时过长,或有 wait 时也会进入重量级锁。
原文地址:https://www.cnblogs.com/xiexiandong/p/12905374.html