Semaphore信号量分析

Semaphore信号量分析

信号量是JUC包下的一个类,它和ReentrantLock,CountDownLatch和CyclicBarrier不同,锁是为了保证原子性,一般是排他的,只能一个线程获取到锁来执行业务。
信号量则可以初始化多个资源,多线程获取资源,如果资源足够,则都可以获取成功。
信号量也是基于AQS实现的,有公平非公平的概念。

例:连接池就比较适合使用信号量实现,使用信号量初始化连接池的连接数量,支持多线程同时去获取资源,如果资源足够,则顺利获取到连接资源,资源不足,线程进入AQS同步队列,等其他线程释放连接资源时在唤醒同步队列中的线程

例:也可以模拟一个景区,景区的接客流量数是有限的,将允许的流量数初始化到信号量中,购票线程去信号量中获取票数,如果票数不足,可以进入同步队列一直等待,也可以购票失败,等待其他线程释放票后,再重新购票



一,内部结构

1.1,同样内置了sync

    /** All mechanics via AbstractQueuedSynchronizer subclass */
    private final Sync sync;
    
    // 继承AQS,重写了些逻辑
	abstract static class Sync extends AbstractQueuedSynchronizer {...}
	
 	// 非公平逻辑
	static final class NonfairSync extends Sync {...}

	// 公平逻辑实现
	static final class FairSync extends Sync

	// 构造方法,可以初始化资源数,默认非公平模式
    public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }
	
	// 构造方法,初始化资源数,并且指定公平/非公平模式
    public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }

1.2,基本方法简介

1,acquire():获取资源,也可以获取指定个数的资源,获取不到就进入AQS同步队列等待,如果线程被中断,抛出异常
2,acquireUninterruptibly():与acquire一样,但是线程被设置中断位时也不结束,继续等待
3,tryAcquire():尝试获取资源,获取不到返回false
4,tryAcquire(long timeout, TimeUnit unit):指定时间内获取不到资源,返回false
5,release():释放一次资源,也可以释放指定个数的资源


提示:以下是本篇文章正文内容,下面案例仅供参考

二、信号量的基本使用

public class SemaphoreTest {
    // 景区有十张票
    static Semaphore semaphore = new Semaphore(10);

    private static Thread thread = new Thread(() -> {
        System.out.println("一家五口买票");
        try {
            // 阻塞方法,一次性买5张票,如果剩余票数不足,线程进入AQS同步队列,等待其他线程释放票
            semaphore.acquire(5);
            // 这时信号量中还剩5张票
            System.out.println("一家五口买到了5张票");

            // 模拟去景区
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 出景区后释放票
            semaphore.release(5);
        }
    });

    public static void main(String[] args) throws InterruptedException {
    	// 一家五口先去买票
        thread.start();
        // 留给子线程的启动时间
        Thread.sleep(100);

		// 其他人去信号量中去购票
        for (int i = 0; i < 15; i++) {
            int finalI = i;
            new Thread(() -> {
                try {
                	// 只买一张票,买不到死等
                    semaphore.acquire();
                    System.out.println(finalI + "获取到票");
					// 模拟去景区
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                	// 出景区后释放票
                    semaphore.release();
                }
            }).start();
        }

        // 主线程也尝试获取票,可能没票了,此时不等待,直接执行else
        if (semaphore.tryAcquire()) {
            System.out.println("main获取到资源");
        	// 释放资源
            semaphore.release();
        } else {
            System.out.println("main未获取到资源");
        }
    }
}

连接池获取数据库连接是同样的逻辑

三、内部方法分析

1. 分析acquire方法

代码如下(示例):

    public void acquire() throws InterruptedException {
    	// 默认获取一个资源
        sync.acquireSharedInterruptibly(1);
    }
    // 获取指定个数的资源
    public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
        // 线程被设置中断时,抛出异常
        if (Thread.interrupted())
            throw new InterruptedException();
        // 尝试获取资源,这里区分公平和非公平
        if (tryAcquireShared(arg) < 0)
        	// 如果返回值小于0,意味着剩余资源不足,将线程放入AQS同步队列
            doAcquireSharedInterruptibly(arg);
    }

	// tryAcquireShared: 非公平模式尝试获取资源
    final int nonfairTryAcquireShared(int acquires) {
    		// 死循环
            for (;;) {
            	// 获取剩余资源数
                int available = getState();
                // 减去当前所需资源
                int remaining = available - acquires;
                // 如果资源不足
                if (remaining < 0 ||
                	// 资源足够,使用CAS更新剩余资源数(如果CAS失败,由于是死循环,会重新获取资源)
                    compareAndSetState(available, remaining))
                    // 返回剩余资源数
                    return remaining;
            }
        }

	// 公平模式尝试获取资源
    protected int tryAcquireShared(int acquires) {
    		// 死循环
            for (;;) {
            	// 判断前面是否有排队的节点,因为是公屏模式,排队获取资源
                if (hasQueuedPredecessors())
                	// 还没有轮到当前线程获取资源
                    return -1;
                // 获取当前剩余资源数
                int available = getState();
                // 减去当前所需资源
                int remaining = available - acquires;
                // 如果资源不足
                if (remaining < 0 ||
                	// 资源足够,使用CAS更新剩余资源数(如果CAS失败,由于是死循环,会重新获取资源)
                    compareAndSetState(available, remaining))
                    // 返回剩余资源数
                    return remaining;
            }
        }
        
    // 获取资源失败,进入AQS同步队列
  	private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
  		// 将当前线程封装成Node,从AQS双向链表(同步队列)的尾部追加当前节点
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
        	// 死循环
            for (;;) {
            	// 获取当前节点的pre
                final Node p = node.predecessor();
                // 如果上一个节点是head头
                if (p == head) {
                	// 则再次尝试获取资源
                    int r = tryAcquireShared(arg);
                    // 如果获取成功
                    if (r >= 0) {
                    	// 需要结合JDK1.5代码来看,里面的propagate是为了唤醒后续的节点
                        setHeadAndPropagate(node, r);
                        // 节点被唤醒后会从队列中移除,此处方便GC回收
                        p.next = null; // help GC
                        failed = false;
                        // 结束死循环
                        return;
                    }
                }
                // 上一个节点不是head或者没拿到资源,当前线程进入AQS同步队列
                // 判断线程是否可以被挂起
                if (shouldParkAfterFailedAcquire(p, node) &&
                	// 线程挂起 LockSupport.park,进入WAITING
                    parkAndCheckInterrupt())
                    // 抛出中断异常也可以结束死循环
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

非公平模式:
尝试获取资源,如果资源数不足,返回负数,进入doAcquireSharedInterruptibly逻辑,将当前线程挂到AQS同步队列的尾部,然后判断上一节点是否是head,如果是再次获取一次资源,获取成功就会从队列中删除当前节点,结束
获取失败或者不是head,则线程挂起,进入WAITING等待唤醒重新获取资源
公平模式:
与非公平一样的流程,区别在于获取资源时判断前面是否有排队的节点,如果有,说明还没有轮到自己获取

tryAcquire没什么好分析的,上述代码包含tryAcquire逻辑

2.release方法分析

代码如下(示例):

    public void release() {
    	// 默认释放一次,也有释放指定次数的方法
        sync.releaseShared(1);
    }
    // 归还指定个数的资源
    public final boolean releaseShared(int arg) {
    	// 尝试归还,这个逻辑是信号量自己实现的,没有使用AQS的,且不区分公平还是非公平
        if (tryReleaseShared(arg)) {
        	// 归还资源成功,唤醒同步队列中的线程去获取资源
            doReleaseShared();
            return true;
        }
        return false;
    }
    // 尝试归还资源
     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");
                // 	CAS归还资源,失败重来
                if (compareAndSetState(current, next))
                	// 归还成功就结束死循环,所以几乎不会归还失败
                    return true;
            }   
     }
     
    // 唤醒后续节点的线程去获取资源
    private void doReleaseShared() {
        // 死循环
        for (;;) {
        	// 获取head
            Node h = head;
            // 判断是否有排队的节点
            if (h != null && h != tail) {
            	// 获取节点状态
                int ws = h.waitStatus;
                // 如果状态是-1,意味着后续有节点需要被唤醒
                if (ws == Node.SIGNAL) {
                	// 将-1改成0,如果失败,继续死循环
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    // 修改成功,根据node找到沉睡的线程并唤醒
                    unparkSuccessor(h);
                }
                // 状态等于0说明是正常节点
                else if (ws == 0 &&
                		// 将0修改成-3,起到向队列后面传播的特性
                		// 解决JDK1.5信号量中,有资源,但是队列中WAITING的线程没有被唤醒的问题
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            // 没有排队的节点,结束
            // 唯一出口
            if (h == head)                   // loop if head changed
                break;
        }
    }

总结

其他的源码方法基本和上述代码类似,没什么可聊的,大部分都使用了AQS的默认实现
AQS的的内部属性state,实现了AQS的锁都是使用CAS去更改state值,成功就是获取到了锁。信号量同样使用了这个变量,只是变成了资源数的概念,可以多线程去获取不会有并发问题,线程使用完资源后,就会归还,如果同步队列中有等待的线程,就会被唤醒,参加到资源的获取中

信号量比其他AQS的实现子类都要简单,解决JDK1.5信号量bug的代码,有兴趣可以研究

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值