把面试中遇到的问题进行了整理. 本篇文章copy+整理自:html
1. http://www.cnblogs.com/lingepeiyong/archive/2012/10/30/2745973.htmljava
2. http://www.cnblogs.com/paddix/p/5405678.html面试
3. https://blog.csdn.net/javazejian/article/details/72828483数组
Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现安全
用javap命令进行反编译. 好比有这样一个java源代码: Main.java数据结构
javac Main.java javap -c Main.class
就能够看到反编译的代码了.多线程
方法级的同步是隐式,即无需经过字节码指令来控制的,它实如今方法调用和返回操做之中。JVM能够从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,若是设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 而后再执行方法,最后再方法完成(不管是正常完成仍是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其余任何线程都没法再得到同一个monitor。若是一个同步方法执行期间抛 出了异常,而且在方法内部没法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法以外时自动释放。app
所谓等待唤醒机制本篇主要指的是notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,不然就会抛出IllegalMonitorStateException异常,这是由于调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,咱们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字能够获取 monitor ,这也就是为何notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的缘由。jvm
与sleep方法不一样的是, wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行,而sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后,并不会立刻释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。性能
举个例子,咱们如今有一个生产者-消费者队列和三个线程。
1) 1号线程从队列中获取了一个元素,此时队列变为空。
2) 2号线程也想从队列中获取一个元素,但此时队列为空,2号线程便只能进入阻塞(cond.wait()),等待队列非空。
3) 这时,3号线程将一个元素入队,并调用cond.notify()唤醒条件变量。
4) 处于等待状态的2号线程接收到3号线程的唤醒信号,便准备解除阻塞状态,执行接下来的任务(获取队列中的元素)。
5) 然而可能出现这样的状况:当2号线程准备得到队列的锁,去获取队列中的元素时,此时1号线程恰好执行完以前的元素操做,返回再去请求队列中的元素,1号线程便得到队列的锁,检查到队列非空,就获取到了3号线程刚刚入队的元素,而后释放队列锁。
6) 等到2号线程得到队列锁,判断发现队列仍为空,1号线程“偷走了”这个元素,因此对于2号线程而言,此次唤醒就是“虚假”的,它须要再次等待队列非空。
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每一个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其余线程进入获取monitor(锁)。以下图所示
Java中任意对象能够做为锁. monitor对象存在于每一个Java对象的对象头中(存储的指针的指向),synchronized锁即是经过这种方式获取锁的,也是notify/notifyAll/wait等方法存在于顶级对象Object中的缘由(关于这点稍后还会进行分析)
通常而言,synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头(若是对象是数组则会分配3个字,多出来的1个字记录的是数组长度)
Hotspot中对象在内存中的结构:
从上面的这张图里面能够看出,对象在内存中的结构主要包含如下几个部分:
1. Mark Word:对象的Mark Word部分占4个字节,其内容是一系列的标记位,好比轻量级锁的标记位,偏向锁标记位等等。
2. Class对象指针:Class对象指针的大小也是4个字节,其指向的位置是对象对应的Class对象(其对应的元数据对象)的内存地址
3. 对象实际数据:这里面包括了对象的全部成员变量,其大小由各个成员变量的大小决定,好比:byte和boolean是1个字节,short和char是2个字节,int和float是4个字节,long和double是8个字节,reference是4个字节
4. 对齐:最后一部分是对齐填充的字节,按8个字节填充。
根据上面的图,那么咱们能够得出Integer的对象的结构以下:
Integer只有一个int类型的成员变量value,因此其对象实际数据部分的大小是4个字节,而后再在后面填充4个字节达到8字节的对齐,因此能够得出Integer对象的大小是16个字节。
所以,咱们能够得出Integer对象的大小是原生的int类型的4倍。
关于对象的内存结构,须要注意数组的内存结构和普通对象的内存结构稍微不一样,由于数据有一个长度length字段,因此在对象头后面还多了一个int类型的length字段,占4个字节,接下来才是数组中的数据,以下图:
对象头中的MarkWord 结构以下:
锁状态 |
25 bit |
4bit |
1bit |
2bit |
||
23bit |
2bit |
是不是偏向锁 |
锁标志位 |
|||
轻量级锁 |
指向栈中锁记录的指针 |
00 |
||||
重量级锁 |
指向互斥量(重量级锁)的指针 |
10 |
||||
GC标记 |
空 |
11 |
||||
偏向锁 |
线程ID |
Epoch |
对象分代年龄 |
1 |
01 |
|
无锁 |
对象的hashCode |
对象分代年龄 |
0 |
01 |
ObjectMonitor在openjdk中的源码路径: openjdk/hotspot/src/share/vm/runtime/objectMonitor.hpp
数据结构里有_count一项, 用于在重入时进行自增操做, 释放时进行自减操做.
// initialize the monitor, exception the semaphore, all other fields // are simple integers or pointers ObjectMonitor() { _header = NULL; _count = 0; _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0; }
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁能够从偏向锁升级到轻量级锁,再升级的重量级锁,可是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级.
偏向锁是Java 6以后加入的新锁,它是一种针对加锁操做的优化手段,通过研究发现,在大多数状况下,锁不只不存在多线程竞争,并且老是由同一线程屡次得到,所以为了减小同一线程获取锁(会涉及到一些CAS操做,耗时)的代价而引入偏向锁。偏向锁的核心思想是,若是一个线程得到了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再作任何同步操做,即获取锁的过程,这样就省去了大量有关锁申请的操做,从而也就提供程序的性能。因此,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续屡次是同一个线程申请相同的锁。可是对于锁竞争比较激烈的场合,偏向锁就失效了,由于这样场合极有可能每次申请锁的线程都是不相同的,所以这种场合下不该该使用偏向锁,不然会得不偿失,须要注意的是,偏向锁失败后,并不会当即膨胀为重量级锁,而是先升级为轻量级锁。下面咱们接着了解轻量级锁。
偏向锁获取过程:
(1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。
(2)若是为可偏向状态,则测试线程ID是否指向当前线程,若是是,进入步骤(5),不然进入步骤(3)。
(3)若是线程ID并未指向当前线程,则经过CAS操做竞争锁。若是竞争成功,则将Mark Word中线程ID设置为当前线程ID,而后执行(5);若是竞争失败,执行(4)。
(4)若是CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时得到偏向锁的线程被挂起,偏向锁升级为轻量级锁,而后被阻塞在安全点的线程继续往下执行同步代码。
(5)执行同步代码。
偏向锁的释放:
偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其余线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,须要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
假若偏向锁失败,虚拟机并不会当即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6以后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁可以提高程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。须要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,若是存在同一时间访问同一锁的场合,就会致使轻量级锁膨胀为重量级锁。
轻量级锁的加锁过程
(1)在代码进入同步块的时候,若是同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中创建一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图2.1所示。
(2)拷贝对象头中的Mark Word复制到锁记录中。
(3)拷贝成功后,虚拟机将使用CAS操做尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。若是更新成功,则执行步骤(3),不然执行步骤(4)。
(4)若是这个更新动做成功了,那么这个线程就拥有了该对象的锁,而且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图2.2所示。
(5)若是这个更新操做失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,若是是就说明当前线程已经拥有了这个对象的锁,那就能够直接进入同步块继续执行。不然说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了避免让线程阻塞,而采用循环去获取锁的过程。
图2.1 轻量级锁CAS操做以前堆栈与对象的状态
图2.2 轻量级锁CAS操做以后堆栈与对象的状态
轻量级锁的解锁过程:
(1)经过CAS操做尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
(2)若是替换成功,整个同步过程就完成了。
(3)若是替换失败,说明有其余线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。
消除锁是虚拟机另一种锁的优化,这种优化更完全,Java虚拟机在JIT编译时(能够简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),经过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,经过这种方式消除没有必要的锁,能够节省毫无心义的请求锁时间,以下StringBuffer的append是一个同步方法,可是在add方法中的StringBuffer属于一个局部变量,而且不会被其余线程所使用,所以StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。
public class StringBufferRemoveSync { public void add(String str1, String str2) { //StringBuffer是线程安全,因为sb只会在append方法中使用,不可能被其余线程引用 //所以sb属于不可能共享的资源,JVM会自动消除内部的锁 StringBuffer sb = new StringBuffer(); sb.append(str1).append(str2); } public static void main(String[] args) { StringBufferRemoveSync rmsync = new StringBufferRemoveSync(); for (int i = 0; i < 10000000; i++) { rmsync.add("abc", "123"); } } }
锁粗化的概念应该比较好理解,就是将屡次链接在一块儿的加锁、解锁操做合并为一次,将多个连续的锁扩展成一个范围更大的锁。举个例子:
package com.paddx.test.string; public class StringBufferTest { StringBuffer stringBuffer = new StringBuffer(); public void append(){ stringBuffer.append("a"); stringBuffer.append("b"); stringBuffer.append("c"); } }