逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术
逃逸分析的基本原理是: 分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用
不逃逸
、方法逃逸
到线程逃逸
,称为对象由低到高的不同逃逸程度。关于逃逸的优化:
确定一个对象不会逃逸出线程之外
,那让这个对象在栈上分配内存将会是一个很不错的主意。即可以让这个对象随栈的弹出而释放,栈上分配可以减轻Java堆垃圾回收的压力能够确定一个变量不会逃逸出线程
,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争, 对这个变量实施的同步措施也就可以安全地消除掉
。Java内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节
JMM规定:
内存直接的交互:八大操作
并且八大操作必须满足如下规则:
Volatile是Java虚拟机提供的轻量级同步机制,具有以下特点
“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。
禁止指令重排:
x = 1;
y = x;
x = 2;
如上,如果允许指令重排,x=2可能被换到x=1的位置,这样最终y的值就不对了
禁止指令重排,可以在加了volatile关键字的变量上加上内存屏障,不让指令重排越过这一层屏障
Java的线程是映射到操作系统的原生内核线程之上的,如果要阻塞或唤醒一条线程,则需要操作系统来帮忙完成。这就不可避免**用户态到内核态的切换
** ,这种状态切换需要耗费很多处理器时间
阻塞同步:
最基本的互斥同步手段就是synchronized关键字,这是一种块结构(Block Structured)的同步语法。
synchronized关键字经过Javac编译 之后,会在同步块的前后分别形成 monitorenter
和monitorexit
这两个字节码指令。
一个对象有一个锁的计数器,当一个线程执行到monitorenter
时会看看这个计数器是否为0,为0就拿到锁,并让这个计数器的值加一。当一个线程执行到monitorexit
时会让整个计数器的值减一。一旦计数器的值为零,锁随即就被释放了。如果获取对象锁失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止。
synchronized锁 在JDK6之前使用的是操作系统内部的互斥量来实现的,持有锁是一个重量级(Heavy-Weight) 的操作,对于一些简单的代码,可能状态切换消耗的时间会比代码本身执行的时间还要长
非阻塞同步:
由于synchronized太笨重了,之后出现了基于冲突检测的乐观并发策略:当发生竞争的时候可以使用硬件指令集不断重试(如CAS),直到资源没有竞争为止,这种方式也常被称为无锁编程
从JDK5到JDK6后,Hotspot耗费了大量资源实现各种锁优化技术
如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁膨胀(Lock Coarsening)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)等
前面说到了,synchronized重量级锁挂起线程和恢复线程需要到内核态去进行,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得
所以当两个或以上线程竞争一个资源的时候,可以让后面请求锁的线程稍微等一下,只需要让线程执行一个忙循环(自旋)
,这就是自旋锁
JDK6之前自旋锁默认关闭,可以使用-XX:+UseSpinning
参数开启。如果超过自旋时间太长也会白白浪费资源,所以长时间自旋应该让这个线程挂起,自旋次数默认为10次,可以使用-XX:PreBlockSpin设置自旋次数
JDK6对自旋锁进行了优化,引入了自适应的自旋,可以让虚拟机自行判断自旋时间,并且越预测越精准
锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。
这时候就需要用到逃逸分析
提供支持,例如一段代码中,在堆上的所有数据都不会逃逸到其他线程,那就可以把它们当作栈上数据对待,栈上数据是线程私有的,同步加锁自然无须进行。
如果一系列连续的操作都对同一个对象反复加锁解锁,甚至加锁操作是出现在循环体之中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗
如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部
轻量级锁是JDK 6时加入的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就被称为“重量级”锁
轻量级锁的工作过程:
锁记录Lock Record
** 的空间锁对象的Mark Word拷贝
** 到这个空间中,这份拷贝加了一个Displaced前缀;改变为一个指向Lock Record的指针
**
虚拟机会检测Mark Work是否指向当前线程的栈帧
偏向锁也是JDK 6中引入的一项锁优化措施,它的目的是**消除数据在无竞争情况下的同步原语, 进一步提高程序的运行性能。
**
如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了。
偏向锁原理:
假设虚拟机开启了偏向锁-XX:+UseBiased Locking
,JDK6默认开启
未锁定"01"
或轻量级锁定状态"00"
**,后续同步操作就按轻量级锁那样执行我们知道Mark Word会存放哈希码,而且这个hashcode值一般是需要强制保持不变的,而偏向锁需要使用Mark Word来存放线程ID,这怎么办呢?
事实上,当一个对象计算过哈希值之后,它就无法进入偏向锁状态了;而当一个对象当前正处于偏向锁状态,又收到需要计算其一致性哈希码请求[时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁。代表重量级锁的ObjectMonitor类里有字段可以记录非加锁状态(标志位为“01”)下的Mark Word,其中自然可以存储原来的哈希码
偏向锁可以提高带有同步但无竞争的程序性能 ,但它同样是一个带有效益权衡(Trade Off)性质的优化,也就是说它并非总是对程序运行有利。如果程序中大多数的锁都总是被多个不同的线程访问,那偏向模式就是多余的。 在具体问题具体分析的前提下,有时候使用参数-XX:- UseBiasedLocking
来禁止偏向锁优化反而可以提升性能。
无锁->偏向锁->轻量级锁->重量级锁
锁只能升级不能降级!!!