zoukankan      html  css  js  c++  java
  • 二、详解 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 时也会进入重量级锁。
  • 相关阅读:
    POJ 2987:Firing(最大权闭合图)
    BZOJ 1001:[BeiJing2006]狼抓兔子(最小割)
    HDU 1007:Quoit Design(分治求最近点对)
    POJ 1986:Distance Queries(倍增求LCA)
    HDU 3879 && BZOJ 1497:Base Station && 最大获利 (最大权闭合图)
    BZOJ-1011 遥远的行星
    BZOJ-1044 木棍分割
    BZOJ-1042 硬币购物
    BZOJ-1050 旅行
    BZOJ-1037 生日聚会
  • 原文地址:https://www.cnblogs.com/xiexiandong/p/12905374.html
Copyright © 2011-2022 走看看