Java面试最容易被刷的重难点之锁的使用策略

目录
  • 一. 乐观锁和悲观锁
    • 1. 字面理解
    • 2. 生活实例
    • 3. 基于版本号方式实现乐观锁
  • 二. 读写锁
    • 1. 理解
    • 2. 用法
  • 三. 重量级锁和轻量级锁
    • 1. 原理
    • 2. 理解
    • 3. 区分用户态和内核态
  • 四. 自旋锁
    • 1. 理解
    • 2. 实现方式
    • 3. 优缺点
  • 五. 公平锁和非公平锁
    • 1. 理解
    • 2. 注意事项
  • 六. 可重入锁和不可重入锁
    • 1. 为什么要引入这两把锁
      • (1)实例一
      • (2)实例二
    • 2. 实现方案
  • 七. 面试题
    • 第一题
    • 第二题
    • 第三题
    • 第四题

在多线程的学习中,很多时候都要用到锁,但我们都知道,加锁这个操作是一个计算机中开销比较大的操作,因此,本篇文章我会带大家学习在不同场景中进行不同的加锁处理方式,以让程序更高效一些有关锁策略不仅仅局限于某一种语言,在很多语言中都可能会遇到加锁操作,而且这部分知识点也是面试中常见的问题,所以本篇文章内容基本都是需要大家自己认真理解并做到会用自己的语言组织起来的。内容均为博主认真总结,大家可以收藏起来慢慢学习,希望可以帮到大家哦!

一. 乐观锁和悲观锁

1. 字面理解

乐观锁认为多个线程访问同一个共享数据时产生并发冲突的概率不大,并不会真的加锁, 而是直接尝试访问数据, 在访问的同时识别当前的数据是否出现访问冲突,若冲突,则会返回当前的错误信息让用户去决定如何去处理悲观锁会认为多个线程访问同一个共享数据时产生并发冲突的概率很大,因此往往会在取数据时会进行加锁操作,这样的话其他线程想要拿到这个数据时就会阻塞等到直到其他线程获取到锁

补充:在Java中synchronized这一加锁操作主要以悲观锁为主,初始使用乐观锁策略,但当发现锁竞争比较频繁的时候,就会自动切换成悲观锁策略

2. 生活实例

在生活中有很多情况都能涉及到乐观和悲观的心态,比如今天是阴天,A认为可能会下雨,会提前带好伞,这就对应到了悲观锁这一策略;而B比较乐观,不会认为会下雨,因此B不会带伞,这显然可以类比为乐观锁这一策略。

3. 基于版本号方式实现乐观锁

实现乐观锁策略这一功能的方式有很多,接下来我带大家去学习一种:基于版本号方式。
假设我们要使用多线程修改用户的账户余额,我们可以引入一个版本号来实现,具体方法如下:

设当前的余额为100,引入一个版本号version,将其初始值设为1,并且我们规定,提交版本必须大于数据库中记录的当前版本号才能执行更新余额的操作,若不满足此条件,则认为修改失败

图示
以线程1想把主内存中的数据减50,线程2把主内存中的数据减20为例:

线程1此时准备将主内存中的数据读入自己的工作内存中并修改,而线程2也想将主内存的数据读入自己的工作内存中并修改,此时线程1和线程2以及主内存中的版本号都为1

当线程1把主内存的数据减50后,即修改后,会将自己工作内存中的版本号加1,此时线程1工作内存中的版本号大于主内存中的版本号(2大于1),因此线程1成功修改了主内存中的数据,并将数据50写入主内存中,最后将主内存中的版本号加1(即为2)

此时线程2修改了自己工作内存中的数据,随后将自己的工作内存版本号改为2:

但正当线程2准备将其改好后的数据80写入主内存时,发现自己的版本号和主内存的版本号都一样,并不满足大于关系,因此此次修改失败,有效避免了多线程并发修改数据时引起的数据安全问题。
总结

基于版本号这样实现乐观锁的机制就是一种典型的实现方式,这个实现方式和之前所学过的单纯的互斥的加锁方式来说更加轻量一些(只修改版本号,只是在计算机中用户态上进行操作,而互斥加锁方式会涉及到用户态和内核态之间的切换,不仅效率不太高,也容易引起线程阻塞)对于这个机制来说,如果修改数据失败,就会涉及到重试操作,如果频繁重试的话那么效率也就不高了,因此,最好在线程并发冲突率比较低的场景下使用乐观锁这一方式比较好

二. 读写锁

1. 理解

我们都知道,当我们通过多线程方式尝试修改同一数据时,一般都可能引发线程安全问题,但当我们通过多线程方式尝试读取同一数据时,一般不会引发线程安全问题,因此,我们可以根据读和写的不同场景来给读和写操作分别加上不同的锁。
Java当中的synchronized不会对读和写进行区分,默认使用后线程都是互斥的

2. 用法

以Java为例,在标准库中存在这样一个类ReentrantReadWriteLock
源代码如下

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    private static final long serialVersionUID = -6992448646407690164L;
    /** Inner class providing readlock */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** Inner class providing writelock */
    private final ReentrantReadWriteLock.WriteLock writerLock;
    /** Performs all synchronization mechanics */
    final Sync sync;

    /**
     * Creates a new {@code ReentrantReadWriteLock} with
     * default (nonfair) ordering properties.
     */
    public ReentrantReadWriteLock() {
        this(false);
    }

该类中提供了两个方法:

    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }

此方法可以创建出一个读锁实例

public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

此方法可以创建出一个写锁实例
某个线程被读锁修饰后,这两个线程之间不会互斥,而是完全同时并发执行,一般将读锁用于线程读取数据比较多的场景;而当某个线程被写锁修饰后,这两个线程会互斥,一个线程会执行,而另一个线程会阻塞等待,因此必须是一个线程执行完了,另一个线程才会执行,一般用于修改数据比较多的场景

三. 重量级锁和轻量级锁

1. 原理

锁的核心特性 “原子性”,这样的机制追根溯源是 CPU 这样的硬件设备提供的

1.CPU 提供了 “原子操作指令”。
2. 操作系统基于 CPU 的原子指令,实现了 mutex 互斥锁.
3. JVM 基于操作系统提供的互斥锁。实现了 synchronized 和 ReentrantLock 等关键字和类。

注意:synchronized 并不仅仅是对 mutex 进行封装, 在 synchronized 内部还做了很多其他的工作

2. 理解

1.重量级锁依赖了OS提供的mutex,的开销一般很大,往往是通过内核来完成的
2.轻量级加锁一般不使用mutex,开销一般比较小,一般通过用户态就能直接完成

3. 区分用户态和内核态

我们可以类比一个生活中的例子,当去银行办理业务时,如果是通过用户在银行工作人员的指导下自己在窗口外完成,那么效率会比较高,就像计算机中的用户态一样。而当我们把自己的业务交给银行相关人员去完成时,由于银行工作人员的闲忙时间是不可控的,因此无法保证效率,就好比计算机中的内核态。

四. 自旋锁

1. 理解

当两个线程为了完成任务同时竞争一把锁时, 拿到锁的那个线程会立马执行任务,而没拿到就会阻塞等待,当一个线程把锁释放后,另一个线程不会被立即唤醒,而是等操作系统将其进行一系列的调度到CPU中的操作才能被唤醒然后执行任务,这种锁叫做挂起等待锁,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度。但实际上,大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放,所以没必要就放弃 CPU。这个时候就可以使用自旋锁来处理这样的问题。

2. 实现方式

自旋锁的伪代码为:while (抢锁(lock) == 失败) {}

如果获取锁失败,就会立即再尝试获取锁,无限循环,直到获取到锁为止。第一次获取锁失败, 第二次的尝试会在非常短的时间内到来,一旦锁被其他线程释放, 就能第一时间获取到锁

3. 优缺点

自旋锁是一种典型的轻量级锁的实现方式,它没有放弃 CPU, 不涉及线程阻塞和调度,一旦锁被释放,就能第一时间获取到锁,这样会大大提高代码的执行效率,但如果锁被其他线程持有的时间比较久, 那么就会持续地消耗 CPU 资源。(而挂起等待的时候是不消耗 CPU 的)
因此,我们应该注意自旋锁的适用场合:

  • 如果多个线程执行任务时锁的冲突比较低,或者线程持有锁的时间比较短,此时使用自旋锁比较合适
  • 如果某个线程任务对CPU比较敏感,且不希望吃太多CPU资源,那么此时就不太适合使用自旋锁。

注意:synchronized自身已经设置好了自旋锁和挂起等待锁,会根据不同的情况自动选择最优的使用方案

五. 公平锁和非公平锁

1. 理解

若有三个线程 A,B,C。
A先尝试获取锁,获取成功了,因为只有一把锁,所以B和C线程都会阻塞等待,那么如果A用完了锁后,B和C线程哪一个会最先获取到锁呢?

  • 公平锁:遵守先来后到原则,因为B线程比C线程来的早一点,所以B线程先获取到锁
  • 非公平锁:没有先来后到原则,B和C线程获取到锁的概率是随机的

2. 注意事项

操作系统内部的线程调度就可以视为是随机的,如果不做任何额外的限制,锁就是非公平锁。如果要想实现公平锁,就需要依赖额外的数据结构(比如队列) 来记录线程们的先后顺序。公平锁和非公平锁没有好坏之分, 关键还是看适用场景(大部分情况下非公平锁就够用了,但当我们希望线程的调度时间成本是可控的,那么此时就需要用到公平锁了)

注意:synchronized为非公平锁

六. 可重入锁和不可重入锁

1. 为什么要引入这两把锁

(1)实例一

在介绍可重入锁和不可重入锁之前,大家先来思考一个问题,为什么Java中的main函数要用static来修饰?

public class Test {
    public static void main(String[] args) {

    }
}

试想以下,如果main函数不是static来修饰的话:

public class Test {
    public void main(String[] args) {
        Test a=new Test();
        a.main();
    }
}

那么此时这段代码能否被执行呢?答案是不能,因为在java中,没有static的变量或函数,如果想被调用的话,是要先新建一个对象才可以。而main函数作为程序的入口,需要在其它函数实例化之前就启动,这也就是为什么要加一个static。main函数好比一个门,要探索其它函数要先从门进入程序。static提供了这样一个特性,无需建立对象,就可以启动。也可以利用反证法说明,如果不是static修饰的,若不是静态的,main函数调用的时候需要new对象,new完对象才能调用main函数。那么你既想进入到main函数new对象,又想new完对象来调用main函数,那么就不行了,相当于自己把自己锁在里面出不来了

(2)实例二

另外一个Java当中的例子:

 synchronized void func1(){
        func2();
    }
    synchronized void func2(){

    }

我们对func1这个方法进行加锁时,是可以成功的,但当我们对func2这个方法再次加锁后,就比较危险了。因为要执行完func1这个方法,就必须执行完func2,而此时锁已经被func1这个方法占用了,func2获取不到锁,所以func2就会一直阻塞等待,去等func1释放锁,但func1一直执行不完成,所以锁永远不会释放,func2永远也获取不到锁,这样就形成了一个闭环,相当于自己把自己锁在里面出不来了,此时这个线程就会崩溃,是比较危险的

2. 实现方案

了解了上面两个实例的严重性后,我们引入了可重入锁这个机制,当我们形成死锁后,如果是可重入锁的话,它不会让线程阻塞等待最终死锁从而奔溃,而是运用计数器的方法,去记录当前某个线程针对某把锁尝试加了几次,每加一次锁计数都会加1,每次解锁计数都会减1,这样当计数器里面的计数完全为0的时候才会真正释放锁,正是因为有了这样的机制,才有效避免了死锁问题。而在Java中,synchronized就是一把可重入锁,它给我们提供了很大的方便,保证在我们即使造成死锁问题时,程序也不至于崩溃。

七. 面试题

第一题

如何理解乐观锁和悲观锁,具体实现方式如何 如何理解?

见乐观锁和悲观锁字面理解部分(尝试用自己的语言组织)实现方式:
(1)乐观锁:见基于版本号方式实现乐观锁部分
(2)悲观锁:多个线程访问同一个共享数据时产生并发冲突时,会在取数据时会进行加锁操作,这样的话其他线程想要拿到这个数据时就会阻塞等到直到其他线程获取到锁

第二题

简单介绍一下读写锁

读写锁实际是一种特殊的自旋锁,它能把同一块共享数据的访问者分为读者和写者,读写锁会把读操作和写操作分别进行加锁,且读锁和读锁之间的线程不会发生互斥,写锁和写锁之间以及读锁和写锁之间的线程会发生互斥。读锁适用于线程读取数据比较多的场景,而写锁适用于线程修改数据比较多的场景。

第三题

简单介绍一下自旋锁

  • 理解:当两个线程为了完成任务同时竞争一把锁时, 拿到锁的那个线程会立马执行任务,而没拿到锁的线程就会立即再尝试获取锁,无限循环,直到获取到锁为止,这样的锁就叫自旋锁
  • 优点和缺点:见自旋锁优缺点部分

第四题

简单介绍一下Java中synchronized充当了哪些锁

  • 主要以悲观锁为主,初始使用乐观锁策略,但当发现锁竞争比较频繁的时候,就会自动切换成悲观锁策略
  • 并不区分读写锁
  • synchronized自身已经设置好了自旋锁和挂起等待锁,会根据不同的情况自动选择最优的使用方案
  • synchronized是一把非公平锁
  • synchronized就是一把可重入锁

到此这篇关于Java面试最容易被刷的重难点之锁的使用策略的文章就介绍到这了,更多相关Java 锁内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 浅谈Java锁机制

    目录 1.悲观锁和乐观锁 2.悲观锁应用 3.乐观锁应用 4.CAS 5.手写一个自旋锁 1.悲观锁和乐观锁 我们可以将锁大体分为两类: 悲观锁 乐观锁 顾名思义,悲观锁总是假设最坏的情况,每次获取数据的时候都认为别的线程会修改,所以每次在拿数据的时候都会上锁,这样其它线程想要修改这个数据的时候都会被阻塞直到获取锁.比如MySQL数据库中的表锁.行锁.读锁.写锁等,Java中的synchronized和ReentrantLock等. 而乐观锁总是假设最好的情况,每次获取数据的时候都认为别的线程不

  • Java的锁机制:synchronized和CAS详解

    目录 一 为什么要用锁 二 synchronized怎么实现的 三 CAS来者何人 四synchronized和CAS孰优孰劣 轻量级锁 重量级锁 总结 提到Java的知识点一定会有多线程,JDK版本不断的更迭很多新的概念和方法也都响应提出,但是多线程和线程安全一直是一个重要的关注点.比如说我们一入门就学习的synchronized怎么个实现和原理,还有总是被提到的CAS是啥,他和synchronized关系是啥?这里大概会让你对这些东西有一个认识. 一 为什么要用锁 我们使用多线程肯定是为了提

  • Java多线程 乐观锁和CAS机制详细

    目录 一.悲观锁和乐观锁 1.悲观锁 2.乐观锁 二.CAS机制 一.悲观锁和乐观锁 1.悲观锁 悲观锁是基于一种悲观的态度类来防止一切数据冲突,它是以一种预防的姿态在修改数据之前把数据锁住,然后再对数据进行读写,在它释放锁之前任何人都不能对其数据进行操作,直到前面一个人把锁释放后下一个人数据加锁才可对数据进行加锁,然后才可以对数据进行操作.synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁. 特点:可以完全保证数据的独占性和正确性,因为每次请求都会先对

  • java中synchronized锁的升级过程

    目录 synchronized锁的升级(偏向锁.轻量级锁及重量级锁) java同步锁前置知识点 synchronized同步锁 java对象头 偏向锁 轻量级锁 重量级锁 关于自旋锁 打印偏向锁的参数 synchronized原理解析 一:synchronized原理解析 1:对象头 2:Synchronized在JVM中的实现原理 三.锁的优化 1.锁升级 2.锁粗化 3.锁消除 synchronized锁的升级(偏向锁.轻量级锁及重量级锁) java同步锁前置知识点 1.编码中如果使用锁可以

  • Java中的15种锁

    目录 一.公平锁 / 非公平锁 1.公平锁 2.非公平锁 二.可重入锁 / 不可重入锁 1.可重入锁 2.不可重入锁 3.ReentrantLock中可重入锁实现 三.独享锁 / 共享锁 四.互斥锁 / 读写锁 1.互斥锁 2.读写锁 五.乐观锁 / 悲观锁 1.悲观锁 2.乐观锁 六.分段锁 七.偏向锁 / 轻量级锁 / 重量级锁 1.锁的状态 2.偏向锁 3.轻量级 4.重量级锁 八.自旋锁 1.什么是自旋锁? 2.Java如何实现自旋锁? 3.自旋锁存在的问题 4.自旋锁的优点 5.自旋锁

  • Java面试最容易被刷的重难点之锁的使用策略

    目录 一. 乐观锁和悲观锁 1. 字面理解 2. 生活实例 3. 基于版本号方式实现乐观锁 二. 读写锁 1. 理解 2. 用法 三. 重量级锁和轻量级锁 1. 原理 2. 理解 3. 区分用户态和内核态 四. 自旋锁 1. 理解 2. 实现方式 3. 优缺点 五. 公平锁和非公平锁 1. 理解 2. 注意事项 六. 可重入锁和不可重入锁 1. 为什么要引入这两把锁 (1)实例一 (2)实例二 2. 实现方案 七. 面试题 第一题 第二题 第三题 第四题 在多线程的学习中,很多时候都要用到锁,但

  • 作为程序员必须掌握的Java虚拟机中的22个重难点(推荐0

    Java虚拟机一直是比较重要的知识点,是Java高级开发必会的.本文为你总结了关于JVM的22个重点.难点,图文并茂的向你展示和JVM有关的重点知识.全文共7000字左右. 概念 虚拟机:指以软件的方式模拟具有完整硬件系统功能.运行在一个完全隔离环境中的完整计算机系统 ,是物理机的软件实现.常用的虚拟机有VMWare,Visual Box,Java Virtual Machine(Java虚拟机,简称JVM). Java虚拟机阵营:Sun HotSpot VM.BEA JRockit VM.IB

  • Java 递归重难点分析详解与练习

    目录 递归是什么 分析递归的过程 递归练习 按顺序打印一个数的每一位 递归是什么 就是一个方法在执行的时候,自己调用自己. 递归的要求: 1 有一个趋近于终止的条件 2 实现递归要去推导出一个递推公式 递归就是递下去,归上来.求 5 的阶乘,代码举例: public static int fact(int n){ if(n == 1){ return n; } return n*fact(n - 1); } public static void main(String[] args) { int

  • Java 类与对象重难点详解

    目录 什么是类.对象? 类和类的实例化 字段的初始化 类当中实现方法 static 静态关键字 封装 private 实现封装 setter 和 getter 方法 构造方法 this 关键字 匿名对象 什么是类.对象? 因为计算机并不能像我们人这样去理解世界上的东西,所以为了更好的编辑,就抽象出了类和对象.类就是把功能放在一起,然后由一个人去调用这个功能,然后再编辑对应的功能.调用者就是对象的实现者 类和类的实例化 类是一类对象的统称,对象就是这一类具体化的实例 创建类的关键字:class 举

  • Java面试必备之JMM高并发编程详解

    目录 一.什么是JMM 二.JMM定义了什么 原子性 可见性 有序性 三.八种内存交互操作 四.volatile关键字 可见性 volatile一定能保证线程安全吗 禁止指令重排序 volatile禁止指令重排序的原理 五.总结 一.什么是JMM JMM就是Java内存模型(java memory model).因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题.所以java内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差

  • java面试常见问题之Hibernate总结

    主要从以下十几个方面对Hibernate做总结,包括Hibernate的检索方式,Hibernate中对象的状态,Hibernate的3种检索策略是什么,分别适用于哪种场合,ORM解决的不匹配问题, Hibernate映射继承关系的3种方式,Session的find()方法以及Query接口的区别等方面问题的总结,具体内容如下: 1  Hibernate的检索方式 Ø  导航对象图检索(根据已经加载的对象,导航到其他对象.) Ø  OID检索(按照对象的OID来检索对象.) Ø  HQL检索(使

  • 详解Java面试官最爱问的volatile关键字

    本文向大家分享的主要内容是Java面试中一个常见的知识点:volatile关键字.本文详细介绍了volatile关键字的方方面面,希望大家在阅读过本文之后,能完美解决volatile关键字的相关问题.  在Java相关的岗位面试中,很多面试官都喜欢考察面试者对Java并发的了解程度,而以volatile关键字作为一个小的切入点,往往可以一问到底,把Java内存模型(JMM),Java并发编程的一些特性都牵扯出来,深入地话还可以考察JVM底层实现以及操作系统的相关知识. 下面我们以一次假想的面试过

  • Java面试之动态规划与组合数

    最近在刷力扣上的题目,刷到了65不同路径,当初上大学的时候,曾在hihocoder上刷到过这道题目,但是现在已经几乎全忘光了,大概的知识点是动态规划,如今就让我们一起来回顾一下. 从题目说起 题目原文是: 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为"Start" ). 机器人每次只能向下或者向右移动一步.机器人试图达到网格的右下角(在下图中标记为"Finish"). 问总共有多少条不同的路径? 例如,上图是一个7 x 3 的网格.有多少可能

  • 学前端,css与javascript重难点浅析

    JavaScript是一种属于网络的脚本语言,已经被广泛用于Web应用开发,常用来为网页添加各式各样的动态功能,为用户提供更流畅美观的浏览效果.通常JavaScript脚本是通过嵌入在HTML中来实现自身的功能的. CSS(Cascading Style Sheet)层叠样式表单 表现(presentation)--设计网页的表现样式,即"如何显示有关内容" CSS是将样式信息与网页内容分离的一种标记语言,我们使用css为每个元素定义样式:它主要用于美化HTML页面. 语法为:sele

  • Java实现批量修改文件名和重命名的方法

    平时下载的文件.视频很多都会有网址前缀,比如一些编程的教学视频,被人共享出来后,所有视频都加上一串长长的网址,看到就烦,所以一般会重命名后看,舒服很多,好了,不多说,直接上代码: 以下代码演示使用递归的方式批量重命名文件 import java.io.File; import java.io.IOException; /** * @Auther: Code * @Date: 2018/9/9 18:02 * @Description: 批量重命名文件 */ public class test {

随机推荐