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

简介: 语法上看,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


相关文章
|
6天前
|
SQL 缓存 监控
大厂面试高频:4 大性能优化策略(数据库、SQL、JVM等)
本文详细解析了数据库、缓存、异步处理和Web性能优化四大策略,系统性能优化必知必备,大厂面试高频。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:4 大性能优化策略(数据库、SQL、JVM等)
|
1月前
|
负载均衡 算法 Java
腾讯面试:说说6大Nginx负载均衡?手写一下权重轮询策略?
尼恩,一位资深架构师,分享了关于负载均衡及其策略的深入解析,特别是基于权重的负载均衡策略。文章不仅介绍了Nginx的五大负载均衡策略,如轮询、加权轮询、IP哈希、最少连接数等,还提供了手写加权轮询算法的Java实现示例。通过这些内容,尼恩帮助读者系统化理解负载均衡技术,提升面试竞争力,实现技术上的“肌肉展示”。此外,他还提供了丰富的技术资料和面试指导,助力求职者在大厂面试中脱颖而出。
腾讯面试:说说6大Nginx负载均衡?手写一下权重轮询策略?
|
1月前
|
NoSQL Java API
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴在面试一线互联网企业时遇到了关于Redis分布式锁过期及自动续期的问题。尼恩对此进行了系统化的梳理,介绍了两种核心解决方案:一是通过增加版本号实现乐观锁,二是利用watch dog自动续期机制。后者通过后台线程定期检查锁的状态并在必要时延长锁的过期时间,确保锁不会因超时而意外释放。尼恩还分享了详细的代码实现和原理分析,帮助读者深入理解并掌握这些技术点,以便在面试中自信应对相关问题。更多技术细节和面试准备资料可在尼恩的技术文章和《尼恩Java面试宝典》中获取。
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
|
2月前
|
消息中间件 安全 前端开发
面试官:单核服务器可以不加锁吗?
面试官:单核服务器可以不加锁吗?
49 4
面试官:单核服务器可以不加锁吗?
|
2月前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
1月前
|
NoSQL Redis
redis 的 key 过期策略是怎么实现的(经典面试题)超级通俗易懂的解释!
本文解释了Redis实现key过期策略的方式,包括定期删除和惰性删除两种机制,并提到了Redis的内存淘汰策略作为补充,以确保过期的key能够被及时删除。
54 1
|
2月前
|
缓存 监控 NoSQL
阿里面试让聊一聊Redis 的内存淘汰(驱逐)策略
大家好,我是 V 哥。粉丝小 A 面试阿里时被问到 Redis 的内存淘汰策略问题,特此整理了一份详细笔记供参考。Redis 的内存淘汰策略决定了在内存达到上限时如何移除数据。希望这份笔记对你有所帮助!欢迎关注“威哥爱编程”,一起学习与成长。
|
1月前
|
存储 Kubernetes 架构师
阿里面试:JVM 锁内存 是怎么变化的? JVM 锁的膨胀过程 ?
尼恩,一位经验丰富的40岁老架构师,通过其读者交流群分享了一系列关于JVM锁的深度解析,包括偏向锁、轻量级锁、自旋锁和重量级锁的概念、内存结构变化及锁膨胀流程。这些内容不仅帮助群内的小伙伴们顺利通过了多家一线互联网企业的面试,还整理成了《尼恩Java面试宝典》等技术资料,助力更多开发者提升技术水平,实现职业逆袭。尼恩强调,掌握这些核心知识点不仅能提高面试成功率,还能在实际工作中更好地应对高并发场景下的性能优化问题。
|
3月前
|
缓存 Java
【多线程面试题二十三】、 说说你对读写锁的了解volatile关键字有什么用?
这篇文章讨论了Java中的`volatile`关键字,解释了它如何保证变量的可见性和禁止指令重排,以及它不能保证复合操作的原子性。
|
3月前
|
Java
【多线程面试题二十二】、 说说你对读写锁的了解
这篇文章讨论了读写锁(ReadWriteLock)的概念和应用场景,强调了读写锁适用于读操作远多于写操作的情况,并介绍了Java中`ReentrantReadWriteLock`实现的读写锁特性,包括公平性选择、可重入和可降级。