synchronized 锁优化(一):自适应自旋锁、锁消除、锁粗化

本文中所提及的锁指的均是 JVM 提供的 synchronized.

在并发编程中,synchronized 一直被称为重量级锁,但是随着 JDK1.6 对 synchronized 进行了各种优化之后,有些情况下它就并不那么重了。本文将介绍从 JDK1.6 开始引入的以下三种锁优化手段。

  • 自适应自旋锁(Adaptive Spining)
  • 锁消除(Lock Eliminate)
  • 锁粗化(Lock Cosarening)

 

自旋锁与自适应自旋锁

1. 自旋锁

互斥同步进入阻塞状态的开销都很大,应该尽量避免。大多数情况下,共享数据的锁定状态持续时间很短。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,不让出 CPU,如果在这段时间内能获得锁,就可以避免进入阻塞状态。

但是,很容易想象到这种情况,如果锁会被线程占用很长时间,那么进行忙循环操作占用 CPU 时间就会造成很大的性能开销,所以自旋锁只适用于共享数据的锁定状态很短的场景。

2. 自适应自旋锁

在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。

 

锁消除

锁消除是一种更为彻底的优化,在 JIT 编译时,对运行上下文进行扫描,去除不可能存在共享资源竞争的锁。

public void add(String str1,String str2){
         StringBuffer sb = new StringBuffer();
         sb.append(str1).append(str2);
}

我们都知道 StringBuffer 是线程安全的,因为它的关键方法都是被 synchronized 修饰过的,但我们看上面这段代码,我们会发现,sb 这个引用只会在 add 方法中使用,不可能被其它线程引用(因为是局部变量,栈私有),因此 sb 是不可能共享的资源,JVM 会自动消除 StringBuffer 对象内部的锁。

 

锁粗化

原则上,我们都知道在加同步锁的时候,尽可能的将同步块的作用范围限制在尽量小的范围,比如下面这两种情况:

package com.util.xgb;

public class SynchronizedTest {
	
	private int count;
	
	public void test() {
		System.out.println("test");
		int a = 0;
		synchronized (this) {
			count++;
		}
		
	}

}
package com.util.xgb;

public class SynchronizedTest {
	
	private int count;
	
	public void test() {
		synchronized (this) {
			System.out.println("test");
			int a = 0;
			count++;
		}
		
	}

}

很明显,第一种实现方式好于第二种,它并不会将对非共享数据的操作划分到同步代码块中,使得同步需要的操作数量更少,在存在锁竞争的情况下,也可以使得等待锁的线程尽快的拿到锁。

对于大多数情况,这种思想是完全正确的,但是如果存在一连串的操作都对同一个对象进行反复的加锁和解锁,甚至加锁的操作出现在循环体中,那么即使没有线程竞争共享资源,频繁的进行加锁操作也会导致性能损耗。

锁粗化的思想就是扩大加锁范围,避免反复的加锁和解锁。这里还是拿 StringBuffer 举例,如下所示

public String test(String str){
       
       int i = 0;
       StringBuffer sb = new StringBuffer():
       while(i < 100){
           sb.append(str);
           i++;
       }
       return sb.toString():

}

在这种情况下,JVM 会检测到这样一连串的操作都对同一个对象加锁(while 循环内 100 次执行 append,没有锁粗化的就要进行 100 次加锁/解锁),此时 JVM 就会将加锁的范围粗化到这一连串的操作的外部(比如 while 虚幻体外),使得这一连串操作只需要加一次锁即可。

你可能感兴趣的:(Java并发)