龙空技术网

Java 线程池的基本介绍和使用

Java合集 258

前言:

此刻兄弟们对“java线程项目”大致比较注意,咱们都需要知道一些“java线程项目”的相关知识。那么小编也在网摘上搜集了一些有关“java线程项目””的相关资讯,希望姐妹们能喜欢,同学们一起来了解一下吧!

在 Java 中,可以利用线程做很多事情,创建多个线程来高效的完成任务,例如 Tomcat 用多个线程来接收和处理请求,我们也可以创建多个线程来批量处理数据,原本串行执行的任务变成并行执行,充分利用 CPU 多个核的性能。

我们可以用这样的方式创建线程执行并发任务:

for (int i = 0; i < 任务数量; i++) {    Thread thread = new Thread(任务);    thread.start();}复制代码

这样子确实能够并发完成任务,但是也带了问题:创建的线程数量不可控制,一个任务就创建一个线程,当任务量庞大的时候,会带来极大的内存开销,反复创建、销毁线程。

通过使用线程池的方式,就可以集中管理线程资源,线程池能够给我们带来如下优点:

复用线程。利用一定数量的线程,反复执行任务,而不用频繁的创建、销毁线程。控制了资源的总量,合理利用 CPU 和内存。由于复用线程,CPU 和 内存相较于创建多个线程来说占用更低。统一管理资源,统一停止线程。相同任务类型的线程被统一管理起来,能够在某些情况统一的停止这些任务。

⭐ 在实际开发中,如果需要创建超过五个线程执行类似的任务,就可以考虑使用线程池了。

创建线程池

我们已经知道了线程池的优点和强大了,现在再介绍一下线程池要怎么去创建,怎么去使用。

线程池创建线程的规则

首先,线程池有几个核心属性,分别是:

corePoolSize,核心线程数量,线程池的线程数量会维持在这个数字上maximumPoolSize,最大线程数量,创建的线程数量不会超过这个数字keepAliveTime,线程存活时间,超过核心线程数的线程,如果空闲时间超过指定时间,就会被回收任务队列,用于接收、存储待执行的任务,当线程空闲下来时,会从任务队列中取出任务并执行 直接交接队列(SynchronousQueue),队列大小为零,新任务直接开始运行,不会等待 有界队列(ArrayBlockQueue),任务队列是有限的 无界队列(LinkedBlockQueue),任务队列是无限的,理论上可以添加任意数量的任务

线程池创建线程的规则是这样的:

如果当前线程数<核心线程数当前线程数 < 核心线程数当前线程数<核心线程数,则接受新任务就创建新线程如果核心线程数≤当前线程数<最大线程数核心线程数 \le 当前线程数 < 最大线程数核心线程数≤当前线程数<最大线程数,则接收新任务时,将新任务加入到任务队列中如果任务队列满了,并且 当前线程数<最大线程数当前线程数 < 最大线程数当前线程数<最大线程数,则创建新的线程执行任务如果任务队列满了,并且 当前线程数=最大线程数当前线程数 = 最大线程数当前线程数=最大线程数,那么拒绝该任务并抛出拒绝任务异常RejectedExecutionException 使用 Executors 创建线程池

上文已经提到了线程池的几个核心属性以及创建线程的规则,其实这些核心属性是创建线程池对象的参数,主要来自于ThreadPoolExecutor类的构造方法:

public ThreadPoolExecutor(int corePoolSize,                              int maximumPoolSize,                              long keepAliveTime,                              TimeUnit unit,                              BlockingQueue<Runnable> workQueue) 复制代码

我们可以利用这个方法来创建一个线程池:

public static void main(String[] args) {      // 创建一个线程池      ThreadPoolExecutor pool = new ThreadPoolExecutor(5, 5, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>());        // 创建 10 个任务,打印线程名      for (int i = 0; i < 10; i++) {          // ThreadPoolExecutor 的 execute 方法可以接收并执行任务          // 接收 Runnable 接口实现类,Runnable 是一个方法接口,因此可以使用 Lambda 表达式          pool.execute(()-> {              System.out.println(Thread.currentThread().getName());          });      }  }复制代码

输出结果:

pool-1-thread-1pool-1-thread-4pool-1-thread-3pool-1-thread-3pool-1-thread-3pool-1-thread-3pool-1-thread-3pool-1-thread-2pool-1-thread-5pool-1-thread-1复制代码

阿里手册里面推荐我们给一个线程池指定一个线程工厂,从而另线程池创建的每个线程的名字都有意义。在使用这个方法创建线程的时候,可以这样创建,以指定具体的线程名:

// 创建一个线程工厂,实现接口 ThreadFactorypublic class MyThreadPoolFactory implements ThreadFactory {      private final AtomicInteger threadNum = new AtomicInteger(1);      private final String prefixName;        public MyThreadPoolFactory(String prefixName) {          this.prefixName = prefixName;      }        @Override      public Thread newThread(Runnable r) {          Thread t = new Thread(r, prefixName + threadNum.getAndIncrement());          return t;      }  }// 修改上面的例子,创建线程池的语句修改为:ThreadPoolExecutor pool = new ThreadPoolExecutor(5, 5, 1, TimeUnit.SECONDS,          new LinkedBlockingQueue<>(), new MyThreadPoolFactory("我的线程"));// 再次运行查看结果复制代码

运行结果:

我的线程2我的线程5我的线程3我的线程1我的线程3我的线程5我的线程2我的线程4我的线程3我的线程1复制代码

也可以参考Java线程池中设置线程名称三种方式 - 屠城校尉杜 - 博客园 (cnblogs.com),用现有的工具类创建。

我们也可以使用 Executor 类快速创建各种类型的线程池,可以创建如下类型的线程池:

线程类型

说明

固定数量的线程池

创建一个线程池,其核心线程数和最大线程数都相同

单线程线程池

创建一个线程池,其最多只有一个线程

缓存线程池

任务队列使用直接交接队列

最大线程数为整形最大值

定时任务线程池

可以定时执行任务或者周期执行任务

我们来看看 Executor 的用法,首先是看看固定数量的线程池的创建方法:

public static void main(String[] args) {      ExecutorService pool = Executors.newFixedThreadPool(5, new MyThreadPoolFactory("固定线程池"));      for (int i = 0; i < 10; i++) {          pool.submit(() -> {              System.out.println(Thread.currentThread().getName());          });      }  }// 运行结果如下:固定线程池3固定线程池5固定线程池5固定线程池4固定线程池1固定线程池2固定线程池1固定线程池4固定线程池3固定线程池5// 方法实际调用情况,还是用到了我们上面提到的方法:public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {      return new ThreadPoolExecutor(nThreads, nThreads,                                    0L, TimeUnit.MILLISECONDS,                                    new LinkedBlockingQueue<Runnable>(),                                    threadFactory);  }复制代码

此外,其他类型的线程池创建方法也大同小异,具体可以看看下面的Executors的相关方法,使用起来还是很简单的。

// 创建一个缓存线程池public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {      return new ThreadPoolExecutor(0, Integer.MAX_VALUE,                                    60L, TimeUnit.SECONDS,                                    new SynchronousQueue<Runnable>(),                                    threadFactory);  }// 创建一个单线程线程池public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {      return new FinalizableDelegatedExecutorService          (new ThreadPoolExecutor(1, 1,                                  0L, TimeUnit.MILLISECONDS,                                  new LinkedBlockingQueue<Runnable>(),                                  threadFactory));  }// 创建一个定时任务线程池,这里其实还是调用了 new ThreadPoolExecutor(),可以进源码查看,任务队列使用延时队列public static ScheduledExecutorService newScheduledThreadPool(          int corePoolSize, ThreadFactory threadFactory) {      return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);  }public ScheduledThreadPoolExecutor(int corePoolSize,                                     ThreadFactory threadFactory) {      super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,            new DelayedWorkQueue(), threadFactory);  }// ScheduledThreadPoolExecutor 的 schedule 方法可以指定任务在一定时间后开始运行,运行结束后任务结束// scheduleAtFixedRate 方法则可以让一个任务以指定的周期运行,运行结束后会在指定时间后继续运行复制代码
推荐的方法

阿里的Java开发手册是推荐我们使用 ThreadPoolExecutor 来创建线程池,并且指定线程工厂从而让每个线程都有自己的具体名字的。因此,在实际开发过程中,我们可以尽量使用 ThreadPoolExecutor 创建线程池。

指定合适的线程数量

创建线程池指定合适的线程数量对于应用的性能也很有影响,线程数量分配少了,则性能有限,线程数量分配多了,则浪费了资源。但是要如何确定线程需要的数量呢?

如果是CPU密集型的应用,例如加密、哈希计算、大量计算的任务,可以考虑使用 1-2 被CPU核心数量的线程数。如果是IO密集型的应用,例如需要反复读写磁盘的任务,则可以指定尽可能多倍于CPU核心数的线程数,因为大多数后线程其实是在等待磁盘 IO 的,可以让其他线程使用资源。 还有一条经验法则,可以利用这条公式设置线程数:线程数=CPU核心数×(1+平均等待时间平均工作时间)线程数=CPU核心数 \times (1 + \frac{平均等待时间}{平均工作时间})线程数=CPU核心数×(1+平均工作时间平均等待时间) 总而言之,线程数的设置没有一个通用的规则,而是根据具体的应用场景不断调试出合适的数量。 停止线程池

在使用线程池的过程中,我们也并不是想要让线程池一直运行下去的,有时候可能根据业务需要,例如项目需要紧急停止、用户需要暂停等等,我们需要让一个正在运行并且正在执行任务的线程池停止下来。 那么,具体可以怎么停止一个线程池呢?可以看看下面的代码:

public class ThreadPoolDemo implements Runnable {        @Override      public void run() {          try {              Thread.sleep(500);          } catch (InterruptedException e) {              throw new RuntimeException(e);          }      }        public static void main(String[] args) throws InterruptedException {          ExecutorService pool = Executors.newFixedThreadPool(5, new MyThreadPoolFactory("演示停止线程池"));          for (int i = 0; i < 50; i++) {              pool.submit(new ThreadPoolDemo());          }            // 查看线程池是否处于 shutdown 状态          System.out.println("线程池 shutdown:" + pool.isShutdown());          System.out.println("停止线程池");          pool.shutdown();          // 线程池已经进入 shutdown 状态,但是应用还在运行,线程池中的任务还在执行          System.out.println("线程池 shutdown:" + pool.isShutdown());            // 提交新任务会报异常          //pool.submit(new ThreadPoolDemo());            // 如果想要立即停止线程池,可以使用 shutdownNow,会返回线程池中未执行完毕的任务列表          // List<Runnable> runnables = pool.shutdownNow();            // 查看线程池是否已经终止          System.out.println("线程池 terminated:" + pool.isTerminated());          // 等待线程池中的任务运行完毕,主线程阻塞指定时间          pool.awaitTermination(1, TimeUnit.MINUTES);          System.out.println("线程池 terminated:" + pool.isTerminated());      }}// 打印结果:线程池 shutdown:false停止线程池线程池 shutdown:true线程池 terminated:false线程池 terminated:true复制代码

上面的例子已经演示了如何停止一个正在运行的线程池,现在总结一下上面相关方法的区别:

方法

说明

shutdown

停止线程池,线程池并不会马上停止。拒绝接受新任务,如果提交新任务会抛出异常

shutdownNow

立即停止线程池,正在执行任务的线程会收到中断通知。返回任务队列中的任务

isShutdown

线程池是否收到了 shutdown 指令

isTerminated

线程池是否已经停止

awaitTerminated

调用该方法的线程阻塞住,等待指定时间,运行结果如下:

如果所有任务执行完毕,返回 true。

超过了等待时间,返回false。

当前线程被中断,抛出中断异常

为线程池指定拒绝策略

线程池在以下这些情况会拒绝任务:

线程池的状态是 shutdown,此时提交任务会执行拒绝策略线程池的任务队列已满,并且线程数量已经到达最大线程数,此时提交任务会执行拒绝策略

可以指定的拒绝策略如下,具体可以查看接口RejectedExecutionHandler 的实现类:

策略

说明

AbortPolicy

拒绝任务并直接抛出异常,默认策略

DiscardPolicy

默默丢弃任务,不抛出异常

CallerRunsPolicy

拒绝任务,并让提交任务的线程运行被拒绝的任务

DiscardOldestPolicy

丢弃最早提交的任务,然后重新尝试接收该任务

可以在 ThreadPoolExecutor 的这个构造方法中指定拒绝策略:

public ThreadPoolExecutor(int corePoolSize,                            int maximumPoolSize,                            long keepAliveTime,                            TimeUnit unit,                            BlockingQueue<Runnable> workQueue,                            ThreadFactory threadFactory,                            RejectedExecutionHandler handler);// 例如 new ThreadPoolExecutor(2,5,1,...,new ThreadPoolExecutor.CallerRunsPolicy());复制代码
线程池的状态

从上文中也可以看出,线程池有着各种状态,或者可以提交并执行任务,或者不再接受任务但是仍然执行运行中的任务,或者直接中断执行的任务…… 下面是线程池的各种状态:

状态

说明

RUNNING

线程池创建后的状态,接收并执行任务

SHUTDOWN

拒绝接收新任务,等待任务执行完成,shutdown() 执行后的状态

STOP

拒绝接收新任务,中断执行中的任务,返回任务队列中的任务,shutdownNow() 执行后的状态

TIDYING

任务已经停止,没有工作线程,执行线程池的 terminated() 方法

TERMINATED

线程池已经停止

标签: #java线程项目