基础无论什么时候都是很重要的,为了防止长期也业务代码就此废掉,博主决定长期研究下多线程的java基础知识,算是巩固。本系列博文博主会不定期更新且长期更新下去,水平不够,如有发现不当之处欢迎批评指正,共同学习。
通过本文你可以了解到:
1、顺序执行线程的方式
2、多个任务终止的方法
3、线程池的工作原理或者线程复用的原理。
今天就来简单介绍Executor这个东西,这玩意是什么呢?官方注释说的很明白:Executes the given command at some time in the future!就是在将来的某个时候执行一个命令。很抽象?上代码就知道了这玩意就是一个接口:
public interface Executor {
void execute(Runnable command);
}
接口设计接单吧,无非就是提供了参数为Runnable的方法而已。就是一个接口而已,怎么实现还不是实现者说了算?(战略上藐视),那么你可以这么用它,:
//该代码来自Executor示例
class DirectExecutor implements Executor {
public void execute(Runnable r) {
r.run();
}
public void static main(String args[]){
DirectExecutor de = new DirectExecutor();
de.exectue(new Runnable() {
void run(){
System.out.println("哪个线程调用我,我就再哪个线程中执行")
}
});
}
}
那么如果执行一个比较耗时的任务呢?开启线程啊!多简单的事儿,所以上述代码改动如下:
class ThreadPerTaskExecutor implements Executor {
public void execute(Runnable r) {
//单独开启一个线程执行
new Thread(r).start();
}
public void static main(String args[]){
ThreadPerTaskExecutor tte = new ThreadPerTaskExecutor();
tte.exectue(new Runnable() {
void run(){
System.out.println("单独开启一个线程执行")
}
});
}
}
上面的代码逻辑很简单,就是每次执行exectue方法的时候都开启一个线程来执行Runnable。随着exectue的多次调用,会开启很多线程,而且这些线程执行的顺序也是不定的。
所以问题来了:如果设计一个多个异步任务顺序执行的框架呢?也就是说让多个线程顺序执行,该怎么设计?顺序执行,先进先出,那么脑海中的一个数据结构就是队列了,所以代码改动如下:
class SerialExecutor implements Executor{
//任务队列
final Queue<Runnable> tasks = new ArrayDeque<>();
//真正执行任务的代理对象
final Executor executor;
Runnable active;
SerialExecutor(Executor executor) {
this.executor = executor;
}
public void execute(final Runnable r) {
//将任务添加到队列中
tasks.add(new Runnable() {
public void run() {
try {
r.run();
} finally {
//当前任务执行完毕后,在执行队列中下一个任务
scheduleNext();
}
}
});
if (active == null) {
scheduleNext();
}
}
//从对列中获取任务并执行
protected void scheduleNext() {
if ((active = tasks.poll()) != null) {
executor.execute(active);
}
}
}
设计理念也很简单,将要执行的任务或者Runnable加入到一个队列中,每次从队列中去一个任务交给一个线程(在此处为ThreadPerTaskExecutor)出执行,注意这里并没有对队列做一个循环遍历操作,而是在run方法后加一个finally,执行scheduleNext来取队列中的下一个任务Runnable。
那么你就可以这么调用:
public static void main(){
SerialExecutor serialExecutor= new SerialExecutor(new ThreadPerTaskExecutor());
for(int i=0;i<10;i++){
final int temp = i;
serialExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("任务依次执行"+temp);
}
});
}
}
执行打印如下:
任务依次执行0
任务依次执行1
任务依次执行2
任务依次执行3
任务依次执行4
任务依次执行5
任务依次执行6
任务依次执行7
任务依次执行8
任务依次执行9
如此一个简单的顺序执行的框架就搭建完毕。
但是上面的框架有个问题:任务执行的时候会依次把人物队列中的任务执行完,但是没有中途终止任务的方法,可能会在应用退出时导致内存泄漏。
为此可以对Executor扩展一个shutDown方法的接口:
interface ShutDownAbleExecutor extends Executor{
void shutDown();
boolean isShutDown();
}
那么上面ThreadPerTaskExecutor的则从原来的实现Executor接口改为实现ShutDownAbleExecutor:
class ThreadPerTaskExecutor implements ShutDownAbleExecutor {
boolean isShutDown = false;
public void execute(Runnable r) {
if(isShutDown){
return;
}
//单独开启一个线程执行
new Thread(r).start();
}
@Override
public void shutDown() {
isShutDown = true;
}
@Override
public boolean isShutDown() {
return isShutDown;
}
}
SerialExecutor的代码主要改动如下:
class SerialExecutor implements ShutDownAbleExecutor {
//任务队列
final ShutDownAbleExecutor executor;
@Override
public void isShutDown() {
executor.isShutDown();
}
public synchronized void execute(final Runnable r) {
//新增
if(isShutDown()){
return;
}
//省略部分代码
}
@Override
public boolean isShutDown() {
return executor.isShutDown();
}
}
再来一个main方法测试:
public static void main(String args[]){
SerialExecutor serialExecutor= new SerialExecutor(new ThreadPerTaskExecutor());
for(int i=0;i<10;i++){
final int temp = i;
serialExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("任务依次执行"+temp);
}
});
}
//主线程睡眠
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
serialExecutor.showDown();
}
则跟上面的比有如下打印:
任务依次执行0
任务依次执行1
任务依次执行2
任务依次执行3
Process finished with exit code 0
但是这解决问题了吗?这种解决方法只是让还没有来的集的执行的任务得不到执行的机会,而已经执行的任务还是会继续执行下去,并没有随着客户端的关闭而停止执行。所以还需要改进。
其实思路也很简单,就是用一个集合来保存已经创建的线程,在shutDown方法的的时候遍历此集合,调用Thread对象的intercept方法来打断正在执行的线程
所以ThreadPerTaskExecutor改动如下:
class ThreadPerTaskExecutor implements ShutDownAbleExecutor {
boolean isShutDown = false;
//持有线程的集合
private HashSet<Thread> threads = new HashSet<>();
public void execute(Runnable r) {
if(isShutDown){
return;
}
Thread thread = new Thread(r);
//将创建的线程加入集合种
threads.add(thread);
thread.start();
}
@Override
public void shutDown() {
isShutDown = true;
//遍历集合
for(Thread thread:threads){
//注意此处需要isInterrupted判断
if (!thread.isInterrupted()){
//执行线程中断
thread.interrupt();
}
}
threads.clear();
}
@Override
public boolean isShutDown() {
return isShutDown;
}
}
关于intercept方法的理解本篇博文限于文章内容的连贯性,暂且不作说明,读者可自行查阅相关资料。
目前来说,上面的功能进一步得到完善,但是呢有一个致命的缺点:
每一个任务都会开启一个线程,任务越多开启的线程也越多。我们知道线程的创建也是个不小的开销。为了解决这个问题就引入来线程池的概念。
在这里先阐明一个观点:线程池的作用就是用来复用线程的,而我们知道线程一单执行完毕后就会销毁,那么还怎么复用呢?其实这个复用的意思是让一个线程由原来执行一个任务,变成执行多个任务,这就是线程复用的核心!原来的逻辑是每一个任务都需要开启一个新线程,而现在呢,只需要开启少量线程,让这些线程加班加点多做些任务,比如本来由三个线程干的事儿,都交给一个线程。伪代码如下:
//线程没有复用的逻辑
while(taskQueue.isNotEmpty()){
//获取一个任务
Runnale task = taskQueue.poll();
//创建一个Thread对象
new Thread(task) {
void run(){
if(task!=null){
task.run();
}
}.start();
}
}
//线程复用后的逻辑:此处的Thread只是线程池种的一个
new Thread() {
void run(){
Runnale task = null;
//循环从任务队列中获取任务
while((task=taskQueue.poll())!=null){
//执行任务
task.run();
}
}
}.start();
这么做的好处是什么?显而易见,可以减少线程对象创建带来的开销,同时最大效率的使用已有的线程,压榨一个线程使之做更多的工作。简单做个比喻使用线程复用之前每个任务都交给一个员工处理,这么弄的话得雇佣好多员工,员工是需要发工资的,就算不发工资也需要浪费公司的位置,水电费的,这么下去公司就拖垮了。为此需要裁员,只保留核心员工,因为人员少了,任务还是那么多任务,没办法剩下员工就多担待点吧,什么?要加薪?呵呵
下面就设计一个简单的不能再简单的线程池,来直观体会一下线程池的工作原理,代码很简单就只贴贴出来了:
public class MyThreadPool implements ShutDownAbleExecutor {
final int maxSize = 5;
//任务队列
private final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>();
//5个工人
private final List<Worker> workers = new ArrayList<>(maxSize);
volatile boolean isShutDown = false;
@Override
public void shutDown() {
isShutDown = true;
for(Worker worker:workers){
worker.thread.interrupt();
}
}
@Override
public boolean isShutDown() {
return isShutDown;
}
@Override
public void execute(Runnable task) {
if (isShutDown) {
return;
}
int workingSize = workers.size();
//还有员工闲着没活干在玩手机
if (workingSize < maxSize) {
Worker worker = new Worker(task);
workers.add(worker);
worker.thread.start();
}else{//全部员工都已经干活了,新任务来临,该加班了
workQueue.offer(task);
}
}
//公司员工
class Worker implements Runnable {
final Thread thread;
Runnable realTask;
Worker(Runnable task) {
this.realTask = task;
//初始化一个线程
this.thread = new Thread(this);
}
@Override
public void run() {
try {
Runnable task = realTask;
realTask = null;
while (!isShutDown && (task != null || (task = workQueue.take()) != null)) {
try {
task.run();
} finally {
task = null;
}
}
} catch (InterruptedException e) {
}
}
}
public static void main(String[] args) throws InterruptedException {
MyThreadPool pool=new MyThreadPool();
for(int i=0;i<30;i++){
final int index = i;
pool.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程"+Thread.currentThread().getName() + " 执行任务"+index);
}
});
}
}
}
执行结果如下:
线程Thread-1 执行任务1
线程Thread-2 执行任务2
线程Thread-0 执行任务0
线程Thread-3 执行任务3
线程Thread-4 执行任务4
线程Thread-4 执行任务5
线程Thread-4 执行任务6
线程Thread-4 执行任务7
线程Thread-2 执行任务8
线程Thread-0 执行任务12
线程Thread-1 执行任务11
线程Thread-4 执行任务9
线程Thread-3 执行任务10
线程Thread-4 执行任务16
线程Thread-1 执行任务15
线程Thread-0 执行任务14
线程Thread-2 执行任务13
线程Thread-0 执行任务20
线程Thread-1 执行任务19
线程Thread-4 执行任务18
线程Thread-3 执行任务17
线程Thread-4 执行任务24
线程Thread-1 执行任务23
线程Thread-0 执行任务22
线程Thread-2 执行任务21
线程Thread-0 执行任务28
线程Thread-1 执行任务27
线程Thread-4 执行任务26
线程Thread-3 执行任务25
线程Thread-2 执行任务29
Process finished with exit code 0
Executor的使用到此为止基本讲解完毕,其实线程池我们都知道java自己就提供了很好的设计方案,后面会开博文详细说明,如有不当之处欢迎批评指正