众所周知,Android中的主线程就是我们俗称的UI线程,我们只能在主线程中操作UI,并且我们会避免在主线程中进行耗时操作,这样会造成主线程阻塞引起ANR,所以我们会把耗时操作(例如流的读写,下载文件等)放入我们新开辟的线程中进行。
我不由得汪汪大笑..啊不,哈哈大笑,这还不简单?请把我尊贵的Filco键盘给我,秀操作的时候到了,于是我敲下了这段大家都熟知的代码~
new Thread(new Runnable() {
@Override
public void run() {
//你可以在这里尽情的做你爱做的事(操作UI除外)
}
}).start();
咱们这么写确实可以达到我们想要的目的,但是这么写是有弊端的,且听我慢慢BB出来:
首先我们这么写意味着我们使用了匿名内部类,那就代表着我们没办法重用,也就是说我们这里新建的这个线程只能完成我们这里所指定的任务然后被销毁回收,这样对程序性能也有影响。我们下次还想进行耗时操作的时候又得开启新线程,这意味着我们不断的在创建新线程对象和销毁它,假如我们现在有1000个任务要在子线程中执行,难道我们要循环创建1000次?1000个线程那得占用很大的系统资源,搞不好会OOM哦~
好了,那么我们这里就可以引入线程池的概念了。
什么是线程池?
顾名思义线程池就是一个池子里全是线程,啊对不起对不起...再也不敢了,这段去掉。
线程池是指在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是新建一个线程。线程池中线程的数量通常完全取决于可用内存数量和应用程序的需求。然而,增加可用线程数量是可能的。线程池中的每个线程都有被分配一个任务,一旦任务已经完成了,线程回到池子中并等待下一次分配任务。
看了这段你懂了吗?我们可以这么理解,有这么一个集合,里边全是线程,不同类型的线程池会有不同的工作模式而已。
接下来我们来介绍下常用的4个线程池。
四种常用的线程池
fixedThreadPool
使用线程池中的fixedThreadPool,这种线程池的特点是在你创建这个线程池时会指定一个最大线程数,
每提交一个任务就会新创建一个线程直到达到最大线程数,如果已经达到了最大线程数的话就会将新提交的任务缓存进线程池队列中等待空闲线程
注意:当所有的任务都完成即线程空闲时,这里的所有线程并不会自己释放,会占用一定的系统资源,除非这整个线程池被关闭(即fixedThreadPool.shutdown();)
先来搞个例子
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);//这里指定最大线程数为3
for (int i = 0; i < 10; i++) {
final int index = i;
fixedThreadPool.execute(new Runnable() {
public void run() {
try {
Log.e("test", "线程编号:" + Thread.currentThread().getId() + "打印数字" + index);
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
运行结果
08-01 14:07:13.751 21958-22058/com.example.android_1.testdemo E/test: 线程编号:3958打印数字1
08-01 14:07:13.751 21958-22059/com.example.android_1.testdemo E/test: 线程编号:3959打印数字2
08-01 14:07:13.751 21958-22057/com.example.android_1.testdemo E/test: 线程编号:3957打印数字0
08-01 14:07:15.753 21958-22059/com.example.android_1.testdemo E/test: 线程编号:3959打印数字4
08-01 14:07:15.753 21958-22058/com.example.android_1.testdemo E/test: 线程编号:3958打印数字3
08-01 14:07:15.754 21958-22057/com.example.android_1.testdemo E/test: 线程编号:3957打印数字5
08-01 14:07:17.757 21958-22059/com.example.android_1.testdemo E/test: 线程编号:3959打印数字7
08-01 14:07:17.757 21958-22057/com.example.android_1.testdemo E/test: 线程编号:3957打印数字6
08-01 14:07:17.757 21958-22058/com.example.android_1.testdemo E/test: 线程编号:3958打印数字8
08-01 14:07:19.758 21958-22057/com.example.android_1.testdemo E/test: 线程编号:3957打印数字9
看是不是至始至終都只有3个线程?
cachedThreadPool
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
}
});
这种线程池会根据需要,在线程可用时,重用之前构造好的池中线程。这个线程池在执行 大量短生命周期的异步任务时(many short-lived asynchronous task),可以显著提高程序性能。调用 execute 时,可以重用之前已构造的可用线程,如果不存在可用线程,那么会重新创建一个新的线程并将其加入到线程池中。如果线程超过 60 秒还未被使用,就会被中止并从缓存中移除。因此,线程池在长时间空闲后不会消耗任何资源。这个线程中是没有限制线程数量的(其实还是有限制的,只不过数量为Interger. MAX_VALUE)
singleThreadExecutor
这种线程池会使用单个工作线程来执行一个无边界的队列。(注意,如果单个线程在执行过程中因为某些错误中止,新的线程会替代它执行后续线程)。它可以保证认为是按顺序执行的,任何时候都不会有多于一个的任务处于活动状态。和 newFixedThreadPool(1) 的区别在于,如果线程遇到错误中止,它是无法使用替代线程的。
通俗的来讲你可以认为你所提交的任务就是在排队,按顺序来,我们敲个例子验证一下~
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
Runnable runnable0=new Runnable() {
@Override
public void run() {
try {
Log.e("test", "runnable0,线程ID"+Thread.currentThread().getId());
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable runnable1=new Runnable() {
@Override
public void run() {
try {
Log.e("test", "runnable1,线程ID"+Thread.currentThread().getId());
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable runnable2=new Runnable() {
@Override
public void run() {
try {
Log.e("test", "runnable2,线程ID"+Thread.currentThread().getId());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
singleThreadExecutor.execute(runnable0);
singleThreadExecutor.execute(runnable1);
singleThreadExecutor.execute(runnable2);
//特意设置了时间差异
看看运行结果~
08-01 16:24:42.131 575-1082/com.example.android_1.testdemo E/test: runnable0,线程ID4124
08-01 16:24:45.134 575-1082/com.example.android_1.testdemo E/test: runnable1,线程ID4124
08-01 16:24:47.135 575-1082/com.example.android_1.testdemo E/test: runnable2,线程ID4124
可以看到是我们的预期结果~
newScheduledThreadPool
这种线程池也可以指定池中存在的最大线程数,而且这种线程池能够延时以及轮询方式执行任务,这种线程池有这几种核心方法,下面我们来一一看一番~
schedule(Runnable command, long delay, TimeUnit unit)
我们直接看代码以及注释
//延时处理Runnable任务
//参数:schedule(需要执行的任务Runnable,延时时间长度,延时时间单位)
//例如下面这段代码的意思就是延时3秒执行Runnable任务
scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
}
}, 3, TimeUnit.SECONDS);
schedule(Callable callable, long delay, TimeUnit unit)
这个和上边那个差不多,只不过一个是Runnable任务,一个是Callable任务
//延时处理Callable任务
//参数:schedule(需要执行的任务Runnable,延时时间长度,延时时间单位)
//例如下面这段代码的意思就是延时3秒执行Callable任务
scheduledExecutorService.schedule(new Callable<Object>() {
@Override
public Object call() {
return null;
}
}, 3, TimeUnit.SECONDS);
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
这个方法就是类似于轮询,先来看一下参数的意思
scheduleAtFixedRate(需要执行的Runnable任务,第一次执行任务所需要延迟的时间, 每一次开始执行任务后与下一次执行任务的延迟时间, 延时时间单位)
像下面这段代码的意思就是延时3秒执行第一次任务,从任务开始的时候计时,过了5秒再执行下一次任务。
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
String date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(System.currentTimeMillis()));
Log.e("test", "开始执行:" + date);
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
String date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(System.currentTimeMillis()));
Log.e("test", date);
}
}, 3, 5, TimeUnit.SECONDS);
我们来看下运行结果
08-02 11:40:50.880 22738-22738/com.example.android_1.testdemo E/test: 开始执行:2018-08-02 11:40:50
08-02 11:40:53.887 22738-22986/com.example.android_1.testdemo E/test: 2018-08-02 11:40:53
08-02 11:40:58.894 22738-22986/com.example.android_1.testdemo E/test: 2018-08-02 11:40:58
08-02 11:41:03.889 22738-22991/com.example.android_1.testdemo E/test: 2018-08-02 11:41:03
08-02 11:41:08.891 22738-22986/com.example.android_1.testdemo E/test: 2018-08-02 11:41:08
从结果我们可以看出,确实如我们所料,但是还有另外一种情况,如果我要执行的任务所需要的时间大于间隔时间怎么办?这个方法的做法是等前一个任务执行完了再继续下一个任务,也就是说,时间间隔会与我们指定的时间不一样,我们写段代码验证一下~
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
//
String date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(System.currentTimeMillis()));
Log.e("test", "开始执行:" + date);
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
String date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(System.currentTimeMillis()));
Log.e("test", date);
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 3, 5, TimeUnit.SECONDS);
这段代码我每个任务需要6s的执行时间,大于5秒的间隔时间,来看看结果如何吧~
08-02 11:44:55.473 23286-23286/com.example.android_1.testdemo E/test: 开始执行:2018-08-02 11:44:55
08-02 11:44:58.480 23286-23422/com.example.android_1.testdemo E/test: 2018-08-02 11:44:58
08-02 11:45:04.499 23286-23422/com.example.android_1.testdemo E/test: 2018-08-02 11:45:04
08-02 11:45:10.505 23286-23435/com.example.android_1.testdemo E/test: 2018-08-02 11:45:10
08-02 11:45:16.516 23286-23422/com.example.android_1.testdemo E/test: 2018-08-02 11:45:16
从结果我们可以看出...时间间隔变成了6秒,那是因为我们指定的5秒间隔过后,系统发现上一个任务还没执行完成,所以等上一个执行完成了再继续执行下一个任务,所以变成了6秒~
但是如果发现已经执行完了就会按我们规定的时间间隔来。
scheduleWithFixedDelay`(Runnable command, long initialDelay, long period, TimeUnit unit)
这个方法的参数意义和上一个是一样的,不同之处在于这个方法是每次任务执行结束之后再去计算延时,而不是每次开始就计时,我们来写段代码验证一下~
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
//
String date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(System.currentTimeMillis()));
Log.e("test", "开始执行:" + date);
scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
String date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(System.currentTimeMillis()));
Log.e("test", date);
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 3, 5, TimeUnit.SECONDS);
看一下运行结果~
08-02 11:48:40.409 23741-23741/com.example.android_1.testdemo E/test: 开始执行:2018-08-02 11:48:40
08-02 11:48:43.416 23741-23862/com.example.android_1.testdemo E/test: 2018-08-02 11:48:43
08-02 11:48:51.430 23741-23862/com.example.android_1.testdemo E/test: 2018-08-02 11:48:51
08-02 11:48:59.444 23741-23865/com.example.android_1.testdemo E/test: 2018-08-02 11:48:59
08-02 11:49:07.450 23741-23862/com.example.android_1.testdemo E/test: 2018-08-02 11:49:07
08-02 11:49:15.456 23741-23873/com.example.android_1.testdemo E/test: 2018-08-02 11:49:15
看,是不是间隔时间变成了任务执行时间+指定间隔时间啦?这就和我们想的完全一样~
这里小结一下:
scheduleAtFixedRate ,是以上一个任务开始的时间计时,period(即第三个参数)时间过去后,检测上一个任务是否执行完毕,如果上一个任务执行完毕,则当前任务立即执行,如果上一个任务没有执行完毕,则需要等上一个任务执行完毕后立即执行。
scheduleWithFixedDelay,是以上一个任务结束时开始计时,period(即第三个参数)时间过去后,立即执行。
最后
这就是我们比较常用的四大线程池,我们平时开发的时候可以根据应用场景不同而选择不同类型的线程池来使用~嘻嘻~