吃透这JAVA并发十二核心,面试官都得对你刮目相看
其实关于Kafka,能问的问题实在是太多了,扒了几天,最终筛选出44问:基础篇17问、进阶篇15问、高级篇12问,个个直戳痛点,不知道如果你不着急看答案,又能答出几个呢?若是对Kafka的知识还回忆不起来,不妨先看我手绘的知识总结脑图(xmind不能上传,文章里用的是图片版)进行整体架构的梳理梳理了知识,刷完了面试,如若你还想进一步的深入学习解读kafka以及源码,那么接下来的这份《手写“kafk
-
yield 跟 sleep 都能暂停当前线程,都不会释放锁资源,sleep 可以指定具体休眠的时间,而 yield 则依赖 CPU 的时间片划分。
-
sleep方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会。yield方法只会给相同优先级或更高优先级的线程以运行的机会。
-
调用 sleep 方法使线程进入等待状态,等待休眠时间达到,而调用我们的 yield方法,线程会进入就绪状态,也就是sleep需要等待设置的时间后才会进行就绪状态,而yield会立即进入就绪状态。
-
sleep方法声明会抛出 InterruptedException,而 yield 方法没有声明任何异常
-
yield 不能被中断,而 sleep 则可以接受中断。
-
sleep方法比yield方法具有更好的移植性(跟操作系统CPU调度相关)
3.6 wait 跟 sleep 区别
- 来源不同
wait 来自Object,sleep 来自 Thread
- 是否释放锁
wait 释放锁,sleep 不释放
- 使用范围
wait 必须在同步代码块中,sleep 可以任意使用
- 捕捉异常
wait 不需要捕获异常,sleep 需捕获异常
3.7 多线程实现方式
-
继承 Thread,实现run方法
-
实现 Runnable接口中的run方法,然后用Thread包装下。Thread 是线程对象,Runnable 是任务,线程启动的时候一定是对象。
-
实现 Callable接口,FutureTask 包装实现接口,Thread 包装 FutureTask。Callable 与 Runnable 的区别在于Callable的call方法有返回值,可以抛出异常,Callable有缓存。
-
通过线程池调用实现。
-
通过Spring的注解 @Async 实现。
3.8 死锁
死锁是指两个或两个以上的线程互相持有对方所需要的资源,由于某些锁的特性,比如syn使用下,一个线程持有一个资源,或者说获得一个锁,在该线程释放这个锁之前,其它线程是获取不到这个锁的,而且会一直死等下去,因此这便造成了死锁。
面试官:你给我解释下死锁是什么,解释好了我就录用你。
应聘者:先发Offer,发了Offer我给你解释什么是死锁。
产生条件:
- 互斥条件:一个资源,或者说一个锁只能被一个线程所占用,当一个线程首先获取到这个锁之后,在该线程释放这个锁之前,其它线程均是无法获取到这个锁的。
- 占有且等待:一个线程已经获取到一个锁,再获取另一个锁的过程中,即使获取不到也不会释放已经获得的锁。
- 不可剥夺条件:任何一个线程都无法强制获取别的线程已经占有的锁
- 循环等待条件:线程A拿着线程B的锁,线程B拿着线程A的锁。。
检查:
1、jps -l 定位进程号
2、jstack 进程号找到死锁问题
避免:
- 加锁顺序:线程按照相同的顺序加锁。
- 限时加锁:线程获取锁的过程中限制一定的时间,如果给定时间内获取不到,就算了,这需要用到Lock的一些API。
========================================================================
4.1 JMM由来
随着CPU、内存、磁盘的高速发展,它们的访问速度差别很大。为了提速就引入了L1、L2、L3三级缓存。以后程序运行获取数据就是如下的步骤了。
这样虽然提速了但是会导致缓存一致性问题
跟内存可见性问题
。同时编译器跟CPU为了加速也引入了指令重排。指令重排的大致意思就是你写的代码运行运算结果会按照你看到的逻辑思维去运行,但是在JVM内部系统是智能化的会进行加速排序的。
1、编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2、指令级并行的重排序:现代处理器采用了指令级并行技术在不影响数据依赖性前提下重排。
3、内存系统的重排序:处理器使用缓存和读/写缓冲区 进程重排。
指令重排这种机制会导致有序性问题
,而在并发编程时经常会涉及到线程之间的通信跟同步问题,一般说是可见性
、原子性
、有序性
。这三个问题对应的底层就是 缓存一致性、内存可见性、有序性。
原子性:原子性就是指该操作是不可再分的。不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。比如 a = 1。
可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。Java保证可见性可以认为通过volatile、synchronized、final来实现。
有序性:程序执行的顺序按照代码的先后顺序执行,Java通过volatile、synchronized来保证。
为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存模式下多线程程序读写操作行为的规范,既JMM模型,注意JMM只是一个约定概念,是用来保证效果一致的机制跟规范。它作用于工作内存和主存之间数据同步过程,规定了如何做数据同步以及什么时候做数据同步。
在JMM中,有两条规定:
- 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写。
- 不同线程之间无法访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
共享变量要实现可见性,必须经过如下两个步骤:
- 把本地内存1中更新过的共享变量刷新到主内存中。
- 把主内存中最新的共享变量的值更新到本地内存2中。
同时人们提出了内存屏障
、happen-before
、af-if-serial
这三种概念来保证系统的可见性
、原子性
、有序性
。
4.2 内存屏障
内存屏障是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序,从而让程序按我们预想的流程去执行。具有如下功能:
- 保证特定操作的执行顺序。
- 影响某些数据(或者是某条指令的执行结果)的内存可见性。
在 volatile 中就用到了内存屏障,volatile 部分已详细讲述。
4.3 happen-before
因为有指令重排
的存在会导致难以理解CPU内部运行规则,JDK用 happens-before 的概念来阐述操作之间的内存可见性。在 JMM 中如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before
关系 。其中CPU的happens-before
无需任何同步手段就可以保证的。
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
- start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
- join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
- 线程中断规则:对线程interrupt方法的调用happens-before于被中断线程的代码检测到中断事件的发生。
4.4 af-if-serial
af-if-serial 的含义是不管怎么重排序(编译器和处理器为了提高并行度),单线程环境下程序的执行结果不能被改变且必须正确。该语义使单线程环境下程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。
=============================================================================
volatile 关键字的引入可以保证变量的可见性,但是无法保证变量的原子性,比如 a++这样的是无法保证的。这里其实涉及到 JMM 的知识点,Java多线程交互是通过共享内存的方式实现的。当我们读写volatile变量时具有如下规则:
- 当写一个volatile变量时,JMM会把该线程对应的本地中的共享变量值刷新到主内存。
- 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
volatile就会用到上面说到的内存屏障,目前有四种内存屏障:
- StoreStore屏障,保证普通写不和volatile写发生重排序
- StoreLoad屏障,保证volatile写与后面可能的volatile读写不发生重排序
- LoadLoad屏障,禁止volatile读与后面的普通读重排序
- LoadStore屏障,禁止volatile读和后面的普通写重排序
volatile 原理:用volatile变量修饰的共享变量进行写操作的时候会使用CPU提供的Lock前缀指令,在CPU级别的功能如下:
- 将当前处理器缓存行的数据写回到 系统内存。
- 这个写回内存的操作会告知在其他CPU你们拿到的变量是无效的下一次使用时候要重新共享内存拿。
========================================================================================
6.1 标准单例模式
高频考点单例模式:就是将类的构造函数进行private化,然后只留出一个静态的 Instance 函数供外部调用者调用。单例模式一般标准写法是 DCL + volatile:
public class SingleDcl {
private volatile static SingleDcl singleDcl; //保证可见性
private SingleDcl(){
}
public static SingleDcl getInstance(){
// 放置进入加锁代码,先判断下是否已经初始化好了
if(singleDcl == null) {
// 类锁 可能会出现 AB线程都在这卡着,A获得锁,B等待获得锁。
synchronized (SingleDcl.class) {
if(singleDcl == null) {
// 如果A线程初始化好了,然后通过vloatile 将变量复杂给住线程。
// 如果此时没有singleDel === null,判断 B进程 进来后还会再次执行 new 语句
singleDcl = new SingleDcl();
}
}
}
return singleDcl;
}
}
6.2 为什么用Volatile修饰
不用Volatile则代码运行时可能存在指令重排,会导致线程一在运行时执行顺序是 1–>2–> 4 就赋值给instance变量了,然后接下来再执行构造方法初始化。问题是如果构造方法初始化执行没完成前 线程二进入发现instance != null,直接给线程二个半成品,加入volatile后底层会使用内存屏障强制按照你以为的执行。
单例模式几乎是面试必考点,,一般有如下特性:
懒汉式:在需要用到对象时才实例化对象,正确的实现方式是 Double Check + Lock + volatile,解决了并发安全和性能低下问题,对内存要求非常高,那么使用懒汉式写法。
饿汉式:在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,对内存要求不高使用饿汉式写法,因为简单不易出错,且没有任何并发安全和性能问题。
枚举式:Effective Java 这本书也列举了使用枚举,其代码精简,没有线程安全问题,且 Enum 类内部防止反射和反序列化时破坏单例。
========================================================================
7.1 五分钟了解线程池
老王是个深耕在帝都的一线码农,辛苦一年挣了点钱,想把钱存储到银行卡里,拿钱去银行办理遇到了如下的遭遇
-
老王银行门口取号后发现有柜台营业ing 但是没人办理业务就
直接办理
了。 -
老王取号后发现柜台上都有人在办理,等待席有空地,去
坐着等
办理去了。 -
老王取号后发现柜台都有人办理,等待席也人坐满了,这个时候银行经理看到老王是老实人本着关爱老实人的态度,新开一个临时窗口给他办理了。
-
老王取号后发现柜台都满了,等待座位席也满了,临时窗口也人满了。这个时候银行经理给出了若干
解决策略
。
- 直接告知人太多不给你办理了。
- 采用冷暴力模式,也不给不办理也不让他走。
- 经理让老王取尝试跟座位席中最前面的人聊一聊看是否可以加塞,可以就办理,不可以还是被踢走。
- 经理直接跟老王说谁让你来的你找谁去我这办理不了。
上面的这个流程几乎就跟JDK线程池的大致流程类似,其中7大参数:
- 营业中的3个窗口对应核心线程池数:corePoolSize
- 银行总的营业窗口数对应:maximumPoolSize
- 打开的临时窗口在多少时间内无人办理则关闭对应:keepAliveTime
- 临时窗口存货时间单位:TimeUnit
- 银行里的等待座椅就是等待队列:BlockingQueue
- threadFactory 该参数在JDK中是 线程工厂,用来创建线程对象,一般不会动。
- 无法办理的时候银行给出的解决方法对应:RejectedExecutionHandler
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,一般有四大拒绝策略:
- ThreadPoolExecutor.AbortPolicy :丢弃任务,并抛出 RejectedExecutionException 异常。
- ThreadPoolExecutor.CallerRunsPolicy:该任务被线程池拒绝,由调用 execute方法的线程执行该任务。
- ThreadPoolExecutor.DiscardOldestPolicy :抛弃队列最前面的任务,然后重新尝试执行任务。
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,也不抛出异常。
7.2 正确创建方式
使用Executors创建线程池可能会导致OOM。原因在于线程池中的BlockingQueue主要有两种实现,分别是ArrayBlockingQueue 和 LinkedBlockingQueue。
- ArrayBlockingQueue 是一个用数组实现的有界阻塞队列,必须设置容量。
- LinkedBlockingQueue 是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,极易容易导致线程池OOM。
正确创建线程池的方式就是自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量就可以了。
private static ExecutorService executor = new ThreadPoolExecutor(10, 10,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue(10));
7.3 常见线程池
罗列几种常见的 线程池创建 方式。
- Executors.newFixedThreadPool
定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程。 使用的无界的等待队列是LinkedBlockingQueue。使用时候小心堵满等待队列。
- Executors.newSingleThreadExecutor
创建单个线程数的线程池,它可以保证先进先出的执行顺序
- Executors.newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- Executors.newScheduledThreadPool
创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行
- ThreadPoolExecutor
最原始跟常见的创建线程池的方式,它包含了 7 个参数、4种拒绝策略 可用。
7.4 线程池核心点
线程池 在工作中常用,面试也是必考点。关于线程池的细节跟使用在以前举例过一个 银行排队 办业务的例子了。线程池一般主要也无非就是下面几个考点了:
- 为什么用线程池。
- 线程池的作用。
- 7大重要参数。
- 4大拒绝策略。
- 常见线程池任务队列,如何理解有界跟无界。
- 常用的线程池模版。
- 如何分配线程池个数,IO密集型还是 CPU密集型。
- 设定一个线程池优先级队列,Runable类要实现可对比功能,任务队列使用优先级队列。
================================================================================
ThreadLocal 可以简单理解为线程本地变量,相比于 synchronized 是用空间来换时间的思想。他会在每个线程都创建一个副本,在线程之间通过访问内部副本变量的形式做到了线程之间互相隔离。这里用到了 弱引用 知识点:
如果一个对象只具有弱引用,那么GC回收器在扫描到该对象时,无论内存充足与否,都会回收该对象的内存。
8.1 核心点
每个Thread内部都维护一个ThreadLocalMap字典数据结构,字典的Key值是ThreadLocal,那么当某个ThreadLocal对象不再使用(没有其它地方再引用)时,每个已经关联了此ThreadLocal的线程怎么在其内部的ThreadLocalMap里做清除此资源呢?JDK中的ThreadLocalMap没有继承java.util.Map类,而是自己实现了一套专门用来定时清理无效资源的字典结构。其内部存储实体结构Entry<ThreadLocal, T>继承自java.lan.ref.WeakReference,这样当ThreadLocal不再被引用时,因为弱引用机制原因,当jvm发现内存不足时,会自动回收弱引用指向的实例内存,即其线程内部的ThreadLocalMap会释放其对ThreadLocal的引用从而让jvm回收ThreadLocal对象。这里是重点强调下,回收的是Key 也就是ThreadLocal对象,而非整个Entry,所以线程变量中的值T对象还是在内存中存在的,所以内存泄漏的问题还没有完全解决。
接着分析底层代码会发现在调用ThreadLocal.get() 或者 ThreadLocal.set() 都会 定期回收无效的Entry 操作。
========================================================================
Compare And Swap:比较并交换,主要是通过处理器的指令来保证操作的原子性
,它包含三个操作数:
V:变量内存地址
A:旧的预期值
B:准备设置的新值
当执行 CAS 指令时,只有当 V 对应的值等于 A 时才会用 B 去更新V的值,否则就不会执行更新操作。CAS 可能会带来ABA问题、循环开销过大问题、一个共享变量原子性操作的局限性。如何解决以前写过,在此不再重复。
==================================================================================
10.1 Synchronized 讲解
Synchronized 是 JDK自带的线程安全关键字,该关键字可以修饰实例方法、静态方法、代码块三部分。该关键字可以保证互斥性、可见性、有序性(不解决重排)但保证有序性。
Syn 的底层其实是C++代码写的,JDK6前是重量级锁,调用的时候涉及到用户态跟内核态的切换,挺耗时的。JDK6之前 Doug Lea写出了JUC包,可以方便的让用于在用户态实现锁的使用,Syn的开发者被激发了斗志所以在JDK6后对Syn进行了各种性能升级。
10.2 Synchronized 底层
Syn里涉及到了 对象头 包含对象头、填充数据、实例变量。这里可以看一个美团面试题:
问题一:new Object()占多少字节
- markword 8字节 + classpointer 4字节(默认用calssPointer压缩) + padding 4字节 = 16字节
- 如果没开启classpointer压缩:markword 8字节 + classpointer 8字节 = 16字节
问题二:User (int id,String name) User u = new User(1,“李四”)
markword 8字节 + 开启classPointer压缩后classpointer 4字节 + instance data int 4字节 + 开启普通对象指针压缩后String4字节 + padding 4 = 24字节
10.3 Synchronized 锁升级
synchronized 锁在JDK6以后有四种状态,无锁
、偏向锁
、轻量级锁
、重量级锁
。这几个状态会随着竞争状态逐渐升级,锁可以升级但不能降级,但是偏向锁状态可以被重置为无锁状态。大致升级过程如下:
锁对比:
| 锁状态 | 优点 | 缺点 | 适用场景 |
| — | — | — | — |
| 偏向锁 | 加锁解锁无需额外消耗,跟非同步方法时间相差纳秒级别 | 如果竞争线程多,会带来额外的锁撤销的消耗 | 基本没有其他线程竞争的同步场景 |
| 轻量级锁 | 竞争的线程不会阻塞而是在自旋,可提高程序响应速度 | 如果一直无法获得会自旋消耗CPU | 少量线程竞争,持有锁时间不长,追求响应速度 |
| 重量级锁 | 线程竞争不会导致CPU自旋跟消耗CPU资源 | 线程阻塞,响应时间长 | 很多线程竞争锁,切锁持有时间长,追求吞吐量时候 |
10.4 Synchronized 无法禁止指令重排,却能保证有序性
指令重排是程序运行时 解释器 跟 CPU 自带的加速手段,可能导致语句执行顺序跟预想不一样情况,但是无论如何重排 也必须遵循 as-if-serial。
避免重排的最简单方法就是禁止处理器优化跟指令重排,比如 volatile 中用内存屏障实现,syn是关键字级别的排他且可重入锁,当某个线程执行到一段被syn修饰的代码之前,会先进行加锁,执行完之后再进行解锁。
当某段代码被syn加锁后跟解锁前,其他线程是无法再次获得锁的,只有这条加锁线程可以重复获得该锁。所以代码在执行的时候是单线程执行的,这就满足了as-if-serial语义,正是因为有了as-if-serial语义保证,单线程的有序性就天然存在了。
10.5 wait 虚假唤醒
虚假唤醒定义:
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
总结:绘上一张Kakfa架构思维大纲脑图(xmind)
其实关于Kafka,能问的问题实在是太多了,扒了几天,最终筛选出44问:基础篇17问、进阶篇15问、高级篇12问,个个直戳痛点,不知道如果你不着急看答案,又能答出几个呢?
若是对Kafka的知识还回忆不起来,不妨先看我手绘的知识总结脑图(xmind不能上传,文章里用的是图片版)进行整体架构的梳理
梳理了知识,刷完了面试,如若你还想进一步的深入学习解读kafka以及源码,那么接下来的这份《手写“kafka”》将会是个不错的选择。
-
Kafka入门
-
为什么选择Kafka
-
Kafka的安装、管理和配置
-
Kafka的集群
-
第一个Kafka程序
-
Kafka的生产者
-
Kafka的消费者
-
深入理解Kafka
-
可靠的数据传递
-
Spring和Kafka的整合
-
SpringBoot和Kafka的整合
-
Kafka实战之削峰填谷
-
数据管道和流式处理(了解即可)
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
style=“zoom: 33%;” />
总结:绘上一张Kakfa架构思维大纲脑图(xmind)
[外链图片转存中…(img-4oWJDsbY-1712878600366)]
其实关于Kafka,能问的问题实在是太多了,扒了几天,最终筛选出44问:基础篇17问、进阶篇15问、高级篇12问,个个直戳痛点,不知道如果你不着急看答案,又能答出几个呢?
若是对Kafka的知识还回忆不起来,不妨先看我手绘的知识总结脑图(xmind不能上传,文章里用的是图片版)进行整体架构的梳理
梳理了知识,刷完了面试,如若你还想进一步的深入学习解读kafka以及源码,那么接下来的这份《手写“kafka”》将会是个不错的选择。
-
Kafka入门
-
为什么选择Kafka
-
Kafka的安装、管理和配置
-
Kafka的集群
-
第一个Kafka程序
-
Kafka的生产者
-
Kafka的消费者
-
深入理解Kafka
-
可靠的数据传递
-
Spring和Kafka的整合
-
SpringBoot和Kafka的整合
-
Kafka实战之削峰填谷
-
数据管道和流式处理(了解即可)
[外链图片转存中…(img-NvWuZoPl-1712878600366)]
[外链图片转存中…(img-7RqzWpMz-1712878600367)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
更多推荐
所有评论(0)