Synchronized锁优化原理
JDK5之后,Synchronized实现了各种锁优化策略,如适应性自旋、锁消除、锁粗化、锁膨胀、轻量级锁、偏向锁等。
适应性自旋
CAS自旋是通过硬件实现的计算机原语来实现的
通常情况下的CAS是比较并替换,在无限循环中直至替换成功
for(;;){
//compareAndSet是本地方法,底层是通过硬件实现的一组计算机原语
if(compareAndSet(expect,news)){
return news;
}
}
在早期的(JDK5及以前)JVM中,自旋锁默认是关闭的,可以通过-XX:+UseSpinning参数开启,并使用-XX:PreBlockSpin参数来自定义自旋次数,默认是10次,这些参数是全局设定的,对于不同的锁都是一样的效果。类似于下面代码
for(int i = 0; i < 10; i ++){
if(compareAndSet(expect,news)){
return news;
}
}
在JDK6之后,对自旋锁进行了优化,也就是优化成了适应性自旋锁,他的自旋次数是由上一次锁的自旋时间及锁的拥有者(持锁线程)的状态决定,如果自旋获取锁的成功率更高,那么自旋允许的次数也相对较多,可能是几十或者上百次。
如果自旋获取锁的成功率较低,说明竞争较大,同步区的执行时间较长,那么自旋次数也相对较少,改用阻塞返回更有利于性能的提升。
锁消除
锁消除顾名思义就是不使用锁,取消锁。这种情况是为了优化一些隐藏式的同步过程,一些看似没有使用锁的代码,也有可能会被编译成同步代码,例如JDK5之前,字符串的相加是会被转换成StringBuffer类来操作的
public String concatString(String s1,String s2,String s3){
return s1+s2+s3;
}
public String concatString(String s1,String s2,String s3){
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
在上述代码中,被编译器转换成了StringBuffer类来进行字符串的加操作,而StringBuffer的append方法则是通过synchronized修饰的,由于s1、s2、s3是在栈中操作的,不会被其他线程访问到,这里就没必要使用同步操作,也就会进行锁消除的优化。
锁粗化
锁粗化是指对同步区域的扩大,在上述示例中,三个append方法都是被synchronized修饰,在没有锁竞争的情况下,完全可以将三个方法放在同一个synchronized作用域中,即消除方法的synchronized修饰,改成三个append外加上synchronized修饰。
偏向锁
偏向锁的使用可以看成是无锁操作,但是仅在无线程竞争的情况下。可以理解成,有一个对象锁,当前有一个线程A访问他,此时没有别的线程争用,那么线程A会被锁记住(使用对象头中哈希码的地址空间),线程A获得“偏向锁”,在这段时间内线程A反复使用都不需要加锁,因为线程A被“偏爱”了,只要识别到是线程A,那么永远都是无锁访问。如果这段时间后有新的线程B来使用,此时会首先判断“偏向锁”记住的是不是线程B,如果不是那么通过CAS来修改,修改成功的话B也可以无锁访问,如果修改失败,偏向锁将升级至轻量级锁。
轻量级锁
轻量级锁也是可以看做是无锁操作,同时也是在无线程竞争的情况下存在,区别在于轻量级锁会使用CAS来更新锁的指向,而偏向锁无需CAS。当CAS次数达到指定次数后(JDK5之前通过-XX:PreBlockSpin指定,JDK6之后按照自适应的次数来指定)还不成功会升级至重量级锁。
锁膨胀
锁膨胀其实是针对小部分场景才有效的优化,是从偏向锁到轻量级锁、重量级锁的过程,是为了优化某些情况下(如无线程竞争)使用重量级锁带来的不必要开销。
一张网图概括Synchronized的锁机制
Synchronized与ReentrantLock的性能对比
public class CompareTest {
private Lock nonFairLock = new ReentrantLock();
private Object monitorLock = new Object();
public static void main(String[] args) throws InterruptedException {
// test01(1000,1000000);
test02(20,100,1000000);
// test03();
}
public static void test01(int cycle,int count) throws InterruptedException {
CompareTest test = new CompareTest();
long l = System.currentTimeMillis();
for (int i = 0; i < cycle; i ++){
test.lockAdd(count);
}
System.out.println("lock 执行时间:"+(System.currentTimeMillis()-l)+" ms");
Thread.sleep(1000);
long l1 = System.currentTimeMillis();
for (int i = 0; i < 1000; i ++){
test.syncAdd(count);
}
System.out.println("synchronized 执行时间:"+(System.currentTimeMillis()-l1)+" ms");
}
public static void test02(int threadCount,int cycle,int count) throws InterruptedException {
CompareTest test = new CompareTest();
long l = System.currentTimeMillis();
List<Thread> ts = new ArrayList<>();
for (int c = 0; c < threadCount; c ++){
Thread t = new Thread(() ->{
for (int i = 0; i < cycle; i ++){
test.lockAdd(count);
}
});
t.start();
ts.add(t);
}
for (Thread t : ts){
t.join();
}
System.out.println("lock 执行时间:"+(System.currentTimeMillis()-l)+" ms");
Thread.sleep(1000);
long l1 = System.currentTimeMillis();
List<Thread> ts1 = new ArrayList<>();
for (int c = 0; c < threadCount; c ++){
Thread t = new Thread(() ->{
for (int i = 0; i < cycle; i ++){
test.syncAdd(count);
}
});
t.start();
ts1.add(t);
}
for (Thread t : ts1){
t.join();
}
System.out.println("synchronized 执行时间:"+(System.currentTimeMillis()-l1)+" ms");
}
public static void test03(int count){
}
public void lockAdd(int count){
nonFairLock.lock();
try {
int total = 1;
for (int i = 0; i < count; i ++){
total ++;
}
}catch (Exception e){
e.printStackTrace();
}finally {
nonFairLock.unlock();
}
}
public void syncAdd(int count){
synchronized (monitorLock){
int total = 1;
for (int i = 0; i < count; i ++){
total ++;
}
}
}
Synchronized和Lock该怎么选择
在JDK6以后,synchronizd在性能上有了质的飞跃,甚至在JDK8以后synchronized在一定程度上也表现的比Lock更好,那么synchronized最显著的特点就是简单易用,同时也比较安全。
在一些简单的业务场景或逻辑中,当然是推荐使用synchronized,可以减少lock带来的不必要麻烦。
lock在功能上显然是比synchronized更加丰富,更加灵活,可以实现复杂的同步场景,在需要实现特殊逻辑的情况下lock是更好的选择,当然使用上也会比synchronized更加复杂。