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也是可重入的
相关文章
|
15天前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
71 17
|
26天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
11天前
|
缓存 安全 算法
Java 多线程 面试题
Java 多线程 相关基础面试题
|
28天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
28天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
27天前
|
并行计算 算法 安全
面试必问的多线程优化技巧与实战
多线程编程是现代软件开发中不可或缺的一部分,特别是在处理高并发场景和优化程序性能时。作为Java开发者,掌握多线程优化技巧不仅能够提升程序的执行效率,还能在面试中脱颖而出。本文将从多线程基础、线程与进程的区别、多线程的优势出发,深入探讨如何避免死锁与竞态条件、线程间的通信机制、线程池的使用优势、线程优化算法与数据结构的选择,以及硬件加速技术。通过多个Java示例,我们将揭示这些技术的底层原理与实现方法。
80 3
|
28天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
54 3
|
28天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
154 2
|
1月前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
51 6
|
1月前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####

热门文章

最新文章