JDK1.6之后对于synchronized锁的优化
如果想了解synchronized的底层实现,以及用法请参考我的上一篇及上上(或者上上上一篇???)一篇博客。
在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起 线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。相比之下使用Java 提供的Lock对象,性能更高一些。
到了JDK1.6,发生了变化,对synchronize加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏 向锁等等。导致在JDK1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronized,在未来 的版本中还有优化余地,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行 同步。
synchronized优化 :
了解了Synchronized的底层实现就知道,它最大的特征就是在同一时刻只有一个线程能够获得对象的监视器 (monitor),从而进入到同步代码块或者同步方法之中,即表现为互斥性(排它性)。这种方式肯定效率低下, 每次只能通过一个线程,既然每次只能通过一个,这种形式(互斥:保证安全)不能改变的话,那么我们能不能让每次通过的速度变快 一点呢。
1、CAS操作(无锁实现的同步—乐观锁)-自旋
之前synchronized获取锁失败是将线程挂起-悲观锁策略(假设只要当前线程进入同步方法或者同步代码块,一定有人竞争,所以挂起。直到有人通知我)
public class Test implements Runnable{
private int i=0;
public synchronized void setI(){
System.out.println(Thread.currentThread().getName()+"----"+i);
this.i=10;
System.out.println(Thread.currentThread().getName()+"----"+i);
}
public static void main(String[]args){
Runnable runnable=new Test();
Thread thread1=new Thread(runnable);
Thread thread2=new Thread(runnable);
Thread thread3=new Thread(runnable);
thread1.start();
thread2.start();
thread3.start();
}
public void run(){
System.out.println(Thread.currentThread().getName()+"----"+i);
setI();
}
}
Compare And Swap(O,V,N)
O 预期的值 (旧值);
V 内存地址存放的实际值;
N 更新的新值
当O==V时,此时表示没有线程修改共享变量的值,此时可以成功的将内存中的值修改为N
当O!=V时,此时表示内存中共享变量的值已被其他线程修改,此时返回将内存中的最新值V,再次尝试修改变量
For/while 循环,减少了挂起阻塞操作
自旋带来的问题
1、ABA问题:
解决方法:添加版本号,比较的时候不仅比较值,还要比较版本号
i = 0(初始化)
线程1 | 线程2 | 线程3 |
|
|
|
i= 1 | I = 0 | i= 0 |
|
|
|
2、自旋在CPU上跑无用指令,会浪费CPU资源
解决方法:JVM尝试自旋一段时间,若在此时间段内,线程成功获取到锁,再下次获取锁时,适当延长自旋时间
若在此时间段内,线程没有获取到锁,再下次获取锁时,适当减少自旋时间
3、公平性问题:处于阻塞态线程比处于自旋状态的锁更慢获取到锁
处于阻塞状态的锁可能一直无法获取到锁
解决方案:只有LOCK锁可以实现公平性,synchronized无法实现公平锁
1、偏向锁:最乐观的一种锁(进入同步方法或同步块的始终是一个线程)
JDK1.6之后默认synchronized。
当出现另一个线程也尝试获取锁(在不同的时刻)时,偏向锁会升级为轻量级锁
2、轻量级锁
不同时刻有不同的线程尝试获取锁,“由于深夜十字路口的车辆来往可能比较少,如果还设置红 绿灯交替,那么很有可能出现四个方向仅有一辆车在等红灯的情况。
因此,红绿灯可能被设置为闪黄灯的情况,代表车辆可以自由通过,但是司机需要注意观察。”
同一时刻有不同的线程尝试获取锁,会将偏向锁升级为重量锁
3、重量级锁
JDK1.6之前的synchronized都是重量级锁,就是将线程阻塞挂起
锁只有升级,没有降级
Java虚拟机中synchronized关键字的实现,按照代价由高到低可以分为重量级锁、轻量锁和偏向锁三种。
1. 重量级锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。JVM采用了自适应自旋,来避免线程在面对非常小的synchronized代码块时,仍会被阻塞、唤醒的情况。
2. 轻量级锁采用CAS操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况。
3. 偏向锁只会在第一次请求时采用CAS操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况。
2、锁粗化:当多次连续的加锁与解锁过程,会将多次加减锁过程粗化为一次加锁与解锁的过程
锁粗化就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成为一个范围更大的锁。举例如 下:
public class Test{
private static StringBuffer sb = new StringBuffer();
public static void main(String[] args) {
sb.append("a");
sb.append("b");
sb.append("c");
}
}
这里每次调用stringBuffffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁
和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次
append方法结束后进行解锁。
3、锁消除:当对象不属于共享资源时,对象内部的同步方法或同步代码块的锁会自动解除
锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,
那么可以认为这段代码是线程安全的,不必要加锁。看下面这段程序:
public class Test{
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
sb.append("a").append("b").append("c");
}
}
虽然StringBuffffer的append是一个同步方法,但是这段程序中的StringBuffffer属于一个局部变量,并且不会从该方
法中逃逸出去,所以其实这过程是线程安全的,可以将锁消除。