【JUC进阶】07. 自旋锁

目录

1、前言

2、基本概述

2.1、什么是自旋锁

2.2、主要特点

2.3、自适应自旋

2.3.1、自适应自旋引入

2.4、传统锁的区别

3、JUC自旋锁的实现

3.1、AtomicReference 实现的自旋锁

3.2、ReentrantLock 的非公平自旋锁

3.3、并发集合中的自旋锁

4、小结


1、前言

从JDK6版本开始,HotSpot虚拟机开发团队就花费了大量的资源来实现各种的锁优化技术,前面介绍到的轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)就是其中的两种方式,而今天要讲的自旋锁(Adaptive Spinning)也是为了在线程之间更高效地共享数据及解决竞争问题,从而提高程序的执行效率。

2、基本概述

并发编程中,我们经常讨论的互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转人内核态中完成,这些操作给 Java 虚拟机的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。

2.1、什么是自旋锁

现在绝大多数的个人电脑和服务器都是多路(核)处理器系统,如果物理机器有一个以上的处理器或者处理器核心,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只须让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。

因此,自旋锁是一种基于忙等待的锁机制,在并发编程中用于保护临界区资源的访问。它的特点是当线程请求获取锁时,如果发现该锁已经被其他线程占用,它并不会阻塞等待,而是通过不断循环检查锁的状态,直到获取到锁为止。

2.2、主要特点

  1. 忙等待:自旋锁使用忙等待的方式,即不断循环检查锁的状态,而不是阻塞线程等待锁的释放。这样可以避免线程切换和上下文切换的开销,提高并发性能。
  2. 自旋时间:自旋锁的自旋时间是有限的,超过一定的自旋次数后,如果还没有获取到锁,线程会被挂起,转而进入阻塞状态,以避免空转消耗CPU资源。
  3. 适用场景:自旋锁适用于对临界区的访问时间较短、线程竞争激烈的情况。在这种情况下,使用自旋锁可以减少线程切换和上下文切换的开销,提高并发性能。

2.3、自适应自旋

自旋锁在JDK 1.4.2 中就已经引人,只不过默认是关闭的,可以使用

-XX:+UseSpinning

参数来开启,在JDK 6中就已经改为默认开启了。

自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,所以如果锁被占用的时间很短,自旋等待的效果就会非常好,反之如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有价值的工作,这就会带来性能的浪费。

因此自旋等待的时间必须有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程。自旋次数的默认值是十次,用户也可以使用参数

-XX:PreBlockSpin

来自行更改。

2.3.1、自适应自旋引入

无论是默认值还是用户指定的自旋次数,对整个 Java 虚拟机中所有的锁来说都是相同的。

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

2.4、传统锁的区别

与传统锁相比,自旋锁具有以下区别和优势:

  1. 策略不同:传统锁采用阻塞等待的策略,即当线程无法获取到锁时,会进入阻塞状态,等待其他线程释放锁。而自旋锁采用忙等待的策略,线程不会主动阻塞,而是循环检查锁的状态。
  2. 开销不同:传统锁涉及线程切换和上下文切换的开销,当线程阻塞时会被操作系统从运行状态切换到阻塞状态。而自旋锁避免了线程切换和上下文切换的开销,仅在自旋时间内消耗CPU资源。
  3. 使用场景不同:传统锁适用于对临界区访问时间较长的情况,因为在长时间的自旋过程中,会消耗大量的CPU资源。而自旋锁适用于对临界区访问时间较短的情况,通过快速自旋等待锁的释放,可以提高并发性能。

3、JUC自旋锁的实现

了解了自旋锁的一些基本知识后,我们来看下JUC中对于自旋锁的实现。JUC中提供了多种实现自旋锁的类,包括AtomicReference、ReentrantLock等。

3.1、AtomicReference 实现的自旋锁

AtomicReference 是 JUC 中的原子类,它提供了对引用类型的原子操作。通过 AtomicReference 可以实现基于 CAS(Compare and Swap)的自旋锁。

import java.util.concurrent.atomic.AtomicReference;

public class SpinLock {
    private AtomicReference<Thread> owner = new AtomicReference<>();

    /**
     * 通过 CAS 操作尝试获取锁,如果成功获取到锁,则退出自旋等待,否则继续自旋等待。
     */ 
    public void lock() {
        Thread currentThread = Thread.currentThread();
        while (!owner.compareAndSet(null, currentThread)) {
            // 自旋等待锁的释放
        }
    }

    /**
     * 通过 CAS 将锁的持有者设置为 null 来释放锁。
     */
    public void unlock() {
        Thread currentThread = Thread.currentThread();
        owner.compareAndSet(currentThread, null);
    }
}

3.2、ReentrantLock 的非公平自旋锁

ReentrantLock 是 JUC 中的可重入锁,它提供了公平锁和非公平锁的实现。在非公平模式下,ReentrantLock 使用自旋来获取锁。

import java.util.concurrent.locks.ReentrantLock;

public class SpinLockExample {
    // false 非公平锁
    private static ReentrantLock lock = new ReentrantLock(false);

    public static void main(String[] args) {
        // 在使用 lock() 方法获取锁时,如果锁已经被其他线程占用,则当前线程会进入自旋等待状态,直到获取到锁为止。
        lock.lock();
        try {
            // 执行临界区操作
        } finally {
            lock.unlock();
        }
    }
}

3.3、并发集合中的自旋锁

除此之外,并发集合类(如 ConcurrentHashMap、ConcurrentLinkedQueue 等)中也使用了自旋锁来保证线程安全性。这些集合类通过使用 CAS 操作来实现对内部数据结构的修改和访问的原子性。

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
    private static ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();

    public static void main(String[] args) {
        map.put("key", "value");

        String value = map.get("key");
        System.out.println(value);
    }
}

ConcurrentHashMap 中使用自旋锁的实现主要体现在两个方面:分段锁和 CAS(Compare and Swap)操作。

  • 分段锁: ConcurrentHashMap 内部使用了一组分段锁,也称为段(Segment)。每个段相当于一个小的哈希表,它独立地控制一部分数据,不同的线程可以同时访问不同的段,从而提高并发性。每个段都有自己的锁,线程在访问某个段时,只需要获取该段的锁,而不需要对整个哈希表进行加锁。

分段锁的优势在于,不同的线程可以同时操作不同的段,不会因为操作同一个段而产生互斥,从而提高了并发性能。只有当多个线程同时访问同一个段时,才会发生竞争,此时才需要通过自旋锁来进行同步。

  • CAS 操作: 在 ConcurrentHashMap 中,当多个线程同时访问同一个段时,会使用 CAS 操作来保证对段的更新的原子性。CAS 是一种乐观锁机制,通过比较内存中的值和期望值,如果相等则更新值,否则重新尝试。

在 ConcurrentHashMap 中,使用 CAS 操作来实现对段内部的数据结构的修改。例如,在插入元素时,会先获取段的锁,然后使用 CAS 操作更新该段中的数据结构,如果 CAS 操作失败,则重新尝试直到成功。

4、小结

最近几篇文章主要讲述的是偏向锁,轻量级锁,以及自旋锁等相关知识。主要是为了后面讲述锁膨胀,升级过程做铺垫,对这几个锁有了基本印象和理解之后,对于后续的升级过程学习起来就会更加轻松。今天就到这,一起加油吧~

猜你喜欢

转载自blog.csdn.net/p793049488/article/details/131448676
今日推荐