(java多线程并发)控制并发线程数的Semaphore、ScheduledThreadPoolExcutor、BlockingQueue、ReadWriteLock...
控制并发线程数的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.案例一:
- packageSemaPhore;
- importjava.util.Random;
- importjava.util.concurrent.*;
- publicclassTest{
- publicstaticvoidmain(String[]args){
- //线程池
- ExecutorServiceexecutor=Executors.newCachedThreadPool();
- //定义信号量,只能5个线程同时访问
- finalSemaphoresemaphore=newSemaphore(5);
- //模拟20个线程同时访问
- for(inti=0;i<20;i++){
- finalintNO=i;
- Runnablerunnable=newRunnable(){
- publicvoidrun(){
- try{
- //获取许可
- semaphore.acquire();
- //availablePermits()指的是当前信号灯库中有多少个可以被使用
- System.out.println("线程"+Thread.currentThread().getName()+"进入,当前已有"+(5-semaphore.availablePermits())+"个并发");
- System.out.println("index:"+NO);
- Thread.sleep(newRandom().nextInt(1000)*10);
- System.out.println("线程"+Thread.currentThread().getName()+"即将离开");
- //访问完后,释放
- semaphore.release();
- }catch(Exceptione){
- e.printStackTrace();
- }
- }
- };
- executor.execute(runnable);
- }
- //退出线程池
- executor.shutdown();
- }
- }
下面是模拟一个连接池,控制同一时间最多只能有50个线程访问。
- importjava.util.UUID;
- importjava.util.concurrent.Semaphore;
- importjava.util.concurrent.TimeUnit;
- publicclassTestSemaphoreextendsThread{
- publicstaticvoidmain(String[]args){
- inti=0;
- while(i<500){
- i++;
- newTestSemaphore().start();
- try{
- Thread.sleep(1);
- }catch(InterruptedExceptione){
- e.printStackTrace();
- }
- }
- }
- /**
- *控制某资源同时被访问的个数的类控制同一时间最后只能有50个访问
- */
- staticSemaphoresemaphore=newSemaphore(50);
- staticinttimeout=500;
- publicvoidrun(){
- try{
- Objectconnec=getConnection();
- System.out.println("获得一个连接"+connec);
- Thread.sleep(300);
- releaseConnection(connec);
- }catch(InterruptedExceptione){
- e.printStackTrace();
- }
- }
- publicvoidreleaseConnection(Objectconnec){
- /*释放许可*/
- semaphore.release();
- System.out.println("释放一个连接"+connec);
- }
- publicObjectgetConnection(){
- try{/*获取许可*/
- booleangetAccquire=semaphore.tryAcquire(timeout,TimeUnit.MILLISECONDS);
- if(getAccquire){
- returnUUID.randomUUID().toString();
- }
- }catch(InterruptedExceptione){
- e.printStackTrace();
- }
- thrownewIllegalArgumentException("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方法是一次调度,期望被立即调度,时间为空
- publicclassScheduledThreadPoolExecutorDemo{
- publicstaticvoidmain(String[]args){
- ScheduledThreadPoolExecutorscheduledExecutor=newScheduledThreadPoolExecutor(1);
- /**
- *newtimeTaskForException()要执行的任务线程
- *1000:延迟多长时间执行
- *2000:每隔多少长时间执行一次
- *TimeUnit.MILLISECONDS:时间单位
- */
- scheduledExecutor.scheduleAtFixedRate(newtimeTaskForException(),1000,2000,TimeUnit.MILLISECONDS);
- scheduledExecutor.scheduleAtFixedRate(newtimeTaskForPrintSYSTime(),1000,3000,TimeUnit.MILLISECONDS);
- }
- staticclasstimeTaskForExceptionimplementsRunnable{
- publicvoidrun(){
- thrownewRuntimeException();
- }
- }
- staticclasstimeTaskForPrintSYSTimeimplementsRunnable{
- publicvoidrun(){
- 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{
@Override
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{
@Override
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{
@Override
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{
@Override
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 来实现的
源码:
- publicinterfaceReadWriteLock{
- /**
- *Returnsthelockusedforreading.
- *
- *@returnthelockusedforreading.
- */
- LockreadLock();
- /**
- *Returnsthelockusedforwriting.
- *
- *@returnthelockusedforwriting.
- */
- LockwriteLock();
- }
5.案例一:
- packageWriteReadLock;
- importjava.util.Random;
- importjava.util.concurrent.locks.Lock;
- importjava.util.concurrent.locks.ReadWriteLock;
- importjava.util.concurrent.locks.ReentrantReadWriteLock;
- publicclassReadWriterLockTest{
- publicstaticvoidmain(String[]args){
- finalQueueq3=newQueue();
- for(inti=0;i<3;i++)
- {
- newThread(){
- publicvoidrun(){
- while(true){
- q3.get();
- }
- }
- }.start();
- }
- for(inti=0;i<3;i++)
- {
- newThread(){
- publicvoidrun(){
- while(true){
- q3.put(newRandom().nextInt(10000));
- }
- }
- }.start();
- }
- }
- }
- classQueue{
- //共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。
- privateObjectdata=null;
- //得到读写锁
- ReadWriteLockreadWriteLock=newReentrantReadWriteLock();
- /**
- *将用于读的get()和写的put()放在同一个类中这样是为了对同一个资源data进行操作,形成互斥
- */
- /**
- *进行读操作
- *可以多个读线程同时进入,写线程不能执行
- */
- publicvoidget(){
- //获取读锁,并加锁
- LockreadLock=readWriteLock.readLock();
- readLock.lock();
- try{
- System.out.println(Thread.currentThread().getName()+"bereadytoreaddata!");
- Thread.sleep((long)(Math.random()*1000));
- System.out.println(Thread.currentThread().getName()+"havereaddata:"+data);
- }catch(InterruptedExceptione){
- e.printStackTrace();
- }finally{
- //!!!!!!注意:锁的释放一定要在trycatch的finally中,因为如果前面程序出现异常,锁就不能释放了
- //释放读锁
- readLock.unlock();
- }
- }
- /**
- *进行写操作
- *只能一个写线程进入,读线程不能执行
- */
- publicvoidput(Objectdata){
- //获取写锁,并加锁
- LockwriteLock=readWriteLock.writeLock();
- writeLock.lock();
- try{
- System.out.println(Thread.currentThread().getName()+"bereadytowritedata!");
- Thread.sleep((long)(Math.random()*1000));
- this.data=data;
- System.out.println(Thread.currentThread().getName()+"havewritedata:"+data);
- }catch(InterruptedExceptione){
- e.printStackTrace();
- }finally{
- //释放写锁
- writeLock.unlock();
- }
- }
- }
在加入读写锁之后:读的过程中,不会有写
6.案例二:
- packageWriteReadLock;
- importjava.util.HashMap;
- importjava.util.Map;
- importjava.util.Random;
- importjava.util.concurrent.locks.Lock;
- importjava.util.concurrent.locks.ReadWriteLock;
- importjava.util.concurrent.locks.ReentrantReadWriteLock;
- publicclassCacheDemo{
- //用map来模拟缓存
- Map<String,Object>cache=newHashMap<String,Object>();
- ReadWriteLockreadWriteLock=newReentrantReadWriteLock();
- publicstaticvoidmain(String[]args){
- finalCacheDemocacheDemo=newCacheDemo();
- for(inti=0;i<6;i++)
- {
- newThread(){
- publicvoidrun(){
- while(true){
- System.out.println(cacheDemo.getData("key1").toString());
- }
- }
- }.start();
- }
- }
- LockreadLock=readWriteLock.readLock();
- LockwriteLock=readWriteLock.writeLock();
- //这里必须要用volatie当一个写线程设置value="aaaabbbb",一定要让其他的线程知道vlue的变化,这样就不会被重复写
- volatileObjectvalue;
- publicObjectgetData(Stringkey){
- 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("findnull");
- value="aaaabbbb";
- cache.put(key,value);
- System.out.println("write");
- }
- writeLock.unlock();
- //从新加上读锁
- readLock.lock();
- }
- returnvalue;
- }catch(Exceptione){
- e.printStackTrace();
- }finally{
- readLock.unlock();
- }
- returnnull;
- }
- }