简介
synchronized是java的元老级角色,现在还有很多人称呼他为重量级锁。在1.6之后,对其优化后,现在没那么重了。
synchronized 是支持原子性和可见性。
可见性:
1)线程解锁前,必须把共享变量的最新值刷新到主内存中。
2)线程加锁时,将清空工作内存中共享变量的值,从而使得使用共享变量时需要从主内存中重新读取最新的值(注意,加锁与解锁是同一把锁)
由于syncronized可以保证原子性及可见性,变量只要被syncronized修饰,就可以放心的使用
锁对象
在java代码中使用synchronized可是使用在代码块和方法中,根据Synchronized用的位置可以有这些使用场景:
- 一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁。也就是说synchronized获得的锁是可重入的。
- 程序在执行过程中,如果出现异常,默认情况锁会被释放
锁的实现
当一个线程试图访问同步代码块时,它首先得到锁,退出或抛出异常时必须释放锁,那锁存在哪里,锁里到底是什么
JVM规范中有synchronized实现原理,jVM是基于进入和退出Moitor对象来实现方法同步和代码块同步
public class SynchronizedDemo {
public static void main(String[] args) {
synchronized (SynchronizedDemo.class) {
}
method();
}
private static void method() {
}
}
执行同步代码块后首先要先执行monitorenter指令,退出的时候monitorexit指令。
从上图中就可以看出,执行静态同步方法的时候就只有一条monitorexit指令,并没有monitorenter获取锁的指令。这就是锁的重入性,即在同一锁程中,线程不需要再次获取同一把锁。Synchronized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。
java对象头
整体的对象头中存储有,markWord、klass、data(实例数据)、padding(对齐填充)
- Mark Word :Mark Word在64位虚拟机下,也就是占用64位大小即8个字节的空间
- klass: klass pointer的存储内容是一个指针,指向了其类元数据的信息,jvm使用该指针来确定此对象是类的哪个实例。
- data:代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来
- 对齐填充:对齐填充并不是必然存在的,也没有特殊的含义,它仅仅起着占位符的作用。就是任何对象的大小都必须是8字节的整数倍。
锁升级
Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级,但是不能降级。这种策略就是为了提高锁的获得和释放的效率。
偏向锁
- 大多数情况下,锁不仅不存在竞争,而且还是同意线程多次获得,为了代价更小而引入了偏向锁。 例如:StringBuffer的多次append,加来加去都是同个线程在执行append命令(一般情况下),OS在每次append时都要给该线程重复加锁解锁,这就很浪费了。
- 当一个线程需要获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出锁时,不需要加锁和解锁。
- 该线程在进入锁时,测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程
- 偏向锁的撤销:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
- 偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态
轻量级锁
轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。
- 加锁:先在当前线程的栈帧中创建由于存储记录的空间,并将对象头的markword复制到锁记录中。然后线程尝试使用CAS将对象头中的markWord替换为指向锁记录的指针。成功则获取锁,失败则表示其他线程竞争,当前线程自旋来继续获取锁。
- 解锁:CAS操作对象头,成功则表示没有竞争发生,失败则存在竞争,锁会膨胀重量级锁。
如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。
但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。
- 轻量级锁升级重量级锁
jdk 1.6 中,当自旋超过10次或者等待的线程超过你CPU个数的二分之一,则升级成重量级锁。 通过参数可调整。自旋次数的默认值是10,用户可以通过-XX:PreBlockSpin来更改。
1.6之后,引入自适应自旋锁,JDK1.6中引入了自适应的自旋锁。 自适应意味着自旋的时间不再是固定的, 而是由前一次在同一个锁上的自旋时间以及锁拥有者的状态来决定。如果在同一个锁对象上, 自旋等待刚好成功获得锁, 并且在持有锁的线程在运行中, 那么虚拟机就会认为这次自旋也是很有可能获得锁, 进而它将允许自旋等待相对更长的时间。
重量级锁
重量级锁是由OS调度的,有竞争锁队列和等待队列,每次抢锁,失败的线程全部立即wait,等锁释放后全部唤醒。虽然被阻塞的线程不会消耗cpu,但线程的切换是需要从用户态转换到内核态,而转换状态是需要消耗很多CPU时间的
优缺点
锁状态 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁解锁无需额外的消耗,和非同步方法时间相差纳秒级 | 如果竞争线程多,纳秒会带来额外的锁撤销的消耗 | 基本没有线程竞争锁的同步场景 |
轻量级锁 | 竞争的线程不会阻塞,使用自旋,提高程序的响应速度 | 如果一直不能获取锁,长时间的自旋会造成CPU消耗 | 适用于少量线程竞争锁对象,且线程持有锁时间不长,最求响应速度的场景 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间长 | 很多线程竞争锁,且锁持有的时间长,追求吞吐量的场景 |
锁升级主要分为偏向锁 - 轻量级锁 - 重量级锁三层,偏向锁、轻量级锁是在Java内部的优化,属于所谓的用户态,而重量级锁则是向操作系统申请,属于内核态。在锁竞争不激烈的时候由jvm自己解决肯定性能是最好的,但是jvm通过自旋方式解决会消耗CPU性能,所以在锁竞争激烈的情况下重量级锁性能更好。
锁粗化和锁消除(两个不重要的锁优化)
- 锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。
- Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间