Java中的锁与锁优化技术

自旋锁与自适应自旋

自旋锁是一种锁的实现机制,其核心思想是当一个线程尝试获取锁时,如果锁已经被其他线程持有,那么这个线程会在一个循环中不断地检查锁是否被释放,而不是进入睡眠状态。

自旋锁在JDK 1.4.2中就已经引入,只不过默认是关闭的,在JDK 6中就已经改为默认开启了。自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,所以如果锁被占用的时间很短,自旋等待的效果就会非常好,反之如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有价值的工作,这就会带来性能的浪费。因此自旋等待的时间必须有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程。自旋次数的默认值是十次。

在 JDK 6中对自旋锁的优化,引入了自适应的自旋。自适应意味着自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而允许自旋等待持续相对更长的时间,比如持续100次忙循环。另一方面,如果对于某个锁,自旋很少成功获得过锁,那在以后要获取这个锁时将有可能直接省略掉自旋过程,以避免浪费处理器资源。

锁消除

锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。

锁消除通常基于逃逸分析(Escape Analysis)。逃逸分析是一种确定对象的作用域和访问范围的技术。如果确定某个对象只能在一个线程内部访问,并且不会“逃逸”到其他线程,那么该对象上的同步操作是不必要的。

public String concatenate(String str1, String str2) {
    
    
    StringBuffer sb = new StringBuffer();
    sb.append(str1);
    sb.append(str2);
    return sb.toString();
}
   /* StringBuffer的append方法上面有synchronized,说明是同步代码块 */
	@Override
    public synchronized StringBuffer append(String str) {
    
    
        toStringCache = null;
        super.append(str);
        return this;
    }

在上面示例中,StringBuffer是线程安全的,其方法是同步的。但在concatenate方法中,sb对象只会被一个线程访问,不会逃逸到其他线程。通过逃逸分析和锁消除,JVM可以确定在这种情况下,sb上的同步操作是不必要的,并且可以安全地消除它们。

锁粗化

锁粗化是Java虚拟机为了优化锁操作而采取的一种技术。基本思想是将多个连续的加锁、解锁操作合并为一个大的锁块,以减少锁操作的开销。

synchronized (lock) {
    
    
    // code block 1
}
// other operations
synchronized (lock) {
    
    
    // code block 2
}

在上面代码中,两个连续的synchronized块可以被合并为如下一个大的synchronized块,从而减少锁的开销:

synchronized (lock) {
    
    
    // code block 1
    // other operations
    // code block 2
}

锁粗化可以减少锁操作的数量,降低锁的开销,从而提高程序的执行效率。同时,它还可以减少线程上下文切换和系统调用的次数,进一步提高系统的性能。

扫描二维码关注公众号,回复: 17146713 查看本文章

轻量级锁

轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”这一经验法则。轻量锁使用操作系统互斥量来实现,轻量级锁并不是用来代替重量级锁的,它设计的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。 使用到了Mark Word

HotSpot虚拟机对象头Mark Word如下

image-20231006011641689

轻量级锁的工作过程如下:

  1. 在代码即将进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈 帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝image-20231006012003324
  2. 虚拟机将使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,即代表该线程拥有了这个对象的锁,并且对象Mark Word的锁标志位(Mark Word的 最后两个比特)将转变为“00”,表示此对象处于轻量级锁定状态。如果这个更新操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁,需要膨胀为重量级锁image-20231006012429417
  3. 解锁过程也同样是通过CAS操作来进行的,如果对象的 Mark Word仍然指向线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来。假如能够成功替换,那整个同步过程就顺利完成了;如果替换失败,则说明有 其他线程尝试过获取该锁,就要在释放锁的同时,唤醒被挂起的线程。

偏向锁

偏向锁是Java为了进一步优化锁的性能而引入的一种锁机制。它是基于一个事实:在大多数情况下,锁不仅不会涉及多线程竞争,而且总是由同一线程多次获得。

假设当前虚拟机启用了偏向锁,那么当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设置为“01”、把偏向模式设置为“1”,表示进入偏向模式。同时使用CAS操作把获取到这个锁的线程 的ID记录在对象的Mark Word之中。如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如加锁、解锁及对Mark Word的更新操作 等)。

一旦出现另外一个线程去尝试获取这个锁的情况,偏向模式就马上宣告结束。根据锁对象目前是否处于被锁定的状态决定是否撤销偏向(偏向模式设置为“0”),撤销后标志位恢复到未锁定(标志位 为“01”)或轻量级锁定(标志位为“00”)的状态,后续的同步操作就按照上面介绍的轻量级锁那样去执行。

image-20231006022415854

需要注意的是,由于偏向锁不存储hash码,所以当对象的hash码被计算之后就无法进入偏向锁了

重量级锁

在Java的重量级锁机制中,对象头的Mark Word存储一个指向监视器(Monitor)的指针。监视器是一个重要的结构,它包括以下几个部分:

  1. 锁信息:包括锁的状态、拥有者和重入次数等。锁的状态表示锁是否被某个线程持有,锁的拥有者指出哪个线程当前持有锁,而重入次数表示锁被重入了多少次。

  2. 等待集:这是一个包含了所有正在等待某个条件成立的线程的集合。线程可以通过调用Object.wait()方法进入等待集,并通过Object.notify()Object.notifyAll()方法被唤醒。

  3. 入口集:这是一个包含了所有正在等待获取锁的线程的集合。当锁被释放时,这些线程会被唤醒,并尝试重新获取锁。

以下是重量级锁的操作过程以及Mark Word的变化:

  1. 锁的获取:当一个线程尝试获取重量级锁时,它会检查Mark Word中的指针以确定监视器的位置,并检查锁的状态。如果锁已经被其他线程持有,它会被放入监视器的入口集,并进入阻塞状态。

  2. 锁的释放:当锁的持有者线程执行完同步代码块并释放锁时,Mark Word中的锁信息会被更新,通常是重入次数的减少或锁状态的变更。同时,监视器的入口集中的线程会被检查,如果有线程在等待,它们会被唤醒并尝试重新获取锁。

  3. 锁的重入:如果一个线程已经持有锁,它可以重入同步代码块而无需重新获取锁。在这种情况下,监视器中的重入次数会增加。

  4. 等待和通知:线程可以通过调用Object.wait()进入监视器的等待集,并通过Object.notify()Object.notifyAll()被唤醒。在这种情况下,等待集和入口集中的线程状态会随着条件的变化而变化。

通过Mark Word中的监视器指针和监视器中的锁信息、等待集和入口集,重量级锁能够实现线程间的同步和通信,保证对共享资源的安全访问。

猜你喜欢

转载自blog.csdn.net/m0_51545690/article/details/133596297
今日推荐