「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」
JVM锁优化
锁偏向
在无竞争的情况下消除整个同步使用的互斥量,连CAS操作都不做了。
偏向锁会偏向第一个获得它的线程,如果在接下来的执行过程中该锁没有被其他线程获取,则持有偏向锁的线程以后进入这个锁相关的同步块不再需要进行同步。
当另一个线程尝试获取这个锁时,偏向模式结束。此时根据锁对象是否处于锁定状态,将锁对象恢复到未锁定状态或者轻量级锁状态(锁的升级,单向不可逆)。
量级锁
如果锁偏向失败,JVM不会立即挂起线程,还会使用一种称为轻量级锁的优化手段. 会将对象的头部作为指针,指向持有锁的线程堆栈内部, 来判断一个线程是否持有对象锁. 如果线程获得轻量级锁成功,就进入临界区. 如果获得轻量级锁失败,表示其他线程抢到了锁,那么当前线程的锁的请求就膨胀为重量级锁.当前线程就转到阻塞队列中变为阻塞状态。
偏向锁,轻量级锁都是乐观锁,重量级锁是悲观锁。
一个对象刚开始实例化时,没有任何线程访问它,它是可偏向的,即它认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程. 偏向第一个线程,这个线程在修改对象头成为偏向锁时使用CAS操作,将对象头中ThreadId改成自己的ID,之后再访问这个对象时,只需要对比ID即可. 一旦有第二个线程访问该对象,因为偏向锁不会主动释放,所以第二个线程可以查看对象的偏向状态,当第二个线程访问对象时,表示在这个对象上已经存在竞争了,检查原来持有对象锁的线程是否存活,如果挂了则将对象变为无锁状态,然后重新偏向新的线程; 如果原来的线程依然存活,则马上执行原来线程的栈,检查该对象的使用情况,如果仍然需要偏向锁,则偏向锁升级为轻量级锁。
轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对同一个锁的操作会错开,或者稍微等待一下(自旋)另外一个线程就会释放锁. 当自旋超过一定次数,或者一个线程持有锁,一个线程在自旋,又来第三个线程访问时, 轻量级锁会膨胀为重量级锁, 重量级锁除了持有锁的线程外,其他的线程都阻塞。
自旋锁
线程阻塞的时候,让等待的线程不放弃cpu执行时间,而是执行一个自旋(一般是空循环),这叫做自旋锁。
在锁争用不多、锁占用时间比较少的情况下,自旋锁让线程去循环获取锁,这比线程阻塞性能更好(包括挂起或唤醒)。自旋锁默认自旋的次数是10次,可以通过JVM参数-XX:PreBlockSpin配置,
锁膨胀后,JVM为了避免线程在真实的层面被挂起,JVM还会做最后的努力,这就是自旋锁. 当前线程无法立即获得锁,但是在什么时候可以获得锁也不一定, 也许在几个CPU周期后就可以得到锁, 如果是这样的话,简单的将线程挂起可能是一种得不偿失的操作. 因此JVM会进行一次赌注: JVM期望在不久的将来可以得到锁. 因为JVM会让当前的线程做几个空循环,在经过若干次循环后,如果可以得到锁就进入临界区,如果还不能得到锁则将线程真实的挂起。
锁粗化
一般我们在写同步代码的时候,都是推荐锁的范围越小越好,能锁代码块就别锁方法(能锁变量就别锁对象),这样能使需要同步的操作数量尽可能的小,等待线程也能尽快的拿到锁。
大部分情况下,这样都是没有毛病的。但是如果一系列的操作都在对同一个对象加锁,甚至在循环体加锁时,即使没有线程竞争,频繁的进行互斥同步操作,也会导致不必要的性能消耗。
锁粗化就是,如果虚拟机探测到有一串零碎的操作都是在对同一个对象加锁时,将会把整个加锁同步操作扩展到整个操作序列的外部,就可以只加一次锁了。
锁消除
虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。一般根据逃逸分析的数据支持来作为判定依据。