2021-Java后端工程师面试指南-(并发-多线程)(上)

简介: 前言文本已收录至我的GitHub仓库,欢迎Star:github.com/bin39232820…种一棵树最好的时间是十年前,其次是现在

Tips


面试指南系列,很多情况下不会去深挖细节,是小六六以被面试者的角色去回顾知识的一种方式,所以我默认大部分的东西,作为面试官的你,肯定是懂的。

www.processon.com/view/link/6…

上面的是脑图地址


叨絮


可能大家觉得有点老生常谈了,确实也是。面试题,面试宝典,随便一搜,根本看不完,也看不过来,那我写这个的意义又何在呢?其实嘛我写这个的有以下的目的

  • 第一就是通过一个体系的复习,让自己前面的写的文章再重新的过一遍,总结升华嘛
  • 第二就是通过写文章帮助大家建立一个复习体系,我会将大部分会问的的知识点以点带面的形式给大家做一个导论

然后下面是前面的文章汇总

今天来看看多线程的,这块是重点,也是难点,硬核有点多哈哈。


并发


记得阿里的第一个题就是面的并发,哈哈 这个小六六得好好总结了。


聊聊Java的并发模型

这个为啥是第一个问题,肯定是有原因的,如果连Java的并发模型都不清楚,你跟我扯一堆的锁,一堆的juc有啥用呢?

  • Java并发 采用的是 共享内存模型,Java线程之前的通信总是隐式进行的。
  • Java线程通信由Java内存模型(简称 JMM)控制,JMM 决定一个线程对共享变量的写入何时对另一个线程可见。从抽象角度看,JMM定义了 线程 和 主内存 之间的抽象关系:线程之间的共享变量储存在主内存中,每个线程都有一个私有的本地内存,本地内存储存了 该线程 以读共享变量的副本。


对多线程了解吗,说说你平时怎么对临界资源的访问控制的。

其实这个题就是一个引人,由浅入深的过程,

  • 如果对应的临界资源是在单JVM的进程中,那么我们可以用Synchronized和lock
  • 对于分布式环境下的多线程中,那么就得用上分布式锁(redis 或者zookeeper实现)


那么聊聊你对Synchronized的认识吧

  • synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
  • synchronized 最主要三种用法
  • 修饰实例方法 要获得当前对象实例的锁
  • 修饰静态方法 获得当前类对象的锁
  • 修饰代码块 synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁
  • synchronized 关键字最主要的二种底层实现方式:
  • synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
  • synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。


聊聊Java对象的布局

  • 首先我们知道Java对象分布由三个部分组成 对象头、实例数据、对对齐填充字节,下面我们来一个个说说
  • 对象头的组成由 Mark Word、类元数据的指针(Klass Pointer)、数组长度(不一定有),在64位Java虚拟机里面的Mark word  包含了我们的 hashcode的值  我们的分代年龄 锁标志位等
  • 实例数据 并不是所有的变量都存放在这里,对象的的所有成员变量以及其父类的成员变量是存放在这里的。
  • JVM要求Java对象的大小必须是8byte的倍数,所以这个的作用就是把对象的大小补齐至8byte的倍数。


那你说说Synchronized锁升级的过程吧

锁级别从低到高依次是:

  • 无锁状态
  • 偏向锁状态
  • 轻量级锁状态
  • 重量级锁状态

小六六在这给大家明确的一点就是,Synchronized锁的是对象而不是其包裹的代码。

  • 对象被new出来后,没有任何线程持有这个对象的锁,这时就是无锁状态;Mark Word锁标识是01
  • 当且仅当只有一个线程A获取到这个对象的锁的时候,对象就会从无锁状态升级成为偏向锁,Mark Word中就会记录这个线程的标识(锁标识是01),此时线程A持有这个对象的锁;
  • 还是这个线程A再次获取这个对象的锁时,发现他是一个偏向锁,并且对象头中记录着自己的线程标识,那么线程A就继续使用这把锁(不需要cas去获取锁了)。这里也是锁的可重入性,所以,synchronized也是可重入锁;
  • 在线程A持有锁的时候,线程B也来争抢这把锁了,线程B发现这是一把偏向锁,并且对象头中的线程标识不是自己。那么首先进行偏向锁的撤销过程,然后偏向锁就会升级为轻量级锁,此时Mark Word 的锁标识是00
  • 又有一些线程来争抢这个轻量级锁了,争抢的过程其实就是利用CAS自旋。为了避免长时间自旋消耗CPU资源,当自旋超过10次的时候,轻量级锁升级为重量级锁(其他线程阻塞,不会耗费cpu)。此时Mark Word的锁标识是10


可以聊聊CAS吗,它有什么问题吗?

CAS,compare and swap的缩写,就是一个保证原子性的一个手段,CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。而整个J.U.C都是建立在CAS之上的,因此对于synchronized阻塞算法,J.U.C在性能上有了很大的提升。


问题

  • ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
  • 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
  • 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。


可以说说Synchronized和Lock的区别嘛

  • 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
  • synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
  • synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可中断、可公平(两者皆可)
  • synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
  • 而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。


既然提到了Lock,那我们来聊聊他最常用的实现ReentrantLock吧,说说的公平和非公平是怎么实现的,他们的哪个效率高,默认是哪个,又是怎么现实可重入的

  • 首先公平和非公平是指多线程下各线程获取锁的顺序,先到的线程优先获取锁,而非公平锁则无法提供这个保障,但是呢 我们知道ReentrantLock的非公平实现,其实并不是随机的,它是有一定顺序的非公平,举个非公平的例子,假设A来获取锁,如果A获得了锁,此时B来获取锁,然后B失败了,B就去队列等待,此时C来了,然后C也失败了,他也去等待,此时D过来了,然后A释放了锁,那你说如果是绝对公平的话这个时候应该是B获取锁才对,但是源码中是D此时有机会去获取锁,所以它是一定顺序的非公平,非公平锁效率高于公平锁的,因为非公平锁减少了线程挂起的几率,后来的线程有一定几率逃离被挂起的开销,所以默认是非公平的锁。而我们的Synchronized也是非公平的
  • 可重入是指,当我一个线程获取了这把锁,下次当前线程再释放锁之前再去获取锁的时候是可以成功,这就是可重入锁,Lock 的实现是判断是当前线程的时候,给锁状态+1,然后我们的Synchronized也是可重入的
相关文章
|
1天前
|
Java
Java一分钟:线程协作:wait(), notify(), notifyAll()
【5月更文挑战第11天】本文介绍了Java多线程编程中的`wait()`, `notify()`, `notifyAll()`方法,它们用于线程间通信和同步。这些方法在`synchronized`代码块中使用,控制线程执行和资源访问。文章讨论了常见问题,如死锁、未捕获异常、同步使用错误及通知错误,并提供了生产者-消费者模型的示例代码,强调理解并正确使用这些方法对实现线程协作的重要性。
10 3
|
1天前
|
安全 算法 Java
Java一分钟:线程同步:synchronized关键字
【5月更文挑战第11天】Java中的`synchronized`关键字用于线程同步,防止竞态条件,确保数据一致性。本文介绍了其工作原理、常见问题及避免策略。同步方法和同步代码块是两种使用形式,需注意避免死锁、过度使用导致的性能影响以及理解锁的可重入性和升级降级机制。示例展示了同步方法和代码块的运用,以及如何避免死锁。正确使用`synchronized`是编写多线程安全代码的核心。
34 2
|
1天前
|
安全 Java 调度
Java一分钟:多线程编程初步:Thread类与Runnable接口
【5月更文挑战第11天】本文介绍了Java中创建线程的两种方式:继承Thread类和实现Runnable接口,并讨论了多线程编程中的常见问题,如资源浪费、线程安全、死锁和优先级问题,提出了解决策略。示例展示了线程通信的生产者-消费者模型,强调理解和掌握线程操作对编写高效并发程序的重要性。
32 3
|
2天前
|
安全 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第11天】在Java并发编程中,线程安全和性能优化是两个重要的主题。本文将深入探讨这两个方面,包括线程安全的基本概念,如何实现线程安全,以及如何在保证线程安全的同时进行性能优化。我们将通过实例和代码片段来说明这些概念和技术。
3 0
|
2天前
|
Java 调度
Java并发编程:深入理解线程池
【5月更文挑战第11天】本文将深入探讨Java中的线程池,包括其基本概念、工作原理以及如何使用。我们将通过实例来解释线程池的优点,如提高性能和资源利用率,以及如何避免常见的并发问题。我们还将讨论Java中线程池的实现,包括Executor框架和ThreadPoolExecutor类,并展示如何创建和管理线程池。最后,我们将讨论线程池的一些高级特性,如任务调度、线程优先级和异常处理。
|
2天前
|
安全 Java
【JAVA进阶篇教学】第十篇:Java中线程安全、锁讲解
【JAVA进阶篇教学】第十篇:Java中线程安全、锁讲解
|
2天前
|
安全 Java
【JAVA进阶篇教学】第六篇:Java线程中状态
【JAVA进阶篇教学】第六篇:Java线程中状态
|
2天前
|
缓存 Java
【JAVA进阶篇教学】第五篇:Java多线程编程
【JAVA进阶篇教学】第五篇:Java多线程编程
|
2天前
|
Java
【JAVA基础篇教学】第十二篇:Java中多线程编程
【JAVA基础篇教学】第十二篇:Java中多线程编程
|
2天前
|
安全 Java
java-多线程学习记录
java-多线程学习记录