Java并发包计数信号量Semaphore

前言

前面我们学习了AQS以及基于AQS实现的Du占锁ReentrantLock和基于ReentrantLock实现的同步辅助工具CyclicBarrier,本节我们学习JDK提供的另一个类Semaphore。Semaphore翻译过来就是“信号量”,JDK提供的这个Semaphore被称之为计数信号量。根据Java Doc的描述,Semaphore维护一个许可集或者一些资源,然后可以限制同时访问这一组许可(也可以称作资源)的线程数量。

 

在本质上,其实我们可以将Semaphore理解为一个“共享锁”,而当Semaphore所维护的许可集或者共享资源只有唯一的一个的时候,它就从“共享锁”退变为ReentrantLock的“Du占锁”,这个时候的信号量也称作“互斥信号量”Mutex。还记得我们在Java并发包核心框架AQS之一同步阻塞与唤醒续一章中自定义共享式同步组件实例时举的例子吗?十个人到只有3窗口柜台的银行办理业务的场景,其实Semaphore就是可以用来解决这种场景的一种共享锁,当然Semaphore的用处不仅仅限于此,我们还可以根据Semaphore限制可以访问某些资源的线程数目的特性,完成很多不同场景的不同需求。

 

使用示例

首先我们还是从使用示例着手了解Semaphore,毕竟深入了解之前,对Semaphore有个直观的初步了解很有必要。我们就拿之前“十个人到只有3窗口柜台的银行办理业务的场景”来举例说明Semaphore的用法。

public static void main(String[] args) {
	Semaphore semaphore = new Semaphore (3);
	for (int i = 0; i < 10 ; i++) {  
		new handleThread(semaphore, "线程"+i).start();  
	}  
}

static class handleThread extends Thread{  
	
	private Semaphore semaphore;  
	  
	public handleThread(Semaphore semaphore, String name) {  
		super();  
		this.semaphore = semaphore;  
		setName(name);  
	}  
	  
	@Override  
	public void run() {  
		System.out.println(Thread.currentThread().getName() +" 开始等候");  
		try {
			semaphore.acquire(); //注意这里获取共享资源失败不应该执行资源释放过程。
		} catch (InterruptedException e1) {
			return;
		}
		try {  
			System.out.println(Thread.currentThread().getName() +" 开始办理");  
			Thread.sleep(5000);  
			System.out.println(Thread.currentThread().getName() +" 办理结束");  
		} catch(Exception e){  
			e.printStackTrace();  
		}finally{  
			semaphore.release();  
		}  
	}  
}

    通过Semaphore构造方法传入3,设定了这一组共享资源数量为3,然后创建了10个线程,每个线程通过semaphore.acquire()申请一个窗口资源,办理完成semaphore.release()释放窗口资源。可见使用Semaphore很方便的达到了让十个线程共享式访问一组(3个)共享资源的作用。值得注意的是,semaphore.acquire()方法是会抛出中断异常的,切记不能再它抛出异常时去释放共享资源,否则将会出错。

 

Semaphore源码分析

通过简单的了解,我们知道Semaphore其实就是对共享锁的实现,接下来我们看看其源码实现。首先我们从其类的结构开始:

从类结构可以发现,它的实现方式和ReentrantLock几乎是一样的,也是将对同步器的具体实现代理到了抽象静态内部类Sync,在此基础上分别实现了公平和非公平的模式,唯一的不同在于Semaphore并没有实现Lock接口。所以Semaphore就不能称作是同步锁。接着,我们看看Semaphore除开构造方法之外提供的方法列表,

 Semaphore主要的方法就这些了,还有几个返回等待队列状态的方法很简单就不列举了。从列举的方法可以看出,其主要集中对在阻塞式和非阻塞式获取许可的实现。下面我们对主要的方法源码进行分析。

 

构造方法

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

    从构造方法可以看出,Semaphore默认情况下也是使用的非公平的模式,这是出于非公平排序的吞吐量通常要比公平模式要高的考虑。

 

非公平获取许可

final int nonfairTryAcquireShared(int acquires) {
	for (;;) {
		int available = getState();//当前可用许可
		int remaining = available - acquires;
                //如果剩下的许可数满足申请的许可数,直接尝试通过CAS获取许可
		if (remaining < 0 || compareAndSetState(available, remaining))
			return remaining;
	}
}

    在Semaphore类的中那四个tryAcquire命名的非阻塞式方法以及非公平模式下的四个阻塞式acquire*方法最终在尝试获取共享资源的时候都是执行的nonfairTryAcquireShared()方法,它的逻辑很简单,如果当前有足够的许可剩余,直接通过CAS获取许可,成功就返回,失败才有走AQS的入队逻辑,阻塞起来等待唤醒。

 

公平式获取许可

protected int tryAcquireShared(int acquires) {
	for (;;) {
                //hasQueuedPredecessors方法很简单,表示查看是否有非当前线程的其他线程正在等待获取共享资源
                //如果队列为空,或者当前线程是头节点的后继节点就返回false
		if (hasQueuedPredecessors())
			return -1;
		int available = getState();
		int remaining = available - acquires;
		if (remaining < 0 ||
			compareAndSetState(available, remaining))
			return remaining;
	}
}

    不同于非公平式获取许可,公平式获取许可的逻辑和ReentrantLock中获取公平锁类似,在尝试获取共享资源之前需要先通过hasQueuedPredecessors()方法判断当前线程是否是FIFO队列中最有资格获取共享资源的线程,否则进入FIFO队列排队。

 

drainPermits方法的含义

对于查看许可数量的两个方法,分别是availablePermits()方法和drainPermits()方法,单凭注释可能很难区别它们的含义,特别是drainPermits(),而availablePermits()很好理解,它其实就是返回当前还剩余的许可数量,直接调用的是AQS的getState()方法,返回当前state变量的值,而drainPermits()是什么意思呢?它最终调用的是Semaphore的抽象内部类Sync的drainPermits()方法:

final int drainPermits() {
	for (;;) {
		int current = getState();
		if (current == 0 || compareAndSetState(current, 0))
			return current;
	}
}

    通过源码我们发现,当剩余许可数为0时直接返回0;当许可数不为0时,使用CAS操作尝试直接将当前许可数设置为0,直到成功才返回原始剩余的许可数。可见,drainPermits()方法其实就是在还剩有许可的时,立即将剩余许可清0,并返回清0之前还剩余的实际许可数。

 

内存可见性

Semaphore其实就是对共享锁的一种实现,只是他没有实现Lock接口,所以不能直接称之为“锁”,但是它依然满足happens-before中的锁定规则,即“一个unlock操作先行发生于后面对同一个锁的lock操作”。所以,某个线程对release方法的调用happens-before紧接着的另一个线程对aquire方法的成功调用。也就是说,在某个线程执行release方法之前对共享变量的修改,对另一个紧跟着的线程成功执行aquire方法后是立即可见的。

 

Semaphore其他应用场景

用Semaphore来实现一个有界容量的List:

public class BoundedList<T> {  
	  
    private final List<T> list;  
    private final Semaphore semaphore;  
  
    public BoundedList(int bound) {  
        list = Collections.synchronizedList(new LinkedList<T>());  
        semaphore = new Semaphore(bound);  
    }  
  
    public boolean add(T obj) throws InterruptedException {  
        semaphore.acquire();  
        boolean addedFlag = false;  
        try {  
            addedFlag = list.add(obj);  
        } finally {  
            if (!addedFlag) {  
                semaphore.release();  
            }  
        }  
        return addedFlag;  
    }  
  
    public boolean remove(Object obj) {  
        boolean removedFlag = list.remove(obj);  
        if (removedFlag) {  
            semaphore.release();  
        }  
        return removedFlag;  
    }  
      
    // 其他操作委托给底层的List,这里只列举出一个方法  
    public T get(int index) {  
        return list.get(index);  
    }  
      
    // 其他方法……  
  
}

在使用Semaphore时,切记不能再semaphore.acquire()方法抛出异常之后释放资源,所以这里 add(T obj)方法直接将中断异常抛出了方法本身。

猜你喜欢

转载自pzh9527.iteye.com/blog/2421581