小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。 本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。
注:掘金潜水怪今日起开更 注:老后端不水图
前言
前文介绍了CountDownLatch和CyclicBarrier跨线程通信,现在给大家介绍一个平时使用较多的线程信号同步工具Semaphore,我们通常都把他和ReentrantLock与synchronized进行比较,后两者只允许一个线程访问某一个资源(下次着重讲一下,这里主要讲前者),而前者Semaphore可以允许多个线程同时访问某一段资源。我们还是从介绍、方法、场景、原理四个角度开展学习,马上开始。
一:介绍
官方介绍:计数信号量。 从概念上讲,信号量维护一组许可。 如有必要,每个acquire块直到许可可用,然后获取它。 每个release增加一个许可,可能会释放一个阻塞的收单方。 但是,没有使用实际的许可对象; Semaphore只是计算可用的数量并相应地采取行动。
信号量通常用于限制可以访问某些(物理或逻辑)资源的线程数。
我来解释一下:
二:方法
Semaphore的主要方法有:
void acquire() //获取令牌,没获取直接阻塞(可以被中断),方法(可传参 permits 令牌个数)
复制代码
void acquireUninterruptibly()//同上(不可被中断),方法(可传参 permits 令牌个数)
复制代码
boolean tryAcquire() //尝试获取一个令牌,没有则返回false,不会阻塞,方法(可传参 permits 令牌个数)
复制代码
void release() // 释放令牌,方法(可传参 permits 令牌个数)
复制代码
int availablePermits() //当前剩余可用令牌个数
复制代码
int drainPermits() // 获取并返回所有立即可用的许可证
复制代码
void reducePermits(int reduction) //减少部分令牌
复制代码
boolean isFair() //是否公平锁
复制代码
boolean hasQueuedThreads() //是否有线程在阻塞等待获取令牌
复制代码
int getQueueLength() //获取阻塞线程个数
复制代码
Semaphore 构造方法有两个,公平锁和非公平锁,官方解释是:非公平锁效率要优于公平队列,默认构造方法采用的是非公平锁
new Semaphore(2,true);可以创建公平锁对象。
来一个简单的示例帮助大家理解:
@SneakyThrows
static void singleSema(){
Semaphore semaphore = new Semaphore(1);
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"获取令牌中");
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"拿到令牌,执行业务操作");
Thread.sleep((int)(Math.random()*1000));
semaphore.release();
System.out.println(Thread.currentThread().getName()+"释放令牌");
}
}, "张三").start();
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"获取令牌中");
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"拿到令牌,执行业务操作");
Thread.sleep((int)(Math.random()*1000));
semaphore.release();
System.out.println(Thread.currentThread().getName()+"释放令牌");
}
},"李四").start();
System.out.println("主线程获取令牌中");
semaphore.acquire();
System.out.println("主线程获取到令牌");
Thread.sleep(100);
System.out.println("主线程释放令牌");
semaphore.release();
}
复制代码
这是一个令牌传递操作,令牌资源只能同时一个人拥有,跟lock和syn实现的效果是一样的。
一起来看运行结果:
张三获取令牌中
主线程获取令牌中
主线程获取到令牌
李四获取令牌中
主线程释放令牌
张三拿到令牌,执行业务操作
张三释放令牌
李四拿到令牌,执行业务操作
李四释放令牌
复制代码
令牌只能在一个人手中。
三:场景
Semaphore的使用场景比较简单,令牌数量是可控的,它既可以用来控制线程资源独占,也可以允许多个线程共享某一资源。在使用的过程中为了避免阻塞可以采用tryQcuire方法来获取令牌。
场景一:线程资源独占
如上所示,实现类似syn的锁效果。参考上述示例代码。。。
场景二:线程资源共享
学校进行短跑测评,要求测出每个学生的100m短跑耗时,但是跑道只有3条,全校共有5名学生,为了最大限度的利用跑道,决定每个学生跑完之后立即让下一个人进入跑道,不再凑齐3跳跑道全满后再开始。
源代码:
@SneakyThrows
static void mulitSema(){
Semaphore semaphore = new Semaphore(3);
new Thread(new Dog(semaphore),"张三").start();
new Thread(new Dog(semaphore),"李四").start();
new Thread(new Dog(semaphore),"王五").start();
new Thread(new Dog(semaphore),"马六").start();
new Thread(new Dog(semaphore),"韩七").start();
Thread.sleep(2000);
while (semaphore.hasQueuedThreads()){
System.out.println("isfair::"+semaphore.isFair()+",availablepermits::"+semaphore.availablePermits()
+",queuelength::"+semaphore.getQueueLength()+",tryacquire::"+semaphore.tryAcquire());
semaphore.release();
Thread.sleep(100);
}
System.out.println("主线程结束::"+Thread.currentThread().getName());
}
复制代码
class Dog implements Runnable{
private Semaphore semaphore;
public Dog(Semaphore semaphores){
semaphore = semaphores;
}
@Override
public void run() {
try {
Thread.sleep((int)(Math.random()*1000));
System.out.println(Thread.currentThread().getName()+"::正在等待起跑指令");
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"::接到起跑指令");
Thread.sleep((int)(Math.random()*1000));
System.out.println(Thread.currentThread().getName()+"::跑步完成,释放信号");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
复制代码
简单分析代码,当某个学生跑完后立即通知下个学生占用该跑道开始跑步。
运行结果:
马六::正在等待起跑指令
马六::接到起跑指令
张三::正在等待起跑指令
张三::接到起跑指令
韩七::正在等待起跑指令
韩七::接到起跑指令
张三::跑步完成,释放信号
马六::跑步完成,释放信号
李四::正在等待起跑指令
韩七::跑步完成,释放信号
王五::正在等待起跑指令
isfair::false,availablepermits::0,queuelength::2,tryacquire::false
李四::接到起跑指令
isfair::false,availablepermits::0,queuelength::1,tryacquire::false
王五::接到起跑指令
主线程结束::main
李四::跑步完成,释放信号
王五::跑步完成,释放信号
复制代码
我们可以发现三条跑道得到了充分的利用,整体测试时间也得到了极大的缩短。
四:原理
- 第一:初始化
- 我们构造new Semaphore(2) 方法后,会创建一个非公平的锁的同步阻塞队列,也可以构建公平锁同步阻塞队列。
- 然后会把传入的令牌数量赋值给队列的state状态,state表示当前剩余的令牌信号数目。
- 第二:获取令牌
- 当前线程会尝试去同步队列获取一个令牌,获取令牌的过程也就是使用原子的操作去修改同步队列的state ,获取一个令牌则修改为state=state-1。
- 如果state>=0,表示获取令牌成功。
- 如果state<0,令牌获取数量不足,此时会创建一个Node节点并加入到阻塞队列,直到有空余令牌。
- 第三:释放令牌
- 线程释放一个令牌,state修改为state=state+1,表示释放成功,释放失败或返回false需要再次尝试。
- 当有个令牌被释放了,系统会唤醒一个等待中的阻塞线程。
- 被唤醒的线程会运算state=state-1,判断state>=0表示获取令牌成功。
感谢大家的阅读,欢迎评论区留言讨论。
近期评论