java并发之 AQS相关同步组件理解及源码分析

写在前面

之前有写过AQS的相关博文,那么本文就来写写使用AQS相关的同步组件。
AQS是java中提供用来构建同步组件的基础框架,它提供了基本的同步状态管理,线程阻塞、排队、唤醒机制。从而方便用户来自定义同步组件。
在java并发包中,也有不少组件使用了AQS。

下面来简单的介绍一下使用到AQS的常用的同步组件。
这里我们按照AQS的资源共享方式来分类:

  • 独占式:ReentrantLock
  • 共享式:Semaphore、CountDownLatch、CycliBarrier
  • 独占+共享式:读写锁

ReentrantLock

重入锁
见名知意,该组件是一个锁(它实现了Lock接口),它支持在已经获得锁对象的情况下,继续获取锁。
(Synchronized是可以隐式的重入锁的)
所以ReentrantLock在持有锁对象时,再次调用lock()方法获取锁而不会被阻塞。

至于如何实现,我们来看源码,这里我们先看非公平锁的实现,至于公平和非公平的区别,我们后面再看。
下面是ReentrantLock源码中集成的AQS非公平锁的获取:

	final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {//如果状态为0,则意为无锁状态,可以获取
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {//如果有锁,判断持锁线程是不是自己
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

这里是ReentrantLock重写了AQS的tryAcquire方法,在其中默认调用了nonFairlyAcquire方法。

  • 如果同步状态为0,则意为无锁状态,可以获取
  • 如果有锁,判断持锁线程是不是自己。如果是自己,则继续增加同步状态变量。

这样一看,逻辑就很清晰了。
再来看看tryRelease方法:

	protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

emmmm,和获取是对应的,要判断持有锁的是不是自己,然后减小同步状态变量。

总结一下:

  • 对于重入锁,它的同步状态变量取值是0-正无穷,其中0为无锁,非0为有多少把锁。
  • 加锁多少次,就得释放锁多少次才能完全成功释放锁。
  • 只有当前持有锁的线程才能再次获取锁。

下面我们再来看看公平锁与非公平锁的概念以及在ReentrantLock中它是如何体现的。
在这里插入图片描述
(图片来源:https://www.cnblogs.com/a1439775520/p/12947010.html)
上面是ReentrantLock的类结构图。
我们只需要看看NonFairSync和FairSync的tryAcquire方法,就能够明白公平/非公平的区别。

下面是公平获取的源码:

	protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&//**看这里**
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

和上面的nonfairTryAcquire方法对比,我们发现只有一个地方不一样,就是在c==0时,多判断了一个hasQueuePredecessors方法。

	public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

看最后一行return语句:

  • h!=t,如果相等,则意味队列为空,如果不相等,则意味至少有两个不同的节点
  • h.next==null如果不为空,还有后继节点
  • s.thread != Thread.currentThread(),如果相等,则意味着,自己的前驱节点就是头结点,已经轮到自己来竞争了,则当然可以竞争

总的来说,该方法就是判断,队列是否为空,或者队列不为空时,自己的前驱是否是头节点,从而边可以进行竞争。除此之外的情况,都返回true,不能竞争。
(ps:这里只是介绍了判断是否有队列然后决定公平与否竞争的问题,因为知识tryAcquire方法,所以不涉及加入队列,实际上Sync的模板方法中,还是要加入同步队列进行排队的,如果失败的话)

总结一下,判断队列中是否有其他线程等待,如果等待,乖乖排队,否则直接竞争。
我举这样一个例子来说明公平锁与非公平锁。

  • 公平锁:食堂打饭,一开始每人,来打饭的人都直接去打饭。当人多了开始排队。新来的人发现排队了,老实的直接去乖乖排队。
  • 非公平锁:食堂打饭,一开始没人,来打饭的人都直接去打饭。当人多了开始排队。此时新来的人发现排队了,它不乖,插队到窗口位置抢饭。如果抢成功了,就吃上饭了,如果失败了,就被别人指责(虚构),乖乖到队尾排队。

当然,二者各有优劣。公平锁能够保证公平,保证FIFO。
非公平锁的吞吐量大,因为不用唤醒后继节点,节省了不少的开销。
但是非公平锁可能会导致饥饿,当你是普通用户去银行办理业务,排着号,然后前面老是有vip插队,你说你饥饿吗?我就遇到过,真是tmd烦死了。

Semaphore

一般称之为信号量,它主要是实现,允许对一个资源,允许同时N个线程访问。

直接看源码吧,Semaphore的源码还是比较少的。

在Semaphore中也是有公平和非公平概念的,这里似乎就不太好说锁这个东西了。
来看其公平的获取:

	protected int tryAcquireShared(int acquires) {
            for (;;) {
                if (hasQueuedPredecessors())
                    return -1;
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

同ReentrantLock,公平与非公平差异不大。
这里的逻辑也是很简单,如果hasQueuedPrecessors,就失败。
如果当前同步状态减参数(一般是1)小于0,就代表获取失败,放弃获取,直接返回remaining(负数)。(同步状态变量大于0则代表还允许线程获取资源)

Semaphore调用的是AQS中的模板方法,当获取失败时,就会加入到同步队列中,然后被阻塞。

然后关于释放,这里的源码为:

	public void release(int permits) {
        if (permits < 0) throw new IllegalArgumentException();
        sync.releaseShared(permits);
    }

当同步状态变量>=0时,才会调用AQS的releaseShared模板方法。

总的来说,Semaphore限制了访问同步资源的线程个数。

CountDownLatch

它的作用是,让若干个线程 等待 若干个线程。

一般我们要实现一个线程等待另外一个线程,我们经常使用锁变量的wait()和notify()方法,
但那只能实现,当我被等待的线程notify()了,就马上有一个wait()的线程被唤醒。同时还伴随着锁的释放和获取,这就达不到我们的效果了。

我们想要的是,一群线程 等待 整整一群线程做完,才继续进行。
于是,CountDownLatch帮我们实现了这个想法。

在这里插入图片描述CountDownLatch countDownLatch = new CountDownLatch(3);
我们在主线程中设置CountDownLatch的计数器为3,
调用其他线程,之后await(),进入阻塞,此时当其他线程执行countDown方法时,就会将CountDownLatch的计数器减1,当减到0时,则主线程会被唤醒。

这里用这样一个例子来演示:

public class CountdownLatchExample {

    public static void main(String[] args) throws InterruptedException {
        final int totalThread = 10;
        CountDownLatch countDownLatch = new CountDownLatch(totalThread);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalThread; i++) {
            executorService.execute(() -> {
                System.out.print("run..");
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        System.out.println("end");
        executorService.shutdown();
    }
}
run..run..run..run..run..run..run..run..run..run..end

这里说明一下,countdownLatch是一次性的,计数器为0了就是为0了,不能重置。
其次,等待的若干线程满足条件之后继续执行,作为条件的被等待的线程,不管,可以继续执行或者等待,无所谓。

CountDownLatch的两个经典用法:

  • 某一线程在开始运行前等待 n 个线程执行完毕。将 CountDownLatch 的计数器初始化为 n :new CountDownLatch(n),每当一个任务线程执行完毕,就将计数器减 1 countdownlatch.countDown(),当计数器的值变为 0 时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
  • 实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 :new CountDownLatch(1),多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。(同时起跑

(关于CountDownLatch的源码部分,之后如果有需要的话再进行补充)

CycliBarrier

循环栅栏

CountDownLatch 的实现是基于 AQS 的,而 CycliBarrier 是基于 ReentrantLock(ReentrantLock 也属于 AQS 同步器)和 Condition 的。

/** The lock for guarding barrier entry */
    private final ReentrantLock lock = new ReentrantLock();
    /** Condition to wait on until tripped */
    private final Condition trip = lock.newCondition();

它是一个屏障,它的功能是,保证在一个位置,足够数量的线程到达这个位置之后,大家才能一起出发(类似于上面的同时起跑)。早到的等待,晚到的不急。

它需要使用默认的构造方法指定屏障拦截的线程数量

public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

这里的parties指的是,当拦截的线程数量达到这个值的时候就打开栅栏,让所有线程通过。
而Runnable指的是,达到值时,先自动执行这个方法。

应用场景:
CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个 Excel 保存了用户所有银行流水,每个 Sheet 保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个 sheet 里的银行流水,都执行完之后,得到每个 sheet 的日均银行流水,最后,再用 barrierAction 用这些线程的计算结果,计算出整个 Excel 的日均银行流水。

当线程到达屏障时,执行await()方法,进入休眠,等待其他线程。
await方法中调用了dowait方法,源码如下:

	private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            final Generation g = generation;

            if (g.broken)
                throw new BrokenBarrierException();

            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }

            int index = --count;//将计数器值减1 count是CycliBarrier的一个变量,就是计数器
            //当计数器值为0时,则可以放行
            if (index == 0) {  // tripped
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    //这里是关键
                    //将count重置
                    //唤醒之前等待的线程
                    //下一波执行开始
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)
                        breakBarrier();
                }
            }

            // loop until tripped, broken, interrupted, or timed out
            for (;;) {
                try {
                    if (!timed)
                        trip.await();
                    else if (nanos > 0L)
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    if (g == generation && ! g.broken) {
                        breakBarrier();
                        throw ie;
                    } else {
                        // We're about to finish waiting even if we had not
                        // been interrupted, so this interrupt is deemed to
                        // "belong" to subsequent execution.
                        Thread.currentThread().interrupt();
                    }
                }

                if (g.broken)
                    throw new BrokenBarrierException();

                if (g != generation)
                    return index;

                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }

通过我的注释可以看到,这里最重要的是nextGeneration方法:

private void nextGeneration() {
        // signal completion of last generation
        trip.signalAll();
        // set up next generation
        count = parties;
        generation = new Generation();
    }

可以看到,该方法是重置计数器,并且唤醒其他所有等待线程。
于是,我们便知道,这里和AQS使用CLH锁是不一样的。
前面的线程被阻塞。
每次都是最后一个线程唤醒其他线程。

这也是因为使用了ReentrantLock和condition的缘由。

对比CountDownLatch,CycliBarrier更像是一个阀门,他是保证所有线程都到达某各自的一个指定位置。
而CountDownLatch更关注于条件这个概念。

读写锁

在平常我们都说28原则,读8写2,且读安全,写不安全
于是读采用共享,写采用独占。

读写锁可以在同一时刻允许多个读线程同时访问; 在没有读线程的时候, 只有一个写线程可以访问

读写状态的设计

  • 将状态值分成高16位和低16位
  • 低位表示写锁的重入次数, 低位0时, 代表写锁释放
  • 高位表示读锁的重入次数, 高位0时, 代表读锁已经释放
if state == 0: 无锁
if state != 0: 有锁, 但不确定是什么锁
	if state & 0x0000FFFF==0: 则一定是读锁
	else: 有写锁
		if state >>> 16 ==0: 无读锁
		else 有读锁   

锁降级:

  • 当前线程是写锁的持有者, 同时获取读锁, 然后又释放写锁

这里我是觉得核心在于state如何表示两个状态, 其他的倒没啥东西了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
AQS(AbstractQueuedSynchronizer)是Java并发编程中的一个重要类,它可以理解为抽象的队列同步器。AQS提供了一种基于FIFO队列的同步机制,用于实现各种同步器,如ReentrantLock、CountDownLatch、Semaphore等。 AQS的核心思想是使用一个volatile的int类型变量state来表示同步状态,通过CAS(Compare and Swap)操作来实现对state的原子更新。AQS内部维护了一个双向链表,用于保存等待获取同步状态的线程。 AQS的具体实现包括以下几个方面: 1. 内部属性:AQS内部有两个重要的属性,一个是head,表示队列的头节点;另一个是tail,表示队列的尾节点。 2. 入队操作:AQS的入队操作是通过enq方法实现的。在入队操作中,首先判断队列是否为空,如果为空,则需要初始化队列;否则,将新节点添加到队列的尾部,并更新tail指针。 3. CAS操作:AQS的CAS操作是通过compareAndSetHead和compareAndSetTail方法实现的。这些方法使用CAS操作来更新head和tail指针,保证操作的原子性。 4. 出队操作:AQS的出队操作是通过deq方法实现的。在出队操作中,首先判断队列是否为空,如果为空,则返回null;否则,将头节点出队,并更新head指针。 5. 同步状态的获取和释放:AQS提供了acquire和release方法来获取和释放同步状态。acquire方法用于获取同步状态,如果获取失败,则会将当前线程加入到等待队列中;release方法用于释放同步状态,并唤醒等待队列中的线程。 通过继承AQS类,可以实现自定义的同步器。具体的实现方式是重写AQS的几个关键方法,如tryAcquire、tryRelease等,来实现对同步状态的获取和释放。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值