Semaphore(信号量)是一个线程同步结构,用于在线程间传递信号,以避免出现信号丢失,或者像锁一样用于保护一个关键区域.
Semaphore可以用于做流量控制,特别公用资源有限的应用场景,比如 数据库连接
Semaphore当前在多线程环境下被扩放使用,操作系统的信号量是个很重要的概念,在进程控制方面都有应用。
Semaphore可以控制某个资源可被同时访问的个数,
通过acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。比如在Windows下可以设置共享文件的最大客户端访问个数。
其中在执行semaphore.acquire()方法时,阻塞时间不同,因此前五个数据在打印时顺序不同,但之后每次都只会释放一个接受一个,
故此,之后都是按顺序来排列的。
Semaphore 注意事项:
1.它的原理就是AQS。 Semaphore只是使用AQS的一种简单例子。
Semaphore借助了线程同步框架AQS。AQS的分析可以参考文章Java同步框架AbstractQueuedSynchronizer,
2. Semaphore(int permits, boolean fair)公平与非公平:
当一个线程 release 释放了一个许可后,fair 决定了正在等待的线程该由谁获取许可,
如果是公平竞争则等待时间最长的线程(基本上是最早建的线程)获取,如果是非公平竞争则随机选择一个线程获取许可。不传 fair 的构造函数默认采用非公开竞争。
fair这个参数则表示是否是公平的,即等待时间越久的线程越能优先获得许可访问的权限。
3.需要注意的是,任何线程在获得许可之后,使用共享资源完毕都需要执行归还操作,否则会有线程一直在等待。
// 阻塞函数,直到有可访问的线程,才继续运行
semaphore.acquire();
4.使用流程:
Semaphore实现为一种基于计数的信号量,Semaphore管理着一组虚拟的许可集合,这种许可可以作为某种凭证,来管理资源,在一些资源有限的场景下很有实用性,比如数据库连接,应用可初始化一组数据库连接,然后通过使用Semaphore来管理获取连接的许可,任何线程想要获得一个连接必须首先获得一个许可,然后再凭这个许可获得一个连接,这个许可将持续到这个线程归还了连接。
在使用上,任何一个线程都需要通过acquire来获得一个Semaphore许可,这个操作可能会阻塞线程直到成功获得一个许可,因为资源是有限的,所以许可也是有限的,没有获得资源就需要阻塞等待其他线程归还Semaphore,而归还Semaphore操作通过release方法来进行,release会唤醒一个等待在Semaphore上的一个线程来尝试获得许可。
5.实现互斥锁功能:
比如任何时刻只能有一个线程获得许可,那么设置Semaphore的数量为1,一个线程获得这个Semaphore之后,任何到来的通过acquire来尝试获得许可的线程都会被阻塞,直到这个持有Semaphore的线程调用了release方法来释放Semaphore。
Semaphore s = new Semaphore(1)",也就是该信号量的初始permits是1,但是在此后每次调用release方法都会导致permits加一。 如果能限制permits最大值1,最小值0,那就是真正的Mutex了。
6.信号量用于线程同步,互斥量用户保护资源的互斥访问。
信号量与互斥量的区别:
互斥量用于线程的互斥,信号线用于线程的同步。
互斥量值只能为0/1,信号量值可以为非负整数。信号量可以实现多个同类资源的多线程互斥和同步。
互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。
7.信号量解决生产者-消费者问题?
更多 Java 并发编程方面的文章,请参见文集《Java 并发编程》
Lock
Lock是一个抽象概念。使得只有一个线程可以访问某个资源,并且Lock是不能被其他线程共享的。
Mutex
全程 MUTual EXclusion。
目的:保护共享资源。
典型的例子就是买票:票是共享资源,现在有两个线程同时过来买票。如果你不用 Mutex 在线程里把票锁住,那么就可能出现“把同一张票卖给两个不同的人(线程)”的情况。
Semaphore
目的:调度线程:一些线程生产(increase)同时另一些线程消费(decrease),Semaphore 可以让生产和消费保持合乎逻辑的执行顺序。
有的人用 Semaphore 也可以把上面例子中的票“保护"起来,以防止共享资源冲突,必须承认这是可行的,
但是 Semaphore 不是让你用来做这个的;如果你要做这件事,请用 Mutex。
一个最典型的使用 Semaphore 的场景:
a
源自一个线程,b
源自另一个线程,计算 c = a + b
也是一个线程。显然,第三个线程必须等第一、二个线程执行完毕它才能执行。在这个时候,我们就需要调度线程了:让第一、二个线程执行完毕后,再执行第三个线程。
Semephore类是java.util.concurrent包下处理并发的工具类,Semephore能够控制任务访问资源的数量,如果资源不够,则任务阻塞,等待其他资源的释放。
比如一个停车场有三个车位,当车位空余数量小于3时,车辆可以进入停车场停车。如果停车场已经没有了空余车位,后面来的车就不能进入停车场,
只能在停车场外等待,等其他车辆离开之后才能进入。
Semephore类的主要方法
公平锁和非公平锁:程序在执行并发任务的时候,拿到同步锁的任务执行代码,其他任务阻塞等待,一旦同步锁被释放,CPU会正在等待的任务分配资源,获取同步锁。
在这里又两种策略,CPU默认从等待的任务中随机分配,这是非公平锁;
公平锁是按照等待时间优先级来分配,等待的时间越久,先获取任务锁。其内部是一个同步列队实现的。
Semaphore 的使用
所在包:java.util.concurrent:
信号量 Semaphore 可以控制同时访问某个资源的线程个数。
-
public Semaphore(int permits)
构造方法,设置许可的个数,默认为非公平锁 -
public Semaphore(int permits, boolean fair)
构造方法,设置许可的个数,可以设置为公平锁(即等待越久的线程优先获得 permits)或非公平锁 -
void acquire()
获得一个许可 permits,没有的话,线程就阻塞 -
void acquire(int arg)
获得多个许可 permits,没有的话,线程就阻塞 -
boolean tryAcquire()
获得一个许可 permits,没有的话就返回 false,线程不阻塞 -
boolean tryAcquire(int permits)
获得多个许可 permits,没有的话就返回 false,线程不阻塞 -
void release()
释放一个许可 permits,在释放之前,必须先获得许可 permits -
void release(int permits)
释放多个许可 permits,在释放之前,必须先获得许可 permits -
int availablePermits()
得到当前可用的许可数目
acquire();获取许可,Semephore任务数加一
release();释放许可,Semephore任务数减一
还有几个方法如tryAcquire()尝试获取许可,返回boolean值,不阻塞。availablePermits()还剩几个任务许可,等等几个方法和Lock类的用法相似。
示例:
假设当前有 5 辆车,10 个司机,每个司机轮流用车。
// 同时只能有 5 个线程访问某个资源 车
private static Semaphore semaphore = new Semaphore(5);
public static void main(String[] args) {
// 10 个司机
for (int i = 1; i <= 10; i++) {
(new Driver(i)).start();
}
}
static class Driver extends Thread {
private int i;
public Driver(int i) {
this.i = i;
}
public void run() {
try {
semaphore.acquire();
System.out.println("Driver " + i + " is using car");
Thread.sleep(1000);
System.out.println("Driver " + i + " return back car");
semaphore.release();
} catch (InterruptedException e) {
}
}
}
}
可能的输出如下:
Driver 1 is using car
Driver 6 is using car
Driver 5 is using car
Driver 2 is using car
Driver 3 is using car
Driver 3 return back car
Driver 5 return back car
Driver 4 is using car
Driver 2 return back car
Driver 1 return back car
Driver 6 return back car
Driver 9 is using car
Driver 8 is using car
Driver 7 is using car
Driver 10 is using car
Driver 4 return back car
Driver 10 return back car
Driver 8 return back car
Driver 7 return back car
Driver 9 return back
Semaphore 可以很轻松完成信号量控制,Semaphore可以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。
Semaphore
Semaphore 有两个构造函数,参数为许可的个数 permits 和是否公平竞争 fair。通过 acquire 方法能够获得的许可个数为 permits,如果超过了这个个数,就需要等待。当一个线程 release 释放了一个许可后,fair 决定了正在等待的线程该由谁获取许可,
如果是公平竞争则等待时间最长的线程获取,如果是非公平竞争则随机选择一个线程获取许可。不传 fair 的构造函数默认采用非公开竞争。
Semaphore(int permits)
Semaphore(int permits, boolean fair)
一个线程可以一次获取一个许可,也可以一次获取多个。 在 acquire 等待的过程中,如果线程被中断,acquire 会抛出中断异常,
如果希望忽略中断继续等待可以调用 acquireUninterruptibly 方法。同时提供了 tryAcquire 方法尝试获取,获取失败返回 false,获取成功返回 true。
tryAcquire 方法可以在获取不到时立即返回,也可以等待一段时间。
需要注意的是,没有参数的 tryAcquire 方法在有许可可以获取的情况下,无论有没有线程在等待都能立即获取许可,即便是公平竞争也能立即获取。
public void acquire()
public void acquireUninterruptibly()
public boolean tryAcquire()
public boolean tryAcquire(long timeout, TimeUnit unit)
public void release()
public void acquire(int permits)
public void acquireUninterruptibly(int permits)
public boolean tryAcquire(int permits)
public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
public void release(int permits)
使用示例
如下的示例中,测试方法 test 创建了多个线程,每个线程启动后都调用 acquire 方法,然后延时 5s 模仿业务耗时,最后调用 release 方法释放许可。
public class SemaphoreTest {
private int threadNum;
private Semaphore semaphore;
public SemaphoreTest(int permits,int threadNum, boolean fair) {
this.threadNum = threadNum;
semaphore = new Semaphore(permits,fair);
}
private void println(String msg){
SimpleDateFormat sdf = new SimpleDateFormat("[YYYY-MM-dd HH:mm:ss.SSS] ");
System.out.println(sdf.format(new Date()) + msg);
}
public void test(){
for(int i = 0; i < threadNum; i ++){
new Thread(() -> {
try {
semaphore.acquire();
println(Thread.currentThread().getName() + " acquire");
Thread.sleep(5000);//模拟业务耗时
println(Thread.currentThread().getName() + " release");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
公平与非公平
在上述的示例中,如果 fair 传的是 true,则各个线程公平竞争,即按照等待时间的长短决定谁先获取许可。
以 9 个线程竞争 3 个许可为例,执行结果如下,首选是线程 0、1、2 获取了许可,5s 后线程 3、4、5 获取了许可,最后是线程 6、7、8 获取许可,
顺序基本上与创建线程并启动的先后顺序一致,也与各个线程等待的时间基本相符。
[2017-08-20 21:47:21.817] Thread-0 acquire
[2017-08-20 21:47:21.817] Thread-2 acquire
[2017-08-20 21:47:21.817] Thread-1 acquire
[2017-08-20 21:47:26.830] Thread-1 release
[2017-08-20 21:47:26.830] Thread-0 release
[2017-08-20 21:47:26.830] Thread-4 acquire
[2017-08-20 21:47:26.830] Thread-3 acquire
[2017-08-20 21:47:26.831] Thread-2 release
[2017-08-20 21:47:26.831] Thread-5 acquire
[2017-08-20 21:47:31.831] Thread-4 release
[2017-08-20 21:47:31.831] Thread-3 release
[2017-08-20 21:47:31.831] Thread-6 acquire
[2017-08-20 21:47:31.831] Thread-7 acquire
[2017-08-20 21:47:31.832] Thread-5 release
[2017-08-20 21:47:31.832] Thread-8 acquire
[2017-08-20 21:47:36.831] Thread-6 release
[2017