并发编程学习笔记之十三并发工具Semaphore信号量

匆匆过客 提交于 2020-01-24 02:31:11

一、Semaphore简介

Semaphore被称之为信号量,可以用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。常常用来做流量控制,特别公共资源有限的应用场景,比如池化资源:数据库连接池,对象池,线程池。来举个更接地气的例子:一个临时停车场只有10个车位,一次来了10辆车,正好将10个车位占满,此时又来了一辆车,只能等其他车离开之后才能找到车位停车。

二、简单应用

下面来看Semaphore一个简单的应用示例:Semaphore相当于车位,此时5辆车依次进入停车产。

package cn.com.threadtx.cn.com;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 *Semaphore信号量示例
 */
public class SemaphoreTest {
    public static void main(String[] args) {
        //初始化线程池
        ExecutorService service = Executors.newCachedThreadPool();
        //创建信号量
        final Semaphore sp = new Semaphore(3);
        for (int i = 0; i <5 ; i++) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(Thread.currentThread().getName()+"_车子准备进入临时停车场");
                        //车子可以进入车位(获得许可)
                        sp.acquire();
                        System.out.println(Thread.currentThread().getName()+"_有剩余的车位车子获取许可,进度停车位");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    try {
                        Thread.sleep(3000);

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    sp.release();
                    System.out.println(Thread.currentThread().getName()+"_车子离开车位");
                }
            };
            service.execute(runnable);
        }
    }
}

打印结果:由打印结果中可以看出,车子1、5、3依次进入停车场,获取许可停在车位上,这次车子2、4准备进入停车场,但是并未获得许可,而是等到1、5、3中有车子离开的时候才获得许可停到车位上,最终车子都离开车位。

pool-1-thread-1_车子准备进入临时停车场
pool-1-thread-1_有剩余的车位车子获取许可,进度停车位
pool-1-thread-5_车子准备进入临时停车场
pool-1-thread-5_有剩余的车位车子获取许可,进度停车位
pool-1-thread-3_车子准备进入临时停车场
pool-1-thread-3_有剩余的车位车子获取许可,进度停车位
pool-1-thread-2_车子准备进入临时停车场
pool-1-thread-4_车子准备进入临时停车场
pool-1-thread-1_车子离开车位
pool-1-thread-2_有剩余的车位车子获取许可,进度停车位
pool-1-thread-5_车子离开车位
pool-1-thread-3_车子离开车位
pool-1-thread-4_有剩余的车位车子获取许可,进度停车位
pool-1-thread-2_车子离开车位
pool-1-thread-4_车子离开车位

三、 源码剖析

已经见识到Semaphore独特的作用,那么接下来看下具体的实现原理

  • 组成:

信号量的结构模型主要包括三部分:计数器,等待队列(支持多个线程进入)、初始化和出入操作。信号量的主要还是AQS队列阻塞器来实现,对应的实现方式:

计数器:volatile修饰的成员变量state,表示信号量的大小

等待队列:AQS共享模式的链表

初始化:设置信号量的大小

出入操作:acquire和release方法

内部类:继承AQS

abstract static class Sync extends AbstractQueuedSynchronizer {...}

非公平模式:

static final class NonfairSync extends Sync {...}

公共模式:

static final class FairSync extends Sync {...}

 构造方法:

    //初始化state为permits,也就是信号量的大小,默认为非公平模式
    public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }
    //自定选择实现公平或者非公平模式
    public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }

    公平模式和非公平模式的区别:
    if (hasQueuedPredecessors())
         return -1;
    //公平模式,进度队列不为空,第一个线程不是是不是当前线程都返回-1,都加入到等待队列的尾部
    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());
    }  
  • 主要方法

 加入和退出包含有参和无参的方法:

主要区别是含参方法都会校验传入的参数不能为负数,而无参方法默认退出1。下面主要分析下无参的方法。

 if (permits < 0) throw new IllegalArgumentException();

加入信号量的方法 acquire()

    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);//1-1调用内部类acquireSharedInterruptibly每次进入都是1
    }

1-1acquireSharedInterruptibly先判断线程状态,如果已中断,则抛出;线程正常,调用1-2进入信号量,返回为负数,说明信号量已满,则调用1-3加入到等待队列中 

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

1-2 tryAcquireShared实际是调用AQS的子类Sync的方法,再调用非公平模式的nonfairTryAcquireShared方法,修改信号量的大小。通过自旋操作初始化信号量大小state的值,如果remaining为负数说明信号量已满,则调用1-3加入到等待队列中。不为负数,调用CAS修改state信号量大小的值。

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

1-3超出信号量的大小,则加入等待队列。3-1addWaiter(Node.SHARED)加入到等待队列,如果当前节点的前置是首节点则再次尝试进入信号量并调整队列结构调用3-2setHeadAndPropagate(node, r);如果不是首节点则修改节点状态,调整队列结构唤醒线程3-3shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()。如果调用过程中有超时或者中断的情况则调用3-4cancelAcquire(node)。

    //1-3
    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //3-1加入到队列尾部,返回当前节点(如果首节点为空时新增一个不包含线程的节点,
        //作为首节点,而首节点也可以理解为最后进入信号量的线程的标志节点,首节点的后继节点
        //是最渴望进入信号量的节点,而首节点也就起到哨兵的作用。)
        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) {
                        //3-2表示成功进入信号量,清除已退出节点在等待队列的中关系,唤醒下个节点线程去抢占信号量
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //3-3当前节点不是首节点,此时要修改节点状态,调整队列结构。
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())//阻塞线程同时判断线程的中断状态,如果线程已中断则抛出异常
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                //3-4失败后的操作,消除失效的节点
                cancelAcquire(node);
        }
    }

 3-1addWaiter加入到队列尾部,返回当前节点(如果首节点为空时新增一个不包含线程的节点,作为首节点,而首节点也可以理解为最后进入信号量的线程的标志节点,首节点的后继节点,是最渴望进入信号量的节点,而首节点也就起到哨兵的作用。)

    //3-1加入等待队列
    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;
                }
            }
        }
    }

3-2 清除原首节点的队列关系,设置当前线程为所在节点为新的首节点,清除线程引用,这里会有四个判断条件判断信号量还有位置,当前线程为阻塞状态,如果满足条件则调用doReleaseShared(),唤醒后继节点线程,修改线程节点的状态,如果线程为阻塞状态SIGNAL则通过CAS修改为0,唤醒最想被唤醒的线程。

     //3-2
     private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        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();//@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);//@2唤醒线程
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head) //退出循环                  // loop if head changed
                break;
        }
    }

    //@2唤醒线程
     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);
    }

3-3如果线程中断或者超时,节点不存在,则直接返回。如果节点从存在,从调整队列结构清除无效节点及线程引用,修改节点状态。

    //3-3
    private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;

        node.thread = null;
        Node pred = node.prev;

        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // predNext is the apparent node to unsplice. CASes below will
        // fail if not, in which case, we lost race vs another cancel
        // or signal, so no further action is necessary.
        Node predNext = pred.next;
        node.waitStatus = Node.CANCELLED;
        
        // If we are the tail, remove ourselves.
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }

 退出信号量的方法:release()

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

 1-1调用sys的releaseShared方

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {//1-2
            doReleaseShared();//1-3
            return true;
        }
        return false;
    }

1-2释放信号量空间,并进行校验 

        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-3修改节点状态,唤醒线程。(参考) 

 //修改节点状态,唤醒等待的线程
    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);//@2唤醒线程
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head) //退出循环                  // loop if head changed
                break;
        }
    }

    //@2唤醒线程
     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);
    }

参考书籍:

方志明 著《Java并发编程艺术》

Doug lea 等著《Java并发编程实战》 

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!