高效并发是从JDK1.5到1.6的一个重要改进,HotSpot团队用了大量的精力进行锁优化技术,适应性锁(Adaptive Spinnig)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)。
锁优化目的是:线程之间更加高效的共享数据,解决资源竞争问题,提高程序执行效率。
下面我们分5点来分别阐述它们的定义,重点关注在“轻量级锁”&“偏向锁”。
winter
必须先提及两个基础概念:Object Header 内存布局 和 CAS。
自旋锁底层用的就是CAS操作,而轻量级锁与偏向锁都用到 Object Header 进行锁状态管理。
Object Header :
Object Header 的 MarkWord 结构:
总结一下:Mark Word 可以存储多种内容,当锁标志位(00/01/10/11/01)处于不同类型时,该存储区域可以分别存储各种数据(对象hashcode和GC-age/锁记录pointer/重量级锁pointer/null/偏向锁的线程ID)
CAS:
乐观锁的核心算法是CAS(Compareand Swap,比较并交换),它涉及到三个操作数:内存值、预期值、新值。当且仅当预期值和内存值相等时才将内存值修改为新值。
自旋锁:线程执行一个忙循环(自旋),这项技术就是自旋锁了(spinlock)。
互斥同步:阻塞的实现导致性能巨大的消耗,因为“挂起和恢复线程的操作都要转入到内核态”。(参考:《操作系统的线程模式》)
使用场景:共享资源的被锁定状态只会持续很短的时间,那么为此频繁挂起和恢复线程并不值得。
实现方法:让后面请求锁的线程“稍等一下”,不要放弃CPU的执行时间,看看持有锁的线程很快就会释放锁了。
JVM配置参数:使用-XX:PreBlockSpin参数来设置自旋锁等待的次数。(JDK1.6之后,默认开启了自旋锁,自旋次数的默认值是10次);超过自旋次数,就应当使用传统方式去挂起线程了。
自旋锁的好处:减少线程上下文切换的消耗(线程切换要从用户态切到内核态,很消耗资源)
自旋锁的不足:如果被锁资源的时间很长,那么自旋的线程只会白白浪费掉处理器资源,没有性能产出。
自适应锁的改进:优化方式:自旋的时间不固定,动态根据前一次在同一个锁的自旋时间&锁拥有者的状态共同决定。(JDK1.6 引入自适应锁)
案例1:(AQS的抽象类 AbstractQueuedSynchronizer,底层就是使用了自旋锁)
public class SpinLockTest {
AtomicReference<Thread> reference = new AtomicReference<>();
//加锁
public void mylock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + " try to get lock..");
//工具类提供了CAS操作
while (!reference.compareAndSet(null,thread)){
//do nothing CPU 空转(自旋)
System.out.println(thread.getName() + " cannot get the lock, so do nothing and waiting...");
}
}
//解锁
public void unlock(){
Thread thread = Thread.currentThread();
reference.compareAndSet(thread,null);
System.out.println(thread.getName()+ " to release lock.");
}
public static void main(String[] args) {
SpinLockTest spinLockTest = new SpinLockTest();
new Thread(()->{
spinLockTest.mylock();
try {
TimeUnit.SECONDS.sleep(5);
}catch (InterruptedException e){
e.printStackTrace();
}
//休眠10s之后,t1 才释放锁(未释放资源期间,其他的线程会一直空转)
spinLockTest.unlock();
},"t1").start();
try {
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
new Thread(()->{
//t2 启动并且申请锁 & t2 释放锁资源
spinLockTest.mylock();
spinLockTest.unlock();
},"t2").start();
}
}
输出结果:
t1 try to get lock..
t2 try to get lock..
t2 cannot get the lock, so do nothing and waiting...(可以注释打印代码来禁止输出这段日志)
t1 to release lock.
t2 to release lock.
锁消除:JVM 的即时编译器运行时,一些代码要求同步,但是检测到不可能存在共享数据的竞争的锁,那么会对这个锁进行消除处理。
判断依据:堆的数据不会逃逸(变量对象是否逃逸:虚拟机会使用数据流分析来确定),那么可以当做栈的数据,认为线程私有,同步加锁自然不需要进行了。
案例2:JDK1.5之前的字符串连接优化
public class LockRemove {
public String concatString(String s1,String s2,String s3){
return s1 + s2 + s3;
}
}
代码分析:
JDK1.5之前,会转化为StringBuffer的append操作,而StringBuffer的拼接操作都是使用了重量级锁 synchronized;经过分析,此处的s1,s2,s3引用都不会“逃逸到”concatString 方法之外,那么就可以安全的消除掉这个锁。
锁粗化:代码块中对一个对象反复加锁&解锁,甚至在循环出现这种情况,那么可以适当地扩大锁的范围,实现“锁粗化”。
案例3:一个for循环里面对一个对象反复加锁&解锁
public class LockExpend {
private final static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
int[] ints = new int[10];
for (int i : ints){
lock.lock();
try {
// do something..
System.out.println(i);
} finally {
lock.unlock();
}
}
}
}
代码分析:
JVM探测到有这样一个零碎操作,对同一个对象加锁,会把这个加锁同步的范围扩展到整个操作序列的外部;这个案例里,会拓展到for循环之外。
轻量级锁,不是传统重量级锁的替代品,而是用于在没有多线程竞争时,减少重量级锁使用操作系统互斥量带来的性能消耗,是用于性能优化的手段。
理解轻量级锁并不难,因为其本质是对象头部的“锁标志”以及堆栈的锁记录更新替换操作,继续看下去。
加锁过程
正常情况下,没有竞态条件发生,轻量级锁加锁过程有3个步骤。
步骤1,见下图:堆栈Stack整个新的“锁记录”区域;
步骤2跟步骤3,见下图:备份锁对象MarkWord后,CAS,改为堆栈Stack的地址,同时要修改MarkWord的锁标志;
至此,通过修改MarkWord,达到一个“轻量级锁”的效果,虚拟机认为锁对象被某个帧栈所在的线程拥有了。
解锁过程
正常情况下,加锁过程中如果没有碰到“第三者线程”进行竞争,那么线程很容易就获取到锁资源的所有权了。这样一来,解锁也变得简单了。
竞争锁资源
我们在上面了解到,正常情况下,没有竞态条件发生,轻量级锁加锁过程有3个步骤。
那如果不凑巧,有两条以上的线程竞争同一个锁呢?(OK,那么轻量级锁就不有效,“膨胀”为重量级锁)。
见下图,膨胀过程,修改MarkWord为互斥锁的指针,并且修改锁标志。
总结“轻量级锁”
轻量级锁的最大作用是:绝大部分锁,在同步周期内不存在多线程竞争访问(经验数据),因此通过CAS操作避免了互斥锁的巨大开销。
优点:偏向锁相对于重量级锁的优点是:因为线程在竞争资源时采用的是自旋,而不是阻塞,也就避免了线程的切换带来的时间消耗,提高了程序的响应速度。
不足:轻量级锁涉及到一个自旋的问题,而自旋操作是会消耗CPU资源的。为了避免无用的自旋,当锁资源存在线程竞争时,偏向锁就会升级为重量级锁来避免其他线程无用的自旋操作。
偏向锁:无线程竞争下,把整个同步都消除,连CAS都不做了。
JVM配置参数:-XX:+UseBiasedLocking
原理:让第一次访问锁资源的线程将直接获取该资源的所有权(锁对象的MarkWord写入该线程的Thread-ID)。
加锁过程
加锁过程见下图,加锁完成后,该锁对象就进入了一个“可偏向状态”了。
解锁过程
偏向锁的解锁,跟轻量级锁不一样。
因为锁资源已经处于“已偏向状态”了,每次线程访问该资源时,都会检测 MarkWord 中存储的 thread ID 是否等于当前 thread ID 。
何为撤销偏向锁?
偏向锁的撤销(Revoke) 操作并不是将对象恢复到无锁可偏向的状态, 而是在发现了线程竞争时,直接锁资源对象“升级到” 被加了轻量级锁,见下面的小点“锁升级过程”。
锁升级过程
锁升级的过程是:偏向锁 -> 轻量级锁 -> 重量级锁。
总结“偏向锁”
偏向锁最大作用:提高带同步但无竞争的程序性能。(对于共享资源极少被多线程同时访问到,因此把轻量级锁的CAS操作也省略了,从而达成了进一步的性能优化。)
偏向锁的特征是:“带效益权衡性质”的优化,如果程序大部分锁都会被多线程竞争访问,那么偏向模式就是多余的。
以上,就是本节的所有内容了,主要介绍了JVM对锁进行的5项锁优化方式:锁自旋、锁消除、锁粗化、轻量级锁以及偏向锁,本节主要侧重了轻量级锁以及偏向锁的讲解。希望对大家理解JVM锁优化有所帮助,晚安~
参考文章: