Java并发编程之Semaphore详解

简介

计数信号量(Counting Semaphore)用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。计数信号量还可以用来实现某种资源池,或者对容器施加边界。

Semaphore中管理着一组虚拟许可(permit),许可的初始数量可以通过构造函数来指定,在执行操作时可以首先获得许可(只要还有剩余的许可),并在使用之后释放许可。如果没有剩余许可,那么获取许可的acquire操作将阻塞直到有许可(或者被中断、或者超时)。通过release方法释放一个许可信号量。计数信号量的一种简化形式是二值信号量,即初始值为1的Semaphore,二值信号量可以用做互斥体(mutex),并具备不可重入的加锁语义:谁拥有了这个唯一的许可,谁就拥有了互斥锁。

Semaphore源码详解

Semaphore的类图如下:


从上图中可以看出,Semaphore的实现依赖于其内部类Sync,Sync类继承自AQS,同时它还有两个子类NonfairSync非公平同步器以及FairSync公平同步器。

Semaphore提供了两个构造函数:

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

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

Semaphore的默认构造方法Semaphore(int permits)接受一个整形的数字,表示可用的许可证数量,它也对应于AQS中的同步状态。Semaphore(10)表示允许10个线程获取许可证,也就是最大并发数是10。通过该构造函数创建的sync默认是非公平的。

Semaphore的另一个构造方法Semaphore(int permits, boolean fair)除了可以设置许可证数量之外,还可以控制同步器的公平性。

信号量获取

Semaphore提供了acquire()方法来获取许可(即信号量):

public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

acquireSharedInterruptibly(int)是AQS实现的模板方法,该方法会以共享响应中断的方式获取同步状态:

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

tryAcquireShared(int)是由子类实现的方法,对于Semaphore来说,因为Semaphore内部有公平和非公平两种自定义同步器,所以tryAcquireShared(int)方法的实现有两种:

公平锁

公平锁对应的tryAcquireShared(int)方法如下:

protected int tryAcquireShared(int acquires) {
    for (;;) {
        // 判断当前节点是否为同步队列的首节点,保证锁的获取是公平的
        if (hasQueuedPredecessors())
            return -1;
        // 获取同步状态,即剩余许可证的数量
        int available = getState();
        // 计算新的同步状态,即再分配acquires个许可证之后的剩余值
        int remaining = available - acquires;
        // 以CAS操作来设置同步状态,即修改许可证的数量
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}
非公平锁

非公平锁对应的tryAcquireShared(int)方法如下:

protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}

它会调用SyncnonfairTryAcquireShared(int)方法:

final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        // 获取同步状态,即剩余许可证的数量
        int available = getState();
        // 计算新的同步状态,即再分配acquires个许可证之后的剩余值
        int remaining = available - acquires;
        // 以CAS操作来设置同步状态,即修改许可证的数量
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

从上述源码可以看出,公平与非公平的差别主要是有没有调用hasQueuedPredecessors()方法,该方法是用来判断同步队列中当前节点是否有前驱节点。如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此要先等待前驱线程获取锁,以保证公平性。

信号量释放

Semaphore提供了release()方法来释放许可:

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

releaseShared(int)是AQS实现的模板方法:

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

tryReleaseShared(int)是由Sync子类实现的方法:

protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        // 获取同步状态,即许可证的剩余量
        int current = getState();
        // 计算新的同步状态,即线程释放releases个许可证后的数量
        int next = current + releases;
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        // 以CAS操作修改同步状态,即修改许可证的数量
        if (compareAndSetState(current, next))
            return true;
    }
}

Semaphore还提供了一些其他方法:

  • int availablePermits():返回此信号量中剩余许可证数
  • int getQueueLength():返回正在等待获取许可证的线程数
  • boolean hasQueuedThreads():判断是否有线程正在等待获取许可证
  • void reducePermits(int reductions):减少reductions个许可证,是protected方法
  • Collection<Thread> getQueuedThreads():返回所有等待获取许可证的线程集合,是protected方法

应用程序示例

我们看一个Semaphore的应用示例:

public class SemaphoreTest {
	// 自定义工作线程
		private static class Worker extends Thread {
			private CountDownLatch countDownLatch;
			private Semaphore semaphore;
			
			public Worker(CountDownLatch countDownLatch, Semaphore semaphore) {
				this.countDownLatch = countDownLatch;
				this.semaphore = semaphore;
			}
			
			@Override
			public void run() {
				super.run();
				
				boolean hasAcquire = false;
				try {
					countDownLatch.await();
					semaphore.acquire();
					hasAcquire = true;
					System.out.println(Thread.currentThread().getName() + "开始执行");
					// 工作线程开始处理,这里用Thread.sleep()来模拟业务处理
					Thread.sleep(1000);
					System.out.println(Thread.currentThread().getName() + "执行完毕");
				} catch (Exception e) {
					e.printStackTrace();
				} finally {
					if (hasAcquire) {
						semaphore.release();
					}
				}
			}
		}

	public static void main(String[] args) {
		int threadCount = 10;
		int permitCount = 3;
		
		CountDownLatch countDownLatch = new CountDownLatch(1);
		Semaphore semaphore = new Semaphore(permitCount);
		
		for (int i = 0; i < threadCount; i++) {
			Worker worker = new Worker(countDownLatch, semaphore);
			worker.start();
		}
		
		countDownLatch.countDown();
	}
}

运行结果(不唯一):

Thread-1开始执行
Thread-9开始执行
Thread-0开始执行
Thread-9执行完毕
Thread-1执行完毕
Thread-0执行完毕
Thread-3开始执行
Thread-2开始执行
Thread-4开始执行
Thread-3执行完毕
Thread-2执行完毕
Thread-4执行完毕
Thread-6开始执行
Thread-5开始执行
Thread-7开始执行
Thread-7执行完毕
Thread-5执行完毕
Thread-6执行完毕
Thread-8开始执行
Thread-8执行完毕

Semaphore有3个许可,但是有10个线程要执行,从执行结果中可以看出,每次都是3个线程一组开始执行,也就是每次只能有3个线程获取许可。

相关博客

AbstractQueuedSynchronizer简介

AbstractQueuedSynchronizer同步队列详解

AbstractQueuedSynchronizer共享式同步状态获取与释放

Java并发编程之ReentrantLock详解

参考资料

方腾飞:《Java并发编程的艺术》

Doug Lea:《Java并发编程实战》

猜你喜欢

转载自blog.csdn.net/qq_38293564/article/details/80562903