Semaphore浅析

文章目录

  • 前言
    • 1、简介及应用
    • 2、源码分析
          • 1.构造方法
          • 2.acquire方法
          • 3.doAcquireSharedInterruptibly方法
          • 4.release方法


前言

在JUC包下,有三个控制并发的工具类Semaphore信号量、CountDownLatch倒计时器和CyclicBarrier循环栅栏,下面对Semaphore这个工具类的使用示例、源码分析做一个简单的阐述


1、简介及应用

Semaphore通过维护许可证的数量来控制线程对共享资源的访问。如果许可证数量大于0,则线程可以访问共享资源,否则,线程不能访问共享资源。
Semaphore的一个经典例子就是停车问题,假设共有3个车位(许可证数量),现在有6辆车来停车(线程),只有车位有空闲时车辆才能抢占车位,否则需要等待,直到占用车位的车辆离开,其代码实现如下所示:

public class semphoreTest {
    private static int park = 3;

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(park);
        for(int i=1;i<7;i++){
            new Thread(()->{
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"号车辆停车==============");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    semaphore.release();
                    System.out.println(Thread.currentThread().getName()+"号车辆离开************");
                }
            },String.valueOf(i)).start();
        }
    }
}

代码运行结果如下所示,可以看到,有剩余的车位就会有车辆抢占停车,车位满了就等待车辆离开车位,而且车辆抢占车位并不是按照顺序抢占的。
Semaphore浅析_第1张图片

2、源码分析

1.构造方法

看完上面的应用示例后,我们看一下其源码,首先看一下它的构造方法,源码中提供了两个有参构造,代码如下:

//传递一个参数,许可证数量,可以看出默认构造非公平模式
public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }
//传递两个参数,许可证数量和公平模式,根据fair值判断创建公平模式还是非公平模式
public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }

可以看出,这两个构造方法都与Sync类有关系,可知Semaphore内部也是定义了一个继承AQS类的Sync内部类,然后分别定义了NonfairSync(非公平模式)和FairSync(公平模式)两个继承Sync类的内部类。new FairSync(permits)new NonfairSync(permits)的代码块定义了相同的方法:

//new NonfairSync
NonfairSync(int permits) {
            super(permits);
        }
//new FairSync
FairSync(int permits) {
            super(permits);
        }
//这两个方法都调用super(permits)方法,由于继承的是Sync类,因此,看super(permits)方法的实现
Sync(int permits) {
            setState(permits);
        }
//最终,调用的是AQS类的setState方法,把许可证数量赋值给共享资源state
protected final void setState(int newState) {
        state = newState;
    }
2.acquire方法

acquire方法既可以无参调用也可以有参调用,这两个的区别在于如果传递参数,那么就需要校验参数是否合法,如果许可证数量参数小于0,会抛出非法数据异常。

//无参调用,默认尝试获取1个许可
public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
//有参调用
public void acquire(int permits) throws InterruptedException {
        if (permits < 0) throw new IllegalArgumentException();
        sync.acquireSharedInterruptibly(permits);
    }

可以看出,最终都是通过Sync类调用AQS的acquireSharedInterruptibly方法实现,代码如下:

public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        //tryAcquireShared(arg)返回值是个int类型数值,只有小于0才能执行doAcquireSharedInterruptibly(arg)方法
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

先看tryAcquireShared(arg)方法,这个方法是一个模板方法,具体实现在子类中,先看nonFairSync类中的实现,可以看出,其实宏观上的组成类似于ReentrantLock,都是Sync类实现非公平锁的tryAcquireXXX方法

protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
        }
final int nonfairTryAcquireShared(int acquires) {
			//自旋
            for (;;) {
            	//获取当前可提供的许可证数量
                int available = getState();
                //获取请求成功后剩余的许可证数量
                int remaining = available - acquires;
                //如果remaining < 0,直接返回remaining,执行AQS的doAcquireSharedInterruptibly方法
                //如果remaining >= 0,继续执行cas操作,修改共享资源state值,修改成功,返回remaining,阻塞doAcquireSharedInterruptibly的执行
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

再来看一下公平锁的的tryAcquireShared方法的实现,代码如下:

protected int tryAcquireShared(int acquires) {
			//自旋
            for (;;) {
            	//hasQueuedPredecessors()判断当前线程是否需要排队,之前ReentrantLock的公平锁tryAcquire方法讲过,只有同步队列的头节点的下个节点才能获取资源,否则都需要排队
                if (hasQueuedPredecessors())
                	//返回负值,执行doAcquireSharedInterruptibly
                    return -1;
                //获取当前许可证的数量
                int available = getState();
                //新的剩余许可证的数量,下面的代码同非公平模式相同
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

从上面的代码分析可知,公平模式和非公平模式的主要区别在于tryAcquireShared方法中,公平模式需要判断线程需要排队,以同步队列的节点顺序获取共享资源。总结一下acquire方法,如果不传入许可证数量,默认获取1个,否则,按照传入数量获取,不过需要先判断传参是否合法,对于公平模式,还需要判断当前线程是否需要排队,做完这些之后,获取剩余的许可数量(当前数量减去获取数量),如果剩余数量小于0,说明资源不足提供,当前线程执行doAcquireSharedInterruptibly方法,否则,会自旋直到更新剩余容量成功。

3.doAcquireSharedInterruptibly方法

前面说过,如果资源不足以提供当前线程获取,会执行doAcquireSharedInterruptibly方法,该方法类似于独占锁的acquireQueued(addWaiter(Node.EXCLUSIVE), arg)),也是将线程包装成节点进行入队出队操作,代码如下:

/**
     * Acquires in shared interruptible mode.
     * @param arg the acquire argument
     */
    //可以看一下它的注释,以共享可中断模式获取,当然还有以不可中断XXX,方法为doAcquireShared,这两个方法大体上没有区别,只不过在处理中断时一个抛异常,一个修改标志位
    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //将当前线程封装成共享模式的节点加入同步队列队尾,返回当前线程节点
        final Node node = addWaiter(Node.SHARED);
        //失败标志
        boolean failed = true;
        try {
        	//自旋
            for (;;) {
            	//获取该节点的前一个节点p
                final Node p = node.predecessor();
                //如果p是同步队列的头节点,当前线程节点可以获取共享资源
                if (p == head) {
                	//尝试获取资源,返回值是剩余资源数量
                    int r = tryAcquireShared(arg);
                    //如果有剩余资源或者资源刚好够获取
                    if (r >= 0) {
                    	//将当前线程节点设置为头节点
                        setHeadAndPropagate(node, r);
                        //释放原来的头节点
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //检查线程节点是否需要挂起,在前面AQS的独占锁模式已经讲过
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
        	//如果失败
            if (failed)
            	//取消获取资源
                cancelAcquire(node);
        }
    }

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

private void doReleaseShared() {
        //自旋
        for (;;) {
        	//获取头节点
            Node h = head;
            //头节点不是null且头节点不是尾节点
            if (h != null && h != tail) {
            	//获取等待状态
                int ws = h.waitStatus;
                //如果等待状态是通知下个节点
                if (ws == Node.SIGNAL) {
                	//cas设置状态等待状态,如果失败,continue继续执行
                    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;
        }
    }
4.release方法

release方法的实现分为有参和无参两种,对于有参的release方法,需要进行参数的验证,后续的方法都一样,都是调用的AQS的releaseShared方法,代码如下:

public final boolean releaseShared(int arg) {
		//需要通过tryReleaseShared方法的结果去判断
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

先看一下在Semaphore中tryReleaseShared方法的实现,代码如下:

protected final boolean tryReleaseShared(int releases) {
			//自旋
            for (;;) {tryR
            	//当前的共享资源数量
                int current = getState();
                //释放后的资源数量
                int next = current + releases;
                //超出限制
                if (next < current) // overflow
                    throw new Error("Maximum permit count exceeded");
                //cas更改state的值
                if (compareAndSetState(current, next))
                    return true;
            }

tryReleaseShared方法执行成功后,才能执行释放资源的方法doReleaseShared,代码如下:

private void doReleaseShared() {
        //自旋
        for (;;) {
        	//同步队列的头节点
            Node h = head;
            //同步队列不为空
            if (h != null && h != tail) {
            	//头节点的等待状态
                int ws = h.waitStatus;
                //如果是通知下一个节点的状态
                if (ws == Node.SIGNAL) {
                	//cas修改头节点的等待状态为初始状态
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            
                    //唤醒一个节点
                    unparkSuccessor(h);
                }
                //如果当前节点
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            //检查到head节点没有变化时退出循环,head节点的变化是由线程唤醒引起的,说明当前没有线程再被唤醒
            if (h == head)                   // loop if head changed
                break;
        }
    }

该方法会尝试唤醒head的后继节点的线程,如果线程已经被唤醒,则仅设置PROPAGATE状态,上述操作都是cas完成的,cas失败后还会continue尝试,当检测到head节点不再发生变化,退出自旋。

你可能感兴趣的:(学习总结,java,开发语言,后端)