说明
JDK1.6为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,所以在JDK1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率
Java 中锁有几种状态:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
注意:在 JDK 15 的版本上面,移除了偏向锁了
锁的几种状态
图片是在百度上面找的
无锁状态
程序不会有锁的竞争。第一个线程执行完之后,第二个线程再去执行。
偏向锁
偏向锁,字面理解就是说会偏向于第一个访问锁的线程
- 当一个线程访问一个同步代码块时,首先会尝试获取偏向锁。
- 如果该对象没有被其他线程锁定,并且没有发生过竞争,那么当前线程会将对象头中的标记设置为偏向锁,并将线程ID记录在对象头中。
- 当其他线程尝试获取该对象的锁时,会检查对象头中的标记,如果是偏向锁并且线程ID与当前线程ID相同,表示可以获取锁,无需进行同步操作。
- 如果其他线程尝试获取该对象的锁时,发现对象头中的标记不是偏向锁或者线程ID不匹配,表示发生了竞争,偏向锁会升级为轻量级锁。
偏向锁的优势在于减少了同步操作的开销,适用于大部分情况下只有一个线程访问同步代码块的场景。但是如果存在多个线程竞争同一个锁的情况,偏向锁会失效,需要升级为轻量级锁或重量级锁。
一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。
一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
轻量级锁
轻量级锁是在 JVM 环境上面
轻量级锁在以下情况下会升级为重量级锁:
- 当多个线程竞争同一个锁时,如果自旋等待超过了一定的次数,轻量级锁会升级为重量级锁。
- 当一个线程已经持有锁,另一个线程在自旋等待获取锁,而此时又有第三个线程试图访问该锁时,轻量级锁也会升级为重量级锁。
这种升级过程是为了处理更复杂的并发情况,确保线程安全,但也会带来一定的性能开销。因此,在设计并发系统时,需要仔细考虑锁的使用和升级策略,以平衡性能和线程安全的需求。
重量级锁
重量级锁是在操作系统环境上面。重量级锁会阻塞线程。
重量级锁是指当一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。 它是依赖于底层操作系统的Mutex实现,也就是互斥锁。重量级锁会让锁从用户态切换到内核态,将线程的调度交给操作系统,因此性能相对较低。
在并发编程中,当锁的状态为轻量级锁时,若有另一个线程尝试获取锁但自旋一定次数后仍未成功,那么该锁就会升级为重量级锁,此时其他申请锁的线程会进入阻塞状态,导致性能下降。
总的来说,重量级锁是为了确保线程安全而采取的一种机制,但使用时应注意其带来的性能开销,并尽量通过合理的锁策略来避免不必要的阻塞和性能损失。
为什么 synchronized 重量级锁,效率低下
Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,时间成本相对较高,这也是为什么早期的synchronized效率低的原因 Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁。
锁的优化
JIT编译器对锁做了两方面的优化:
适应性自旋(Adaptive Spinning)
从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
锁粗化(Lock Coarsening)
锁粗化的概念应该比较好理解,就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁。举个例子:
public void lockCoarsening() {
int i=0;
synchronized (this){
i=i+1;
}
synchronized (this){
i=i+2;
}
}
上面的两个同步代码块可以变成一个
public void lockCoarsening() {
int i=0;
synchronized (this){
i=i+1;
i=i+2;
}
}
锁消除(Lock Elimination)
锁消除即删除不必要的加锁操作的代码。比如下面的代码,下面的for循环完全可以移出来,这样可以减少加锁代码的执行过程
public void lockElimination() {
int i=0;
synchronized (this){
for(int c=0; c<1000; c++){
System.out.println(c);
}
i=i+1;
}
}
文章部分参考
锁升级过程(无锁、偏向锁、轻量级锁、重量级锁)