大三Java后端暑期实习面经总结——Java多线程并发篇
博主现在大三在读,从三月开始找暑期实习,暑假准备去tx实习啦!总结下了很多面试真题,希望能帮助正在找工作的大家!相关参考都会标注原文链接,尊重原创!
目录
- 1. synchronized底层实现原理
- 2. sychronized锁升级过程
- 1. Java对象头
- 2. 锁的状态
- --- 无锁
- --- 偏向锁
- --- 轻量级锁
- --- 重量级锁
- 3. 其他优化
- 3. ThreadLocal原理和使用场景
- 1. 什么是ThreadLocal
- 2. ThreadLocal的使用
- 3. ThreadLocal应用场景
- 4. ThreadLocal内存泄漏原因,如何避免
- 5. 为什么要用线程池?相关参数?
- 6. 线程池的处理流程
- 7. 线程池中阻塞队列的作用?为什么先添加队列而不是先创建最大线程?
- 8. 线程池中线程复用原理
- 9. 线程的生命周期?有几种状态
- 10. sleep、wait、join、yeild的区别
- 11. 对线程安全的理解
- 12. Thread和Runnable区别
- 13. 说说你对守护线程的理解
- 14. 乐观锁/悲观锁
- 15. 乐观锁和悲观锁如何选择
- 16. 公平/非公平锁
- 17. 可重入锁
- 18. 自旋锁
- 19. 死锁
- 20. 死锁问题排查与避免
- 21. 无锁是怎么实现的
- 22. JMM(Java内存模型)
- 主内存与工作内存
- 内存间交互操作
- 23. 创建线程的四种方式
- 24. notify()和 notifyAll()的区别
- 25. run()和 start()的区别
- 26. 创建线程池有哪几种方式
- 27. 线程池中submit()和execute()方法的区别
- 28. synchronized和Lock的区别
- 29. synchronized和volatile的区别
- 30. synchronized和ReentrantLock的区别
- 31. 说一下atomic的原理
参考:
- https://zhuanlan.zhihu.com/p/64147696
- https://www.bilibili.com/video/BV1Eb4y1R7zd?p=2&t=4948
1. synchronized底层实现原理
synchronized有两种形式上锁:同步方法、同步代码块。它们底层实现其实都一样,在进入同步代码之前先获取锁,获取到锁之后锁的计数器+1,同步代码执行完锁的计数器-1,如果获取失败就阻塞式等待锁的释放。只是他们在同步块识别方式上有所不一样,从class字节码文件可以表现出来,一个是通过方法flags标志,一个是monitorenter和monitorexit指令操作。
1️⃣ 同步代码块
//synchronized修饰代码块
public class Test implements Runnable {@Overridepublic void run() {// 加锁操作synchronized (this) {System.out.println("hello");}}public static void main(String[] args) {Test test = new Test();Thread thread = new Thread(test);thread.start();}
}
javap -c
查看相应的class文件:
可以看出在执行同步代码块之前之后都有一个monitor字样,其中前面的是monitorenter,后面的是离开monitorexit,不难想象一个线程也执行同步代码块,首先要获取锁,而获取锁的过程就是monitorenter ,在执行完代码块之后,要释放锁,释放锁就是执行monitorexit指令。
为什么会有两个monitorexit呢?
这个主要是防止在同步代码块中线程因异常退出,而锁没有得到释放,这必然会造成死锁(等待的线程永远获取不到锁)。因此最后一个monitorexit是保证在异常情况下,锁也可以得到释放,避免死锁。
2️⃣ 同步方法
public class Test implements Runnable {@Overridepublic synchronized void run() {System.out.println("hello again");}public static void main(String[] args) {Test test = new Test();Thread thread = new Thread(test);thread.start();}
}
再次用javap -c
查看相应的class文件:
仅有ACC_SYNCHRONIZED这么一个标志,该标记表明线程进入该方法时,需要monitorenter,退出该方法时需要monitorexit
Monitor
可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个 Java 对象就有一把看不见的锁,称为内部锁或者 Monitor 锁。
Monitor 是线程私有的数据结构,每一个线程都有一个可用 monitor record 列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个 monitor 关联,同时 monitor 中有一个 Owner 字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
Synchronized
是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为重量级锁。
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。JDK 1.6中默认是开启偏向锁和轻量级锁的,我们也可以通过-XX:-UseBiasedLocking=false来禁用偏向锁。
2. sychronized锁升级过程
深入理解synchronized底层原理,一篇文章就够了!
synchronized的底层实现原理及各种优化
关于 锁的四种状态与锁升级过程 图文详解
1. Java对象头
synchronized
用的锁是存在Java对象头里的,那么什么是对象头呢?
在JVM中,对象是分成三部分存在的:对象头、实例数据、对其填充。实例数据存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐;对其填充不是必须部分,由于虚拟机要求对象起始地址必须是8字节的整数倍,对齐填充仅仅是为了使字节对齐。
以上两者与synchronized无关,==对象头==是我们需要关注的重点,它是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。
我们以 Hotspot 虚拟机为例,Hopspot 对象头主要包括两部分数据:Mark Word(标记字段)和Klass Pointer(类型指针)
- Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
- Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
在上面中我们知道了,synchronized
用的锁是存在Java对象头里的,那么具体是存在对象头哪里呢?答案是:存在锁对象的对象头的Mark Word中,那么MarkWord在对象头中到底长什么样,它到底存储了什么呢?
在64位的虚拟机中:
在32位的虚拟机中:
下面我们以 32位虚拟机为例,来看一下其 Mark Word 的字节具体是如何分配的
无锁:对象头开辟 25bit 的空间用来存储对象的 hashcode ,4bit 用于存放对象分代年龄,1bit 用来存放是否偏向锁的标识位,2bit 用来存放锁标识位为01
偏向锁: 在偏向锁中划分更细,还是开辟 25bit 的空间,其中23bit 用来存放线程ID,2bit 用来存放 Epoch,4bit 存放对象分代年龄,1bit 存放是否偏向锁标识, 0表示无锁,1表示偏向锁,锁的标识位还是01
轻量级锁:在轻量级锁中直接开辟 30bit 的空间存放指向栈中锁记录的指针,2bit 存放锁的标志位,其标志位为00
重量级锁: 在重量级锁中和轻量级锁一样,30bit 的空间用来存放指向重量级锁的指针,2bit 存放锁的标识位,为11
GC标记: 开辟30bit 的内存空间却没有占用,2bit 空间存放锁标志位为11。
2. 锁的状态
锁的状态总共有四种,级别由低到高依次为:无锁、偏向锁、轻量级锁、重量级锁,这四种锁状态分别代表什么,为什么会有锁升级?
其实在 JDK 1.6之前,synchronized 还是一个重量级锁,是一个效率比较低下的锁,最初的实现方式是 “阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,这种方式就是 synchronized
实现同步最初的方式,这也是当初开发者诟病的地方,这也是在JDK6以前 synchronized
效率低下的原因
在JDK 1.6后,Jvm为了提高锁的获取与释放效率对synchronized进行了优化,引入了 偏向锁 和 轻量级锁 ,从此以后锁的状态就有了四种(无锁、偏向锁、轻量级锁、重量级锁),并且四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,也就是说只能进行锁升级(从低级别到高级别),不能锁降级(高级别到低级别),意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
锁的类型和状态在对象头Mark Word
中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word
数据。
锁状态 | 存储内容 | 标志位 |
---|---|---|
无锁 | 对象的hashCode、对象分代年龄、是否是偏向锁(0) | 01 |
偏向锁 | 偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 |
重量级锁 | 指向互斥量的指针 | 11 |
锁对比
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到索竞争的线程,使用自旋会消耗CPU | 追求响应速度,同步块执行速度非常快 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较慢 |
— 无锁
无锁是指没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
无锁的特点是修改操作会在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。
— 偏向锁
偏向锁针对的是锁不存在竞争,每次仅有一个线程来获取该锁,为了提高获取锁的效率,因此将该锁偏向该线程。提升性能。
初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。
当一个线程访问同步代码块并获取锁时,会在 Mark Word 里存储锁偏向的线程 ID。在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向当前线程的偏向锁。轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令即可。
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。
关于偏向锁的撤销,需要等待全局安全点,即在某个时间点上没有字节码正在执行时,它会先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁,恢复到无锁(标志位为01)或轻量级锁(标志位为00)的状态。
— 轻量级锁
偏向锁考虑的是不存在多个线程竞争同一把锁,而轻量级锁考虑的是,多个线程不会在同一时刻来竞争同一把锁。
轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋(关于自旋的介绍见文末)的形式尝试获取锁,线程不会阻塞,从而提高性能。
轻量级锁的获取主要由两种情况:
① 当关闭偏向锁功能时;
② 由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。
一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。这里要明确一下什么是锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。
在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。
长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。
— 重量级锁
重量级锁描述同一时刻有多个线程竞争同一把锁。
重量级锁显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。
重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
简言之,就是所有的控制权都交给了操作系统,由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资源
3. 其他优化
从最近几个jdk版本中可以看出,Java的开发团队一直在对synchronized优化,其中最大的一次优化就是在jdk6的时候,除了新增了两个锁状态,还通过锁消除、锁粗化、自旋锁等方法使用各种场景,给synchronized性能带来了很大的提升
自旋锁:
线程未获得锁后,不是一昧的阻塞,而是让线程不断尝试获取锁。
缺点:若线程占用锁时间过长,导致CPU资源白白浪费。
解决方式:当尝试次数达到每个值得时候,线程挂起。
自适应自旋锁:
自旋得次数由上一次获取锁的自旋次数决定,次数稍微延长一点点。
锁消除
对于线程的私有变量,不存在并发问题,没有必要加锁,即使加锁编译后,也会去掉。
锁粗化
当一个循环中存在加锁操作时,可以将加锁操作提到循环外面执行,一次加锁代替多次加锁,提升性能
3. ThreadLocal原理和使用场景
1. 什么是ThreadLocal
ThreadLocal 是线程本地存储,在每个线程中都创建了一个ThreadLocalMap 对象,它存储本线程中所有ThreadLocal对象及其对应的值,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。
由于每一条线程都含有线程私有的ThreadLocalMap容器,这些容器间相互独立不影响,因此不会存在线程安全的问题,从而无需使用同步机制来保证多条线程访问容器的互斥性
ThreadLocalMap
是ThreadLocal
的内部类,可以理解为一个map容器,由一个个key-value对象Entry
构成
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xWiFADeq-1621873942557)(C:/Users/zsr204/AppData/Roaming/Typora/typora-user-images/image-20210430082031627.png)]
看到Entry
继承自WeakReferencr<ThreadLocal<?>>
,就是一个key-value形式的对象。它的key就是ThreadLocal对象,并且是一个弱引用,如果没有指向key的强引用后,该key就会被垃圾回收器回收;Entry的value就是存储相应ThreadLocal对象的值
2. ThreadLocal的使用
当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象,再以当前的ThreadLocal对象为key,将值存入ThreadLocalMap对象中
get方法执行过程类似,首先ThreadLocal获取当前线程对象,然后获取当前线程的ThreadLocalMap对象,再以当前的ThreadLocal对象为key,获取对应的value
3. ThreadLocal应用场景
- 避免参数的显示传递(比如线程中处理一个非常复杂的业务,有很多方法,使用 ThreadLocal 可以代替一些参数的显式传递,直接从当前线程中存取)
- 线程间数据隔离
- 进行事务操作时存储线程事务信息,因为事务和线程绑定在一起(Spring在事务开始时会给当前线程绑定一个Jdbc Connection对象,放在ThreadLocal中存储,这样在整个事务执行过程中都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性)
- 数据库连接(经典的使用场景是为每个线程分配一个JDBC Connection连接对象,这样可以保证每个线程的都在各自的Connection上进行数据库的操作,不会出现A线程关了B线程正在使用的Connection)
- session会话等线程级别的操作(Session 的特性很适合 ThreadLocal ,因为 Session 之前当前会话周期内有效,会话结束便销毁)
4. ThreadLocal内存泄漏原因,如何避免
**内存泄露 **为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存迟早会被占光
简单来说,不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。与OOM的区别:内存泄漏是内存占用无法释放,而OOM是内存不够,内存泄漏会导致OOM
强引用:通过new或反射构造出来的对象都具有强引用,不会被垃圾回收器回收。当内存空间不足时,JVM宁愿OOM报错,使程序异常终止,也不会回收这种对象
大三Java后端暑期实习面经总结——Java多线程并发篇相关推荐
- 大三Java后端暑期实习面经总结——Java基础篇
博主现在大三在读,从三月开始找暑期实习,暑假准备去tx实习啦!总结下了很多面试真题,希望能帮助正在找工作的大家!相关参考都会标注原文链接,尊重原创! 目录 1. JDK.JRE.JVM的区别和联系 2 ...
- 大三Java后端暑期实习面经总结——Java容器篇
博主现在大三在读,从三月开始找暑期实习,暑假准备去tx实习啦!总结下了很多面试真题,希望能帮助正在找工作的大家!相关参考都会标注原文链接,尊重原创! 目录 1. List和Set区别 2. hashM ...
- 大三Java后端暑期实习面经总结——JVM篇
博主现在大三在读,从三月开始找暑期实习,暑假准备去tx实习啦!总结下了很多面试真题,希望能帮助正在找工作的大家!相关参考都会标注原文链接,尊重原创! 目录 1. jvm体系结构 2. 类加载器 3. ...
- 美团配送 二面(40min) Java后端 暑期实习面经
1. 自我介绍(提到了了解Java并发.Java虚拟机) 2. 既然你提到Java并发,那能不能谈谈你对并发的理解?(先说了单线程的一些缺点,所以需要多线程并发.但是多线程的话可能会出现线程安全的问题 ...
- 2021年3月7日 蚂蚁金服的OceanBase Java后端开发实习面经(一面)
title: 2021年3月7日 蚂蚁金服的OceanBase Java后端开发实习面经(一面) tags: 面经 2021年3月7日 蚂蚁金服的OceanBase Java后端开发实习面经(一面) ...
- 计算机大三如何找名企实习?
计算机大三如何找名企实习呢?相比其他中小企业,学计算机专业的学生都知道,去名企的实习经历含金量会高于中小企业,一是企业知名度,二是自我成长速度,更多计算机大三找名企实习资讯敬请关注我们. 计算机大三找 ...
- 计算机大三如何找名企实习呢?
计算机大三如何找名企实习呢?相比其他中小企业,学计算机专业的学生都知道,去名企的实习经历含金量会高于中小企业,一是企业知名度,二是自我成长速度,更多计算机大三找名企实习资讯敬请关注我们. 计算机大三找 ...
- 阿里巴巴面试题- - -多线程并发篇(三十七)
前言:七月末八月初的时候,秋招正式打响,公司会放出大量的全职和实习岗位.为了帮助秋招的小伙伴们,学长这里整理了一系列的秋招面试题给大家,所以小伙伴们不用太过焦虑,相信你们一定能超常发挥,收到 ...
- 大三后端暑期实习面经总结——SSM微服务框架篇
博主现在大三在读,从三月开始找暑期实习,暑假准备去tx实习啦!总结下了很多面试真题,希望能帮助正在找工作的大家!相关参考都会标注原文链接,尊重原创! 目录 1. mvc.mvp.mvvm MVC架构 ...
最新文章
- 整合PyTorch 0.4和Caffe 2,PyTorch 1.0能挑战TensorFlow吗?
- centos下面搭建svn服务器详细流程
- 【任务脚本】双十一将至,任务脚本预热,OrangeJs基于autojs全自动程序,向大神致敬...
- Oracle 用拼接字符串更新表 测试
- 【洛谷P2743】【poj 1743】[USACO5.1]乐曲主题Musical Themes
- Elasticsearch数据库下载
- 局部光照与全局光照的区别
- discord android .apk,Discord语音交流app
- css背景图铺满后图片变模糊的解决办法
- 2014-2015-2 《Java程序设计》课程学生博客列表
- 【LLS-Player】webrtc m94下载
- Javaweb学生信息管理系统(Mysql+JSP+MVC+CSS)
- 计算机应用基础蓝色方框在哪,word段落设置3磅蓝色单线边框并加底纹怎...
- java 连接 websphere_本地java类访问websphere的JNDI
- JavaScript js如何代码加密绑定域名
- UE4 移动端最小包
- IOC之bean之间的关系讲解
- java面试题总结算法问题
- 为什么建议大家使用 Linux 开发?爽(外加七个感叹号)
- Unity真实榴弹炮模拟(真正的大型榴弹炮实现)
热门文章
- golang通过RSA算法生成token,go从配置文件中注入密钥文件,go从文件中读取密钥文件,go RSA算法下token生成与解析;go java token共用
- Linux shell 学习笔记(4)— linux 环境变量(全局变量、局部变量及变量持久化)
- Unity5.6+ 导出Android 应用程序apk的环境配置及导出过程
- 学习,思维三部曲:WHAT、HOW、WHY
- LeetCode简单题之最长回文串
- TensorFlow创建DeepDream网络
- 2020年Yann Lecun深度学习笔记(下)
- 三维目标检测算法原理
- TCP/UDP协议基本概念
- Make sure no other Soong process is using it