以下文章来源于捡田螺的小男孩 ,作者捡田螺的小男孩
https://mp.weixin.qq.com/s/N7FCMMXiukMv_8xzb3o43g
1.synchronized的实现原理以及锁优化?
</>synchronized的实现原理
- synchronized作用于【方法】或者【代码块】,保证被修饰的代码在同一时间只能被一个线程访问。
- synchronized修饰代码块时,JVM采用[monitorenter、monitorexit]来个指令来实现同步
- synchronized修饰同步方法时,JVM采用[ACC_SYNCHRONIZED]标记符来实现同步
- monitorenter、monitorexit或者ACC_SYNCHRONIZED都是基于[Monitor]实现的
- 实例对象里有对象头,对象头里面有Mark Word,Mark Word指针指向了[monitor]
- Monitor其实是一种[同步工具],也可以说是一种[同步机制]。
- 在Java虚拟机(HotSpot)中,Monitor是由[ObjectMonitor实现]的
ObjectMonitor体现出Monitor的工作原理
ObjectMonitor的几个关键属性 _count、_recursions、_owner _、_WaitSet、_EntryList体现了monitor的工作原理
</>锁优化
在讨论锁优化前,先看看JAVA对象头(32位JVM)中Mark Word的结构图
Mark Word存储对象自身的运行数据,如[哈希码,GC分代年龄,锁状态标志,偏向时间戳,(Epoch)]等,为什么区分[偏向锁,轻量级锁,重量级锁]等几种锁状态呢?
在JDK1.6之前,synchronized的实现直接调用ObjectMonitor的enter和exit,这种锁被称之为[重量级锁]。从JDK6开始,HotSpot虚拟机开发团队对Java中的锁进行优化,如增加了适量性自旋,锁消除,锁粗化,轻量级锁和偏向锁等优化策略。
- 偏向锁:在无竞争的情况下,把整个同步都消除掉,CAS操作都不做。
- 轻量级锁:在没有多线程竞争时,相对重量级锁,减少操作系统互斥量带来的性能消耗。但是,如果存在锁竞争,除了互斥量本事开销,还额外有CAS操作的开销。
- 自旋锁:减少不必要的CPU上下文切换。在轻量级锁升级为重量级锁时,就使用了自旋加锁的方式
- 锁粗化:将多个连续的加锁,解锁操作连接在一起,扩展程ige范围更大的锁。
举个例子,买门票进动物园。老师带一群小朋友去参观,验票员如果知道他们是一个集体,就可以把他们看成一个整体(锁粗化),一次性验票过,而不需要一个一个找他们验票。
- 锁消除:虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。
2.ThreadLocal原理,使用注意点,应用场景有哪些?
回答四个主要点:
- ThreadLocal是什么?
- ThreadLocal原理?
- ThreadLocal使用注意点?
- ThreadLocal的应用场景?
</>ThreadLocal是什么?
ThreadLocal,即线程本地变量。如果你创建了一个ThreadLoca变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。
</>ThreadLocal原理
ThreadLocal内存结构图:
由结构图可以看出:
- Thread对象中持有一个ThreadLocal.ThreadLocalMap的成员变量。
- ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。
对照几段关键源码
ThreadLocal中的关键方法set()和get()
ThreadLocalMap的Entry数组
如下,结合以上结构图说明[ThreadLocal的实现原理]
- Thread类有一个ThreadLocal.ThreadLocalMap的实例变量ThreadLocals,即每个线程都有一个属于自己的ThreadLocalMap
- ThreadLocalMap内部维护着一个Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值
- 每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
</>ThreadLocal内存泄露问题
先看一下ThreadLocal的引用示意图
ThreadLocalMap中使用的key为ThreadLocal的弱引用,如下:
弱引用:只要垃圾回收机制一运行,不管JVM内存空间是否充足,多会回收该对象占用的内存。
弱引用比较容易被回收。因此,如果ThreadLocal(ThreadLocalMap的key)被垃圾回收器回收了,但是因为ThreadLocalMap生命周期和Thread是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap的key没了,value还在,这就会[造成内存泄露问题]。
如何[解决内存泄露问题]?使用完ThreadLocal后,及时调用remove()方法释放内存空间。
</>ThreadLocal的应用场景
- 数据库连接池
- 会话管理中使用
3.synchronized和ReentrantLock的区别?
~可以从锁的实现,功能特点,性能等几个维度去回答这个问题
- [锁的实现:]synchronized是Java语言的关键字,基于JVM实现。而ReentrantLock是基于JDK的API层面实现的(一般是lock()/unlock()方法配合try/finally语句块来完成)
- [性能:] 在JDK1.6锁优化之前,synchronized的性能比ReentrantLock差很多。但是JDK1.6开始,增加了适应性自旋,锁消除等,两者性能就差不多了。
- [功能特点:]ReentrantLock比synchronized增加了一些高级功能,如等待可中断,可实现公平锁,可实现选择性通知。
- ReentrantLock提供了一中可以中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。
- ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程线获得锁。
- synchronized与wait()和notify()/notifyAll()方法结合实现等待/通知机制,ReentrantLock类借助Condition接口与newCondition() 方法来实现。
- ReentrantLock需要手工声明来加锁和释放锁,一般跟finally配合释放锁。而synchronized不用手动释放锁。
4.说说CountDwonLatch 与 CyclicBarrier 区别
- CountDwonLatch:一个或者多个线程,等待其他多个线程完成某件事请之后才能执行;
- CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行。
举个例子:
- CountDownLatch:假设老师跟同学约定周末在公园门口集合,等人齐了再发门票。那么,发门票(这个主线程),需要等各位同学都到齐(多个其他线程都完成),才能执行。
- CyclicBarrier:多名短跑运动员要开始比赛,只有等所有运动员都准备好,裁判才会鸣枪开始,这时候所有运动员才会疾步如飞。
5,Fork/Join框架的理解
Fork/Join是 Java 7 提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小人物结果后得到大任务结果的框架。
Fork/Join框架需要理解两个点,[分而治之]和[工作窃取算法]。
[分而治之]
以下 Fork/Join 框架的定义,就是分而治之思想的体现啦
[工作窃取算法]
把大任务拆分成小任务,放到不同队列执行,交由不同的线程分别执行时。有的线程优先把自己负责的任务执行完了,其他线程还在慢慢悠悠处理自己的任务,这时候为了充分提高效率,就需要工作窃取算法啦~
工作窃取算法就是,[某个线程从其他队列中窃取任务进行执行的过程]。一般就是指做的快的线程(盗窃线程)抢慢的线程的任务来做,同时为了减少锁竞争,通常使用双端队列,即快线程和慢线程各在一端。
6.为什么我们调用start()方法时会执行run() 方法,为什么我们不能直接调用run() 方法呢?
看看Thread的start()方法说明哈~
JVM执行 start() 方法,会另启一条线程执行Thread的run方法,这才起到多线程的效果。
[为什么我们不能直接调用run()方法?]直接调用Thread的run()方法,其方法还是运行在主线程中,没有起到多线程的效果。
7.CAS?CAS有什么缺陷,如何解决?
CAS:Compare And Swap,比较并替换;
CAS涉及三个操作数,内存地址V,预期原值A,新值B;如果内存位置的值V与预期原A值相匹配,就更新为新值B,否则不更新
CAS有什么缺陷?
[ABA问题]
并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即时成功修改,也可能有问题。
可以通过AtomicStampedReference[解决ABA问题],它,一个带有标记的原子引用类,通过控制变量值的版本来保证CAS的正确性。
[循环时间长开销]
自旋CAS,如果一致循环执行,一致不成功,会给CPU带来非常大的执行开销。
很多时候,CAS思想体现,是有个自旋次数的,就是为了避开这个耗时问题
[只能保证一个变量的原子操作]
CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。
可以通过这两个方式来解决这个问题:
- 使用互斥锁来保证原子性;
- 将多个变量封装成对象,通过AtomicReference来保证原子性;
8,如何保证多线程下i++结果正确?
- 使用循环CAS,实现i++ 原子操作
- 使用锁机制,实现 i++ 原子操作
- 使用synchronized,实现 i++ 原子操作
9,如何检测死锁?怎么预防死锁?死锁的四个必要条件?
死锁是指多个线程因竞争资源而造成的一种互相等待的僵局。如图:
[死锁的四个必要条件]
- 互斥:一次只有一个进程可以使用一个资源。其他进程不能访问已分配给其他进行的资源。
- 占有且等待:当一个进程在等待分配得到其他资源时,其继续占有已分配得到的资源。
- 非抢占:不能强行抢占进程中已占有的资源。
- 循环等待:存在一个封闭的进程链,使得每个资源至少占有此链中下一个进程所需要的一个资源。
[如何预防死锁?]
- 加锁顺序(线程按顺序办事)
- 加锁时限(线程请求锁加上权限,超时就放弃,同时释放自己占有的锁)
- 死锁检测