前言:在慕课网上学习剑指Java面试-Offer直通车时所做的笔记,供本人复习之用,比较难,我也没大懂,只记录大概意思以后有接触了再改,想要详细解说的不建议看这篇博客.
目录
第五章 Synchronized和ReentrantLock的区别
第一章 对象在内存中的布局
HotSpot 对象在内存中的布局分为三块区域,对象头,实例数据,对齐填充,我们在这里主要讲解对象头.
synchronized使用的锁对象是存储在java对象头里的,其主要结构是由Mark Word和Class Metadata Address组成,Class Metadata Address是指向类元数据的指针,虚拟机通过这个指针确认其是哪个对象的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键.
Mark Word:
第二章 Monitor
上面介绍了java对象头,下面我们介绍Monitor.
Monitor:每个对象天生自带了一把看不见的锁,叫做内部锁或者Monitor锁.
Monitor也称为管程或者监视器锁,我们可以把它理解为一个同步工具,也可以描述为一种同步机制,通常它被描述为一个对象.
这里我们拿重量级锁进行分析,锁的标识位为10,指针指向的是monitor对象的起始地址,每个对象都存在着一个monitor与之关联,monitor存在于对象的对象头中,对象与monitor之间有多存在多种实现方式,如monitor可以与对象一起存在销毁,或当线程试图获取对象锁时,自动生成.当monitor被某个线程持有后,它便处于锁定状态.
在java虚拟机HotSpot中monitor由ObjectMonitor来实现,位于HotSpot虚拟机源码objectMonitor里,通过C++来实现.
ObjectMonitor中有两个队列,一个是WaitSet一个是EntryList,可以与等待池与锁池联系起来,他们就是用来保存ObjectWaiter的对象列表,每个对象锁的线程都会被封装成ObjectWaiter来保存到里面,owner指向持有ObjectMonitor的线程.
当多个线程访问同一段同步代码的时候,首先会进入到EntryList集合中,当线程获取到对象的Monitor之后,就进入到Object区域,并把Monitor中的Owner变量设置为当前线程,同时Monitor中的count就会加1,如果线程调用wait方法将会释放当前持有的Monitor,owner会被恢复成null,count也会被减1,同时该线程ObjectWaiter实例就会进入到waitSet集合等待被唤醒,若当前线程执行完毕,它也将释放monitor锁,并复位对应变量的值,以便其它线程进入获取monitor锁.
Monitor锁的竞争,获取与释放.
2.1 Monitor在字节码中的表示
java代码:
对应的字节码:
syncsTask方法字节代码:
可以分析出,同步语句块的实现依赖的是monitorenter与monitorexit指令.
monitorenter指向代码块的开始位置,首先获取printStream这个类,传入参数,执行方法.
monitorexit指明同步代码块的结束位置.
当执行monitorenter指令时,当前线程将试图获取对象锁即Object(没听清,音译)所对应的持有权,当Object的Monitor进入计数器的count为0时,线程就可以成功的获取到Monitor,并将计数器设置为1表示取锁成功,如果我们当前线程在之前已经拥有了objectMonitor的持有权,它可以重入这个monitor.假如其它线程已经先于当前线程拥有ObjectMonitor的所有权,那么当前线程将会被阻塞在这里,直到持有该锁的线程执行完毕,即monitorexit被执行,执行线程将释放Monitor锁,并设置计数器为0,其它线程将有机会持有Monitor.
为了保证该方法异常时也能正确执行,编译器会自动生成一个异常处理器,包含另一个monitorexit方法.
什么是重入:
重入代码如下:
syncTask方法字节代码:
可以看到它并没有monitorenter和monitorexit,字节码也比较短,因为方法集的同步时隐式的,即无需通过字节码指令来控制.
我们可以看到有ACC_SYNCHORONIZED一个访问标志,用来区分一个方法是否是同步方法,当方法调用时此访问标志会被检测,当被设置时线程将会持有monitor,然后再执行方法,最后在方法无论是正常还是异常完成的情况下执行monitorexit.
第三章 锁的优化
为什么会对synchronized嗤之以鼻?
java6之后,synchronized性能得到了极大的提升.
3.1 自旋锁与自适应自旋锁
3.1.1 自旋锁
许多情况下,共享数据的锁定状态持续时间短,切换线程不值.
可以让没获取到monitor锁的线程在门外等待一会儿,但不放弃CPU的执行时间.即通过让线程执行忙循环(自旋)等待锁的释放,不让出CPU.不像sleep一样会让出CPU的执行时间.
缺点:若锁被其它线程长时间占用,会白白等很久,会带来许多性能上的开销.所以可以设置如果在一定时间内没有等到锁,就应该挂起线程了.
3.1.2 自适应自旋锁
由于每次线程需要等待的时间不是固定的,所以想设定时间比较合理是很困难的,所以我们需要自适应自旋锁.
自旋的次数不再固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定.
如果对于某个锁,自旋经常获取到锁,那么就增加等待时间,如果对于某个锁,自旋很少获取到锁,那在以后获取这个锁时可能省略到这个过程,以避免浪费时间.
3.2 锁消除
java 虚拟机在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁.
可以看到appen是加锁的,但是这个锁永远不会发生竞争,sb属于本地变量没有return,所以这个锁可以消除.
3.3 锁粗化
我们有时会将锁限制在尽量小的范围,这样需要同步的数量尽可能的变小,使等待锁的线程尽可能块的拿到锁.
但是如果一连串系类操作都会同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中,那即使没有锁竞争,频繁的进行互斥锁操作也会导致不必要的性能浪费,此时我们可以矿大枷锁的范围,避免反复加锁和解锁,
如下图所示,JVM会把加锁的范围到循环的外部,使整个操作只需要加锁一次.
第四章 synchronized的四种状态
无锁就是没有锁,重量级锁我们刚才说过,所以我们这里主要介绍偏向锁和轻量级锁.
4.1 偏向锁
应用在锁不存在多线程竞争,总是由同一线程多次获得.
不适用于锁竞争比较激烈的多线程场合.
4.2 轻量级锁
偏向锁失败后会膨胀为轻量级锁.
具体过程:
解锁的过程:
锁的内存语义:
总的来说:
总结:对象头里有mark word,markword里存储着这个锁的状态,
当多个线程竞争一个锁时,有几种不同的情况:
偏向锁,
轻量级锁,
重量级锁
第五章 Synchronized和ReentrantLock的区别
在java5以前,Synchronized是仅有的同步手段,从java5开始,提供了ReentrantLock(再入锁)的实现,它的语义和synchronized基本相同,通过代码直接调用lock方法去获取,代码编写也更加灵活.
ReentrantLock的特点:
查看ReentrantLock源码,进入其lock方法中.
继续点进其acquire方法中,再点进其acquireQueued方法中.
我们发现acquireQueued在类AbstractQueuedSynchronizer中,AbstractQueuedSynchronizer是队列同步器,简称AQS,它是java并发用来构建锁或其它同步组件的基础框架,是JUCpacakge的核心,一般使用AQS的方式是继承.
像ReentrantLock这些子类是通过AQS实现的抽象方法来管理这些同步状态,一种同步结构往往是可以利用其它的结构去实现的,但是对某种同步结构的倾向会导致复杂晦涩的实现逻辑,所以把基础的同步相关的操作抽象在AQS中了,利用AQS为我们提供同步结构实现了范本.
AQS内部的数据和方法可以简单拆分为一个volatile数组成员表征状态即state,一个先进先出的等待队列,各种针对CAS的基础操作方法,以及期望各种同步结构去实现的acquire,release方法,利用AQS实现一个同步结构至少要实现两个基本类型的方法,acquire操作,获取资源的独占权,release操作,用来释放对某个资源的独占.
5.1 ReentrantLock公平性设置
传入true表示是公平锁.
建议程序确实有公平需要的时候再用,不用会有额外开销.
具体代码:
public class ReentrantLockDemo implements Runnable{
private static ReentrantLock lock = new ReentrantLock(true);
@Override
public void run(){
while (true){
try{
lock.lock();
System.out.println(Thread.currentThread().getName() + " get lock");
Thread.sleep(1000);
} catch (Exception e){
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
ReentrantLockDemo rtld = new ReentrantLockDemo();
Thread thread1 = new Thread(rtld);
Thread thread2 = new Thread(rtld);
thread1.start();
thread2.start();
}
}
公平锁输出:
非公平锁输出:
ReentrantLock相比Synchronized因为可以像普通对象一样使用,所以可以利用它来提供各种便利的方法,进行精细的同步操作.甚至可以实现Synchronized难以表达的用例.
5.2 wait/notify/notifyAll对象化
ReentrantLock将Synchronized转变成了可控的对象,是不是也能将前面讲的wait/notify/notifyAll对象化?
位于JUC包中的locks.Condition做到了这一点.Condition最为典型的应用场景就是标准类库中的ArrayBlockingQueue,ArrayBlockingQueue是数组实现的线程安全的,有界的阻塞队列,线程安全是指ArrayBlockingQueue内部通过互斥锁保护竞争资源,其互斥锁是通过ReentrantLock来实现的,实现了多线程对竞争资源的互斥访问,而有界则指的是ArrayBlockingQueue对应的数组是有界限的,阻塞队列是指多线程访问竞争资源时,当竞争资源已被某线程获取时,其它要获取该资源的线程要阻塞等待.
ArrayBlockingQueue与condition是组合的关系,ArrayBlockingQueue内部有两个condition对象,一个是notEmpty,一个是notFull,而且condition依赖于ArrayBlockingQueue存在,通过condition可以实现对ArrayBlockingQueue更精确的访问,
可以看其构造函数,notEmpty与notFull都是同一个lock创建出来的,然后使用在特定的操作中.
notEmpty用在take方法中,用在当count=0的时候,满足当队列为空时,试图take线程的正确行为,应该是等待有新的消息加入到队列才去做返回,如同之前的future,使用notEmpty就可以优雅的实现等待逻辑,take操作的前提是要保证消息入队,只有队列有消息才触发去取走.
看一下入队的方法.可以看到一旦有消息被放入队列当中,count便会++,notEmpty会调用signal函数去通知等待的线程,signal如同之前说的notify,此时非空条件就会满足,take就能取到对应的东西了.
通过signal和await的组合,ArrayBlockingQueue就能优雅的完成了条件判断和通知等待线程,非常顺畅完成了状态流转.
注意到notEmpty是从new condition出来的,发现condition来自于ConditionObject,ConditionObject来源于AbstractQueuedSynchronizer也就是AQS框架,就是将我们的wait,notify,notifyall等操作转换成了相对应的对象,将复杂而晦涩的同步操作转变为直观可控的对象行为.
总结:
针对最后一条我们去找到park方法.
首先找到ReentrantLock类的lock方法,点击进acquire方法中.再点击进acquireQueued方法,
在acquireQueued中找到parkAndCheckInterrupt方法
点击进去,发现其调用的是LockSupport方法,再点进去发现其调用的是U.park方法.
再点进去,最后发现park方法位于unsafe类里,unsafe是一个类似于后门的工具,可以在任意内存位置处读写数据.另外unsafe还支持一些CAS的操作.