线程安全是---什么是AQS?

本文详细介绍了AQS(AbstractQueuedSynchronizer)在Java并发编程中的重要性,对比了它与Synchronized的区别,列举了ReentrantLock、Semaphore和CountDownLatch等常见AQS实现类,并通过实例演示了AQS的使用和工作机制。
摘要由CSDN通过智能技术生成

什么是AQS?

引言:
在Java并发编程中,锁和同步器是确保线程安全的关键工具。
其中,AQS(AbstractQueuedSynchronizer)是一个核心框架,为构建锁和其他同步组件提供了强	大的基础。
本文将深入探讨AQS的工作原理、与Synchronized的区别以及常见的实现类,并通过示例展示其用法。
  • 全称是 (A bstract Q ueued S ynchronizer),即抽象队列同步器。它是构建锁或者其他同步组件的基础框架。
  • 可以简单地将 AQS 理解为 Java 并发包( java.util.concurrent,简称JUC)提供的一种锁机制。
  • 更准确地说,AQS 是一个用于构建锁和同步器的框架。它本身并不是一个具体的锁,而是提供了一套机制,使得开发者能够更容易地创建出符合自己需求的同步工具。

一、AQS 和 Synchronized 的区别

SynchronizerAQS
关键字,c++语言实现java 语言实现
悲观锁,自动释放锁悲观锁,手动开启和关闭
锁竞争激烈都是重量级锁,性能差锁竞争激烈的情况下,提供了多种解决方案

二、AQS 常见的实现类

  • ReentrantLock 阻塞式锁,允许线程重复获取已经持有的锁,实现了可重入性。
  • Semaphore 信号量,用于控制同时访问共享资源的线程数。
  • CountDownLatch 倒计时锁,允许一个或多个线程等待其他线程完成操作。

三、AQS 的简单使用

AQS是一个抽象类,它本身并不能直接使用。我们通常通过继承AQS并实现必要的方法来创建自定义同步器。但是,为了简化说明,我们可以先看一下如何使用基于AQS实现的几个常见类,比如ReentrantLock。

3.1 ReentrantLock 示例
// 使用ReentrantLock 来确保 performTask 方法中的临界区代码在任何时候只能由一个线程访问。示例中启动了两个线程来执行任务,通过锁来保证线程安全。
public class ReentrantLockExample {  
    private final ReentrantLock lock = new ReentrantLock();  
  
    public void performTask() {  
        lock.lock(); // 请求锁  
        try {  
            // 执行临界区代码  
            System.out.println(" 任务执行 " + Thread.currentThread().getName());  
            Thread.sleep(1000); // 模拟耗时操作  
        } catch (InterruptedException e) {  
            Thread.currentThread().interrupt();  
        } finally {  
            lock.unlock(); // 释放锁  
        }  
    }  
  
    public static void main(String[] args) {  
        ReentrantLockExample example = new ReentrantLockExample();  
        Runnable task = () -> {  
            for (int i = 0; i < 5; i++) {  
                example.performTask();  
            }  
        };  
  
        // 启动两个线程执行任务  
        new Thread(task, "Thread-1").start();  
        new Thread(task, "Thread-2").start();  
    }  
}

在上诉例子中,使用了 ReentrantLock 来保证 performTask 方法中的临界区代码在任何时候只能由一个线程访问。当一个线程获得锁并执行临界区代码时,其他尝试获得锁的线程将被阻塞,直到当前线程释放锁。

3.2 自定义同步器示例

基于AQS的自定义同步器示例。我们将实现一个简单的互斥锁(Mutex)

// 通过继承AQS并实现必要的方法来创建一个简单的互斥锁。
// 示例中展示了如何控制锁的获取和释放,并通过isLocked方法检查锁是否被当前线程占用。 
 public class Mutex {  
    // 自定义同步器,继承 AQS  
    private static class Sync extends AbstractQueuedSynchronizer {  
        // 是否处于占用状态  
        @Override  
        protected boolean isHeldExclusively() {  
            return getState() == 1;  
        }  
  
        // 当状态为 0 的时候获取锁  
        @Override  
        public boolean tryAcquire(int acquires) {  
            assert acquires == 1; // 只能为 1  
            if (compareAndSetState(0, 1)) { // state为0才设置为1,不可重入
                setExclusiveOwnerThread(Thread.currentThread()); // 设置为当前线程独占资源  
                return true;  
            }  
            return false;  
        }  
  
        // 释放锁,将状态设置为0  
        @Override  
        protected boolean tryRelease(int releases) {  
            assert releases == 1; // 只能为 1  
            if (getState() == 0) throw new IllegalMonitorStateException();  
            setExclusiveOwnerThread(null);  
            setState(0); // state置0  
            return true;  
        }  
  
        // 提供一个方法,让我们的锁支持可重入  
        // 这里为了简单起见,我们不实现可重入功能,因此这个方法不会被使用到。  
        @Override  
        protected int tryAcquireShared(int acquires) {  
            throw new UnsupportedOperationException();  
        }  
  
        // 提供一个方法,让我们的锁支持可重入时的释放  
        // 同样地,为了简单起见,我们也不实现这个方法。  
        @Override  
        protected boolean tryReleaseShared(int releases) {  
            throw new UnsupportedOperationException();  
        }  
    }  
  
    // 同步器对象  
    private final Sync sync = new Sync();  
  
    // 获取锁  
    public void lock() {  
        sync.acquire(1);  
    }  
  
    // 尝试获取锁  
    public boolean tryLock() {  
        return sync.tryAcquire(1);  
    }  
  
    // 释放锁  
    public void unlock() {  
        sync.release(1);  
    }  
  
    // 锁是否被当前线程占用  
    public boolean isLocked() {  
        return sync.isHeldExclusively();  
    }  
}

在上诉例子中,我们创建了一个简单的互斥锁 Mutex ,它内部使用了一个自定义的同步器 Sync ,该同步器继承了AbstractQueuedSynchronizer。我们实现了tryAcquire和tryRelease方法来控制锁的获取和释放。
注意,为了简化示例,我们没有实现锁的可重入功能。在实际应用中,通常会更复杂一些,并需要考虑更多的边界条件和性能优化。


四、AQS-基本工作机制:

AQS内部维护了一个状态state和一个先进先出的双向队列。
state用于表示同步状态,队列中存储了等待获取锁的线程。
当线程尝试获取锁时,它会通过CAS操作尝试修改state。
如果修改成功,则当前线程获取了锁;否则,线程将进入队列等待。
当持有锁的线程释放锁时,它会唤醒队列中的头节点线程让其尝试获取锁。
这样就形成了一个简单的排队效果。
  1. 在 AQS 内部有个状态 statestate 是由 volatile 修饰的,来保证多线程的空间行,并且 state 有两个 int ( 1有锁 | 0无锁 ) 值。
    在这里插入图片描述
  2. 假如现在有 线程 0想要获得一个这个锁,它获取修改 state 默认值 0 修改为 1,这样这个 线程 0 就有锁了。
    在这里插入图片描述
  3. 当线程 线程 1来的时,state 已经是 1 了 , 线程 1修改失败,然后 线程 1 —> 到 FIFO 队列进行等待!
    在这里插入图片描述在这里插入图片描述
  4. 当线程 线程 2来的时,state 已经是 1 了 , 线程 2也修改失败,然后 线程 2 —> 到 FIFO 队列进行等待!
    在这里插入图片描述
    在这里插入图片描述
  5. FIFO 队列 是一个先进先出的双向队列,内部是一个双向链表,在FIFO还有两个属性分别为 headtail 在这里插入图片描述
  6. 上述图中,**线程 0 ** 执行完了会将 state值修改为 0,同时它会唤醒 FIFO 队列队列中的 head元素让它去持有锁。
  7. 这就是一个简单的排队效果(AQS 基本的工作机制)

4.1 AQS-多个线程共同去抢这个资源是如何保证原子性的呢

  1. 目前 state 为 0
    在这里插入图片描述
  2. 线程同同时来了 线程0和线程4,他们两个多要去修改 state,在这里面为了保证原子性用的是 cas 的设置
    在这里插入图片描述
  3. 比如**线程 0 ** 它抢到了锁,它就会把 state修改为 1,**线程 4 ** 就要到队列中进行等待
    在这里插入图片描述

五、总结:

  1. 什么是AQS?
    • 是多线程中的队列同步器。是一种锁机制,它是做为一个基础框架使用的像ReentrantLock、Semaphore都是基于AQS实现的
    • AQS内部维护了一个先进先出的双向队列,队列中存储的排队的线程
    • 在AQS 内部还有一个属性 state,这个 state 就相当于是一个资源,默认是0(无锁状态),如果队列中的有一个线程修改成功了state为1,则当前线程就相等于获取了资源
    • 在对state修改的时候使用的cas操作,保证多个线程修改的情况下原子性
  • 25
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值