前置知识:Java对象头
Java对象保存在内存中时,由以下三部分组成:
- 对象头
- 实例数据
- 对齐填充字节
而java的对象头由以下三部分组成:
- Mark Word
- 指向类的指针
- 数组长度(只有数组对象才有)
Java对象头存储结构
锁优化
JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
锁消除
例如下面这个方法
public String getString(String a,String b,String c) {
return a + b + c;
}
因为String 对象是不可变的,每次修改都是产生新的 String对象,所以上诉代码编译后会被优化,如下:
JDK5之前
public String getString(String a,String b,String c) {
StringBuffer buffer = new StringBuffer();
buffer.append(a);
buffer.append(b);
buffer.append(c);
return buffer.toString();
}
JDK5之后
public String getString(String a,String b,String c) {
StringBuilder sb= new StringBuilder();
sb.append(a);
sb.append(b);
sb.append(c);
return sb.toString();
}
StringBuffer的append方法
public synchronized StringBuffer append(CharSequence s) {
toStringCache = null;
super.append(s);
return this;
}
StringBuilder的append方法
public StringBuilder append(String str) {
super.append(str);
return this;
}
区别很明显,StringBuffer加了synchronized是线程安全的,但是为什么JDK5之后还要将其优化成StringBuilder呢?
因为 我们只在 getString() 方法中使用到了该对象 ,也就是说StringBuffer对象永远不会逃到 getString() 方法之外,所以即使append方法有锁,在经过服务端即时编译之后,也会忽略掉所有同步措施直接执行。
锁消除设计到一个概念即:逃逸分析技术。简单来说就是:如果判断一段代码,其在堆上的数据都不会逃逸出去被其他线程访问到,那么就可以把他们当作栈上数据看待,认为它们是线程私有的,同步加锁就不需要进行了。
当对象没有发生逃逸时,该对象就可以通过标量替换分解成成员标量分配在栈内存中,和方法的生命周期一致,随着栈帧出栈时销毁,减少了 GC 压力,提高了应用程序性能。
建议大家去了解了解 逃逸分析技术。
锁粗化
例如:
public static void main(String[] args) {
Vector<Integer> vector = new Vector<>();
for (int i = 0; i < 20; i++) {
vector.add(i);
}
System.out.println(vector);
}
vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。
锁升级
锁的状态共有四种:
- 无锁
- 偏向锁
- 轻量级锁
- 重量级锁
锁升级过程不可逆,也就说从偏向锁升级到轻量级锁后不能再转变为偏向锁
目的:为了提高获得锁和释放锁的效率
偏向锁
偏向锁就像它的名字一样是偏心的,该锁会偏向于第一个获取到它的线程,如果在接下来的执行过程中,该锁一直没有被其他线程获取,则只有该偏向锁的线程永远不需要再次同步。
- 当锁对象第一次被线程获取时,虚拟机会将对象头中的锁标识位设置为 “01”,偏向锁标识设置为 “1”,同时通过 CAS操作将获取到这个锁的线程 ID 记录在对象的 Mark Word之中,
- 以后该线程在进入退出该同步块时,就不需要进行CAS加锁、解锁,只是需要简单的测试一下:看对象头中的Mark Word是否记录着当前线程的 ID,如果是则不需要加锁,如果不是就再看Mark Word的偏向锁标识是否为1(也就是是否为偏向锁),如果不是则用CAS竞争锁,如果是则用CAS将对象头的偏向锁指向当前线程。
当然。一旦出现另一个线程去获取锁,偏向模式宣告结束。根据锁对象目前的状态决定是否撤销,撤销后恢复到未锁定状态,锁标识为"01"或轻量级锁状态 “00”.
轻量级锁
目的:减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
- 线程在执行同步代码块之前,如果该同步对象没有被锁定,则JVM会在当前线程的栈帧中创建用于存储 锁记录的空间,用于存储锁对象的Mark Word的拷贝,官方称为 Displaced Mark Word。
- 然后虚拟机会通过CAS尝试把对象的Mark Word 更新为 指向锁记录的指针,如果更新成功,代表当前线程该线程获取到了对象的锁,并将对象的Mark Word的锁标志位设置为 “00” (代表轻量级锁),如果失败的话,说明锁对象已经被其他线程抢占了,当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。
如果出现两条以上的线程竞争同一把锁,那轻量级锁不再有效,就必须膨胀为重量级锁,锁标识变为 “10”,后面等待的线程必须进入阻塞状态。
自旋锁
从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数(默认为10次),例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋。
适应性自旋代表着自选次数不确定,每一次的自旋时间由前一次在同一锁上的自旋时间及锁拥有者的状态决定的。
觉得不错的小伙伴可以一键三连哦!,感谢支持!!!
更多面试题请移步 大厂面试题专栏