Unsafe的CAS操作及线程park与unpark

文章描述了一个使用CAS操作实现的自定义锁,通过AQS模型处理多线程并发,加锁失败时采用阻塞和唤醒机制,确保线程安全。
摘要由CSDN通过智能技术生成

如下是一个参照AQS进行的一个加锁及解锁的简单实现:

  1. 多线程并发进行同步业务操作;
  2. 加锁:尝试进行cas 0->1操作;
  3. 如果加锁成功则进行业务处理,然后进行锁释放 1->0,然后将列头的线程进行唤醒;
  4. 如果加锁失败则先将线程加入到队列中,然后再进行一次CAS加锁尝试及判断;
  5. 在确认此刻锁是被占用状态(cas仍然失败)后方可将当前线程进行park操作;
  6. 线程被唤醒,重新进行cas加锁操作;

需要区别于【无限CAS】判断操作,如果线程过多或大量的耗费cpu资源。park于unpark的运用可以起到阻塞线程跟唤醒线程的功效,可以不会造成cpu资源的大量浪费。


public class Lock {

    private volatile int status;

    private static final Unsafe unsafe;

    // 对于 Java AbstractQueuedSynchronizer (AQS) 中的 stateOffset 字段,
    // 通常会使用 static final 修饰是因为这个偏移量值在整个类中都是不变的,且所有实例都需要使用相同的偏移量来进行 CAS 操作。
    // 即:不管有多少个 Lock 对象,对于Bean对象中的status操作的偏移量值是固定的,可以使用 static final
    // statusOffset 只是一个相对的偏移量,跟status具体的值无关,所以可以多实例共享statusOffset
    private static final long statusOffset;

    // 模拟一个先进先出的队列
    private static final Queue<Thread> threadQueue = new ConcurrentLinkedQueue<>();

    static {
        try {
            // sun.misc 中的class无法像AQS中那样通过 private static final Unsafe unsafe = Unsafe.getUnsafe(); 直接调用
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
            statusOffset = unsafe.objectFieldOffset(Lock.class.getDeclaredField("status"));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private void lock() {
        int expect = 0;
        int update = 1;
        while (true) {
            // 进行cas尝试
            if (unsafe.compareAndSwapInt(this, statusOffset, expect, update)) {
                System.out.println(Thread.currentThread().getName() + ":加锁成功");
                break;
            } else {
                // CAS失败,阻塞线程并放入队列
                threadQueue.offer(Thread.currentThread());
                // 队头元素
                Thread peek = threadQueue.peek();
                // 在进行线程park前,这里一定需要再进行一次cas尝试,否则可能会出现并发情况下一些线程无法被唤醒的可能
                if(peek == Thread.currentThread()
                        // 如果这里cas失败,那么说明锁肯定还被某个线程占用着没有释放,而当前线程已经确认加入到队列中,
                        // 那么就能确保,解锁逻辑中poll出来的线程肯定有一个是当前线程的,就不会存在线程不会被唤醒的情况了,
                        // 所以else逻辑中当前线程可以尽情park,肯定会有人叫醒它
                        && unsafe.compareAndSwapInt(this, statusOffset, expect, update)) {
                    // 如果队头元素就是刚刚放进去的元素,并且cas成功了,那么也代表获取锁成功
                    System.out.println(Thread.currentThread().getName() + ":加锁成功");
                    break;
                } else {
                    try {
                        // 放大被park的延迟,以更好的观测极端情况
                        TimeUnit.MICROSECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    unsafe.park(false, 0);
                }
            }
        }
    }

    protected void unlock() {
        int expect = 1;
        int update = 0;
        unsafe.compareAndSwapInt(this, statusOffset, expect, update);
        System.out.println(Thread.currentThread().getName() + ":释放锁成功");
        // 获取队列头并唤醒线程
        Thread thread = threadQueue.poll();
        // 这里可能没有拿到,但是 下一刻 队列中就加入了一个新的线程
        // 需要考虑并发情况下被遗漏的线程,被遗漏的线程会一直得不到唤醒:见lock逻辑中的再次cas操作判断
        if (thread != null) {
            unsafe.unpark(thread);
        }
    }

    // 非线程安全的list 被用来作加锁线程安全测试
    static List<Integer> businessData = new ArrayList<>();

    public static void business(Integer i) {
        // CAS成功,执行业务逻辑
        businessData.add(i);
        System.out.println(Thread.currentThread().getName() + ":Business logic executed");
    }

    public static void main(String[] args) throws InterruptedException {
        Lock lock = new Lock();
        // 模拟多个线程并发进行CAS操作
        for (int i = 0; i < 100; i++) {
            // 进行线程并发,模拟进行加锁及业务处理
            int finalI = i;
            Thread thread = new Thread(() -> {
                lock.lock();
                try {
                    business(finalI);
                } finally {
                    lock.unlock();
                }
            });
            thread.setName("线程:" + (i + 1));
            thread.start();
        }
        // 这里简单进行睡眠,确保线程都已经执行完
        TimeUnit.SECONDS.sleep(5);
        System.out.println(businessData.size());
        System.out.println(businessData);

        // 逻辑验证一下结果
        HashSet<Integer> set = new HashSet<>(businessData);
        for (int i = 0; i < businessData.size(); i++) {
            if(!set.contains(i)) {
                throw new RuntimeException("自建锁线程安全测试失败");
            }
        }}
}

运行结果如图:

这段代码实现了一个简单的自定义锁 Lock,通过 CAS 操作来实现加锁和解锁的功能,并在加锁失败时将线程放入队列进行阻塞等待。下面是对代码的简要分析:

  1. 在静态代码块中,使用 Unsafe 类获取对 status 字段的偏移量 statusOffset,并初始化了一个空的线程队列 threadQueue

  2. lock() 方法实现了加锁的逻辑:

    • 首先尝试通过 CAS 操作将 status 从 0 改为 1,如果成功表示加锁成功。
    • 如果 CAS 失败,则将当前线程加入队列,并尝试再次通过 CAS 将 status 改为 1,如果成功则表示当前线程获取到了锁。
    • 如果 CAS 失败,说明锁还被其他线程占用,当前线程会被阻塞并进入休眠状态,直到被唤醒后再次尝试获取锁。
  3. unlock() 方法实现了解锁的逻辑:

    • 通过 CAS 操作将 status 从 1 改为 0,表示释放锁成功。
    • 从队列中取出一个线程,并唤醒该线程。
  4. business(Integer i) 方法模拟了业务逻辑,向一个非线程安全的列表中添加元素。

  5. main() 方法模拟了多个线程并发进行 CAS 操作,加锁后执行业务逻辑,最后输出业务数据列表的大小和内容,并验证数据的正确性。

总体来说,这段代码实现了简单的自定义锁,并在多线程环境下进行了加锁和解锁操作,可以保证业务逻辑的线程安全性。但需要注意,这里只是一个简单的示例,实际场景中若涉及更复杂的业务逻辑和线程交互,可能需要更完善的设计和测试。

  • 18
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: LockSupport是Java并发包中的一个核心基础类,它提供了线程阻塞和唤醒的功能。在AQS框架中,它主要被用来实现线程的阻塞和唤醒,而Unsafe则提供了CAS操作,用于实现AQS的同步机制。因此,LockSupport和Unsafe这两个类都是AQS框架不可或缺的组成部分,两者配合使用,使得AQS能够高效地实现线程同步。 ### 回答2: LockSupport是Java并发编程中的一个核心基础类,主要提供了线程阻塞和唤醒的功能。 在AQS框架中,LockSupport被广泛应用于线程的阻塞与唤醒操作,是实现锁和同步器的重要组成部分。 首先,LockSupport提供了park()和unpark()两个方法,这两个方法可以实现线程的阻塞和唤醒操作线程调用park()方法后会进入阻塞状态,直到其他线程调用unpark()方法唤醒该线程;而线程调用unpark()方法后,如果该线程已经被阻塞,会立即解除阻塞状态。通过这两个方法的灵活组合,能够很方便地实现线程的阻塞与唤醒。 其次,LockSupport内部使用了类似于信号量的机制,每个线程都会关联一个许可证。调用一次park()方法会消耗一个许可证,而调用一次unpark()方法会增加一个许可证。如果线程在调用park()方法前已经有了许可证,那么调用park()方法后不会阻塞,而是直接消耗掉这个许可证。这种在许可证上的管理,使得LockSupport能够实现更加灵活的线程阻塞和唤醒。 最后,LockSupport使用了底层的操作系统机制来实现线程的阻塞和唤醒,效率较高。相比于传统的等待-通知机制(wait-notify),LockSupport的实现更加简洁高效,并且不需要先获得对象的监视器,而是通过许可证的方式进行线程的阻塞和唤醒。 综上所述,LockSupport作为AQS框架的重要组成部分,提供了灵活、高效的线程阻塞和唤醒机制,是Java并发编程中不可或缺的核心基础类之一。 ### 回答3: LockSupport是Java并发编程中的核心基础类之一,它提供了线程阻塞和唤醒的功能,可以在任意线程中进行阻塞和唤醒操作。 首先,LockSupport类提供的park/unpark操作相比其他线程同步器,如synchronized关键字或者wait/notify机制,具有更高的灵活性和性能。它不依赖于任何特定的对象,而是直接与线程进行交互。这使得LockSupport可以用于更广泛的场景,例如线程间的通信、同步控制和中断处理等。 其次,LockSupport是AQS(AbstractQueuedSynchronizer)框架的关键组件之一。AQS框架提供了一种可重入的、高效的同步器实现方式,被广泛应用于Java并发编程中。而LockSupport作为AQS框架的辅助类,为其提供了底层的线程阻塞和唤醒的能力。 在AQS框架中,LockSupport的park/unpark方法被用于实现线程的阻塞和唤醒操作,实现了线程间的同步与互斥。它通过使用不可重入的同步状态来管理线程的阻塞和唤醒,避免了传统同步机制中存在的死锁和饥饿问题。 此外,LockSupport的park/unpark方法还可以与其他并发类库结合使用,如Future、CountDownLatch和Semaphore等。通过LockSupport类,我们可以实现更加灵活和高效的并发编程,提高系统的吞吐量和性能。 综上所述,LockSupport作为AQS框架的关键组件之一,不仅提供了线程阻塞和唤醒的功能,还具有较高的灵活性和性能。因此,LockSupport也被认为是Java并发编程中的核心基础类。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

进窄门见微光行远路

如果对你有比较大的帮助

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值