当任务A与任务B执行顺序之间有逻辑关系时(例如先执行A,再执行B),单单使用多线程并发技术并不能确保这种逻辑关系的能够被实现,因为即使任务A线程优先于任务B线程首先被创建,但是由于时间片切换机制,可能任务A线程在创建之后就被挂起不执行,而任务B线程创建之初就开始执行,这样就造成程序运行结果不如预期,而且这种逻辑错误难以通过debug技术中的单步运行发现。例如下面代码,就不满足先创建先执行的逻辑。
- public class Snippet
- {
- public static void main(String[] args)
- {
- for (int i = 1; i <= 20; i++)
- {
- new Thread(new test(i)).start();
- }
- }
- }
- class test implements Runnable
- {
- /** 记录线程创建时间*/
- private int time;
- public test(int time)
- {
- this.time = time;
- }
- @Override
- public void run()
- {
- System.out.print(time+" ");
- }
- }
- 执行结果:3 2 1 4 6 5 7 8 10 11 9 12 13 14 16 15 17 18 20 19
可以看出有很多线程先被创建后于后被创建的线程执行。
利用线程池技术可以改变线程之间逻辑执行顺序问题,使任务A执行完成之后再开启任务B。
从应用线程池的角度,简单介绍一下线程池
一、创建线程池:
四种类型:
- newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
- newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
- newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
- newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
- 如果不想使用上述现成的线程池,也可以自己设定参数
ThreadPoolExecutor executor = new ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue);
参数解释:
- corePoolSize:核心池的大小。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
- maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
- keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
- unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
- workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;
二、往线程池中加入任务
executor.execute(thread);
通过这个方法可以向线程池提交一个任务,交由线程池去执行。
三、关闭线程池
executor.shutdown();
调用了shutdown()方法,线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕
四、等待线程池里的任务执行完毕
while (!executor.isTerminated());
通过上述方法,可以实现任务A执行完成之后再开启任务B的逻辑。
- public class Snippet
- {
- public static void main(String[] args)
- {
- ExecutorService executor;
- for (int i = 1; i <= 20; i++)
- {
- executor = Executors.newCachedThreadPool();
- executor.execute(new test(i));
- executor.shutdown();
- while (!executor.isTerminated())
- {
- }
- }
- }
- }
- class test implements Runnable
- {
- /** 记录线程创建时间*/
- private int time;
- public test(int time)
- {
- this.time = time;
- }
- @Override
- public void run()
- {
- System.out.print(time+" ");
- }
- }
- 运行结果:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
可以看出现在程序执行就是按照先创建的先执行的逻辑顺序执行
在本次实验线程之间执行顺序也有类似的逻辑关系,例如:在第n秒,所有未过河的猴子需要根据决策采取行动之后,才能开启第n+1秒的猴子进程。这就需要用线程池技术管理猴子线程,使猴子线程满足时间上先后执行顺序的关系。
在本次实验中思路如下:
Step1:判断猴子是否全部过河完毕,如果是,结束程序,如果不是执行step2
Step2:新建线程池,为每一只未过河的猴子创建一个线程,加入到线程池
Step3:关闭线程池,等待线程执行完毕
Step4:时间自增1,重复上述操作
注:本人实现本次实验思路是以秒为单位,在第i秒里都会为每一只未过河的猴子创建线程(在每一个线程里,未过河的猴子只会执行一步操作),然后加入到线程池,利用线程池管理这些线程,等待这些线程全部执行完毕,然后销毁,才进入到i+1秒的状态。到下一秒后,继续为未过河猴子创建线程,重复上述操作,知道所有猴子过河。