2024-03-23  阅读(103)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/mianshi/baodian/detail/2825178454

在 JDK 1.6之前,synchronized 是一个重量级、效率比较低下的锁,但是在JDK 1.6后,JVM 为了提高 synchronized 的性能,HotSpot 虚拟机开发团队做了大量的优化工作,如自旋锁、自适应性自旋、锁消除、锁粗化、偏向锁、轻量级锁。其中偏向锁、轻量级锁已经在文章synchronized的锁升级过程是怎样的?讲解过,这里就不做说明了。

自旋锁

线程的阻塞和唤醒都需要依赖底层操作系统,会涉及到用户态、内核态的切换,这种操作是非常消耗资源的。

如果一个同步代码块执行的时间非常短,为了这一段很短的时间去频繁阻塞和唤醒线程其实时非常不值得的。为了解决这种很短时间的任务,Java 引入自旋锁。

何谓自旋锁?就是当一个线程尝试去获取某个锁对象时,如果该锁对象被其他线程持有,那么该线程不会被挂起,而是一直循环检测锁是否已被释放,通过自旋而不是挂起线程,可以减少线程上下文切换的开销。。

需要注意的是,自旋锁基于的条件是:任务执行时间很短,那么自旋等待的效果就会很好,反之,如果任务执行的时间比较长,那么自旋的线程就会白白浪费资源,会带来更多的性能消耗。

所以,自旋等待的时间我们需要控制下,不能长时间的自旋,如果自旋的次数超过某个阈值后还没有获取到锁,就应该使用传统的方式去挂起线程。默认自旋次数为十次,我们也可以通过 -XX:PreBlockSpin来自行更改。

自适应性自旋

自适应性是对自旋锁的一种优化,它的次数不再是固定的,而是根据前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:

  • 动态调整自旋次数:根据之前的锁竞争情况,动态调整自旋的次数。如果之前的自旋锁获取经常成功,则增加自旋次数;如果很少成功,则减少自旋次数,甚至可以不自旋。
  • 考虑锁的拥有者状态:如果锁的持有者正在运行,则自旋的机会就会增加,因为锁可能很快就会被释放。相反,如果锁的拥有者不在运行状态,自旋的次数可能就会减少。

锁消除

为了保证多线程环境下数据的安全性,我们在编写代码时可能会进行一些同步操作或者使用一些具有同步功能的 API(例如 StringBuffer、Vector、HashTable等)。但是在有些情况下,JVM 检测到某个锁对象的锁定状态是不会逃逸到方法或者线程的外部,那么这个锁就可以被认为是不必要的,可以被安全地去除。通过这种方式消除没有必要的锁,可以节省毫无意义的锁获取时间。例如如下代码:

public String concatStrings(List<String> strings) {
    StringBuffer sb = new StringBuffer();
    for(String s : strings) {
        sb.append(s);
    }
    return sb.toString();
}

我们知道 StringBuffer 是一个线程安全的类,它内部使用 synchronized 来保证线程安全,如 append()

    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

synchronized 作用在方法上,监视器锁为实例对象,锁定的是调用该方法的对象实例,上面例子中,锁定的是 sb 对象,但是 sb 对象是局部变量,它的引用不会逃逸出这个方法的。也就是说,sb 是不会被多个线程共享。因此,JVM 可以安全地消除掉 sb.append(s) 操作中的同步锁。

锁粗化

原则上,我们在编写代码的时候需要尽可能地控制锁的粒度,将锁的范围控制得尽可能小。在大多数情况下,这个是没问题的。但是如果我们一个操作频繁地获取、释放同一个锁对象,那么即使是没有锁竞争,也会因为频繁的锁操作而导致性能损耗。

所以,当 JVM 检测到一系列的连续锁操作实际上是对同一个对象的操作时,JVM 会尝试将这些锁操作合并为一个更大范围的锁操作,从而减少锁的获取和释放的次数。

例如(一般没人会写这种代码,仅供参考):

public void appendString(List<String> strings) {
    for (String s : strings) {
        synchronized (this) {
            // 进行一些操作
        }
    }
}

在这个例子中,每次循环时都会获取并释放对 this 对象的锁。JVM 会检测到这种请,它可能会做如下优化:

public void appendString(List<String> strings,) {
    synchronized (this) {
        for (String s : strings) {
            // 进行一些操作
        }
    }
}

synchronized (this) 移动到 for 循环外面去,这样就可以在整个循环过程中只锁定一次,而不是在每次迭代时都进行锁定和解锁。

阅读全文
  • 点赞