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

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

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

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

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


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

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

image.png

🍬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的操作

image.png

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

image.png

🌳4. CAS中的ABA问题

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

image.png

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

🍖ABA问题的解决?

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

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

🍏四. 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 的调用都会涉及加锁和解锁,但如果只是在单线程中执行这个代码, 那么这些加

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

image.png

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

image.png



相关文章
|
2月前
|
存储 关系型数据库 MySQL
最全MySQL面试60题(含答案):存储引擎+数据库锁+索引+SQL优化等
最全MySQL面试60题(含答案):存储引擎+数据库锁+索引+SQL优化等
194 0
|
18天前
|
存储 安全 Java
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(下)
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(下)
43 0
|
18天前
|
存储 安全 Java
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(上)
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)
35 0
|
22天前
|
安全 Java
大厂面试题详解:synchronized的偏向锁和自旋锁怎么实现的
字节跳动大厂面试题详解:synchronized的偏向锁和自旋锁怎么实现的
10 0
|
3月前
|
机器学习/深度学习 算法 搜索推荐
|
3月前
|
存储 缓存 测试技术
面试题6: 系统测试策略有哪些?
面试题6: 系统测试策略有哪些?
|
4月前
|
算法 Java
【面试问题】锁如何优化?
【1月更文挑战第27天】【面试问题】锁如何优化?
|
4月前
|
监控 Java
【面试问题】什么是锁的自适应自旋?
【1月更文挑战第27天】【面试问题】什么是锁的自适应自旋?
|
4月前
|
设计模式 消息中间件 Java
面试官:什么是JIT、逃逸分析、锁消除、栈上分配和标量替换?
面试官:什么是JIT、逃逸分析、锁消除、栈上分配和标量替换?
537 1
|
2月前
|
Java 程序员
java线程池讲解面试
java线程池讲解面试
62 1