控制并发线程数的Semaphore
java
1.简介
信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施, 它负责协调各个线程, 以保证它们可以正确、合理的使用公共资源。编程
2.概念
Semaphore分为单值和多值两种,前者只能被一个线程得到,后者能够被若干个线程得到。缓存
以一个停车场运做为例。为了简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时若是同时来了五辆车,看门人容许其中三辆不受阻碍的进入,而后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,若是又离开两辆,则又能够放入两辆,如此往复。多线程
在这个停车场系统中,车位是公共资源,每辆车比如一个线程,看门人起的就是信号量的做用。并发
更进一步,信号量的特性以下:信号量是一个非负整数(车位数),全部经过它的线程(车辆)都会将该整数减一(经过它固然是为了使用资源),当该整数值为零时,全部试图经过它的线程都将处于等待状态。在信号量上咱们定义两种操做: Wait(等待) 和 Release(释放)。 当一个线程调用Wait(等待)操做时,它要么经过而后将信号量减一,要么一直等下去,直到信号量大于一或超时。Release(释放)其实是在信号量上执行加操做,对应于车辆离开停车场,该操做之因此叫作“释放”是由于加操做其实是释放了由信号量守护的资源。dom
在java中,还能够设置该信号量是否采用公平模式,若是以公平方式执行,则线程将会按到达的顺序(FIFO)执行,若是是非公平,则能够后请求的有可能排在队列的头部。
JDK中定义以下:
Semaphore(int permits, boolean fair)
建立具备给定的许可数和给定的公平设置的Semaphore。socket
Semaphore当前在多线程环境下被扩放使用,操做系统的信号量是个很重要的概念,在进程控制方面都有应用。Java并发库Semaphore 能够很轻松完成信号量控制,Semaphore能够控制某个资源可被同时访问的个数,经过 acquire() 获取一个许可,若是没有就等待,而 release() 释放一个许可。好比在Windows下能够设置共享文件的最大客户端访问个数。ide
Semaphore实现的功能就相似厕全部5个坑,假若有10我的要上厕所,那么同时只能有多少我的去上厕所呢?同时只能有5我的可以占用,当5我的中 的任何一我的让开后,其中等待的另外5我的中又有一我的能够占用了。另外等待的5我的中能够是随机得到优先机会,也能够是按照先来后到的顺序得到机会,这取决于构造Semaphore对象时传入的参数选项。单个信号量的Semaphore对象能够实现互斥锁的功能,而且能够是由一个线程得到了“锁”,再由另外一个线程释放“锁”,这可应用于死锁恢复的一些场合。性能
3.案例一:ui
下面是模拟一个链接池,控制同一时间最多只能有50个线程访问。
Timer 的优势在于简单易用,但因为全部任务都是由同一个线程来调度,所以全部任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到以后的任务。
咱们关于定时/周期操做都是经过Timer来实现的。可是Timer有如下几种危险
a. Timer是基于绝对时间的。容易受系统时钟的影响。
b. Timer只新建了一个线程来执行全部的TimeTask。全部TimeTask可能会相关影响
c. Timer不会捕获TimerTask的异常,只是简单地中止。这样势必会影响其余TimeTask的执行。
鉴于 Timer 的上述缺陷,Java 5 推出了基于线程池设计的ScheduledThreadPoolExecutor。其设计思想是,每个被调度的任务都会由线程池中一个线程去执行,所以任务是并发执行的,相互之间不会受到干扰。需 要注意的是,只有当任务的执行时间到来时,ScheduedExecutor 才会真正启动一个线程,其他时间 ScheduledExecutor 都是在轮询任务的状态。
有如下四个调度器的方法:
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit); public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit); public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit); public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit);
那么这四个方法有什么区别呢?其实第一个和第二个区别不大,一个是Runnable、一个是Callable,内部包装后是同样的效果;因此把头两个方法几乎当成一种调度,那么三种状况分别是:
一、 进行一次延迟调度:延迟delay这么长时间,单位为:TimeUnit传入的的一个基本单位,例如:TimeUnit.SECONDS属于提供好的枚举信息;(适合于方法1和方法2)。
二、 屡次调度,每次依照上一次预计调度时间进行调度,例如:延迟2s开始,5s一次,那么就是二、七、十二、17,若是中间因为某种缘由致使线程不够用,没有获得调度机会,那么接下来计算的时间会优先计算进去,由于他的排序会被排在前面,有点相似Timer中的:scheduleAtFixedRate方法,只是这里是多线程的,它的方法名也叫:scheduleAtFixedRate,因此这个是比较好记忆的(适合方法3)
三、 屡次调度,每次按照上一次实际执行的时间进行计算下一次时间,同上,若是在第7秒没有被获得调度,而是第9s才获得调度,那么计算下一次调度时间就不是12秒,而是9+5=14s,若是再次延迟,就会延迟一个周期以上,也就会出现少调用的状况(适合于方法3);
四、 最后补充execute方法是一次调度,指望被当即调度,时间为空
1.阻塞队列的概念
阻塞队列与普通队列的区别在于,当队列是空的时,从队列中获取元素的操做将会被阻塞,或者当队列是满时,往队列里添加元素的操做会被阻塞。试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其余的线程往空的队列插入新的元素。一样,试图往已满的阻塞队列中添加新元素的线程一样也会被阻塞,直到其余的线程使队列从新变得空闲起来,如从队列中移除一个或者多个元素,或者彻底清空队列,下图展现了如何经过阻塞队列来合做:
线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素
从刚才的描述能够看出,发生阻塞起码得知足下面至少一个条件: (前提:队列是有界的)
1.从队列里取元素时,若是队列为空,则代码一直等在这里(即阻塞),直到队列里有东西了,拿到元素了,后面的代码才能继续
2.向队列里放元素时,若是队列满了(即放不下更多元素),则代码也会卡住,直到队列里的东西被取走了(即:有空位能够放新元素了),后面的代码才能继续
下面先使用Object.wait()和Object.notify()、非阻塞队列实现生产者-消费者模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
public
class
Test {
private
int
queueSize =
10
;
private
PriorityQueue<Integer> queue =
new
PriorityQueue<Integer>(queueSize);
public
static
void
main(String[] args) {
Test test =
new
Test();
Producer producer = test.
new
Producer();
Consumer consumer = test.
new
Consumer();
producer.start();
consumer.start();
}
class
Consumer
extends
Thread{
public
void
run() {
consume();
}
private
void
consume() {
while
(
true
){
synchronized
(queue) {
while
(queue.size() ==
0
){
try
{
System.out.println(
"队列空,等待数据"
);
queue.wait();
}
catch
(InterruptedException e) {
e.printStackTrace();
queue.notify();
}
}
queue.poll();
//每次移走队首元素
queue.notify();
System.out.println(
"从队列取走一个元素,队列剩余"
+queue.size()+
"个元素"
);
}
}
}
}
class
Producer
extends
Thread{
public
void
run() {
produce();
}
private
void
produce() {
while
(
true
){
synchronized
(queue) {
while
(queue.size() == queueSize){
try
{
System.out.println(
"队列满,等待有空余空间"
);
queue.wait();
}
catch
(InterruptedException e) {
e.printStackTrace();
queue.notify();
}
}
queue.offer(
1
);
//每次插入一个元素
queue.notify();
System.out.println(
"向队列取中插入一个元素,队列剩余空间:"
+(queueSize-queue.size()));
}
}
}
}
}
|
这个是经典的生产者-消费者模式,经过阻塞队列和Object.wait()和Object.notify()实现,wait()和notify()主要用来实现线程间通讯。
具体的线程间通讯方式(wait和notify的使用)在后续问章中会讲述到。
下面是使用阻塞队列实现的生产者-消费者模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
public
class
Test {
private
int
queueSize =
10
;
private
ArrayBlockingQueue<Integer> queue =
new
ArrayBlockingQueue<Integer>(queueSize);
public
static
void
main(String[] args) {
Test test =
new
Test();
Producer producer = test.
new
Producer();
Consumer consumer = test.
new
Consumer();
producer.start();
consumer.start();
}
class
Consumer
extends
Thread{
public
void
run() {
consume();
}
private
void
consume() {
while
(
true
){
try
{
queue.take();
System.out.println(
"从队列取走一个元素,队列剩余"
+queue.size()+
"个元素"
);
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
}
}
class
Producer
extends
Thread{
public
void
run() {
produce();
}
private
void
produce() {
while
(
true
){
try
{
queue.put(
1
);
System.out.println(
"向队列取中插入一个元素,队列剩余空间:"
+(queueSize-queue.size()));
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
|
有没有发现,使用阻塞队列代码要简单得多,不须要再单独考虑同步和线程间通讯的问题。
在并发编程中,通常推荐使用阻塞队列,这样实现能够尽可能地避免程序出现意外的错误。
阻塞队列使用最经典的场景就是socket客户端数据的读取和解析,读取数据的线程不断将数据放入队列,而后解析线程不断从队列取数据解析。还有其余相似的场景,只要符合生产者-消费者模型的均可以使用阻塞队列。
3.实现原理:
这里只贴几段主要的代码,体会一下思想:
1
2
3
4
5
6
7
8
|
/** Main lock guarding all access */
final
ReentrantLock lock;
/** Condition for waiting takes */
private
final
Condition notEmpty;
/** Condition for waiting puts */
private
final
Condition notFull;
|
这3个变量很重要,ReentrantLock重入锁,notEmpty检查不为空的Condition 以及 notFull用来检查队列未满的Condition
Condition是一个接口,里面有二个重要的方法:
await() : Causes the current thread to wait until it is signalled or interrupted. 即阻塞当前线程,直到被通知(唤醒)或中断
singal(): Wakes up one waiting thread. 唤醒阻塞的线程
再来看put方法:(jdk 1.8)
1
2
3
4
5
6
7
8
9
10
11
12
|
public
void
put(E e)
throws
InterruptedException {
checkNotNull(e);
final
ReentrantLock lock =
this
.lock;
lock.lockInterruptibly();
try
{
while
(count == items.length)
notFull.await();
enqueue(e);
}
finally
{
lock.unlock();
}
}
|
1.先获取锁
2.而后用while循环检测元素个数是否等于items长度,若是相等,表示队列满了,调用notFull的await()方法阻塞线程
3.不然调用enqueue()方法添加元素
4.最后解锁
1
2
3
4
5
6
7
8
9
10
|
private
void
enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final
Object[] items =
this
.items;
items[putIndex] = x;
if
(++putIndex == items.length)
putIndex =
0
;
count++;
notEmpty.signal();
}
|
这是添加元素的代码(jdk 1.8),注意最后一行notEmpty.signal()方法,表示添加完元素后,调用singal()通知等待(从队列中取元素)的线程,队列不空(有值)啦,能够来取东西了。
相似的take()与dequeue()方法则至关于逆过程(注:一样都是jdk 1.8)
1
2
3
4
5
6
7
8
9
10
11
|
public
E take()
throws
InterruptedException {
final
ReentrantLock lock =
this
.lock;
lock.lockInterruptibly();
try
{
while
(count ==
0
)
notEmpty.await();
return
dequeue();
}
finally
{
lock.unlock();
}
}
|
相似的:
1. 先加锁
2. 若是元素个数为空,表示队列已空,调用notEmpty的await()阻塞线程,直接队列里又有新元素加入为止
3. 而后调用dequeue 从队列里删除元素
4. 解锁
dequeue方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
private
E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final
Object[] items =
this
.items;
@SuppressWarnings
(
"unchecked"
)
E x = (E) items[takeIndex];
items[takeIndex] =
null
;
if
(++takeIndex == items.length)
takeIndex =
0
;
count--;
if
(itrs !=
null
)
itrs.elementDequeued();
notFull.signal();
return
x;
}
|
倒数第2行,元素移除后,调用notFull.singnal唤醒等待(向队列添加元素的)线程,队列有空位了,能够向里面添加元素了。
1.排他锁(互斥锁)的概念:
synchronized,ReentrantLock这些锁都是排他锁,这些锁同一时刻只容许一个线程进行访问。
2.读写锁的概念:
分为读锁和写锁,多个读锁不互斥,读锁和写锁互斥,写锁与写锁互斥。
3.读写锁的好处:
为了提升性能,Java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,若是没有写锁的状况下,读是无阻塞的,在必定程度上提升了程序的执行效率。
原来使用的互斥锁只能同时间有一个线程在运行,如今的读写锁同一时刻能够多个读锁同时运行,这样的效率比原来的排他锁(互斥锁)效率高。
4.读写锁的原理分析:
Java中读写锁有个接口java.util.concurrent.locks.ReadWriteLock,也有具体的实现ReentrantReadWriteLock,
lock方法 是基于CAS 来实现的
源码:
5.案例一:
在加入读写锁以后:读的过程当中,不会有写
6.案例二: