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,不能竞争。

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

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

当然,二者各有优劣。公平锁能够保证公平,保证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,且读安全,写不安全
于是读采用共享,写采用独占。

这里有点累了,后面再写。

猜你喜欢

转载自blog.csdn.net/qq_34687559/article/details/114276489