四种线程池的使用(JAVA笔记-线程基础篇)

线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。——百度百科

  • 简单来说,线程池(thread pool)就像池子一样,不过池子里面可能放的是水,需要水就去池子里打水。而线程池里面放的是线程,需要线程的时候就去线程池里取线程。因为一个线程可能只执行一个很小的功能,比如计算个1+1等于几,如果每次都重新创建一个新线程计算,用完就把线程给销毁了,这样很浪费效率(创建和销毁线程很费时费力)。
  • 如果我创建一个线程去计算1+1,计算完把这条线程放到线程池里面,让线程池帮我维护,别让程序给销毁了。下次计算1+2的时候,直接跟线程池要线程就好,不用再创建了。

JAVA提供的线程池工具类

在JAVA的java.util.concurrent包(大佬们都叫这个包JUC)下面有一个工具类Executors。这个类封装了创建四种线程池的方法。底层都是通过new ThreadPoolExecutor(...)来实现ExecutorService对象的。

  1. newSingleThreadExecutor()单线程池

    • 作用:创建一个只有一个线程的线程池,如果有超过一个的任务进来,就放在队列中等待。等上一个任务执行完再来执行下一个。
    • 适用范围:适合长期执行的任务,或者需要按照顺序执行的一系列任务。

    创建线程池

     ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
    

    为了使用这个线程池,我们创建3个Thread对象,也就是线程需要执行的任务。

    Thread thread1=new Thread(new Runnable() {
         public void run() {
             System.out.println(Thread.currentThread().getName()+"使用线程池中的线程执行的!");
         }
     });
     Thread thread2=new Thread(new Runnable() {
         public void run() {
             System.out.println(Thread.currentThread().getName()+"使用线程池中的线程执行的!");
         }
     });
     Thread thread3=new Thread(new Runnable() {
         public void run() {
             System.out.println(Thread.currentThread().getName()+"使用线程池中的线程执行的!");
         }
     });
    

    使用刚才创建的线程池来执行thread对象。

    singleThreadExecutor.execute(thread1);
    singleThreadExecutor.execute(thread2);
    singleThreadExecutor.execute(thread3);
    

    完整代码

    public class MyTest {
         public static void main(String[] args) {
             ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
    
             Thread thread1=new Thread(new Runnable() {
             public void run() {
                 //使用getName()获取执行这段代码的线程名。下同
                 System.out.println(Thread.currentThread().getName()+"  使用线程池中的线程执行的!");
             }
             });
             Thread thread2=new Thread(new Runnable() {
                 public void run() {
                     System.out.println(Thread.currentThread().getName()+"  使用线程池中的线程执行的!");
                 }
             });
             Thread thread3=new Thread(new Runnable() {
                 public void run() {
                     System.out.println(Thread.currentThread().getName()+"  使用线程池中的线程执行的!");
                 }
             });
             singleThreadExecutor.execute(thread1);
             singleThreadExecutor.execute(thread2);
             singleThreadExecutor.execute(thread3);
         }
     }
    

    输出:
    pool-1-thread-1 使用线程池中的线程执行的!
    pool-1-thread-1 使用线程池中的线程执行的!
    pool-1-thread-1 使用线程池中的线程执行的!

    看!上面输出的结果很好玩,线程名称都是pool-1-thread-1
    如果我们不用singleThreadExecutor.execute(thread1);而直接使用thread1.start();会怎么样呢?
    直接改代码

    public class MyTest {
         public static void main(String[] args) {
             Thread thread1=new Thread(new Runnable() {
                 public void run() {
                     System.out.println(Thread.currentThread().getName()+"  使用线程池中的线程执行的!");
                 }
             });
             Thread thread2=new Thread(new Runnable() {
                 public void run() {
                     System.out.println(Thread.currentThread().getName()+"  使用线程池中的线程执行的!");
                 }
             });
             Thread thread3=new Thread(new Runnable() {
                 public void run() {
                     System.out.println(Thread.currentThread().getName()+"  使用线程池中的线程执行的!");
                 }
             });
             //这里跟上面的例子不一样
             thread1.start();
             thread2.start();
             thread3.start();
         }
     }
    

    输出结果:
    Thread-0 使用线程池中的线程执行的!
    Thread-2 使用线程池中的线程执行的!
    Thread-1 使用线程池中的线程执行的!

    这次运行三段代码的线程名不同了,也就是说是三个不同的线程。
    而且更有意思的是,如果多运行几遍,这三个线程输出顺序也可能会改变。这就说明他们三个线程被CPU“翻牌”的概率是不一定的,并不是哪个线程先.start()哪个线程里的代码先执行完。有时候Thread-0更受宠一点,而有时候Thread-2更受宠一点。

    结论:

    • newSingleThreadExecutor()创建的线程池里面只有一个可用线程。
    • 任务的执行是按照添加的顺序执行的。
    • 如果线程正在执行。这时候有其他的任务进来,需要线程执行,就会把其放在队列中等待。队列是五界的。

      队列无界,通俗的说就是来者不拒,不管执不执行,你既然任务来了,就放在队列里等着吧。等这一个线程挨个的执行。
      细想这个线程池是不是又问题。来者不拒?我给你一百万、一千万、一亿个任务呢??你内存不是爆表了嘛?就OOM拉。

  2. newFixedThreadPool()

    • 作用:创建一个有固定大小的线程池,也就是指定线程池里面的线程数量。
    • 适合场景:适合长期执行的任务,并且希望控制线程数量。

    创建一个有两个线程的线程池

    ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2);
    

    写三个thread对象来测试一下

    public class MyTest {
         public static void main(String[] args) {
             ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2);
             Thread thread1=new Thread(new Runnable() {
                 public void run() {
                     System.out.println(Thread.currentThread().getName()+" 使用线程池中的线程执行的!thread1");
                 }
             });
             Thread thread2=new Thread(new Runnable() {
                 public void run() {
                     System.out.println(Thread.currentThread().getName()+" 使用线程池中的线程执行的!thread2");
                 }
             });
             Thread thread3=new Thread(new Runnable() {
                 public void run() {
                     System.out.println(Thread.currentThread().getName()+" 使用线程池中的线程执行的!thread3");
                 }
             });
             fixedThreadPool.execute(thread1);
             fixedThreadPool.execute(thread2);
             fixedThreadPool.execute(thread3);
         }
     }
    

    输出:
    pool-1-thread-1 使用线程池中的线程执行的!thread1
    pool-1-thread-2 使用线程池中的线程执行的!thread2
    pool-1-thread-1 使用线程池中的线程执行的!thread3

    可以看出pool-1-thread-1执行了thread1thread3的代码,而pool-1-thread-2执行了thread2的代码。这也说明线程池里至少有两个线程。

    结论:

    • newFixedThreadPool()创建一个拥有固定线程数量的线程池,线程数量通过传参设置。如:Executors.newFixedThreadPool(2)
  3. newCachedThreadPool()缓存型线程池

    • 作用:创建一个缓存型的线程池,有新任务进来,先查找有没有空闲的线程,如果有拿过来用,如果没有,就会创建新的线程并放到核心线程池里。
      此线程池里的线程存活是有一定时间的,如果一个线程空闲了一定时间没有被使用,就会被销毁。
      所以这个线程池里的线程不会太多空闲,也不会有不足。
    • 使用场景:适合执行周期短而多的任务。
      创建一个缓存型线程池
    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
    

    用法上同。

  4. newScheduledThreadPool()计划型线程池

    • 作用:创建一个固定大小的线程池,线程池内的线程存活周期无限长,支持定时或者周期性的执行某个任务(比如隔3秒执行一次)等。
    • 使用场景:有周期性或者定时执行某个任务的需要。

    声明一个有三个线程的计划型线程池

    ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
    

    创建三个任务,也就是Thread对象。

    Thread thread1=new Thread(new Runnable() {
         public void run() {
             System.out.println(Thread.currentThread().getName()+"使用线程池中的线程执行的!thread1");
         }
     });
     Thread thread2=new Thread(new Runnable() {
         public void run() {
             System.out.println(Thread.currentThread().getName()+"使用线程池中的线程执行的!thread2");
         }
     });
     Thread thread3=new Thread(new Runnable() {
         public void run() {
             System.out.println(Thread.currentThread().getName()+"使用线程池中的线程执行的!thread3");
         }
     });
    

    十秒后执行任务1,thread1

    scheduledExecutorService.schedule(thread1,10,TimeUnit.SECONDS);
    

    说明:
    第一个参数是任务,也就是thread实例对象
    第二个参数是一个数字,跟第三个参数组合使用来确定多长时间后执行。
    第三个参数是一个TimeUnit的枚举类型。TimeUnit.SECONDS是秒、TimeUnit.HOURS是小时等等。

    10秒后开始,每个3秒执行一遍任务2

    scheduledExecutorService.scheduleAtFixedRate(thread2,10,3,TimeUnit.SECONDS);
    

    比上面的多了一参数,就是第三个(上面例子中的3),这个参数代表每隔多长时间执行一次任务。

    完整测试代码:

    public class MyTest {
        public static void main(String[] args) {
            ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
            Thread thread1=new Thread(new Runnable() {
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"使用线程池中的线程执行的!thread1");
                }
            });
            Thread thread2=new Thread(new Runnable() {
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"使用线程池中的线程执行的!thread2");
                }
            });
            Thread thread3=new Thread(new Runnable() {
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"使用线程池中的线程执行的!thread3");
                }
            });
            //10秒后执行任务1
            scheduledExecutorService.schedule(thread1,10,TimeUnit.HOURS);
            //10秒后开始,每个3秒执行一遍任务2
            scheduledExecutorService.scheduleAtFixedRate(thread2,10,3,TimeUnit.SECONDS);
            //没有任何特点的执行任务3
            scheduledExecutorService.execute(thread3);
        }
    }
    

结尾

很多大佬都不推荐直接使用Executors封装好的实现线程的方法,因为在大型项目中会暴露出很多问题。比如我们常用的newFixedThreadPool()它的储存多余任务的队列是无边界的。
Executors中的源码是这么实现的:

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, 
                                  nThreads,
                                  0L,
                                  TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}

先不用管其他的参数,看看第五个参数new LinkedBlockingQueue<Runnable>(),它是一个无界的队列。所以就可以无限制的往里面增加等待任务。这样添加个几千万几百万的不久内存溢出了嘛。

其他的也多多少少存在一些问题。所以大佬们推荐自己动手创建自定义的线程池。

如果有缘,我们下一篇笔记见。

作者:BobC

文章原创。如你发现错误,欢迎指正,在这里先谢过了。博主的所有的文章、笔记都会在优化并整理后发布在个人公众号上,如果我的笔记对你有一定的用处的话,欢迎关注一下,我会提供更多优质的笔记的。
原文地址:https://www.cnblogs.com/Eastry/p/13033888.html