美文网首页
知识回顾|并发|synchronized、CAS

知识回顾|并发|synchronized、CAS

作者: 三更冷 | 来源:发表于2023-01-30 20:15 被阅读0次

    并发

    synchronized

    ① 偏向锁、轻量级锁、重量级锁的概念以及升级机制?

    关于偏向锁、轻量级锁、重量级锁存在的理解误区:
    1、无锁->偏向锁->轻量级锁->重量级锁
    ps: 不存在无锁->偏向锁
    2、轻量级锁自旋获取锁失败后,会膨胀升级为重量级锁
    ps:轻量级锁不存在自旋
    3、重量级锁不存在自旋
    ps:重量级锁反而存在自旋

    锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级,但偏向锁状态可以被重置为无锁状态。

    偏向锁: 不存在竞争、偏向某个线程
    轻量级锁: 线程间存在轻微的竞争(线程交替执行,临界区逻辑简单)
    重量级锁: 多线程竞争激烈的场景、膨胀期间创建一个monitor对象

    对象内存布局中 Mark Word 部分,占用8字节
    • JVM锁的膨胀升级过程

    代码启动前需要睡眠一段时间,因为java对于偏向锁的启动是在启动几秒之后才激活。JVM启动的过程中会有大量的同步块,且这些同步块都有竞争,如果一启动就启动偏向锁,会出现很多没有必要的锁撤销。

    1. 创建锁对象,未出现任何线程获取锁的时候锁对象的锁状态为偏向锁(准确来说是匿名偏向、预备偏向,线程ID为0);
    2. 一个线程获取锁之后,锁的状态为偏向锁(cas修改线程id,绑定到偏向线程);偏向锁解锁还是偏向锁;偏向锁撤销后可能是无锁、轻量级锁、重量级锁之一;
    3. 重量级锁撤销之后是无锁状态,撤销锁之后会清除创建的monitor对象并修改markword,这个过程需要一段时间。Monitor对象是通过GC来清除的。GC清除掉monitor对象之后,就会撤销为无锁状态。
    4. 无锁状态可以直接升级为重量级锁。当竞争激烈的时候,cas失败导致升级为轻量级锁失败,会直接升级为重量级锁。

    附:关于对象hashcode保存的位置

    偏向锁没有地方保存hashcode;
    轻量级锁会在锁记录中记录hashCode(线程栈的Lock Record中markword副本里);
    重量级锁会在Monitor中记录hashCode

    当锁对象当前正处于偏向锁状态时,收到需要计算其一致性哈希码请求,它的偏向状态会被立即撤销,锁膨胀为重量级锁(即在同步代码块内打印hashcode,对象进入重量级锁,这时hashcode就会转移到monitor中,在monitor中有字段可以记录markword;在同步代码块外打印hashcode,偏向状态降级表现为无锁状态,再进入同步代码块后,升级为轻量级锁)

    轻量级锁是如何存储hashCode的?会在线程私有栈中创建一个锁记录Lock Record,将无锁状态下的Mark Word复制一份,原内容更新为指向线程栈Lock Record的指针;并且这个markword还要用于锁撤销后的还原,如果轻量级锁解锁为无锁状态,直接将拷贝的markword CAS修改到锁对象的markword中即可。

    ② synchronized与ReentrantLock的区别?

    1. 底层实现上来说,synchronized 是 JVM 内置锁,是Java关键字,基于Monitor机制实现,依赖底层操作系统的互斥原语 Mutex(互斥量);在同步块上:synchronized关键字被编译成字节码后会被翻译成 monitorenter 和 monitorexit 两条指令分别在同步块逻辑代码的起始位置与结束位置,monitorenter指令尝试获取monitor的所有权;在同步方法上:方法的同步是通过方法中的 access_flags 中设置 ACC_SYNCHRONIZED 标志来实现。
      ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁。

    2. 是否可手动释放
      synchronized 不需要用户去手动释放锁,ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。

    3. 是否可中断
      synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成; ReentrantLock则可以中断。

    1. 是否公平锁
      synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁。

    2. 锁的对象
      synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。

    3. 是否可绑定等待队列Condition
      synchronized不能绑定; ReentrantLock一个锁对象可以绑定多个Condition,实现线程的精确唤醒。

    4. 是否非阻塞
      使用synchronized关键字获取锁时,如果没有成功获取,只有被阻塞;而使用Lock.tryLock()获取锁时,如果没有获取成功也不会阻塞而是直接返回false。

    CAS

    ① AtomicInteger实现原理?

    用于通过原子的方式更新基本类型,Atomic包提供了以下三个类:
    ● AtomicBoolean:原子更新布尔类型。
    ● AtomicInteger:原子更新整型。
    ● AtomicLong:原子更新长整型。

    Atomic底层实现是基于无锁算法CAS, 基于魔术类Unsafe提供的三大cas-api完成:
    compareAndSwapObject、compareAndSwapInt、compareAndSwapLong
    基于硬件原语-CMPXCHG指令实现原子操作CAS

    AtomicInteger在并发量较低的环境下,线程冲突的概率比较小,自旋的次数不会很多。但是,高并发环境下,N个线程同时进行自旋操作,会出现大量失败并不断自旋的情况,此时AtomicLong的自旋会成为瓶颈。

    • LongAdder

    解决高并发环境下AtomicInteger,AtomicLong的自旋瓶颈问题。

    LongAdder原理:设计思路AtomicLong中有个内部变量value保存着实际的long值,所有的操作都是针对该变量进行。也就是说,高并发环境下,value变量其实是一个热点,也就是N个线程竞争一个热点。LongAdder的基本思路就是分散热点,将value值分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。(写热点的分散)

    ② CAS适用场景?

    适用于资源竞争较少(线程冲突较轻)的情况

    CAS缺陷:CAS虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方面:

    1. 高并发下自旋CAS长时间地不成功,则会给CPU带来非常大的开销,空等待占用cpu资源;
    2. 只能保证一个共享变量的原子操作;
    3. ABA问题

    ③ 如何实现一个乐观锁?

    乐观锁常见的两种实现方式:版本号、CAS无锁算法

    /**
     * CAS操作包含三个操作数——内存位置、预期原值及新值。
     * 执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。
     */
    public class CASTest {
        public static void main(String[] args) {
            CASTest cas = new CASTest();
            // true
            System.out.println(cas.compareAndSwapState(0,1));
            // false
            System.out.println(cas.compareAndSwapState(0,1));
            // true
            System.out.println(cas.compareAndSwapState(1,2));
        }
    
        private static final Unsafe unsafe = reflectGetUnsafeInstance();
    
        private volatile int state = 0;
    
        private static final long stateOffset;
    
        public final boolean compareAndSwapState(int oldValue, int newValue){
            // var1:要修改值的对象
            // var2:要修改值在内存中的偏移量
            // oldValue:线程工作内存当中的值
            // newValue:要替换的新值
            return unsafe.compareAndSwapInt(this, stateOffset, oldValue, newValue);
        }
    
        static {
            try {
                stateOffset = unsafe.objectFieldOffset(CASTest.class.getDeclaredField("state"));
            } catch (Exception e) {
                throw new Error();
            }
        }
    
        public static Unsafe reflectGetUnsafeInstance() {
            try {
                Field field = Unsafe.class.getDeclaredField("theUnsafe");
                field.setAccessible(true);
                return (Unsafe) field.get(null);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    }
    
    

    ④ 乐观锁和悲观锁的区别?

    对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。
    乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。
    乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

    悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
    乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

    参考文章

    https://zhuanlan.zhihu.com/p/126085068
    https://pdai.tech/md/java/thread/java-thread-x-lock-all.html
    https://github.com/farmerjohngit/myblog/issues/12
    https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html

    相关文章

      网友评论

          本文标题:知识回顾|并发|synchronized、CAS

          本文链接:https://www.haomeiwen.com/subject/gyxuhdtx.html