Java并发编程面试-锁

乐观锁 && 悲观锁

悲观锁共享资源每次只给一个线程使用,其他线程阻塞,用完后再把资源转让给其他线程。

public void performSynchronisedTask() {
    synchronized (this) {
        // 需要同步的操作
    }
}

private Lock lock = new ReentrantLock();
lock.lock();
try {
   // 需要同步的操作
} finally {
    lock.unlock();
}

高并发场景下,会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销,并且还可能存在死锁问题

上下文切换:

  1. 线程 A 正在执行计算任务,比如在做数学运算。它执行了一段时间后,操作系统的调度器决定它的时间片(CPU 执行时间)到了,需要把 CPU 分配给其他线程。
    • 此时,操作系统会进行上下文切换:保存线程 A 的状态(当前执行到哪一步,寄存器里的值,堆栈指针等),然后将 CPU 的控制权交给线程 B。
  2. 线程 B 被切换过来执行,假设线程 B 正在处理文件 I/O 操作,读取硬盘文件。操作系统让线程 B 开始执行它的任务,直到线程 B 等待文件系统响应,此时发生了 I/O 阻塞。
    • 当线程 B 因 I/O 操作阻塞时,操作系统无法让它继续执行,因此会再次进行上下文切换:保存线程 B 的状态,将 CPU 控制权交还给线程 A。
  3. 线程 A 再次执行:操作系统恢复线程 A 的状态,使其从刚才停止的地方继续执行数学运算。

这个过程反复进行,每次切换 CPU 需要先保存当前线程的状态,再恢复另一个线程的状态。虽然从外部看,线程 A 和 B 好像在并行执行,但其实它们是通过上下文切换轮流执行的。

乐观锁:乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。

CAS算法:

Compare And Swap(比较与交换),思路:用一个预期值要更新的变量值进行比较,两值相等才会进行更新,用于实现乐观锁

CAS涉及到三个操作数:

  • V:要更新的变量值(Var)
  • E:预期值(Expected)
  • N:拟写入的新值(New)

当且仅当V等于E时,CAS通过原子(原子性)方式用新值N来更新V的值。如果不相等,说明已经有其他线程更新了V,则当前线程放弃更新。

自旋锁机制:由于CAS操作可能会因为并发冲突而失败,因此通常与while循环搭配使用,在失败后不断重试,直到操作成功。(弊端:如果一直不成功,会给CPU带来非常大的执行开销)

CAS算法存在问题:ABA问题

ABA问题:线程读取到某个变量的值为A,并准备更新它时,另一个线程可能已经将这个变量的值从A修改为B,随后又改回A。由于CAS只检查当前值是否为A,便认为值没有变化,并成功执行了更新操作,但实际上在此过程中发生了变化,这可能导致数据一致性问题。

解决:

  1. 添加版本号或时间戳,Java的AtomicStampedReference
  2. 锁机制(Synchronized-同步)
  3. GC托管对象

如何使用Synchronized?

  1. 修饰实例方法(锁当前对象实例)
synchronized void method() {
    //业务代码
}
  1. 修饰静态方法(锁当前类)

静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。

synchronized static void method() {
    //业务代码
}

静态 synchronized 方法和非静态 synchronized 方法之间的调用互斥么?不互斥!如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

  1. 修饰代码块(锁指定对象 / 类)
  • synchronized(object) 表示进入同步代码库前要获得 给定对象的锁
  • synchronized(类.class) 表示进入同步代码前要获得 给定 Class 的锁
synchronized(this) {
    //业务代码
}

尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能。

Synchronized底层原理

synchronized 同步语句块的实现使用的是 monitorentermonitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取而代之的是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。

不过两者的本质都是对对象监视器 monitor 的获取。

锁升级:锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

ReentrantLock

ReentrantLock 实现了 Lock 接口,是一个可重入且独占式的锁,和 synchronized 关键字类似。不过,ReentrantLock 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。

ReentrantLock 里面有一个内部类 SyncSync 继承 AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在 Sync 中实现的。Sync 有公平锁 FairSync 和非公平锁 NonfairSync 两个子类。

公平锁 : 按顺序来,先申请,先拿锁。锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。

非公平锁随机或按照其他顺序。锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。

可重入锁(递归锁):线程可以再次获取自己的内部锁。比如一个线程获取了某个对象的锁,此时这个对象还没有释放锁,当其再想获取这个对象的锁时,还是可以获取的,如果是不可重入锁,就会造成死锁。Lock实现类、synchronized关键字锁都是可重入的。

可中断锁:获取锁的过程中可以被中断,不需要等到获取锁之后才能进行其他逻辑处理。ReentrantLock

不可中断锁:获取锁的过程中不可被中断,一旦线程申请了锁,就只能等到拿到锁之后才能进行其他的逻辑处理。Synchroniezd

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值