Java Executor浅入浅出

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/chunqiuwei/article/details/81387724

基础无论什么时候都是很重要的,为了防止长期也业务代码就此废掉,博主决定长期研究下多线程的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自己就提供了很好的设计方案,后面会开博文详细说明,如有不当之处欢迎批评指正

猜你喜欢

转载自blog.csdn.net/chunqiuwei/article/details/81387724