深入理解 Semaphore 【源码分析】

Semaphore

Semaphore 是信号量的意思,一般用来控制同时访问某个资源的线程数量,协调各个线程合理的访问公共资源,Semaphore 的底层依赖的是 AQS。
Semaphore 使用计数器来控制对共享资源的访问, 如果计数器大于0,则表示允许访问共享资源, 如果为 0,则表示共享资源已经达到访问的上限就拒绝访问, 计数器的计数的就是允许同时访问共享资源的线程数。

知识储备传送门:

深入理解 AbstractQueuedSynchronizer(AQS)【源码分析】
深入理解 ReentrantLock 【源码分析】
CAS的使用以及底层原理详解
深入理解 CountDownLatch 【源码分析】

Semaphore 的应用场景:

控制并发量,通常用于那些资源有明确访问数量限制的场景,常用于限流 ,比如停车场场景,停车场内部车位有限,同时只能停有限车辆,比如窗口办业务场景,窗口有限,同时只能有限的人同时办业务。

Semaphore 使用简单举例:

public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2);
        for (int a = 0; a < 4; a++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println("当前线程获取许可,线程名称:" + Thread.currentThread().getName() + ";当前时间:" + System.currentTimeMillis() / 1000);
                    Thread.sleep(2000);
                    semaphore.release();
                    System.out.println("当前线程释放许可,线程名称:" + Thread.currentThread().getName() + ";当前时间:" + System.currentTimeMillis() / 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println();
            }).start();
        }
    }

执行结果:

当前线程获取许可,线程名称:Thread-0;当前时间:1712322271
当前线程获取许可,线程名称:Thread-1;当前时间:1712322271
当前线程释放许可,线程名称:Thread-0;当前时间:1712322273
当前线程获取许可,线程名称:Thread-3;当前时间:1712322273
当前线程获取许可,线程名称:Thread-2;当前时间:1712322273
当前线程释放许可,线程名称:Thread-1;当前时间:1712322273
当前线程释放许可,线程名称:Thread-2;当前时间:1712322275
当前线程释放许可,线程名称:Thread-3;当前时间:1712322275

执行结果分析:

我们的案例代码是想同时启动 4 个线程,Semaphore 只有两个信号量,我们发现 1712322271 时候,有两个线程获取了许可,执行了业务代码,1712322273 时刻有两个线程释放了许可,同时另外两个线程才获取到许可,开始执行业务代码,达到了使用 Semaphore 信号量控制并发的目的。

Semaphore 常用方法:

  • acquire() :获取一个执行许可,在获取到执行许可之前或者被其他线程中断之前,线程一直会被阻塞。
  • acquire(int permits) :获取 permits 个执行许可,在获取到执行许可之前或者被其他线程中断之前,线程一直会被阻塞。
  • acquireUninterruptibly() :获取一个执行许可,在获取到执行许可之前(忽略中断),线程一直会被阻塞。
  • tryAcquire() :尝试获取一个执行许可,返回获取的结果,线程不会被阻塞。
  • release() :释放一个执行许可,同时唤醒一个在阻塞中小获取执行许可的线程。
  • hasQueuedThreads () :队列中是否还存在等待的线程。
  • getQueueLength() :获取等待队列中的线程数
  • drainPermits() :清空所有执行许可,返回清空了多少个执行许可。
  • availablePermits() :获取有效的执行许可数量。

Semaphore 类结构:

在这里插入图片描述

类结构分析:

Semaphore 类内部总共存在 Sync、NonfairSync、FairSync 三个类,NonfairSync(非公平锁) 与 FairSync(公平锁) 类都继承 Sync 类,Sync 类继承自 AbstractQueuedSynchronizer 抽象类,这跟我们前面分析的 ReentrantLock 很像。

Semaphore 源码分析

Semaphore#acquire 方法源码分析:

//从信号量中获取许可,阻塞直到有一个信号量可用,或者线程被中断
 public void acquire() throws InterruptedException {
		//这里本质是调用 AQS#acquireSharedInterruptibly 方法
        sync.acquireSharedInterruptibly(1);
    }
	

//以共享的模式获取信号量
public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        //检查线程中断状态 如果中断则中止
		if (Thread.interrupted())
            throw new InterruptedException();
		//以共享的模式去获取信号量 返回小于0 表示获取信号量失败
        if (tryAcquireShared(arg) < 0)
			//进入阻塞队列
            doAcquireSharedInterruptibly(arg);
    }

acquire 方法逻辑比较简单,就是一些简单的判断和对一些方法的封装,我们接下来分析一下 tryAcquireShared、 doAcquireSharedInterruptibly 方法。

NonfairSync#tryAcquireShared 源码分析:

AQS 把 tryAcquireShared 方法交给了子类重写,因此 tryAcquireShared 方法实际上调用的是 NonfairSync 或者 FairSync 的 tryAcquireShared 方法,这里我们分析 NonfairSync 的 tryAcquireShared 方法,因为 Semaphore 的默认实现也是 Semaphore。

//以非公平的方式获取信号量许可 只是一个方法封装
protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}

//以非公平的方式获取信号量许可
final int nonfairTryAcquireShared(int acquires) {
	//自旋 保证一定能够获取到剩余的信号量许可 有可能返回负数
	for (;;) {
		//获取可用的信号量 getState 获取 volatile 修饰的变量 state
		int available = getState();
		//用可用的信号量减去想要获取的信号量就是剩余的信号量
		int remaining = available - acquires;
		if (remaining < 0 ||
			compareAndSetState(available, remaining))
			//1.如果剩余的信号量小于 0 
			//2.设置信号量为刚刚减法得到的信号量 并使用 CAS 设置给 state 
			//返回获取到的信号量 结束自旋
			return remaining;
	}
 }

AQS#doAcquireSharedInterruptibly 方法源码分析:

//就是将当前线程加入等待队列
private void doAcquireSharedInterruptibly(int arg)
	throws InterruptedException {
	//将当先线程封装成一个 node 节点
	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) {
					//剩余信号量大于等于0 表示获取信号量许可成功 设置头节点并传播信号量
					setHeadAndPropagate(node, r);
					//把刚刚的头结点的后驱节点设置为 null 方法JVM GC
					p.next = null; // help GC
					failed = false;
					return;
				}
			}
			 //根据当前节点的前驱节点 判断当前节点是否应该被阻塞
			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) {
			//1.剩余信号量大于0 
			//2.头结点不为空
			//3.头接待的状态为 SIGNAL
			//4.头节点就是刚刚设置的节点 且为空
			//5.头接待的状态为 SIGNAL 
			//SIGNAL 状态 则其后驱节点等待被唤醒 也可以理解为当前节点将要释放锁 需要唤醒后续节点
			//获取当前节点的后驱节点
		   Node s = node.next;
			//当前节点的后驱节点不为空 且也是共享模式
            if (s == null || s.isShared())
				//释放信号量许可
                doReleaseShared();
        }
    }
	
 //根据当前节点的前驱节点 判断当前节点是否应该被阻塞
 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	//当前节点的前驱节点的状态
	int ws = pred.waitStatus;
	if (ws == Node.SIGNAL)
		// 根据 Node  源码中的状态解析 我们知道当前节点的前驱节点处于唤醒状态
		return true;
	if (ws > 0) {
	   // 根据 Node  源码中的状态解析 我们知道 前驱节点是取消状态 我们需要在队列中移除它 并循环移除它前面的节点 并找到有效的前驱节点
		do {
			node.prev = pred = pred.prev;
		} while (pred.waitStatus > 0);
		pred.next = node;
	} else {
		//当前节点的前驱节点既不是唤醒状态 也不是取消节点 那就 CAS 设置当前节点的前驱节点状态为 Node.SIGNAL 阻塞状态
		//此时前驱节点的状态 只能是0  或者是  PROPAGATE -3
		compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
	}
	//不阻塞
	return false;
}
	
	
//设置节点状态为取消节点
private void cancelAcquire(Node node) {
   //为空判断
	if (node == null)
		return;

	//设置当前节点的所属线程为空
	node.thread = null;

	//获取当前节点的前驱节点
	Node pred = node.prev;
	//前驱节点状态大于0 表示取消节点 一直找到有效的前驱节点
	while (pred.waitStatus > 0)
		node.prev = pred = pred.prev;

	//获取当前节点的 前驱节点的 后驱节点
	Node predNext = pred.next;

	//设置当前节点的状态为 取消状态
	node.waitStatus = Node.CANCELLED;

	
	if (node == tail && compareAndSetTail(node, pred)) {
	//如果当前节点是尾节点 且 CAS 操作将当前节点的前驱节点设置为 尾节点成功  则 CAS 操作将当前节点的前驱节点的后驱节点设置为 null
		compareAndSetNext(pred, predNext, null);
	} else {
	//如果当前节点是尾节点 且 CAS 操作将当前节点的前驱节点设置为 尾节点失败
	   //节点状态
		int ws;
		if (pred != head &&
			((ws = pred.waitStatus) == Node.SIGNAL ||
			 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
			pred.thread != null) {
			//当前节点的前驱节点不是头节点
			//1 当前节点的前驱节点状态为阻塞
			//2 当前节点的前驱节点状态小于0 且将前驱节点的状态设置为 Node.SIGNAL
			//1  2 有一个为 true 在判断当前节点的前驱节点线程是否为空
			//获取当前节点的后驱节点
			Node next = node.next;
			if (next != null && next.waitStatus <= 0)
				//当前节点的后驱节点不为空 且状态小于0
				//CAS 操作把当前节点的前驱节点的后驱节点设置为当前节点的后驱节点
				compareAndSetNext(pred, predNext, next);
		} else {
			//如果当前节点就是head 节点的后驱节点 或者不满足上述条件 就唤醒当前节点的后面的节点
			unparkSuccessor(node);
		}

		//被设置为取消的节点的后驱节点指向自己 方便JVM内存回收
		node.next = node; // help GC
	}
}

doAcquireSharedInterruptibly 方法就是把获取信号量许可失败的线程加入到阻塞队列中,在这个过程中主要由两个步骤,如下:

  • 将获取信号量许可失败的线程包装为 Node 节点添加到阻塞队列中。
  • 判断当前节点是否是头结点的后驱节点,如果是则再次尝试获取信号量许可,获取成功传播信号量使用情况并返回,否则则进行睡眠等待唤醒。

Semaphore#release 方法源码分析:

Semaphore#release 方法的作用就是释放信量许可,并唤醒后续节点的线程去获取信号量。

//释放信号量许可
public void release() {
	//调用AQS 的releaseShared方法
	sync.releaseShared(1);
}

//释放信号量许可
public final boolean releaseShared(int arg) {
	if (tryReleaseShared(arg)) {
		doReleaseShared();
		return true;
	}
	return false;
}

//释放信号量许可 Semaphore#tryReleaseShared 方法
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))
			// CAS 把上面计算出来的信号量设置给信号量许可 返回释放成功
			return true;
	}
}

//共享模式的释放操作 向后继者发出信号并确保传播
private void doReleaseShared() {
    //自旋
	for (;;) {
		//获取头节点
		Node h = head;
		//头结点不为空 且头结点是尾节点 表示有等待获取许可的节点
		if (h != null && h != tail) {
			//获取头节点的等待状态
			int ws = h.waitStatus;
			if (ws == Node.SIGNAL) {
				//节点为 SIGNAL 状态 则其后驱节点等待被唤醒 也可以理解为当前节点将要释放锁 需要唤醒后续节点
				if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
					//CAS 设置头节点的状态为0 0表示初始化节点
					continue;            // loop to recheck cases
				//唤醒头结点的后续节点
				unparkSuccessor(h);
			}
			//如果头结点的等待状态为 0 就设置等待状态为 PROPAGATE -3 表示要唤醒头节点后驱节点的后驱节点
			//这种情况发生在线程A 刚释放完线程许可通过 CAS compareAndSetWaitStatus(h, Node.SIGNAL, 0)
			//此时线程B 也要释放许可 发现等待状态为 0 此时线程B 执行 CAS compareAndSetWaitStatus(h, 0, Node.PROPAGATE)
			//这样以来就有释放了两个信号量许可  因此唤醒头节点后驱节点的后驱节点
			else if (ws == 0 &&
					 !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
					 // 并发情况下,可能会出现wa为0,需要状态为PROPAGATE,保证唤
				continue;                // loop on failed CAS
		}
		if (h == head)                   // loop if head changed
			break;
	}
}

Semaphore#release 释放信号量方法总结:

  • 释放信号量许可,使用 CAS 更新信号量许可值。
  • 唤醒阻塞队列中的有效节点(因为并发问题,引入了 PROPAGATE 状态),让节点所在的线程继续去争抢信号量许可。

Semaphore 总结:

  • 熟悉 Semaphore 信号量许可的获取和释放流程,其实就是共享锁的获取和释放流程。
  • 理解 Semaphore 的底层原理,其实是使用了 AQS 的共享锁模式。

如有错误的地方欢迎指出纠正。

  • 23
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值