知识回顾|并发|synchronized、CAS

2023-01-30  本文已影响0人  三更冷

并发

synchronized

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

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

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

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

对象内存布局中 Mark Word 部分,占用8字节

代码启动前需要睡眠一段时间,因为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的自旋会成为瓶颈。

解决高并发环境下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

上一篇下一篇

猜你喜欢

热点阅读