大白话讲并发编程之Semaphore源码

大白话讲并发编程之Semaphore源码,也就明白限流和降级是怎么回事了


前言

上一节分享了 ReentrantLock 这个独占锁后,那么接下来就分享 Semaphore 这个共享锁的源码,当然讲的还是公平锁。

Semaphore这个类主要作用是用来作限流和降级;先说这个类怎么用;

	Semaphore semaphore = new Semaphore(3)
        
    for (int i = 0; i < 10; i++) {
        new Thread(){
            @Override
            public void run() {
                semaphore.acquire();  //获取钥匙
                	//业务逻辑
                Thread.sleep(2000);
                	//业务逻辑
                semaphore.release();  //释放钥匙
            }
        }.start()
    }

这段代码的意思是:池中共有3把钥匙,有10个线程竞争,同一时刻最多只能有3个线程能够并行执行业务逻辑,这也是为什么能够用来做限流的原因,至于降级就是 tryAcquire(...) 方法的作用了,大致意思就是你自定义一个时间,比如1秒钟,1秒钟没有获得锁,就会返回 false ,我们就能够用来作降级处理了。这里暂时不细说,有兴趣的朋友可以在评论区里留言。

acquire() 是获取钥匙的方法,每成功获取一次,钥匙就会减 1 ,钥匙没有时就会入队等待;

release() 是用来释放锁的,归还钥匙,每成功释放一次锁,钥匙就会加 1;

当然上面这两个方法也有带 int 参数的,参数为多少,每次就获取 (释放) 几把钥匙。

Semaphore 怎么用说完了,接着就该看源码了



一、先看构造器

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

    public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }

Semaphore总共有两个构造器:

第一个构造器只有 一个int 参数,创建的是一个非公平锁,int 参数最终会作为 state ( AQS 中最重要的变量)的值,在这里可以简单理解为共享池中的钥匙个数。只有持有钥匙的线程才能够执行业务逻辑,池中每次被线程成功获取一次,钥匙就会减1,也就是state=state-1

第二个构造器当布尔值为 true 时,创建公平锁,为false时,创建非公平锁,而 int 参数和第一个构造器用法是一致的;



二、acquire()方法

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

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

    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;
        }
    }

acquire() 方法,其实就是调用 acquireSharedInterruptibly(1) 方法,这个方法先会判断该线程有没有中断信号,如果有就抛出异常;没有就判断 tryAcquireSgared() 方法有没有小于 0 ,这个方法的作用是获取钥匙,获取失败就会调用 doAcquireSharedInterruptibly() 方法。

先进入tryAcquireSgared() 方法,可以看到里面是一个自旋,想要获取钥匙成功,必须同时满足三个条件:

  1. 队列中没有其他节点
  2. 钥匙池中的钥匙减去一个后还是不小于 0
  3. CAS 设置钥匙个数 state=state-1 成功

如果条件 3 不满足就会继续循环,如果条件 1 或条件 2 不满足就会获取失败,调用 doAcquireSharedInterruptibly() 方法。

    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) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
    
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

//这两部就是传播行为唤醒下一个节点
	Node s = node.next;
    if (s != null)
        LockSupport.unpark(s.thread);

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            //修改成功后第二次就会调这个
            return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //第一次调用这个方法会执行这个语句
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

doAcquireSharedInterruptibly() 这个方法,先调用入队方法 addWaiter(Node.SHARED) ,这个方法与之前讲的 ReentrantLock 里的入队方法是一样的,区别在于这里创建的节点是个共享节点不是独占节点,这里就不细讲了。入队成功后接下来的代码又很相似,先获得当前节点的前驱节点赋值给常量 p ,判断前驱节点是不是头节点,是的话就调用上面讲的 tryAcquireShared(1) 方法,去尝试获取钥匙,如果获取成功,就把当前节点设置成头节点,并且将当前节点的 thread 和前驱指针置空,接着唤醒后驱节点(这里面的逻辑比较复杂,单靠文字描述有些难度,我就直接简化了,如果希望我详细讲的就在评论区留言),同时把前驱节点的后驱指针置空。

doAcquireSharedInterruptibly 方法的第一个 if 条件就结束了,如果前面的条件没满足,就会进入 shouldParkAfterFailedAcquire(p, node) 方法,这个方法的作用是将前驱节点的生命状态( waitStatus )修改成 SIGNAL(-1) ,修改完后再循环一次,调用 parkAndCheckInterrupt() 方法,这个方法就会调用 LockSupport.pack(this) 进行阻塞。



三、release()方法

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

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

    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;
        }
    }

    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;   // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;       // loop on failed CAS
            }
            if (h == head)     // loop if head changed
                break;
        }
    }

    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

release() 其实就是 releaseShared() 方法,它先调用了 tryReleaseShared(int releases) 尝试去释放锁,释放锁成功就会调用 doReleaseShared() 方法。

tryReleaseShared(int releases) 方法里面有个自旋,保证锁一定能够释放成功,可以看到只要 CASstate = state +1设置成功,就说明释放成功了。

doReleaseShared() 方法里面也是个自旋,先判断头节点是否不为空并且不等于尾节点,满足就会使用 CAS 将头节点的生命状态 ( waitStatus ) 设置为初始值 (0) ,接着调用 unparkSuccessor(h) 方法

由于 doReleaseShared() 方法的 continue 已经保证了它的生命状态为 初始值(0),所以会直接进入最后一个 if 条件,唤醒头结点的后驱节点, unparkSuccessor(h) 方法也就结束了。



结尾

那么今天的要讲的内容也就分享完了,如果大家有什么疑问或者发现什么错误,欢迎在评论区留言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值