多线程之Semaphore

Semaphore

1、Semaphore概念

Semaphore,俗称信号量,它是操作系统中PV操作的原语在java的实现,它也是基于AbstractQueuedSynchronizer实现的。

Semaphore的功能非常强大,大小为1的信号量就类似于互斥锁,通过同时只能有一个线程获取信号量实现。大小为n(n>0)的信号量可以实现限流的功能,它可以实现只能有n个线程同时获取信号量

1.1、PV操作

PV操作是操作系统一种实现进程互斥与同步的有效方法。PV操作与信号量(S)的处理相关,P表示通过的意思,V表示释放的意思。用PV操作来管理共享资源时,首先要确保PV操作自身执行的正确性。

P操作的主要动作是

①S减1;

②若S减1后仍大于或等于0,则进程继续执行;

③若S减1后小于0,则该进程被阻塞后放入等待该信号量的等待队列中,然后转进程调度。

V操作的主要动作是

①S加1;

②若相加后结果大于0,则进程继续执行;

③若相加后结果小于或等于0,则从该信号的等待队列中释放一个等待进程,然后再返回原进程继续执行或转进程调度。

2、Semaphore 常用方法

2.1、构造器确定资源数目

permits 表示许可证的数量(资源数)

fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程 。

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


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

查看源码进去之后,可以看到最终到了:

    protected final void setState(int newState) {
        state = newState;
    }

可以看到传入进去的参数将会作为资源数目的多少来进行定义。

3、常用方法

public void acquire() throws InterruptedException
public boolean tryAcquire()
public void release()
public int availablePermits()
public final int getQueueLength()
public final boolean hasQueuedThreads()
protected void reducePermits(int reduction)
protected Collection<Thread> getQueuedThreads()
方法名称 语义
acquire() 表示阻塞并获取许可
tryAcquire() 方法在没有许可的情况下会立即返回 false,要获取许可的线程不会阻塞
release() 表示释放许可
int availablePermits() 返回此信号量中当前可用的许可证数。
int getQueueLength() 返回正在等待获取许可证的线程数。
boolean hasQueuedThreads() 是否有线程正在等待获取许可证。
void reducePermit(int reduction) 减少 reduction 个许可证
Collection getQueuedThreads() 返回所有等待获取许可证的线程集合

4、应用场景

可以用于做流量控制,特别是公用资源有限的应用场景

示例:

/**
 * @Description 有三个许可,五个线程来抢。效果:三个线程执行成功,另外两个线程进行排队
 * @Author liguang
 * @Date 2022/03/18/11:13
 */
public class SemphoreTestOne {
    public static void main(String[] args) {
        Semaphore windows = new Semaphore(3);
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                try {
                    windows.acquire();
                    System.out.println("开始卖票");
                    Thread.sleep(5000);
                    System.out.println("购票成功");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                  windows.release();
                }
            }).start();
        }
    }
}

控制台展示:

开始卖票
开始卖票
开始卖票
-----------------停顿5s钟----------------    
购票成功
购票成功
开始卖票
开始卖票
购票成功

效果上:许可只有三个,但是有多个线程,那么多个线程来获取得到三个有限的资源的时候,对于已经获取得到了的线程来说,是没有任何影响,而没有获取得到的线程只能够排队等待资源。

5、源码分析

这里将会涉及到共享锁的一些概念。

从两行代码中来进行分析:

 Semaphore windows = new Semaphore(3);
try {
    windows.acquire();
    xxxxx(业务方法);
} catch (InterruptedException e) {
    e.printStackTrace();
} finally {
    windows.release();
}

那么在acquire和release就可以实现这样的逻辑,那么看下具体的实现逻辑。

5.1、acquire

只需要来分析这段代码即可:

        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);

那么看下尝试获取得到共享锁,当对应的值小于0的时候,才会去将线程进行阻塞。

首先看下尝试获取得到共享锁的方法(非公平场景下来进行分析):

final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        // 1、当许可的数量小于0(表示的当前资源不足);2、大于等于0的时候,表示资源还是足够的,可以分配给当前线程
        if (remaining < 0 || compareAndSetState(available, remaining))
            // 许可剩余数量
            return remaining;
    }
}

线程进来的时候,进行比较并交换,将许可证数量减少一个,这里有两种情况:

1、然后如果是大于等于0的,比较并交换即可;

2、如果是小于0的,那么将会直接进入到阻塞队列中去;

对于没有获取得到成功的,那么将继续循环操作。最终将返回许可的剩余数量。

对应的流程图如下所示:

那么在回到上面的方法中来,只有当许可小于0的时候,才会将对应的线程进行阻塞。而大于等于0的,则是直接获取得到资源进行操作了

    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        // 这里和之前的还是有区别的,这里使用到节点的另外一种状态。SHARED,共享状态。构建一个共享队列来进行创建
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    // 如果是头节点!那么这里再来尝试获取得到一次锁,如果得到的
                    int r = tryAcquireShared(arg);
                    // 如果获取得到的资源大于等于0,那么将会进行下面的操作
                    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);
        }
    }

那么下面只需要关注一下setHeadAndPropagate方法即可

    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head;
        setHead(node);
        if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            // 在这里要来唤醒下一个节点。这里直接在这里来进行唤醒了!
            if (s == null || s.isShared())
                // 可以看到释放锁也是在这里来进行操作的。在这里直接来通知后面的排队的线程来进行操作。
                // 唤醒后面的线程来抢锁
                doReleaseShared();
        }
    }

5.2、release

释放也是非常简单的,直接上源码:

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

直接获取得到原来的值,然后进行相加重新设置+1即可。

看看释放锁的过程:

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

可以看到是将节点状态SINAL修改成0,然后唤醒后面的线程。这里就跟之前的是类似的。

6、总结

这个源码比较简单,所以不过来过多解释。无非是检测到了状态是SHARED并且不为null的,直接进行唤醒来尝试通知下一个线程即可。

但是使用的时候需要注意的一点是:要有效率的来获取得到许可数量。

6.1、DEMO1

线程获取得到许可证的数量不能够大于许可证原始数量

如下所示:

/**
 * @Description 获取得到许可过大。注意这里的许可数量设置
 * @Author liguang
 * @Date 2022/03/18/11:13
 */
public class SemphoreTestTwo {
    public static void main(String[] args) {
        
        Semaphore windows = new Semaphore(5);
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                try {
                    windows.acquire(6);
                    System.out.println("开始卖票");
                    Thread.sleep(5000);
                    System.out.println("购票成功");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                  windows.release();
                }
            }).start();
        }
    }
}

这种操作最终将会导致所有的线程都将会在阻塞队列中进行排队,等待被激活。无法被唤醒,除非是因为操作不当,因为中断引起。

6.2、DEMO2

线程获取得到多少许可证,在释放的时候就要释放多少许可证

再来一个例子:

/**
 * @Description 获取得到许可证的的数量要和释放许可证的数量保持一致
 * @Author liguang
 * @Date 2022/03/18/11:13
 */
public class SemphoreTestTwo {
    public static void main(String[] args) {
        Semaphore windows = new Semaphore(5);
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                try {
                    windows.acquire(3);
                    System.out.println("开始卖票");
                    Thread.sleep(5000);
                    System.out.println("购票成功");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                  windows.release(3);
                }
            }).start();
        }
    }
}
posted @ 2022-03-19 22:58  写的代码很烂  阅读(342)  评论(0编辑  收藏  举报