一看就懂的Semaphore源码解析,诸佬们快来看看吧

前言:一位朋友问到了我Semaphore类相关的知识,简单看了一下源码复习了一下,写下本篇文章做一个回顾。
希望能够加深自己的印象以及帮助到其他的小伙伴儿们😉😉。
如果文章有什么需要改进的地方还请大佬不吝赐教👏👏。
小威在此先感谢各位大佬啦~~🤞🤞

在这里插入图片描述

🏠个人主页:小威要向诸佬学习呀
🧑个人简介:大家好,我是小威,一个想要与大家共同进步的男人😉😉
目前状况🎉:24届毕业生,曾经在某央企公司实习,目前在某税务公司实习👏👏

💕欢迎大家:这里是CSDN,我总结知识的地方,欢迎来到我的博客,我亲爱的大佬😘

在这里插入图片描述

以下正文开始

在这里插入图片描述

Semaphore简单介绍

Semaphore,在中文中有信号量的意思,它被用来限制能同时访问共享资源的线程上限。

可以将Semaphore比喻为停车场,permits(许可)好比停车位的数量,线程好比汽车,这个过程就好像我们限制了停车场汽车停放的数量。当每个汽车获取一个停车位,停车场可共享的停车位数量将减一,反之加一。

通过一个小案例看下Semaphore的用法,然后再进行源码分析。

Semaphore案例详解

首先,我们新建一个semaphore对象,并传入信号量的值为3,代表能同时访问共享资源的线程上限为3;接着循环创建10个线程,让每个线程获取信号量资源,等待线程运行1秒后,释放信号量资源,代码如下:

public class TestSemaphore {
    public static void main(String[] args) {
        //创建semaphore对象
        Semaphore semaphore = new Semaphore(3);
        for (int i=0;i<10;i++){
            new Thread(()->{
                try{
                    //获取信号量资源
                    semaphore.acquire();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    System.err.println(Thread.currentThread().getName()+"正在运行中……");
                    sleep(1000);
                    System.out.println(Thread.currentThread().getName()+"运行已结束");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放信号量资源
                    semaphore.release();
                }
            }).start();
        }
    }
}

运行测试案例可以得到下面的结果:
在这里插入图片描述
由此可见,semaphore限制了同时访问共享资源的线程数,每次只能三个线程获取共享资源。
在这里插入图片描述

源码剧透

在前面的文章中详细记录过AQS(Abstract Queued Synchronizer)的知识,不得不说AQS是真的强大,在Semaphore类的源码实现中也用到了AQS的原理,这点知识忘记的大佬可以看下前面AQS的文章。

获取许可(acquire)

讲到线程获取许可,还需要从Semaphore的构造方法详细说起,Semaphore类位于JUC(java.util.concurrent)包下:

    public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }

构造方法中新建了一个非公平的同步器方法,并将许可值传入了进去,点入NonfairSync( )一看,大吃一惊!

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -2694183684443567898L;
        NonfairSync(int permits) {
            super(permits);
        }
}

它居然调用的是父类的方法,耐住性子,点入super发现,我的老天爷,简直不敢看:

    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 1192457210091910933L;

        Sync(int permits) {
            setState(permits);
        }

        final int getPermits() {
            return getState();
        }

从这块源代码中可以看出,同步器类继承了AQS,Semaphore’类的底层实现还是依靠了AQS先进先出的阻塞队列。而且传入的permits许可值被赋予了AQS的state状态字段。

当线程获取资源时调用的acquire()方法,我们搜索acquire看看源码吧:

    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

上述代码表示线程可中断地获取1个许可值,点入acquireSharedInterruptibly(1)方法来看看它的实现:

    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

首先对当前线程进行一个判断,如果当前线程被中断,那么直接抛出异常,如果没有,则会调用tryAcquireShared(arg)方法,此方法是由子类具体实现的。在之前的AQS文章中,尝试获取共享资源时记录过这块的知识。如果tryAcquireShared(arg) 方法返回的是大于0的数,表示获取成功,如果线程获取许可失败,该方法返回值表示剩余的资源数,tryAcquireShared(arg)方法如下:

protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

这个方法有四个具体的实现(四大天王),我们点入想看的Semaphore实现类中去:

        protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
        }
    }

上述代码又调用到了nonfairTryAcquireShared(),点入 nonfairTryAcquireShared(非公平尝试获取共享资源)方法查看得到:

        final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

这段代码意思是,拿到state的状态值,减去传入的许可数,remaining就是剩余的许可数,如果remaining不小于0,则调用CAS机制,安全的更改state的值,将remaining返回。

如果当前state状态为0,减去acquire之后变成负数,接下来判断remaining<0为TRUE,不会在CAS更新state的值,短路,直接返回remaining(负数)。

如果返回的remaining小于0,则会调用doAcquireSharedInterruptibly(arg)方法,这里又涉及到了AQS中的常用方法,这里可以看注释理解:

    /**
     * Acquires in shared interruptible mode.
     * @param arg the acquire argument
     */
    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //将节点加入阻塞队列尾部
        final Node node = addWaiter(Node.SHARED);
        //是否获取到许可,当前表示没有获取到
        boolean failed = true;
        try {
            //自旋
            for (;;) {
                找到当前节点的前驱节点
                final Node p = node.predecessor();
                if (p == head) {
                    //看看是否能够获取到许可,和上面的方法一样
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        //如果获取到许可,唤醒node节点,释放p(头结点)
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //如果前面没有获取成功,将线程阻塞住并将state设置为-1
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

private Node addWaiter(Node mode) {
       
        /* 这个可以参考上面Node的构造方法
        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }
        */
        
        //构造新的等待线程节点
        Node node = new Node(Thread.currentThread(), mode);
       
        //新建临时节点pred指向尾节点
        Node pred = tail;
        //队列不为空的话,通过CAS机制将node放到队列尾部
        if (pred != null) {
            //将node的prev域指向尾节点
            node.prev = pred;
            //通过CAS机制将node放到队列尾部
            if (compareAndSetTail(pred, node)) {
                //将原来尾节点的next域指向当前node节点,node现在为尾节点
                pred.next = node;//形成双向链表
                return node;
            }
        }
        //如果队列为空的话
        enq(node);
        return node;
    }

在多线程并发情况下,如果有多个线程同时争夺尾节点的位置,会调用enq(node)方法,使用CAS自旋机制挂到双向链表的尾部,下面是源码:

    private Node enq(final Node node) {
        //死循环(自旋)
        for (;;) {
            Node t = tail;
            //尾节点为null,说明头结点也为null,可能是还没有创建队列的时候
            if (t == null) { 
                //多线程并发情况下,利用CAS机制创建头结点和尾节点,CAS保证此时只有一个头节点被创建,下次自旋时,就会满足队列不为空的条件
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //如果存在尾节点,将当前节点的prev域指向尾节点
                node.prev = t;
                //利用CAS机制完成双向链表的绑定,让之前尾节点指向当前node节点
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

接下来看一看compareAndSetTail方法使用CAS乐观锁机制的方法源码:

    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }

如果当前的前驱节点表示头结点,并且获取资源成功,那么直接将当前线程设为头结点,释放之前头结点与后继节点的链接,帮助垃圾回收(GC),如果前面当前节点的前驱不为头结点或者没有获取到资源,那么会调用shouldParkAfterFailedAcquire(Node pred, Node node)方法来判断当前线程是否能够进入waiting状态,如果可以进入,并且进入到了阻塞状态,那会阻塞,直到调用了LockSupport中的unpark()方法唤醒线程。

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //保存前驱节点的状态
        /*
        提示:
        当waitState>0时,表示该线程处于取消状态(线程中断或者等待锁超时),需要移除线程;
        当waitState=0时,默认值,表示初始化状态,表示线程还未完成初始化操作;
        当waitState<0,表示有效状态,线程处于可唤醒状态。
        */
        int ws = pred.waitStatus;
        //等待唤醒后置节点,SIGNAL为-1
        if (ws == Node.SIGNAL)
            return true;
        //如果前置节点不是正常的等待状态(CANCELLED结束状态),那么从当前节点开始往前寻找正常的等待状态
        if (ws > 0) {
            do {
                //后面的节点断开与前驱节点的链接
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            //双向连接
            pred.next = node;
        } else { //小于0时,可能为共享锁
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

如果前驱节点的SIGNAL值为-1,会返回true。

compareAndSetWaitStatus(pred, ws, Node.SIGNAL)方法内部也使用了CAS锁机制,源码:

    private static final boolean compareAndSetWaitStatus(Node node,
                                                         int expect,
                                                         int update) {
        return unsafe.compareAndSwapInt(node, waitStatusOffset,
                                        expect, update);
    }

如果shouldParkAfterFailedAcquire(Node pred, Node node)方法返回true,则会调用parkAndCheckInterrupt()方法阻塞当前线程,线程等待,如果线程被中断过则返回true:

private final boolean parkAndCheckInterrupt() {
    // 调用park让线程进入wait状态
    LockSupport.park(this);
    // 检查线程是否中断过。
    return Thread.interrupted();
}

如果线程在等待的过程中被中断过,那么获取到资源后会通知线程中断:

    /**
     * Convenience method to interrupt current thread.
     */
    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

在这里插入图片描述

释放许可(release)

在Semaphore类中找到release()方法,

    public void release() {
        sync.releaseShared(1);
    }

调用releaseShared(1)方法释放1个许可,点入releaseShared(1)方法的:

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

继续点入tryReleaseShared(arg)查看源码得:

    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }

在这里插入图片描述
这个类有三个实现类,点击Semaphore的实现类中查看方法:

        protected final boolean tryReleaseShared(int releases) {
            for (;;) {
                int current = getState();
                int next = current + releases;
                if (next < current) // overflow
                    throw new Error("Maximum permit count exceeded");
                if (compareAndSetState(current, next))
                    return true;
            }
        }

这个方法和上面获取许可时的方法类似,也是获取当前的状态值,将当前状态值和释放的许可值相加,最后调用CAS方法替换返回即可。

替换成功后,返回true,将继续向下进行doReleaseShared()方法,这个也在AQS中详细提到过,这里由于篇幅原因就不再复述了。

在这里插入图片描述

小结

其实从此可以看出,Semaphore类大多数用到了AQS源码的相关知识,因此AQS的知识还是挺重要的,这块也是面试中的重点,希望我们都能把握住哦~~~

好了,本篇文章就先分享到这里了,后续会继续分享其他方面的知识,感谢大佬认真读完支持咯~
在这里插入图片描述

文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起讨论😁
希望能和诸佬们一起努力,今后我们顶峰相见🍻
再次感谢各位小伙伴儿们的支持🤞

在这里插入图片描述

  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小威要向诸佬学习呀

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值