并发工具类源码分析---Semaphore

通过本篇我们将解决以下几个问题。

一、思考

  • 问题一:该类提供了怎样的功能?使用场景?
  • 问题二:该类的实现原理是什么?

一、注释翻译

如果您不知道该类的作用或者设计初衷,请跟我一起从注释中找答案:

一个计数信号量。信号量维护了一组概念上的许可证。每一个调用acquire方法的线程将阻塞直到一个许可证可用,然后这个线程将取走它。每一个release方法调用都将增加一个许可证,其会潜在的释放一个阻塞的获取者。

然而,没有实际的许可证对象被使用。该类只是维护一个可用数量的计数,并执行相应的操作。

信号量经常被用来限制访问物理或者逻辑资源的线程数量。比如这里有一个类使用信号量来控制对资源池的访问:

class Pool {
    //最大可用许可证
    private static final int MAX_AVAILABLE = 100;
    //初始化许可证
    private final Semaphore available = new Semaphore(MAX_AVAILABLE, true);
 
    //资源访问
    public Object getItem() throws InterruptedException {
      //资源访问之前获取许可证
      available.acquire();
      //获取可用资源
      return getNextAvailableItem();
    }

    //释放资源 
    public void putItem(Object x) {
      if (markAsUnused(x))
        //获取许可证
        available.release();
    }
 
    // Not a particularly efficient data structure; just for demo
 
    //资源
    protected Object[] items = ... whatever kinds of items being managed
    protected boolean[] used = new boolean[MAX_AVAILABLE];
 
    protected synchronized Object getNextAvailableItem() {
      for (int i = 0; i < MAX_AVAILABLE; ++i) {
        if (!used[i]) {
           used[i] = true;
           return items[i];
        }
      }
      return null; // not reached
    }
 
    protected synchronized boolean markAsUnused(Object item) {
      for (int i = 0; i < MAX_AVAILABLE; ++i) {
        if (item == items[i]) {
           if (used[i]) {
             used[i] = false;
             return true;
           } else
             return false;
        }
      }
      return false;
    }
 }}

在获取item之前,每个线程必须从semaphore中获取一个许可证,这保证池中会有一个可用的item。当线程用完item之后,该item将被返回到池中,同时一个许可证也将被返回到semaphore,然后允许另一个线程去获取该许可证。注意调用acquire方法时,不会持有同步锁,因为这样做将会阻止item返回到池中。信号量封装了限制对池的访问所需要的同步,与维护池本身一致性所需的任何同步分开。

如果semphore被初始化为1,表明其最多只有一个许可证可供使用,可以作为互相排斥的独占锁使用。这通常称为“二进制信号量”,因为它只有两个状态:要么用1表示许可证可用,要么用0表示许可证可用。当semaphore被这样使用时,其有这样的特性(不同于Lock的实现),锁可以被另一个不是锁的拥有者释放(因为信号量没有所有权的概念)。这在一些特定的场景中将会很有用,比如死锁恢复。

该类的构造函数提供了一个可选择的“fairness”参数,当设为false,不保证获得许可证的线程的顺序。尤其是,如果“突然闯入”被允许,一个调用acquire方法的线程可以优先于在等待的线程获取许可证,,逻辑上该线程将其自己置于等待队列的头部。当fairness被设置为true,semaphore将会保证调用acquire方法的线程将有序获得许可证。请注意FIFO顺序必须应用于其中特定的内部执行点方法。因此,可能一个线程优先于另一个线程调用acquire方法,但是它在另一个线程之后到达排序点。

通常,用于控制资源访问的信号量应该初始化为公平的,以确保没有线程因为访问资源而耗尽。当将信号量用于其他类型的同步控制时,非公平方式的吞吐量常常会超过公平方式,如果考虑到这点,非公平方式会有更多优势。

该类还为acquire和 release提供了方便的方法,可以同时获得多个许可。当使用这些方法而不将公平性设置为true时,要注意无限期延迟的风险增加。

内存一致性影响:某个线程中的其他操作要优先于对release方法的调用。

二、源码分析

在研究源码之前,我们先看看Semaphore的使用方式(结合上面注释中的例子),公分为三个部分:

  1. Sempahore定义:定义许可证数量,公平或者非公平模式
  2. 许可证的获取:semaphore.acquire()
  3. 许可证的释放:semaphore.release()

接下来我们将从这三部分分析Semaphore的源码:

非公平模式(默认)

  • Semaphore定义

在了解Semaphore之前,我们先看下Semaphore是如何使用AQS的

//我们先看下Nofair类的继承关系(只看实现AQS的部分)
//注意:Java中不存在多继承,这里只是举例
static final class NonfairSync extends Sync extends AbstractQueuedSynchronizer{
    //这里只是列举方法,具体分析请看下文
    
    //构造函数
    NofairSync(int permits){
    }

    Sync(int permits){
    }

    //共享模式获取
    protected int tryAcquireShared(int acquires){
    }

    //共享模式释放
    protected final boolean tryReleaseShared(int releases){
    }

}

源码分析开始:

Method:Semaphore(int)

public Semaphore(int permits) {
        //通过内部类NonfairSync(继承AQS)实现,请看下文
        sync = new NonfairSync(permits);
}

Method:NofairSync(int permits)

NonfairSync(int permits) {
    //调用父类Sync构造函数,请看下文        
    super(permits);
}

Method:Sync(int)

Sync(int permits) {
    //通过构造函数初始化许可证数量。
    //通过AQS的state代表可用许可证数量:permits= state       
    setState(permits);
}
  • 非公平模式许可证的获取

Method:Semaphore.acquire()

//从Semaphore获取一个许可证,该方法将阻塞,直到以下两种情况发生:
//1.许可证可用
//2.其他线程调用thread.interrupt方法
public void acquire() throws InterruptedException {
    
    //该方法将调用子类tryAcquireShared尝试获取,如果获取失败,则入队阻塞。如果获取成功将通过传播方 
    //式唤醒后继线程。
    
    //该方法在博文【AQS分析第四篇】已经讲过,这里不再赘述,如果有疑问的可以看那篇博文。这里我们重点 
    //关注下文子类的tryAcquireShared方法看下“阻塞条件的产生”
    sync.acquireSharedInterruptibly(1);
}

Method:NofairSync.tryAcquireShared(int args)

protected int tryAcquireShared(int acquires) {
    //请看其父类Sync.nonfairTryAcquireShared方法        
    return nonfairTryAcquireShared(acquires);
}

Method:Sync.nonfairTryAcquireShared(int)

//该方法通过非公平模式获取许可证,如果获取不成功,该线程将阻塞。
//在这个方法里,我们将分析两个问题:
//1.如何获取许可证
//2.怎样做到非公平模式的
final int nonfairTryAcquireShared(int acquires) {
            //如果可用许可证数量大于等于获取数量,通过CAS加失败重试的方式更新许可证数量
            for (;;) {
                int available = getState();
                //剩余许可证数量=当前可用数量-请求数量
                int remaining = available - acquires;
                //返回值<0:获取许可证失败,需要阻塞
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
}

1.如何获取许可证:许可证数量等于state的值,通过比较可用许可证数量与请求获取许可证数量大小来判断是否可以获取成功。
2.怎样做到非公平模式:注意多线程环境下,可能后来的线程调用了acquire方法,但此时阻塞队列中可能已经有先获取失败的线程阻塞,这将导致,后来的线程可能优先于先来的线程获取到许可证,所以为非公平模式。
  • 许可证的释放

Method:Semaphore.release()

//释放一个许可证,将许可证返回到Semaphore,其他在尝试获取许可证的线程将被选中一个并获取这个释放的许 
//可证。

//这里并不会要求释放许可证的线程必须先调用了Semaphore.acquire方法。但是正确的使用方式建立在编程
//约定之上。

public void release() {
       //默认释放一个许可证,请看下文Sync.releaseShared方法
        sync.releaseShared(1);
}

Method:Sync.releaseShared(int arg)

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            //传播唤醒在acquire上阻塞的线程,具体请看博文【AQS第四篇】对于该方法的解释
            doReleaseShared();
            return true;
        }
        return false;
}

公平模式

由上图可以看到,公平和非公平模式的差异仅在定义和获取许可证的实现上不同,在释放的实现上没有差异,接下来我们将只分析差异部分。

  • Semaphore定义

Method:Semaphore(int permits,boolean fairness)

//permits:初始许可证数量
//fair:是否公平模式
public Semaphore(int permits, boolean fair) {
        //非公平模式上文分析过,请看下文公平模式分析
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

Method:Fair(int permits)

FairSync(int permits) {
            //同公平模式,设置AQS的state=permits
            super(permits);
}
  • 公平模式获取

Method:FairSync.tryAcquireShared(int acquires)

//公平模式VS非公平模式

//公平模式:
protected int tryAcquireShared(int acquires) {
            for (;;) {
                //通过对比发现,两者的差异性仅在下面这个if判断(这点类似【AQS分析第三篇】中
                //ReentrantLock公平模式和非公平模式实现)

                //判断队列中是否已经有线程阻塞,保证后获取的线程必然加在阻塞线程后面,不能优先于
                //队列中的线程抢夺许可证
                if (hasQueuedPredecessors())
                    return -1;
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
}

//非公平模式:
final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
}


至此,Semaphore的基本源码(不包括限时获取)已经分析完毕,现在我们总结一下:

  • Semaphore定义:

1.初始化许可证数量,许可证数量=AQS中state

2.可定义为公平模式和非公平模式

3.内部类实现AQS,为共享模式

  •  许可证获取

通过计算:

剩余可用许可证数量(x)=当前可用许可证数量(y)-请求获取许可证数量(z)

通过判断:

x>0:可获取成功

x<=0:获取失败,阻塞

  •  许可证释放

将许可证返回到Semaphore,并传播唤醒队列中阻塞的线程。

 三、回答开篇

  • 问题一:该类提供了怎样的功能?使用场景?

1.功能:通过上文分析,该类提供了一些虚拟的许可证,限制多线程对于逻辑或者物理资源的并发访问。每个线程在执行之前必须先获取许可证。在执行之后将许可证返回。

2.使用场景:控制同一时刻访问逻辑或者物理资源的最大线程数。

  • 问题二:该类的实现原理是什么?

通过上文源码分析,我们可以了解到:

1.AQS共享模式

2.公平模式和非公平模式

如果你看了博文【AQS分析系列前几篇】,那你就能很容易理解其实现原理。

 结束。

-------------------------------------------------------------------------------------------------------------------

知其然,更要知其所以然...

-------------------------------------------------------------------------------------------------------------------

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值