控制并发线程数的Semaphore
1.简介
信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施, 它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。
2.概念
Semaphore分为单值和多值两种,前者只能被一个线程获得,后者可以被若干个线程获得。
以一个停车场运作为例。为了简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆不受阻碍的进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,如果又离开两辆,则又可以放入两辆,如此往复。
在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。
更进一步,信号量的特性如下:信号量是一个非负整数(车位数),所有通过它的线程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Wait(等待) 和 Release(释放)。 当一个线程调用Wait(等待)操作时,它要么通过然后将信号量减一,要么一直等下去,直到信号量大于一或超时。Release(释放)实际上是在信号量上执行加操作,对应于车辆离开停车场,该操作之所以叫做“释放”是因为加操作实际上是释放了由信号量守护的资源。
在java中,还可以设置该信号量是否采用公平模式,如果以公平方式执行,则线程将会按到达的顺序(FIFO)执行,如果是非公平,则可以后请求的有可能排在队列的头部。
JDK中定义如下:
Semaphore(int permits, boolean fair)
创建具有给定的许可数和给定的公平设置的Semaphore。
Semaphore当前在多线程环境下被扩放使用,操作系统的信号量是个很重要的概念,在进程控制方面都有应用。Java并发库Semaphore 可以很轻松完成信号量控制,Semaphore可以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。比如在Windows下可以设置共享文件的最大客户端访问个数。
Semaphore实现的功能就类似厕所有5个坑,假如有10个人要上厕所,那么同时只能有多少个人去上厕所呢?同时只能有5个人能够占用,当5个人中 的任何一个人让开后,其中等待的另外5个人中又有一个人可以占用了。另外等待的5个人中可以是随机获得优先机会,也可以是按照先来后到的顺序获得机会,这取决于构造Semaphore对象时传入的参数选项。单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合。
3.案例一:
- package SemaPhore;
- import java.util.Random;
- import java.util.concurrent.*;
- public class Test {
- public static void main(String[] args) {
- //线程池
- ExecutorService executor = Executors.newCachedThreadPool();
- //定义信号量,只能5个线程同时访问
- final Semaphore semaphore = new Semaphore(5);
- //模拟20个线程同时访问
- for (int i = 0; i < 20; i++) {
- final int NO = i;
- Runnable runnable = new Runnable() {
- public void run() {
- try {
- //获取许可
- semaphore.acquire();
- //availablePermits()指的是当前信号灯库中有多少个可以被使用
- System.out.println("线程" + Thread.currentThread().getName() +"进入,当前已有" + (5-semaphore.availablePermits()) + "个并发");
- System.out.println("index:"+NO);
- Thread.sleep(new Random().nextInt(1000)*10);
- System.out.println("线程" + Thread.currentThread().getName() + "即将离开");
- //访问完后,释放
- semaphore.release();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- };
- executor.execute(runnable);
- }
- // 退出线程池
- executor.shutdown();
- }
- }
下面是模拟一个连接池,控制同一时间最多只能有50个线程访问。
- import java.util.UUID;
- import java.util.concurrent.Semaphore;
- import java.util.concurrent.TimeUnit;
- public class TestSemaphore extends Thread {
- public static void main(String[] args) {
- int i = 0;
- while (i < 500) {
- i++;
- new TestSemaphore().start();
- try {
- Thread.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- /**
- * 控制某资源同时被访问的个数的类 控制同一时间最后只能有50个访问
- */
- static Semaphore semaphore = new Semaphore(50);
- static int timeout = 500;
- public void run() {
- try {
- Object connec = getConnection();
- System.out.println("获得一个连接" + connec);
- Thread.sleep(300);
- releaseConnection(connec);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- public void releaseConnection(Object connec) {
- /* 释放许可 */
- semaphore.release();
- System.out.println("释放一个连接" + connec);
- }
- public Object getConnection() {
- try {/* 获取许可 */
- boolean getAccquire = semaphore.tryAcquire(timeout, TimeUnit.MILLISECONDS);
- if (getAccquire) {
- return UUID.randomUUID().toString();
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- throw new IllegalArgumentException("timeout");
- }
- }
Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务。
我们关于定时/周期操作都是通过Timer来实现的。但是Timer有以下几种危险
a. Timer是基于绝对时间的。容易受系统时钟的影响。
b. Timer只新建了一个线程来执行所有的TimeTask。所有TimeTask可能会相关影响
c. Timer不会捕获TimerTask的异常,只是简单地停止。这样势必会影响其他TimeTask的执行。
2.ScheduledThreadPoolExecutor
鉴于 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,内部包装后是一样的效果;所以把头两个方法几乎当成一种调度,那么三种情况分别是:
1、 进行一次延迟调度:延迟delay这么长时间,单位为:TimeUnit传入的的一个基本单位,例如:TimeUnit.SECONDS属于提供好的枚举信息;(适合于方法1和方法2)。
2、 多次调度,每次依照上一次预计调度时间进行调度,例如:延迟2s开始,5s一次,那么就是2、7、12、17,如果中间由于某种原因导致线程不够用,没有得到调度机会,那么接下来计算的时间会优先计算进去,因为他的排序会被排在前面,有点类似Timer中的:scheduleAtFixedRate方法,只是这里是多线程的,它的方法名也叫:scheduleAtFixedRate,所以这个是比较好记忆的(适合方法3)
3、 多次调度,每次按照上一次实际执行的时间进行计算下一次时间,同上,如果在第7秒没有被得到调度,而是第9s才得到调度,那么计算下一次调度时间就不是12秒,而是9+5=14s,如果再次延迟,就会延迟一个周期以上,也就会出现少调用的情况(适合于方法3);
4、 最后补充execute方法是一次调度,期望被立即调度,时间为空
- public class ScheduledThreadPoolExecutorDemo {
- public static void main(String[] args) {
- ScheduledThreadPoolExecutor scheduledExecutor = new ScheduledThreadPoolExecutor(1);
- /**
- * new timeTaskForException() 要执行的任务线程
- * 1000:延迟多长时间执行
- * 2000: 每隔多少长时间执行一次
- * TimeUnit.MILLISECONDS:时间单位
- */
- scheduledExecutor.scheduleAtFixedRate(new timeTaskForException(), 1000, 2000, TimeUnit.MILLISECONDS);
- scheduledExecutor.scheduleAtFixedRate(new timeTaskForPrintSYSTime(), 1000, 3000, TimeUnit.MILLISECONDS);
- }
- static class timeTaskForException implements Runnable{
- public void run() {
- throw new RuntimeException();
- }
- }
- static class timeTaskForPrintSYSTime implements Runnable {
- public void run() {
- System.out.println(System.nanoTime());
- }
- }
- }
(java多线程与并发)java并发库中的阻塞队列--BlockingQueue
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 来实现的
源码:
- public interface ReadWriteLock {
- /**
- * Returns the lock used for reading.
- *
- * @return the lock used for reading.
- */
- Lock readLock();
- /**
- * Returns the lock used for writing.
- *
- * @return the lock used for writing.
- */
- Lock writeLock();
- }
5.案例一:
- package WriteReadLock;
- import java.util.Random;
- import java.util.concurrent.locks.Lock;
- import java.util.concurrent.locks.ReadWriteLock;
- import java.util.concurrent.locks.ReentrantReadWriteLock;
- public class ReadWriterLockTest {
- public static void main(String[] args) {
- final Queue q3 = new Queue();
- for(int i=0;i<3;i++)
- {
- new Thread(){
- public void run(){
- while(true){
- q3.get();
- }
- }
- }.start();
- }
- for(int i=0;i<3;i++)
- {
- new Thread(){
- public void run(){
- while(true){
- q3.put(new Random().nextInt(10000));
- }
- }
- }.start();
- }
- }
- }
- class Queue{
- //共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。
- private Object data = null;
- //得到读写锁
- ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
- /**
- * 将用于读的get()和写的put()放在同一个类中这样是为了对同一个资源data进行操作,形成互斥
- */
- /**
- * 进行读操作
- * 可以多个读线程同时进入,写线程不能执行
- */
- public void get(){
- //获取读锁,并加锁
- Lock readLock = readWriteLock.readLock();
- readLock.lock();
- try {
- System.out.println(Thread.currentThread().getName() + " be ready to read data!");
- Thread.sleep((long)(Math.random()*1000));
- System.out.println(Thread.currentThread().getName() + "have read data :"+ data);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }finally {
- //!!!!!!注意:锁的释放一定要在trycatch的finally中,因为如果前面程序出现异常,锁就不能释放了
- //释放读锁
- readLock.unlock();
- }
- }
- /**
- * 进行写操作
- * 只能一个写线程进入,读线程不能执行
- */
- public void put(Object data){
- //获取写锁,并加锁
- Lock writeLock = readWriteLock.writeLock();
- writeLock.lock();
- try {
- System.out.println(Thread.currentThread().getName() + " be ready to write data!");
- Thread.sleep((long)(Math.random()*1000));
- this.data = data;
- System.out.println(Thread.currentThread().getName() + " have write data: " + data);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }finally {
- //释放写锁
- writeLock.unlock();
- }
- }
- }
在加入读写锁之后:读的过程中,不会有写
6.案例二:
- package WriteReadLock;
- import java.util.HashMap;
- import java.util.Map;
- import java.util.Random;
- import java.util.concurrent.locks.Lock;
- import java.util.concurrent.locks.ReadWriteLock;
- import java.util.concurrent.locks.ReentrantReadWriteLock;
- public class CacheDemo {
- //用map来模拟缓存
- Map<String,Object> cache = new HashMap<String,Object>();
- ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
- public static void main(String[] args) {
- final CacheDemo cacheDemo = new CacheDemo();
- for(int i=0;i<6;i++)
- {
- new Thread(){
- public void run(){
- while(true){
- System.out.println( cacheDemo.getData("key1").toString());
- }
- }
- }.start();
- }
- }
- Lock readLock = readWriteLock.readLock();
- Lock writeLock = readWriteLock.writeLock();
- //这里必须要用volatie当一个写线程设置value="aaaabbbb",一定要让其他的线程知道vlue的变化,这样就不会被重复写
- volatile Object value;
- public Object getData(String key){
- readLock.lock();
- try {
- Thread.sleep(300);
- System.out.println(" read");
- value = cache.get(key);
- if (value == null) {
- //这里已经加了读锁,读锁中写是不能允许的,所以要把这个锁释放掉
- readLock.unlock();
- writeLock.lock();
- //防止,当多个写者进程在等待,前面的写进程已经赋值了,value已经不为空了后面的等着的写进程仍然继续赋值
- if(value == null){
- System.out.println("find null");
- value="aaaabbbb";
- cache.put(key, value);
- System.out.println("write");
- }
- writeLock.unlock();
- //从新加上读锁
- readLock.lock();
- }
- return value;
- } catch (Exception e) {
- e.printStackTrace();
- }finally {
- readLock.unlock();
- }
- return null;
- }
- }