锁的面试八股

目录

常用锁策略

1.乐观锁 VS 悲观锁

2.轻量级锁 VS 重量级锁

3.自旋锁 VS 挂起等待锁

4.互斥锁 VS 读写锁

5.公平锁 VS 非公平锁

6.可重入锁 VS 可重入锁

CAS

ABA问题

Synchronized原理

1. 锁升级/锁膨胀

2.锁消除

3.锁粗化


常用锁策略

1.乐观锁 VS 悲观锁

站在锁冲突概率的预测角度.乐观锁预测冲突概率较小,悲观锁预测锁冲突概率较大

synchronized既是一个悲观锁,也是一个乐观锁.它默认是一个乐观锁,但当锁竞争比较激烈,就会变成悲观锁.

2.轻量级锁 VS 重量级锁

站在加锁操作的开销角度. 轻量级锁开销较小,重量级锁开销较大.

synchronized默认是一个轻量级锁,但发现锁竞争比较激烈的时候就会转转换成重量级锁.

3.自旋锁 VS 挂起等待锁

自旋锁是一种典型的轻量级锁,对于自旋锁,当锁被释放后,线程能第一时间感知到锁,从而有机会获取到锁

挂起等待锁是一种典型的重量级锁.当锁被释放后,继续等待,不知道什么时候能过获取到锁

synchronized这里的轻量级锁是基于自旋锁的方式实现的,而synchronized的重量级锁是针对挂起等待锁的方式实现的.

4.互斥锁 VS 读写锁

互斥锁提供加锁和解锁两种操作,如果一个线程加锁,另外一个线程也尝试加锁,就会产生阻塞等待.

读写锁提供了三种操作,分别是针对读操作加锁,针对写操作加锁和解锁操作.多线程针对同一个变量并发读,这个时候没有线程安全问题,也不需要加锁控制.

读锁和读锁之间没有互斥,写锁和写锁之间存在互斥,写锁和读锁之间存在互斥.

 synchronized不是读写锁

5.公平锁 VS 非公平锁

所谓公平,就是指 " 先来后到 ",下面举个栗子

 公平锁: 当女神分手后,由等待队列中最早来的舔狗上

 非公平锁: 就是当女神分手后,三个滑稽老铁都有了追求女神的机会,而和之前追了多长时间没有关系.

在操作系统和 Java synchronized 中都是非公平锁,操作系统针对加锁的控制,本身依赖线程的调度顺序,这个调度顺序是随机的,不会考虑线程等待了多长时间.

6.可重入锁 VS 可重入锁

不可重入锁: 一个线程针对一把锁,连续加锁两次出现死锁

可重入锁: 一个线程针对一把锁,连续加锁多次都不会死锁.

synchronized是可重入锁.

关于可重入问题和synchronized的相关操作

CAS

CAS指的是 compare and swap指的是 比较并交换,一个CAS涉及到一下操作:

上述这个CAS过程并非是通过异端代码实现的,而是通过一条 CPU指令完成的.而CAS操作是原子的,在一定程度上就回避了线程安全问题,同时在解决线程安全问题除了加锁之外,又可以使用CAS方法了.

CAS相当于通过一个原子的操作 , 同时完成 " 读取内存 , 比 较是否相等, 修改内存 " 这三个步骤 .

CAS可以实现原子类,在Java标准库中提供的类 

接下来用原子类写一个两个线程并发自加的操作

import java.util.concurrent.atomic.AtomicInteger;

    public static void main(String[] args) {
        //这些原子类,就是基于 CAS 实现了 自增,自减等操作
        //此时进行这类操作不用加锁也是线程安全的
        AtomicInteger count = new AtomicInteger(0);
        Thread t1 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                count.getAndIncrement();  //count++
                // count.incrementAndGet();  //++count
                // count.getAndDecrement();  //count--
                // count.decrementAndGet();  //count--
            }
        });
        Thread t2 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                count.getAndIncrement();  //count++
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count.get());

    }

上面操作用伪代码进行实现

原子类这里的实现,每次修改之前,在确认一下这个值是否符合要求.

CAS还可以实现自旋锁

接下来看看CAS实现的自旋锁的伪代码

注意: 在java中并没有提供CAS方法,此处CAS相当于是一个简化的方式

ABA问题

CAS在运行中的核心是检查value和oldValue是否一致,如果一致,就视为value中途没有被修改过,在进行下一步操作,但可能在检查value和oldValue是否一致之前,可能会出现value从A修改成B,又从B修改为A这样的操作,CAS无法判断这种操作是否发生,这样的问题就叫ABA问题

针对这样的问题,采取的方案就是给要修改的数据加版本号,想象初始版本号是1,每次修改版本号都+1,人后进行CAS的时候,不是一以金额为基准了,而是以版本号为基准,因为版本号是只会自加的,而不会减少.

Synchronized原理

前面提到过,synchronized关键字,两个线程针对同一个线程加锁,就会产生阻塞等待.但在synchronized内部还有一些优化机制,存在的目的就是为了让锁更高效.

1. 锁升级/锁膨胀

通过Synchronized关键字进行加锁的的过程中,Synchronized会经历,无锁,偏向锁,轻量级锁,和重量级锁四种状态.

        第一个尝试加锁的线程,优先进入偏向锁的状态.

偏向锁不是真的 " 加锁 ", 只是给对象做一个 " 偏向锁的标记 ", 记录这个锁属于哪个线程 .
如果后续没有其他线程来竞争该锁 , 那么就不用进行其他同步操作了 ( 避免了加锁解锁的开销 )
如果后续有其他线程来竞争该锁 ( 刚才已经在锁对象中记录了当前锁属于哪个线程了 , 很容易识别 当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态 , 进入一般的轻量级锁状态 .
偏向锁本质上相当于 " 延迟加锁 " . 能不加锁就不加锁 , 尽量来避免不必要的加锁开销 .
但是该做的标记还是得做的 , 否则无法区分何时需要真正加锁 .
        当synchronized发生锁竞争的时候就会从偏向锁升级成轻量级锁,此时,synchronized相当于通过自旋的方式,来进行加锁的.(和上面CAS讲的伪代码一样).
        如果竞争进一步加强,自旋锁不能获取到当前锁状态,就会进入重量级锁.重量级锁是操作系统内核原生的API实现的,此时这个加锁功能就会影响到 线程的调度.如果线程进入重量级加锁,并且发生锁竞争,此时线程会被放到阻塞队列里,暂时不参与调度,直到锁被释放,才有机会被调度,并重新获取到锁.

2.锁消除

编译器智能的判定,看当前的代码是否真的要加锁,如果这个场景不需要加锁,而我们加了,编译器聚会自动把锁干掉.

3.锁粗化

锁的粒度: synchronized 包含的代码越多,粒度就越粗,包含的代码越少,粒度就越细.

通常情况下,认为锁的粒度细一点比较好,因为加锁部分的代码,是不能并发执行的,锁的粒度越细,能并发执行的代码就越多;反之,就越少.

但也有特殊的情况,有时候可能没有现成来抢占这个锁,jvm就会自动把锁粗化,避免频繁申请释放锁.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值