Java并发基础六:并发工具类(3)Semaphore

Semaphore是什么?

Semaphore中文意思是信号量,也是一个线程并发的辅助类,Semaphore实现了线程同步框架AQS,它的本质是一个"共享锁",使用Semaphore可以控制同时访问资源的线程个数,但是不保证线程执行顺序。

Semaphore原理

Semaphore维护了有限数量的许可证,只有得到了许可证的线程才能进行共享资源的访问,如果得不到许可证,说明当前共享资源的访问已经达到最大限制,所以会挂起当前线程,直到前面的线程处理完任务之后,把许可证归还,后面排队的线程才有机会获取,然后处理任务。

Semaphore的构造及常用方法

构造方法:

Semaphore(int permits) //非公平模式指定最大允许访问许可证数量
Semaphore(int permits, boolean fair)//可以通过第二个参数控制是否使用公平模式

一些常用的方法:

acquire() //申请获取一个许可证,如果没有许可证,就阻塞直到能够获取或者被打断
availablePermits() // 返回当前有多少个有用的许可证数量hasQueuedThreads()//查询是否有线程正在等待获取许可证

drainPermits()//获得并返回所有立即可用的许可证数量
getQueuedThreads()//返回一个List包含当前可能正在阻塞队列里面所有线程对象
getQueueLength()//返回当前可能在阻塞获取许可证线程的数量
hasQueuedThreads()//查询是否有线程正在等待获取许可证
isFair()//返回是否为公平模式
reducePermits(int reduction)//减少指定数量的许可证
reducePermits(int reduction)//释放一个许可证
release(int permits)//释放指定数量的许可证
tryAcquire()//非阻塞的获取一个许可证

使用案例

Demo:模拟用户同时访问时,同时只能允许两个线程执行。

public class UseSemaphore {

        public static void main(String[] args) {
            // 线程池
            ExecutorService exec = Executors.newCachedThreadPool();
            // 只能2个线程同时访问
            final Semaphore semp = new Semaphore(2);
            // 模拟12个客户端访问
            for (int index = 0; index < 6; index++) {
                final int NO = index;
                Runnable run = new Runnable() {
                    public void run() {
                        try {
                            System.out.println("用户开始进入"+Thread.currentThread().getName());
                            // 获取许可
                            semp.acquire();
                            System.out.println("拿到进入许可 Accessing: " + Thread.currentThread().getName());
                            //模拟实际业务逻辑
                            Thread.sleep(5000);
                            // 访问完后,释放
                            System.out.println("我处理完事情了,释放许可:"+Thread.currentThread().getName());
                            semp.release();
                        } catch (InterruptedException e) {
                        }
                    }
                };
                exec.execute(run);
            }

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //System.out.println(semp.getQueueLength());
            // 退出线程池
            exec.shutdown();
        }
    }

Semaphore底层原理:

Semaphore底层与CountDownLatch类似都是通过AQS的共享锁机制来实现的,指定的数量会设置到AQS里面的state里面,然后对于每一个 调用acquire方法线程,state都会减去一,如果state等于0,那么调用该方法的线程会被添加到同步队列里面,同时使用 LockSupport.park方法挂起等待,知道有线程调用了release方法,会对state加1,然后唤醒共享队列里面的线程,注意这里如果是 公平模式,就直接唤醒下一个等待线程即可,如果是非公平模式就允许新加入的线程与已有的线程进行竞争,谁先得到就是谁的,如果新加入的 竞争失败,就会走公平模式进入队列排队。

源码:

1.acquire函数--获取许可

  先从获取一个许可看起,并且先看非公平模式下的实现。首先看acquire方法,acquire方法有几个重载,但主要是下面这个方法

   public void acquire(int permits) throws InterruptedException {
        if (permits < 0) throw new IllegalArgumentException();
        sync.acquireSharedInterruptibly(permits);
    }

从上面可以看到,调用了Sync的acquireSharedInterruptibly方法,该方法在父类AQS中,如下:

  public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        //如果线程被中断了,抛出异常
        if (Thread.interrupted())
            throw new InterruptedException();
        //获取许可失败,将线程加入到等待队列中
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

AQS子类如果要使用共享模式的话,需要实现tryAcquireShared方法,下面看NonfairSync的该方法实现:   

     protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
        }
该方法调用了父类中的nonfairTyAcquireShared方法,如下:

        final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                //获取剩余许可数量
                int available = getState();
                //计算给完这次许可数量后的个数
                int remaining = available - acquires;
                //如果许可不够或者可以将许可数量重置的话,返回
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

这里的释放就是对 state 变量减一(或者更多)的。

返回了剩余的 state 大小。

当返回值小于 0 的时候,说明获取锁失败了,那么就需要进入 AQS 的等待队列了。代码如下:

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    // 添加一个节点 AQS 队列尾部
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        // 死循环
        for (;;) {
            // 找到新节点的上一个节点
            final Node p = node.predecessor();
            // 如果这个节点是 head,就尝试获取锁
            if (p == head) {
                // 继续尝试获取锁,这个方法是子类实现的
                int r = tryAcquireShared(arg);
                // 如果大于0,说明拿到锁了。
                if (r >= 0) {
                    // 将 node 设置为 head 节点
                    // 如果大于0,就说明还有机会获取锁,那就唤醒后面的线程,称之为传播
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            // 如果他的上一个节点不是 head,就不能获取锁
            // 对节点进行检查和更新状态,如果线程应该阻塞,返回 true。
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 阻塞 park,并返回是否中断,中断则抛出异常
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            // 取消节点
            cancelAcquire(node);
    }
}

总的逻辑就是:

创建一个分享类型的 node 节点包装当前线程追加到 AQS 队列的尾部。
如果这个节点的上一个节点是 head ,就是尝试获取锁,获取锁的方法就是子类重写的方法。如果获取成功了,就将刚刚的那个节点设置成 head。
如果没抢到锁,就阻塞等待。
看完了非公平的获取,再看下公平的获取,代码如下:

  protected int tryAcquireShared(int acquires) {
            for (;;) {
                //如果前面有线程再等待,直接返回-1
                if (hasQueuedPredecessors())
                    return -1;
                //后面与非公平一样
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

从上面可以看到,FairSync与NonFairSync的区别就在于会首先判断当前队列中有没有线程在等待,如果有,就老老实实进入到等待队列;而不像NonfairSync一样首先试一把,说不定就恰好获得了一个许可,这样就可以插队了。
看完了获取许可后,再看一下release()方法。

   2.release()函数--释放许可

 释放许可也有几个重载方法,但都会调用下面这个带参数的方法

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

releaseShared方法在AQS中,如下:

 public final boolean releaseShared(int arg) {
        //如果改变许可数量成功
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

AQS子类实现共享模式的类需要实现tryReleaseShared类来判断是否释放成功,实现如下:

  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");
                //CAS改变许可数量成功,返回true
                if (compareAndSetState(current, next))
                    return true;
            }
        }

从上面可以看到,一旦CAS改变许可数量成功,那么就会调用doReleaseShared()方法释放阻塞的线程

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                // 设置 head 的等待状态为 0 ,并唤醒 head 上的线程
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            // 成功设置成 0 之后,将 head 状态设置成传播状态
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

文章参考:

https://blog.csdn.net/qq_39241239/article/details/87069630

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章