锁优化(synchronized)
JDK1.6的对锁进行了大量的优化以满足不断发展的性能要求.JDK主要的锁优化如下
-
自旋锁和自适应自旋锁
-
偏向锁
-
轻量级锁
-
锁粗化
-
锁消除
自旋锁和自适应自旋锁
挂起线程和恢复线程都需要转入内核态完成,这些操作给Java带来了很大的压力消耗很大,而对于大部分线程获取锁的时间很短,当一个线程等待另一个线程释放锁的时候将此线程挂起然后很短的时间恢复并不值得,因此可以将该线程"稍等一会"也就是不放弃cpu的执行时间,看他等待的线程是否很快的释放锁.为了让线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就是所谓的自旋.
如果锁在很短的时间内释放了,那么自旋的效果就很好,如果锁在一段时间内都没有释放,那个长时间的占用cpu而不执行白白浪费了cpu资源,因此我们需要将该线程挂起让出cpu 的执行权.而什么时间让出cpu执行权不好把握.自旋的次数默认是10次之后将挂起,用户也可以通过 -XX:PreBlockSpin来自行修改.
JDK6之后引入了自适应自旋.自旋时间不再是固定的,而是由前一次在同一个锁上的自旋时间及锁拥有者的状态来决定的.有了自适应性自旋,随着程序的运行时间和性能监控信息的完善,虚拟机对程序锁的情况的判断越来越精准,虚拟机就会变得越来越聪明了.
偏向锁
偏向锁也是1.6之后引入的,他的目的是消除数据在无竞争情况下的同步语,进一步提高程序的运行性能.如果说轻量级锁是为在无竞争的情况下使用CAS操作取出同步的互斥量,那么偏向锁就是在无竞争的情况下把整个同步去掉,连CAS操作都不用做了.
偏向锁的意思是这个锁会偏向第一个获取到他的锁,如果在接下来执行的过程中,该锁一直没有被其他的锁获取的话,则持有偏向锁的线程永远不需要再进行同步.一旦有新的线程试图获取该锁,偏向模式会被撤销.撤销后进入无锁状态.这里会改变对象头的关于偏性模式的标志位和关于锁的标志位
轻量级锁
加锁的过程:
JVM在当前线程的栈帧中创建用于存储锁记录的空间(Lock Record),然后把对象头中的Mark Word复制进去,同时生成一个叫Owner的指针指向那个加锁的对象,同时用CAS尝试把对象头的MarkWord更新为指向锁记录(Lock Record)的指针。成功了就拿到了锁。
那么失败了呢?失败了的说法比较多。主流有《深入理解JVM》的说法和《并发编程的艺术》的说法。
《深入理解JVM》的说法:
失败了,去查看MarkWord的值。有2种可能:1,指向当前线程的指针,2,别的值。
如果是1,那么说明发生了“重入”的情况,直接当做成功获得锁处理。
其实这个有个疑问,为什么获得锁成功了而CAS失败了,这里其实要牵扯到CAS的具体过程:先比较某个值是不是预测的值,是的话就动用原子操作交换(或赋值),否则不操作直接返回失败。在用CAS的时候期待的值是其原本的Mark Word。发生“重入”的时候会发现其值不是期待的原本的Mark Word,而是一个指针,所以当然就返回失败,但是如果这个指针指向这个线程,那么说明其实已经获得了锁,不过是再进入一次。如果不是这个线程,那么情况2:
如果是2,那么发生了竞争,锁会膨胀为一个重量级锁(MutexLock)
《并发编程的艺术》的说法:
失败了直接自旋。期望在自旋的时间内获得锁,如果还是不能获得,那么开始膨胀,修改锁的Mark Word改为重量级锁的指针,并且阻塞自己。
解锁过程:
(那个拿到锁的线程)用CAS把Mark Word换回到原来的对象头,如果成功,那么没有竞争发生,解锁完成。如果失败,表示存在竞争(之前有线程试图通过CAS修改Mark Word),这时要释放锁并同时唤醒被挂起的线程。
锁消除
指的是虚拟机即时编译器在运行的时候对一些代码要求同步,但是检测到不可能存在共享数据竞争的锁进行消除.锁消除的主要判定依据来源于逃逸分析的数据支持.如果判断到一段代码中,在堆上的所有数据不会逃逸出去被其他的线程访问到那么就可以把他们当做栈上面数据对待,认为他们是线程私有的,同步枷锁就无需进行.
例如StringBuffer是线程安全的,因为在append方上上面都有一个同步块
public String concatString(String s1, String s2){
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
我们发现sb的引用被限制在了concatStirng方法里面他永远不可能被其他线程访问到,因此虽然这里有锁但是可以被安全的消除掉.在解释执行时这里仍然会枷锁,但是经过服务端编译器即时编译后,这段代码会自动忽略所有的同步措施直接执行.
锁粗化
一般情况下我们希望同步的代码越少越好.只在共享数据的实际作用域中才进行同步,这样子是为了使得同步的操作尽可能的少就算存在竞争也可以使得下一个线程尽快的获取到锁.
这个原则大部分时间是对的但是如果一个系列的连续操作都是对同一个对象反复的加锁和解锁,甚至加锁操作出现在循环体之中,即使没有线程竞争,频繁的进行互斥同步的操作也会导致不必要的性能损耗.
总结
上述的锁不是Java语言层面的锁优化方法,是内置在JVM当中的。
首先偏向锁是为了避免某个线程反复获得/释放同一把锁时的性能消耗,如果仍然是同个线程去获得这个锁,尝试偏向锁时会直接进入同步块,不需要再次获得锁。
而轻量级锁和自旋锁都是为了避免直接调用操作系统层面的互斥操作,因为挂起线程是一个很耗资源的操作。
为了尽量避免使用重量级锁(操作系统层面的互斥),首先会尝试轻量级锁,轻量级锁会尝试使用CAS操作来获得锁,如果轻量级锁获得失败,说明存在竞争。但是也许很快就能获得锁,就会尝试自旋锁,将线程做几个空循环,每次循环时都不断尝试获得锁。如果自旋锁也失败,那么只能升级成重量级锁。
可见偏向锁,轻量级锁,自旋锁都是乐观锁。
本文详细解析了JDK1.6中的锁优化技术,包括自旋锁、自适应自旋锁、偏向锁、轻量级锁、锁粗化和锁消除等,阐述了这些技术如何提升多线程环境下Java程序的执行效率。
170万+

被折叠的 条评论
为什么被折叠?



