今天跟着CSDN的一篇blog来学习锁的四种优化
1. 锁消除
原理:JVM在JIT(即时编译)的时候,扫描上下文,去除掉那些不可能发生共享资源竞争的锁,从而而节省了线程请求这些锁的时间。
例子:StringBuffer的append方法是一个同步方法,如果StringBuffer类型的变量是一个局部变量,则该变量就不会被其它线程所使用,即对局部变量的操作是不会发生线程不安全的问题。
在这种情景下,JVM会在JIT编译时自动将append方法上的锁去掉。
2. 锁粗化
原理:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
例子:在for循环里的加锁/解锁操作,一般需要放到for循环外。因为每次循环都要加锁不如一次性加完锁
3. 使用偏向锁和轻量级锁
说明:
1)java6为了减少经常获取和释放锁带来的开销,引入了轻量级锁和偏向锁
2)锁一共有4中状态,级别从低到高为:无锁状态、偏向级锁、轻量级锁、重量级锁
3)锁的状态会随着竞争的情况逐渐升级,而且只能升级不能降级。
【偏向锁】
概念:大多数情况下,锁不仅不会被多线程竞争,反而还会只被一个线程多次的获取,这个时候就存在偏向锁。
原理:当一个线程拿到当前锁后,锁会在对象头和栈帧中记录这个线程的ID,等到下次这个线程来获取锁时,不用进行CAS的加锁和解锁,只需查看Mark Word中是否存储指向当前线程的偏向锁。
偏向锁的获取:
(1)访问对象的Mark Word中偏向锁的标志位是否为1,如果是1则为偏向锁
①:如果偏向锁的标志位为0说明为无锁状态,线程通过CAS操作尝试获取偏向锁,如果获取锁成功,则将Mark Word的偏向线程ID设置成当前线程ID,并且标志位置1
②如果标志位不为0也不为1说明存在锁竞争,偏向锁已经膨胀为轻量级锁,这时候通过CAS获取锁
(2)如果是偏向锁,则判断当前线程ID是否和Mark Word中的偏向线程ID一致,如果相同则无需进行CAS操作得到锁
(3)如果不一致则通过CAS操作获取锁,成功后修改MarkWord中的偏向线程ID指向这个线程ID。
(4)如果CAS获取偏向锁失败,则表示存在竞争。当达到安全局安全点时(在这个时间点没有正在执行的字节码),获得偏向锁的线程被挂起,偏向锁膨胀升级成轻量锁,然后被阻塞在安全点的线程进行往下执行同步代码。
偏向锁的释放:
(1)当其他线程尝试获取偏向锁时,持有偏向锁的线程才会释放偏向锁
(2)释放偏向锁需要等待到全局安全点
(3)过程:
①若持有该锁的线程不活动了,则释放该线程的锁,将对象头设置成无锁状态
②若持有锁的线程还在活动,则说明发生了竞争,挂起该进程,锁升级为轻量锁然后刚刚被暂停的线程继续执行该同步代码。
(4)优点:加锁和解锁不需要额外的消耗,和执行非同步代码块的效率仅存在纳秒级的差距
(5)缺点:如果存在线程竞争,锁撤销会带来多于的开销
(6)额外说明:
①偏向锁默认在应用程序启动几秒钟之后才激活。
②可以通过设置 -XX:BiasedLockingStartupDelay=0 来关闭延迟。
③可以通过设置 -XX:-UseBiasedLocking=false 来关闭偏向锁,程序默认会进入轻量级锁状态。(如果应用程序里的锁大多情况下处于竞争状态,则应该将偏向锁关闭)
【轻量级锁】
原理:
①当使用轻量级锁时(锁标识位为00),线程在执行同步代码之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中Mark Word复制到锁记录中(这个锁记录在栈帧中名为 Displaced Mark Word)
②将对象头的Mark Word复制到栈帧中的锁记录后,虚拟机将尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针,如果此时没有线程占用锁或者没有线程竞争锁,则当前线程获取到该锁,然后执行同步代码块。
③如果在获取锁并且执行同步代码块的过程中,另外一个线程也完成了栈帧中锁记录的创建,并且已经将Mark Word复制到自己栈的锁记录中,然后尝试用CAS将对象头中的Mark Word修改为自己的锁记录指针,当时由于之前获取了轻量锁的线程已经修改过Mark Word了,所以此时对象头中的Mark Word与当前线程锁记录中的Mark Word值不相同,导致CAS失败。然后改线程就会不断的执行CAS操作去替换Mark Word中的值,(当循环次数或者循环时间达到了上限则停止)如果在结束之前CAS操作成功,则改线程获取该锁并且成功的修改了Mark Word中的值,如果修改失败,则对象头中的Mark Word的值会被修改成指向重量锁的指针,然后该线程挂起进入阻塞态。
④当持有锁的那个进程执行完代码块后,使用CAS操作将对象头Mark Word还原为最初的状态时(将对象头中指向锁记录的指针替换为Displaced Mark Word),发现Mark Word已经被修改为指向重量锁的指针,因此CAS操作失败,该线程唤起挂起中的线程进行新一轮竞争,而此时,所有竞争失败的锁都会被阻塞,而不是自旋。
自旋锁的概念:
(1)没有得到锁的线程将自己运行一段时间自循环,而不是挂起
(2)自旋的代价就是该线程一直占用着处理器,如果占有锁的线程执行代码时间越短,则自旋的效果就越好,反之开销很大
(3)所以自旋的等待时间必须要有一定的限度。
轻量锁的优点:在没有多线程的竞争下,可以减少传统重量锁带来的消耗
缺点:竞争失败的锁会进行自旋,消耗CPU
应用:追求响应时间,同步代码块执行速度要非常快。
【重量级锁】
说明:
(1)java6之前的synchronized锁都是重量级锁,效率很低,因为monitor是依赖操作系统的Mutex Lock(互斥量)来实现的
(2)多线程竞争锁时,会引起线程的上下文切换(在时间片还没有用完的情况下进行了上下文切换),需要从用户态转化到核心态,这个状态切换需要很长的时间,所以时间成本很高
(3)在互斥的状态下,没有得到锁的线程将会被挂起,而挂起和恢复线程的操作都需要从用户态转化成内核态中完成。
优点:没有得到锁的线程不用自旋,直接挂起,不用消耗cpu
缺点:在大量竞争下,效率会异常低
应用:追求吞吐量,同步块执行速度较长。