锁策略相关问题(面试常考)

简介: 语法上看,sychronized是自动加锁与释放锁,lock是手动的加锁与释放锁,相对lock比较灵活,但需要保证不管是否发生异常都需要释放锁。

🍑一. JUC包(java.util.concurrent)下的常见类

juc包下的所有类都是提供多线程并发编程用的,不仅满足线程安全而且效率也很高


ReentranLock是可重入锁,具体用法:

Lock lock = new ReentrantLock();
        try{
            lock.lock(); //锁对象加锁,只有一个线程获取到锁
            //需要保证线程安全的代码
        }finally{
            lock.unlock(); //不管是否出现异常都需要释放锁
        }

   

lock提供了更多获取锁的方式:

微信图片_20221029145050.jpg

🍬synchronized与lock的区别:


🍂synchronized是一个关键字在JVM内部实现的,lock是标准库的一个类在JVM外部实现

🍂语法上看,sychronized是自动加锁与释放锁,lock是手动的加锁与释放锁,相对lock比较灵活,但需要保证不管是否发生异常都需要释放锁

🍂lock获取锁的方式相对更加丰富,提供了多种api

🍂从效率上看,当线程冲突严重时,lock性能要高很多


🍖线程冲突严重时lock性能高的原因:


synchronized在释放锁以后,之前因为申请锁失败而阻塞的线程,都会再次竞争

lock是基于aqs来实现,aqs是一个双端队列专门用来管理线程的状态,竞争失败的线程就入队列并设置状态,释放锁后把队列中的线程引用拿出来并设置状态(获取到锁)


🍬如何选择使用哪个锁呢?


🍃当锁竞争不激烈的时候,使用synchronized效率高,自动释放更方便

🍃当锁竞争激烈的时候,使用lock,搭配tryLock更灵活控制加锁的行为,而不是死等

🍃需要使用公平锁的时候使用lock,lock默认是非公平的锁,但可以通过构造方法传入true开启公平模式(后面在锁策略中介绍)


🍉二. 常见的锁策略

🌴1. 乐观锁与悲观锁

悲观锁:大部分时间来看,都是存在线程冲突的(悲观的看待问题),每次拿数据的时候都认为会修改,所以每次拿数据的时候都先加锁,完成执行操作后再释放锁


乐观锁:大部分时间来看,是不存在线程冲突的(乐观的看待问题),每次都直接执行修改数据的操作,在数据提交更新的结果时,才会检测是否产生并发冲突,返回是否修改成功的结果,让程序自行处理逻辑


🍖乐观锁最重要的就是检测出是否发生线程冲突,这里引入一个版本号(version)解决:


主存中有变量和一个版本号,两个线程同时将主存中的信息读到各自的寄存器中,然后进行修改,线程1先修改然后往主存中写,然后对比版本号,相同的话version++,当线程2执行相同操作时发现版本号不相同,就写入失败


具体过程如下图:

image.png

乐观锁从Java层面来看是无锁操作,直接修改共享变量

返回true,修改成功

返回false,修改失败


🌾2. 自旋锁(Spin Lock)

线程在竞争锁失败的时候会进入阻塞状态,放弃CPU,需要过很久才能被再次调度,但是实际上,虽然当前竞争锁失败但是没有过多久就竞争到锁,没必要放弃CPU,这个时候就可以使用自旋锁来解决这种情况


自旋锁实际也是Java层面的无锁操作

while(true){
    boolean result = 乐观锁方式修改数据;
    if(result) break;
}

说明:如果获取锁失败,就立即在尝试获取锁,无限循环直到获取到锁,一旦其他线程释放锁,就能第一时间获取到锁


如果满足乐观锁的使用条件:冲突概率比较小,如果发生冲突也能够很快的得到执行

一般来说乐观锁都考虑结合自旋锁来操作


🌵3. 读写锁

多个线程在数据读取时不会存在线程安全问题,但多个线程的写与读和写需要进行加锁,如果这两种场景下都用同一个锁就会产生很大的性能损耗,所以就要使用读写锁


读写锁就是对读和写操作区分对待,Java标准库中提供了ReentrantReadWriteLock类,实现了读写锁


ReentrantReadWriteLock.ReadLock表示一个读锁,提供了lock/unlock方法进行加锁释放锁

ReentrantReadWriteLock.WriteLock表示一个写锁,提供了lock/unlock方法进行加锁释放锁


关于读写锁:


🍁读与读之间不互斥

🍁写与写之间互斥

🍁读与写之间互斥


🍬读写锁适用频繁读,不频繁写的场景中


🌳4. 重量级锁与轻量级锁

重量级锁:加锁机制重度依赖OS提供的mutex

大量的内核态与用户态切换

容易引发线程的调度

操作成本高


轻量级锁:加锁机制尽可能不用mutex,而是尽量在用户态代码完成

少量的内核态与用户态切换

不太容易引发线程调度

操作成本相对低


🌴5. 公平锁与非公平锁

非公平锁:不以申请锁的时间先后顺序来获取到锁

缺陷:可能出现线程饥饿现象

优点:效率更高


公平锁:以申请锁的时间先后顺序来获取到锁


🍂synchronized是非公平锁,因为多个线程之间申请锁是竞争关系,不存在时间先后顺序

🍂lock默认下是非公平锁,但可以修改构造方法的参数来启用公平锁

image.png

🌾6. 可重入锁与不可重入锁

可重入锁:同一个线程可以重复申请到同一个对象的锁

不可重入锁:同一个线程不可以重复申请到同一个对象的锁


Java中,只要以Reentrant开头命名的锁都是可重入锁,lock实现类,synchronized关键字锁都是可重入锁


🍋三. CAS

🌴1. 认识CAS

CAS全称Compared And Swap(比较并交换),是jdk提供的一种乐观锁的实现,能够满足线程安全,以乐观锁的方式修改变量


比较并交换(比较,+1,交换)为一条CAS指令,是线程安全的


此处所谓的CAS指的是CPU提供了一个单独的CAS指令,通过这个指令可以完成比较并交换的过程


🍬CAS操作步骤:


🍀从主存拿值读取到自己的工作内存中,记为值A,执行修改操作后,值为B,主存的最新值记为C

🍀将B写入主存时比较A与主存的最新值C是否相等

🍀相等就写入成功,不相等就写入失败

🍀返回修改操作的结果


画图说明如下:

image.png

🌾2. jdk如何实现CAS

jdk提供了一个Unsafe的类,来执行cas的操作

微信图片_20221029145323.jpg

jdk中unsafe提供的cas方法,是基于操作系统提供的cas方法,又是基于CPU提供的cas硬件支持,且CPU使用了lock加锁


简单说,Unsafe是基于操作系统和CPU提供的cas来实现的


🌵3. CAS的常见应用

Java的java.util.concurrent.atomic包下都是使用了cas来保证线程安全的,例如:++,--,!flag


原因:++,--,!flag这种操作执行速度非常快,很快能得到执行,使用cas+自旋锁效率更高


既能够保证线程安全又能够比synchronized高效,因为不涉及竞争锁的竞争,线程要等待


典型的就是AtomicInteger类:


🍂addAndGet(int delta)        i += delta

🍂decrementAndGet()          --i

🍂getAndDecrement()          i--

🍂incrementAndGet()          ++i

🍂getAndIncrement()           i++


👁‍🗨️例如:执行1000次n++和n--操作看结果

import java.util.concurrent.atomic.AtomicInteger;
public class AutoInt {
    //共享变量n初始值为0
    private static AtomicInteger n = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0;i < 1000;i++){
                    n.getAndIncrement(); //1000次n++;
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0;i < 1000;i++) {
                    n.getAndDecrement(); //1000次n--
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(n);
    }
}

结果:打印结果为0

微信图片_20221029145405.jpg


🌳4. CAS中的ABA问题

在没有引入版本号的情况下,CAS是基于变量的值,在读和写的时候比较的,但这个时候会存在下面的一个问题:

image.png


从中发现,如果在当前线程写入值进行比较的时候,如果有其他线程对主存中的的值进行修改,修改为当前线程从主存读取的值的时候,当前线程仍然能写入成功,但是存在线程安全问题


🍖ABA问题的解决?


引入版本号,线程每次修改共享变量的时候版本号+1,比较的时候比较读和写时候版本号是否相同,相同的话写入成功,否则写入失败


jdk中,提供了一个AtomicStampedReference<E>的类,这个类可以对某个类进行包装,在其内部就提供了版本号的管理


🍏四. Synchronized的原理

🌴1.synchronized的特性

synchronized是基于对象头加锁,实现线程间的同步互斥

同步互斥:对同一个对象进行加锁的多个线程,同一个时间只有一个线程获取到锁,相当于多个线程获取锁执行同步代码是互斥的

image.png

🍬synchronized加锁操作,,只涉及对象头状态升级能升级不能降级,升级的过程:


无锁

偏向锁

轻量级锁

重量级锁


🌾2. 加锁的工作过程

image.png

偏向锁:第一次进入的线程,或是这个线程再次申请同一个对象锁


偏向锁不是真的 "加锁", 只是给对象头中做一个 "偏向锁的标记", 记录这个锁属于哪个线程.如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)如果后续其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态


轻量级锁:随着其他线程的竞争进入轻量级锁状态,CAS+(自适应的)自旋锁


自适应:自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源,因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了,也就是所谓的 "自适应"


重量级锁:竞争锁进一步激烈,就会升级为重量级锁(使用操作系统mutex加锁)


🌵3. JVM对synchronized的优化方案

锁消除,前提:变量只有一个线程可以操作


比如这段代码:

StringBuffer sb = new StringBuffer();
        sb.append("a");
        sb.append("b");
        sb.append("c");
        sb.append("d");

 

此时每个 append 的调用都会涉及加锁和解锁,但如果只是在单线程中执行这个代码, 那么这些加

锁解锁操作是没有必要的, 白白浪费了一些资源开销


微信图片_20221029145541.jpg

锁粗化,前提:变量是多个线程可以使用,,但是一个线程出现多次连续sychronized加锁

image.png


相关文章
|
2月前
|
消息中间件 前端开发 NoSQL
腾讯面试:什么锁比读写锁性能更高?
在并发编程中,读写锁 ReentrantReadWriteLock 的性能已经算是比较高的了,因为它将悲观锁的粒度分的更细,在它里面有读锁和写锁,当所有操作为读操作时,并发线程是可以共享读锁同时运行的,这样就无需排队执行了,所以执行效率也就更高。 那么问题来了,有没有比读写锁 ReentrantReadWriteLock 性能更高的锁呢? 答案是有的,在 Java 中,比 ReentrantReadWriteLock 性能更高的锁有以下两种: 1. **乐观锁**:乐观锁是一种非阻塞锁机制,它是通过 Compare-And-Swap(CAS)对比并替换来进行数据的更改的,它假设多个线程(
33 2
|
10天前
|
安全 Android开发 Kotlin
Android面试题之Kotlin协程并发问题和互斥锁
Kotlin的协程提供轻量级并发解决方案,如`kotlinx.coroutines`库。`Mutex`用于同步,确保单个协程访问共享资源。示例展示了`withLock()`、`lock()`、`unlock()`和`tryLock()`的用法,这些方法帮助在协程中实现线程安全,防止数据竞争。
13 1
|
11天前
|
存储 缓存 安全
架构面试题汇总:并发和锁(2024版)
架构面试题汇总:并发和锁(2024版)
|
2天前
|
Java
java面试之线程八锁
java面试之线程八锁
7 0
|
29天前
|
Java 程序员
【面试官】知道synchronized锁升级吗
线程A获取了某个对象锁,但在线程代码的流程中仍需再次获取该对象锁,此时线程A可以继续执行不需要重新再获取该对象锁。既然获取锁的粒度是线程,意味着线程自己是可以获取自己的内部锁的,而如果获取锁的粒度是调用则每次经过同步代码块都需要重新获取锁。此时synchronized重量级锁就回归到了悲观锁的状态,其他获取不到锁的都会进入阻塞状态。来获得锁,CAS操作不需要获得锁、释放锁,减少了像synchronized重量级锁带来的。轻量级锁通过CAS自旋来获得锁,如果自旋10次失败,为了减少CPU的消耗则锁会膨胀为。
|
2月前
|
消息中间件 安全 前端开发
字节面试:说说Java中的锁机制?
Java 中的锁(Locking)机制主要是为了解决多线程环境下,对共享资源并发访问时的同步和互斥控制,以确保共享资源的安全访问。 锁的作用主要体现在以下几个方面: 1. **互斥访问**:确保在任何时刻,只有一个线程能够访问特定的资源或执行特定的代码段。这防止了多个线程同时修改同一资源导致的数据不一致问题。 2. **内存可见性**:通过锁的获取和释放,可以确保在锁保护的代码块中对共享变量的修改对其他线程可见。这是因为 Java 内存模型(JMM)规定,对锁的释放会把修改过的共享变量从线程的工作内存刷新到主内存中,而获取锁时会从主内存中读取最新的共享变量值。 3. **保证原子性**:锁
32 1
|
2月前
|
安全 Java
【JAVA面试题】什么是对象锁?什么是类锁?
【JAVA面试题】什么是对象锁?什么是类锁?
|
2月前
|
数据库连接 开发者 Python
Python高级特性解析与面试应对策略
【4月更文挑战第13天】本文介绍了Python面试中常见的三个高级特性问题:列表推导式与生成器表达式、装饰器和上下文管理器。列表推理解析表达式和生成器的区别在于内存使用,装饰器用于修改函数行为,with语句则用于自动管理资源释放。文章提供了相关代码示例,帮助应聘者在面试中更好地展示Python技能。理解并熟练运用这些特性,能在面试中脱颖而出。
23 1
|
2月前
|
安全 Java 程序员
【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)
【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)
24 0
|
2月前
|
存储 设计模式 Python
Python高级特性解析与面试应对策略2
【4月更文挑战第14天】本文介绍了Python高级特性在求职面试中的重要性,包括列表推导式、生成器表达式、装饰器、上下文管理器、元类和迭代器协议。文章列举了面试常见问题,如列表推导式与生成器混淆、装饰器误用、上下文管理器误操作、元类滥用以及对`yield`关键字的理解不透彻,并提供了相应的应对策略和代码示例,帮助求职者在面试中更好地展示技术实力。
36 0