文章目录
- 8.9 高级并发之阻塞队列与线程池
- 8.9.1 阻塞队列
- 8.9.2 线程池
8.9 高级并发之阻塞队列与线程池
8.9.1 阻塞队列
前面介绍的对象互斥锁无论是synchronized+wait)/notify()机制,还是lock+await()/signal()机制,都需要自行判断何时阻塞、何时唤醒,以及采用何种数据结构处理正在等待的多个线程,一旦线程同步协调,就容易产生死锁、饥饿等问题。Java.util.concurrent
包的BlockingQueue
接口通过队列的方式完成线程间的高效传输数据,提供强大的功能解决了多个生产者与多个消费者共享资源的问题,实现自动阻塞机制,不必担心何时阻塞,何时唤醒。当存储区的产品满时,BlockingQueue
能自动阻塞生产者放人,唤醒消费者
取出产品;而当存储区空时,BlockingQueue
也能自动阻塞消费者取出产品,唤醒生产者放入产品。此外,BlockingQueue
还提供了独立锁的线程同步机制,与互斥锁相比,独立锁能够高效地实现生产者和消费者真正地并行运行,同时访问共享资源。BlockingQueue
的核心方法如表所示。
方法 | 说明 |
offer(anObject) | 将anObject加到队列中,如果队列没有空间,则返回false |
offer(E o, long timeout,TimeUnit unit) | 将anObject加到队列中,如果队列没有空间,设定等待的时间 |
put(anObject) | 将anObject加到队列中,如果队列没有空间,则调用此方法的线程被阻断,直到队列中有空间再继续添加 |
poll(time) | 取走队列中排在首位的对象 |
poll(long timeout,TimeUnit unit) | 取走队首的对象,如果队列中没有可取的对象,则指定等待时间 |
take() | 取走队首的对象,如果队列中没有可取的对象,则等待直到有对象可取 |
drain To(Collection<?super E> c) | 一次性从队列中获取所有可用的数据添加到集合中 |
BlockingQueue
接口的具体类主要有ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousBlockingQueue
, 在程序中根据需来选用不同的具体类,通常ArrayBlockingQueue和LinkedBlockingQueue
两类足够可以处理线程同步问题了。以下是这四种类的使用说明:
1 ) ArrayBlockingQueue :这种BlockingQueue的大小必须指定,由构造方法带-一个int参数来指明其大小,BlockingQueue中的对象以FIFO (先人先出)的顺序排序。使用时需注意,生产者放入数据和消费者获取数据,都是共用一个对象互斥锁,因此两者无法真正并行运行。
2 )LinkedBlockingQueue :这种BlockingQueue的大小没有固定,可以指定大小,也可以不指定,不指定时,构造方法可以没有参数,默认最大值由Integer.MAX_ VALUE
决定,所含的对象以FIFO (先入先出)顺序排序。使用LinkedBlockingQueue时,生产者和消费者
分别采用独立的锁来控制数据同步,这也意味着在高并发的情况下,生产者和消费者可以并行地操作队列中的数据,以此来高效提升整个队列的并发性能。
3 )PriorityBlockingQueue :类似于LinkedBlockQueue,但其所含对象的排序方式不是FIFO,而是按照优先级排序。PriorityBlockingQueue
中存储的对象必须实现Comparable接口,队列通过这个接口的compare()
方法确定对象的优先级。
**4 )SynchronousBlockingQueue 😗*与ArrayBlockingQueue、LinkedBlockingQueue 不同,SynchronousBlockingQueue内部并没有数据缓存空间,数据直接在生产者和消费者线程之间传递,并不会将数据缓冲到队列中,因此遍历这个队列的操作也是不允许的。生产者线程的放入操作put必须等待消费者的取出操作take完成后再执行,反过来也一样。
**示例:**采用LinkedBlockingQueue实现生产者消费者问题
代码如下:
public class Product {
int id;
String name;
public Product(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Product{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
public class Producer implements Runnable {
BlockingQueue<Product> queue;//利用阻塞队列存储
public Producer(BlockingQueue<Product> queue) {
this.queue = queue;
}
@Override
public void run() {
try{
Product item1 = new Product(1,"ipad");
System.out.println("I have made a product: "+Thread.currentThread().getName());
queue.put(item1);//放入产品
System.out.println("I put in a product:"+item1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Consumer implements Runnable {
BlockingQueue<Product> queue;
public Consumer(BlockingQueue<Product> queue) {
this.queue = queue;
}
@Override
public void run() {
try{
Product temp = queue.take();//取出产品,如果队列为空,会阻塞当前线程
System.out.println("I took out "+temp);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ProductConsumer {
public static void main(String[] args) {
//创建一个LinkdeBlockQueue
BlockingQueue<Product>queue = new LinkedBlockingQueue<Product>();
//创建Consumer对象和Producer对象
Consumer consumer = new Consumer(queue);
Producer producer = new Producer(queue);
//创建5个Consumer线程和5个Producer线程
for (int i = 0; i < 5; i++) {
new Thread(producer,"Producer"+(i+1)).start();
new Thread(consumer,"consumer"+(i+1)).start();
}
}
}
运行结果如下:
I have made a product: Producer1
I have made a product: Producer4
I have made a product: Producer5
I have made a product: Producer2
I have made a product: Producer3
I put in a product:Product{id=1, name='ipad'}
I took out Product{id=1, name='ipad'}
I put in a product:Product{id=1, name='ipad'}
I put in a product:Product{id=1, name='ipad'}
I put in a product:Product{id=1, name='ipad'}
I took out Product{id=1, name='ipad'}
I took out Product{id=1, name='ipad'}
I took out Product{id=1, name='ipad'}
I put in a product:Product{id=1, name='ipad'}
I took out Product{id=1, name='ipad'}
该例首先创建一个LinkedBlockingQueue用于放置产品,然后创建5个Producer和5个Consumer, Producer 每生产一 个产品会调用queue.put() 把产品放入队列中,Consumer 调用queue.take()从队列中取出产品,在放人和取出操作中自动进行阻塞处理,适用于大量线程的并发处理,非常方便,并且代码也简洁,推荐在线程编程采用这种方式。
8.9.2 线程池
大多数网络服务器程序都离不开多线程,每当一个请求到达时,立即需要一个单独的线程为请求服务,但当有大量请求并发访问时,假设一个服务器一天 要处理50 000个请求,服务器不断创建和销毁对象的开销很大,这种方式会因线程创建得太多而消耗过多的内存,影响到执行效率。为了减少创建和销毁线程的次数,并能重复利用线程执行多个任务,引人“池”的概念,线程池提供了限制系统中执行线程数量的解决方案。**线程池是在任务到来之前,预先创建一定数目线程的机制,可以根据系统环境,自动或手动设置线程数量,创建线程放人空闲队列中,这些线程均处于睡眠状态,不消耗CPU,仅占用非常少量的内存空间。**当接收到一个请求时,缓冲池为该请求分配一个空闲线程,将请求传人此线程中运行,进行处理。如果预先创建的线程都处于运行状态,即创建的线程不够使用,线程池可再创建一定数量的新线程,用于处理更多的请求。如果请求不多,系统比较清闲,也可以移除一部分一直处于停用状态的线程。如此采用线程池机制以达到运行的最佳效果,既不至于浪费资源,又不会造成系统拥挤影响效率。
java.util.concurrent.Executors
类提供了创建4种线程池newSingle ThreadExecutor、new-FixedThreadPool、newScheduledThreadPool、newCachedThreadPool
的方法,如下表所示。
方法 | 说明 |
ScheduledExecutorServicenewSingleThreadExecutor() | 创建-一个单线程的线程池,它只有一个工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行 |
ExecutorServicenewFixedThreadPool(int nThreads) | 创建一个可重用固定保存nThreads个线程的线程池,超出的线程在队列中等待 |
ScheduledExecutorServicenewScheduledThreadPool(int corePoolSize) | 创建一个可保存线程数为corePoolSize的线程池,可设置定时运行及周期性执行任务 |
ExecutorServicenewCachedThreadPool() | 创建一个无界限的线程池,如果线程池长度超过任务数量,可回收60秒不执行任务的空闲线程。若任务数量增多,则自动 新建线程 |
ExecutorService
是一个接口,ExecutorService
有 两个具体类ThreadPoolExecutor和ScheduledTheadPoolExecutor,
其中ThreadPoolExecutor是线程池中最核心的类,继承自AbstractExecutorsService
类,创建线程池的工作由它的构造方法完成。ThreadPoolExecutor的构造方法定义如下:
ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAlive Time,TimeUnit :unit,
BlockihgQueue< Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
下面解释各个参数的含义。
corePoolSize:线程池的大小,即可放置的线程数量。
maximumPoolSsize: 线程池最多能创建的线程数。
keepAliveTime:表示空闲线程没有任务执行时,最多等待多少时间会终止。
unit:参数keepAliveTime的时间单位,可设置为天、小时、分钟、秒、毫秒、微秒及纳秒。
workQueue:存放线程的阻塞队列。
threadFactory:创建新线程时使用的线程工厂。
handler:对某些异常的处理程序,比如超出线程范围和队列容量而使执行被阻塞的异常。
从参数列表中可看出,ThreadPoolExecutor 构造方法提供了创建线程池的必要参数。如果请求执行的线程数量少于corePoolSize,则不排队,直接执行;反之,Executor 将请求加入队列,而不添加新的线程。线程的排队方式由工作队列workQueue决定,
有ArrayBlockingQueue、LinkedBlockingQueue以及synchronousQueue
三种阻塞队列可供选用,此队列仅保持由execute()方法提交的Runnable 任务,关于阻塞队列前面已经介绍过。
ThreadPoolExecutor类中有几个常用的方法: excute(runnable command)、submit()、shutdown()。
●execute(runnable command)是个重要的方法,在ExecutorService的父接口中声明,由ThreadPoolExecutor完成具体的实现,该方法负责向线程池提交请求并由线程池来执行。
●submit()方法在ExecutorService接口中声明,在AbstractExecutorService中实现,该
方法与execute()功能相似,也是向线程池提交执行请求,不同的是它提供执行的返
回结果。●Shutdown( 在最后用于关闭线程池。
**示例:**创建一个newCachedThreadPool类型的线程池,其他类型线程池的创建方式与之类似
代码如下:
public class ExecutorTest {
public static void main(String[] args) {
//创建一个可调节大小的线程池
ExecutorService pool_1 = Executors.newCachedThreadPool();
// //创建一个单线程运行的线程池
// ExecutorService pool_2 = Executors.newSingleThreadExecutor();
// //创建一个可重复固定线程数的线程池
// ExecutorService pool_3= Executors.newFixedThreadPool(5);
// //创建一个可可重用固定线程数的线程池
// ExecutorService pool_4= Executors.newScheduledThreadPool(1);
//创建5个线程
Thread thread_1 = new MyThread();
Thread thread_2 = new MyThread();
Thread thread_3 = new MyThread();
Thread thread_4 = new MyThread();
Thread thread_5 = new MyThread();
//将线程放入线程池中进行执行
pool_1.execute(thread_1);
pool_1.execute(thread_2);
pool_1.execute(thread_3);
pool_1.execute(thread_4);
pool_1.execute(thread_5);
}
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" is running");
}
}
运行结果如下:
pool-1-thread-3 is running
pool-1-thread-4 is running
pool-1-thread-2 is running
pool-1-thread-1 is running
pool-1-thread-5 is running