线程池的好处:
减少创建和销毁线程时消耗的时间以及系统资源的开销。提高性能,尤其是创建大量生存期很短的线程时,更应该使用线程池。
线程池和数据库连接池有类似之处,启动时会大量创建一些空闲线程。线程池可以接收Runnable/Callable对象,之后会通知空闲线程去执行run()/call()方法,执行结束后线程并不会死亡,而是返回线程池中,等待执行下一次run()/call()方法。
如果不使用线程池,需要自己创建线程实例然后传入Runnable对象,然后启动线程。线程简化了流程,直接往线程池中提交Runnable/Callable对象,线程池就会执行run()/call()任务。注意,线程池仅仅只是用于管理线程,它本身并不实现线程同步功能。如果遇到多个线程访问共享资源情况要使用同步去访问。
创建线程池的四种方式:
Java创建线程池会返回ExecutorService对象和ScheduledExecutorService对象,ExecutorService 的默认实现是ThreadPoolExecutor,普通类Executors创建线程池的静态方法就是使用ThreadPoolExecutor来完成的。
在Java中,使用Executors工厂类的静态方法创建线程池,具体方法如下:
newCachedThreadPool()_______________________创建具有缓存功能的线程池。如果线程数超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
newFixedThreadPool(int nThreads)____________创建具有固定线程数的线程池
newSingleThreadExecutor()___________________创建只有一个工作线程的线程池,等同于newFixedThreadPool()传入1作为参数
newScheduledThreadPool(corePoolSize)________创建一个具有固定线程数的线程池,支持定时和延迟执行任务
newSingleThreadScheduledExecutor()__________创建只有一个工作线程的线程池,支持定时和延迟执行延迟执行任务
Java8新增了充分利用多CPU并行能力的线程池,方法如下:
newWorkStealingPool()_______________________根据计算机的CPU数量来设置并行级别。
newWorkStealingPool(int parallelism)________创建有足够线程的线程池来支持给定的并行级别。
线程池的方法:
带Scheduled的是根据时间来延迟执行任务的线程池对象:ScheduledExecutorService对象。别的是ExecutorService对象,使用方法分别如下:ExecutorService类的常用方法:
- submit():提交一个Runnable/Callable对象给线程池,线程池会通知空闲线程去执行run()/call()方法。
- shutdown:关闭线程池,关闭之前会执行完以前的任务。
- isShutdown():如果此执行程序已关闭,则返回 true。
- boolean isTerminated():如果关闭后所有任务都已完成,则返回 true。
- awaitTermination(timeout, unit):请求关闭、发生超时或者当前线程中断,会导致阻塞,直到所有任务完成。
- 如果要了解更多方法,请查看JDK文档。
ScheduledExecutorService的方法:
| schedule(Callable<V> callable, long delay,TimeUnit unit) 创建并执行在给定延迟后启用的 ScheduledFuture。 | |
ScheduledFuture<?> | schedule(Runnable command, long delay,TimeUnit unit) 创建并执行在给定延迟后启用的一次性操作。 | |
ScheduledFuture<?> | scheduleAtFixedRate(Runnable command, long initialDelay, long period,TimeUnit unit) 创建并执行一个在给定初始延迟后首次启用的定期操作,后续操作具有给定的周期;也就是将在 initialDelay 后开始执行,然后在initialDelay+period 后执行,接着在initialDelay + 2 * period 后执行,依此类推。 | |
ScheduledFuture<?> | scheduleWithFixedDelay(Runnable command, long initialDelay, long delay,TimeUnit unit) 创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟。 |
线程池执行任务实例
演示固定线程数的线程池
public class ThreadPoolDemo {
public static void main(String[] args) {
ExecutorService esPool = Executors.newFixedThreadPool(3);//线程数为5的线程池
ScheduledExecutorService ses = Executors.newScheduledThreadPool(5);//可以延迟执行的线程池
//匿名内部类
Runnable r = new Runnable() {
int i = 0;
@Override
public void run() {
for(; i < 100; i++)
System.out.println(Thread.currentThread().getName()+ "->" + i);
}
};
//提交三次同一个Runnable对象
Future f = esPool.submit(r);
esPool.submit(r);
esPool.submit(r);
//关闭线程池,不再接收新任务
esPool.shutdown();
System.out.println("Future任务是否取消:" + f.isCancelled());
System.out.println("Future任务是否完成:" + f.isDone());
System.out.println(esPool.isShutdown());//查看线程池是否关闭
}
}
- 实例说明:想要让线程池执行任务就使用submit()方法,根据提交的次数来确定通知多少个线程来执行,如果任务超过线程数,那任务就需要排队。submit()提交任务后会返回一个Future对象,和以前使用Callable对象很类似,我们可以通过这个Future对象来查看任务是否完成或者取消了。当然,线程池也有对应方法,使用Future对象一般是用来获取Callable对象的返回值,如果线程池执行的是run()方法,那会返回null。
演示支持延迟执行任务的线程池
public class ThreadPoolDemo {
public static void main(String[] args) {
//可以延迟执行的线程池
ScheduledExecutorService sesPool = Executors.newScheduledThreadPool(5);
//创建Runnable对象
RunTest rt = new RunTest();
//延迟2s后启动两个线程执行任务
sesPool.schedule(rt, 2, TimeUnit.SECONDS);
sesPool.schedule(rt, 2, TimeUnit.SECONDS);
// sesPool.submit(rt);
sesPool.shutdown();//线程池停止执行任务
}
}
//线程池执行的Runnable任务
class RunTest implements Runnable{
private int i = 0;
@Override
public void run() {
// synchronized(this) {
for(; i <= 50; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(1000);//当前线程睡眠1s
} catch (InterruptedException e) {}//省略捕获异常
}
// }
}
}
- 实例说明:ScheduledExecutorService对象支持延迟执行任务,上面代码中延迟2s执行提交的任务,因为提交两次,所以会有两个线程来执行同一个Runnable。注意:因为没有把for循环放在同步代码块中,而且在打印结果之后睡眠当前线程1s,但每次都是两个线程打印输出,而不是两个线程交替输出。这一点我估计是执行同一个任务时才会出现的状况吧。
- 去掉对同步代码块的注释的话,就会看到只有一个线程1执行。虽然每次打印输出结果都要睡眠线程,但因为同步监视器的锁未被释放,所以线程1睡眠之后,线程2因为得不到锁而得不到执行。直到for循环结束,线程1才会释放锁。此时任务也结束了,自然就没线程2什么事情了。如果想看到两个线程协调运行,那可以使用wait/notify机制或者Lock/Condition机制,因为这两个机制能够释放锁。
使用线程池来执行管道流
public class PipeDemo{
public static void main(String[] args) throws Exception {
//创建管道输入流/输出流
PipedInputStream pis = new PipedInputStream();
PipedOutputStream pos = new PipedOutputStream();
pis.connect(pos); //连接双方管道
Consumer cs = new Consumer(pis);
Producer pro = new Producer(pos);
ExecutorService esPool = Executors.newFixedThreadPool(4);//固定线程数为4的线程池
esPool.submit(cs);
esPool.submit(pro);
esPool.shutdown();//关闭线程池
}
}
//消费者线程
class Consumer implements Runnable {
private PipedInputStream pis;
public Consumer(PipedInputStream pis) {
this.pis = pis;
}
@Override
public void run() {
try{
byte[] buf = new byte[1024];
int len = 0;
System.out.println("--消费者抢到执行权--");
while((len = pis.read(buf)) != -1)//会导致阻塞状态
System.out.println("消费者收到数据:" + new String(buf,0,len));
pis.close();
}catch(Exception e){ e.printStackTrace(); }
}
}
//生产者线程
class Producer implements Runnable {
private PipedOutputStream pos;
public Producer(PipedOutputStream pos) {
this.pos = pos;
}
public void run(){
System.out.println("--生产者抢到执行权--");
try {
System.out.println("延迟3s后,往管道中输出数据");
Thread.sleep(3000);
//管道输出流——输出数据
pos.write("输出数据:我是由生产者制造的一段话".getBytes());
pos.close();//关闭管道输出流
}catch(Exception e) {}//省略捕捉异常
}
}
- 实例说明:管道流在前面线程通信中提起过,管道流只能用于两个线程之间进行通讯,虽然创建的线程池有固定线程数4个,但只提交了两个任务(一个生产者和一个消费者),所以等同于之前使用两个线程实例来分别启动生产者线程和消费者线程。
- 回顾一下上面的管道流执行流程:因为线程调度的不确定性,生产者和消费者线程谁先启动都不确定。如果消费者线程先启动,因为管道输入流循环读取数据返回-1,说明没数据,则进入阻塞状态。这时生产者线程得到执行权,睡眠生产者线程3s,因为消费者线程还是不满足条件,又进入阻塞状态。3s后使用管道输出流写入数据,生产者线程执行结束,这时候消费者线程满足执行条件了,成功打印来自生产者线程的一段话。至此线程池的任务都执行完成了,然后线程回到线程池,等待下一次的任务。
在最后放上一篇对多线程整体讲解非常好的博客,地址点击打开链接