(2)通过submit()方法,如:
Future<?> future = threadpool.submit(new Runnable(){...});
try {
Object res = future.get();
} catch (InterruptedException e) {
// 处理中断异常
e.printStackTrace();
} catch (ExecutionException e) {
// 处理无法执行任务异常
e.printStackTrace();
}finally{
// 关闭线程池
executor.shutdown();
}
使用submit 方法来提交任务,它会返回一个Future对象,通过future的get方法来获取返回值,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。
线程池工作流程分析:(来自参考文章)
从上图我们可以看出,当提交一个新任务到线程池时,线程池的处理流程如下:
1、首先线程池判断基本线程池是否已满(< corePoolSize ?)?没满,创建一个工作线程来执行任务。满了,则进入下个流程。
2、其次线程池判断工作队列是否已满?没满,则将新提交的任务存储在工作队列里。满了,则进入下个流程。
3、最后线程池判断整个线程池是否已满(< maximumPoolSize ?)?没满,则创建一个新的工作线程来执行任务,满了,则交给饱和策略来处理这个任务。
也就是说,线程池优先要创建出基本线程池大小(corePoolSize)的线程数量,没有达到这个数量时,每次提交新任务都会直接创建一个新线程,当达到了基本线程数量后,又有新任务到达,优先放入等待队列,如果队列满了,才去创建新的线程(不能超过线程池的最大数maxmumPoolSize)。
关于线程池的配置原则可阅读参考文章。
ThreadPoolExecutor简单实例:
public class BankCount {
public synchronized void addMoney(int money){//存钱
System.out.println(Thread.currentThread().getName() + ">存入:" + money);
}
public synchronized void getMoney(int money){//取钱
System.out.println(Thread.currentThread().getName() + ">取钱:" + money);
}
}
测试类:
public class BankTest {
public static void main(String[] args) {
final BankCount bankCount = new BankCount();
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.execute(new Runnable() {//存钱线程
@Override
public void run() {
int i = 5;
while(i-- > 0){
bankCount.addMoney(200);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Future<?> future = executor.submit(new Runnable() {//取钱线程
@Override
public void run() {
int i = 5;
while(i-- > 0){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
bankCount.getMoney(200);
}
}
});
try {
Object res = future.get();
System.out.println(res);
} catch (InterruptedException e) {
// 处理中断异常
e.printStackTrace();
} catch (ExecutionException e) {
// 处理无法执行任务异常
e.printStackTrace();
}finally{
// 关闭线程池
executor.shutdown();
}
}
}
打印结果如下:
pool-1-thread-1>存入:200
pool-1-thread-1>存入:200
pool-1-thread-2>取钱:200
pool-1-thread-1>存入:200
pool-1-thread-2>取钱:200
pool-1-thread-1>存入:200
pool-1-thread-2>取钱:200
pool-1-thread-1>存入:200
pool-1-thread-2>取钱:200
pool-1-thread-2>取钱:200
null
可以看到,打印出来的future.get()获取的结果为null,这是因为Runnable是没有返回值的,需要返回值要使用Callable,这里就不再细说了,具体可参考如下文章:
http://blog.csdn.net/xiaojin21cen/article/details/41820983
http://icgemu.iteye.com/blog/467848
2、生产者和消费者模型
生产者消费者模型,描述是:有一块缓冲区作为仓库,生产者可以将产品放入仓库,消费者可以从仓库中取走产品。解决消费者和生产者问题的
核心在于保证同一资源被多个线程并发访问时的完整性
。一般采用信号量或加锁机制解决。下面介绍Java中解决生产者和消费者问题主要三种仿:
(1)wait() / notify()、notifyAll()
wait和notify方法是Object的两个方法,因此每个类都会拥有这两个方法。
wait()方法:使当前线程处于等待状态,放弃锁,让其他线程执行。
notify()方法:唤醒其他等待同一个锁的线程,放弃锁,自己处于等待状态。
如下例子:
/**
* 仓库
*/
public class Storage {
private static final int MAX_SIZE = 100;//仓库的最大容量
private List<Object> data = new ArrayList<Object>();//存储载体
/**
* 生产操作
*/
public synchronized void produce(int num){
if(data.size() + num > MAX_SIZE){//如果生产这些产品将超出仓库的最大容量,则生产操作阻塞
System.out.println("生产操作-->数量:" + num + ",超出仓库容量,生产阻塞!------库存:" + data.size());
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//到这里,表示可以正常生产产品
for(int i = 0; i < num; i++){//生产num个产品
data.add(new Object());
}
System.out.println("生产操作-->数量:" + num + ",成功入库~------库存:" + data.size());
//生产完产品后,唤醒其他等待消费的线程
notify();
}
/**
* 消费操作
*/
public synchronized void consume(int num){
if(data.size() - num < 0){//如果产品数量不足
System.out.println("消费操作-->数量:" + num + ",库存不足,消费阻塞!------库存:" + data.size());
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//到这里,表示可以正常消费
for(int i = 0; i < num; i++){//消费num个产品
data.remove(0);
}
System.out.println("消费操作-->数量:" + num + ",消费成功~------库存:" + data.size());
//消费完产品后,唤醒其他等待生产的线程
notify();
}
}
生产者:
public class Producer implements Runnable{
private Storage storage;
private int num;//每次生产多少个
public Producer(Storage sto,int num){
storage = sto;
this.num = num;
}
@Override
public void run() {
storage.produce(num);
}
}
消费者:
public class Consumer implements Runnable{
private Storage storage;
private int num;//每次消费多少个
public Consumer(Storage sto,int num){
storage = sto;
this.num = num;
}
@Override
public void run() {
storage.consume(num);
}
}
测试类:
public class StorageTest {
public static void main(String[] args) {
Storage storage = new Storage();
ExecutorService taskSubmit = Executors.newFixedThreadPool(10); //来使用使用上一节我们总结的线程池知识
//给定4个消费者
taskSubmit.submit(new Consumer(storage, 30));
taskSubmit.submit(new Consumer(storage, 10));
taskSubmit.submit(new Consumer(storage, 20));
//给定6个生产者
taskSubmit.submit(new Producer(storage, 70));
taskSubmit.submit(new Producer(storage, 10));
taskSubmit.submit(new Producer(storage, 20));
taskSubmit.submit(new Producer(storage, 10));
taskSubmit.submit(new Producer(storage, 10));
taskSubmit.submit(new Producer(storage, 10));
taskSubmit.shutdown();
}
}
打印结果:
消费操作-->数量:30,库存不足,消费阻塞!------库存:0
生产操作-->数量:10,成功入库~------库存:10
生产操作-->数量:70,成功入库~------库存:80
生产操作-->数量:10,成功入库~------库存:90
生产操作-->数量:10,成功入库~------库存:100
生产操作-->数量:20,超出仓库容量,生产阻塞!------库存:100
消费操作-->数量:10,消费成功~------库存:90
生产操作-->数量:20,成功入库~------库存:110
生产操作-->数量:10,超出仓库容量,生产阻塞!------库存:110
消费操作-->数量:20,消费成功~------库存:90
消费操作-->数量:30,消费成功~------库存:60
生产操作-->数量:10,成功入库~------库存:70
在仓库中,唤醒我们使用的是notify()而没有使用notifyAll(),是因为在这里,如果测试数据设置不当很容易造成死锁(比如一下唤醒了所有的生产进程),因为使用wait和notify有一个缺陷:
逻辑本应该要这样设计的,在produce()操作后,只要唤醒等待同一把锁的消费者进程,在consume()后,唤醒等待同一把锁的生产者进程,而notify()或notifyAll()将生产者和消费者线程都唤醒了。下面的第二种方法可以解决这个问题。
wait和notify在“类消费者和生产者”问题上也很有用,比如,在A类的某个方法中调用了传进来的B对象的一个方法,A类方法的后面代码依赖于刚刚调用的B的返回值,但是B对象的这个方法是一个异步的操作,此时就可以在A方法中调用完B对象的方法后自我阻塞,即调用wait()方法,而在B对象的那个方法中,待异步操作完成后,调用notify(),唤醒处于等待同一锁对象的线程。如下:
A类的某个方法中:
XmppManager xmppManager = notificationService.getXmppManager();
if(xmppManager != null){
if(!xmppManager.isAuthenticated()){
try {
synchronized (xmppManager) {//等待客户端连接认证成功
Log.d(LOGTAG, "wait for authenticated...");
xmppManager.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//运行到此处,说明是认证成功的,有两种可能,一是运行速度很快调用notificationService.getXmppManager()后直接返回了结果,二是B中处理完了调用notify方法
Log.d(LOGTAG, "authenticated already. send SetTagsIQ now...");
B中处理完后:
//客户端连接认证成功后,唤醒拥有xmppManager锁的对象
synchronized (xmppManager) {
xmppManager.notifyAll();
}
(2)await() / signal()
在JDK1.5之引入concurrent包之后,新引入了await()和signal()方法来做同步,功能和wait()和notify()方法相同,可以完全取代,但await()和signal()需要和Lock机制(关于Lock机制前面已总结)结合使用,更加灵活。正如第一种所说,可以通过调用Lock的newCondition()方法依次获取两个条件变量,一个针对仓库空的,一个针对仓库满的条件变量,通过添加变量进行同步控制。
修改仓库类Storage:
/**
* 仓库
*/
public class Storage {
private static final int MAX_SIZE = 100;//仓库的最大容量
private List<Object> data = new ArrayList<Object>();//存储载体
private Lock lock = new ReentrantLock();//可重入锁
private Condition full = lock.newCondition();//仓库满的条件变量
private Condition empty = lock.newCondition();//仓库空时的条件变量
/**
* 生产操作
*/
public void produce(int num){
lock.lock(); //加锁
if(data.size() + num > MAX_SIZE){//如果生产这些产品将超出仓库的最大容量,则生产操作阻塞
System.out.println("生产操作-->数量:" + num + ",超出仓库容量,生产阻塞!------库存:" + data.size());
try {
full.await(); //阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//到这里,表示可以正常生产产品
for(int i = 0; i < num; i++){//生产num个产品
data.add(new Object());
}
System.out.println("生产操作-->数量:" + num + ",成功入库~------库存:" + data.size());
//生产完产品后,唤醒其他等待消费的线程
empty.signalAll();
lock.unlock(); //释放锁
}
/**
* 消费操作
*/
public void consume(int num){
lock.lock(); //加锁
if(data.size() - num < 0){//如果产品数量不足
System.out.println("消费操作-->数量:" + num + ",库存不足,消费阻塞!------库存:" + data.size());
try {
empty.await(); //阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//到这里,表示可以正常消费
for(int i = 0; i < num; i++){//消费num个产品
data.remove(0);
}
System.out.println("消费操作-->数量:" + num + ",消费成功~------库存:" + data.size());
//消费完产品后,唤醒其他等待生产的线程
full.signalAll();
lock.unlock(); //释放锁
}
}
打印结果:
消费操作-->数量:30,库存不足,消费阻塞!------库存:0
消费操作-->数量:10,库存不足,消费阻塞!------库存:0
消费操作-->数量:20,库存不足,消费阻塞!------库存:0
生产操作-->数量:70,成功入库~------库存:70
生产操作-->数量:10,成功入库~------库存:80
生产操作-->数量:10,成功入库~------库存:90
生产操作-->数量:10,成功入库~------库存:100
生产操作-->数量:10,超出仓库容量,生产阻塞!------库存:100
消费操作-->数量:30,消费成功~------库存:70
消费操作-->数量:10,消费成功~------库存:60
消费操作-->数量:20,消费成功~------库存:40
生产操作-->数量:10,成功入库~------库存:50
生产操作-->数量:20,成功入库~------库存:70
使用await和signal后,加锁解锁操作就交给了Lock,不用再使用synchronized同步(具体可看前面总结的同步的实现方法),在produce中满仓后阻塞,生产完后唤醒等待的消费线程,consume中库存不足后阻塞,消费完后唤醒等待的生产者线程,表示可以消费了。
(3)BlockingQueue阻塞队列方式
在上一节关于线程池的总结中,我们看到了要创建一个线程池如ThreadPoolExecutor,需要传入一个任务队列即BlockingQueue,BlockingQueue(接口)用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue。
>ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
>LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
>SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
>PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
BlockingQueue的所有实现类内部都是已经实现了同步的队列,实现的方式采用的是上面介绍的第二种await()/signal() + Lock同步的机制。在生成阻塞队列时,可以指定队列大小。用于阻塞操作的方法主要为:
put()方法:插入一个元素,如果超过容量则自我阻塞,等待唤醒;
take()方法:取走一个元素,如果容量不足了,自我阻塞,等待唤醒;
put和take内部自己实现了await和signal、lock的机制处理,不再需要我们做相应操作。修改Storage代码如下:
public class Storage {
private static final int MAX_SIZE = 100;//仓库的最大容量
private BlockingQueue<Object> data = new LinkedBlockingQueue<Object>(MAX_SIZE); //使用阻塞队列作为存储载体
/**
* 生产操作
*/
public void produce(int num){
if(data.size() == MAX_SIZE){//如果仓库已达最大容量
System.out.println("生产操作-->仓库已达最大容量!");
}
//到这里,表示可以正常生产产品
for(int i = 0; i < num; i++){//生产num个产品
try {
data.put(new Object()); //put内部自动实现了判断,超过最大容量自动阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生产操作-->数量:" + num + ",成功入库~------库存:" + data.size());
}
/**
* 消费操作
*/
public void consume(int num){
if(data.size() == 0){//如果产品数量不足
System.out.println("消费操作--库存不足!");
}
//到这里,表示可以正常消费
for(int i = 0; i < num; i++){//消费num个产品
try {
data.take(); //take内部自动判断,消耗后库存是否充足,不足自我阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费操作-->数量:" + num + ",消费成功~------库存:" + data.size());
}
}
打印结果:
消费操作--库存不足!
消费操作--库存不足!
消费操作--库存不足!
生产操作-->数量:70,成功入库~------库存:45
消费操作-->数量:30,消费成功~------库存:45
生产操作-->数量:10,成功入库~------库存:56
生产操作-->数量:20,成功入库~------库存:75
生产操作-->数量:10,成功入库~------库存:85
生产操作-->数量:10,成功入库~------库存:89
消费操作-->数量:10,消费成功~------库存:60
生产操作-->数量:10,成功入库~------库存:70
消费操作-->数量:20,消费成功~------库存:70
可以看到,Storage中produce和consume方法中我们直接通过put和take方法往容器中添加或移除产品即可,没有进行逻辑控制(其实上面两个方法中if都可以去掉,只是为了打印效果才加上的),这是因为BlockingQueue内部已经实现了,不需要我们再次控制。
同时,我们看到打印的库存信息出现了不匹配,这个主要是因为我们的打印语句Systm.out.println()没有被同步导致的,因为同步语句只是在put和take方法内部,而我们打印语句中使用了data这个共享变量。这里因为我们需要看效果,所以才加的打印语句,并不影响我们对BlockingQueue的使用。
因此,在Java中,使用BlockingQueue阻塞队列的方式可以很方便的为我们处理生产者消费则问题,推荐使用。