Java并发 之 线程池系列 (1) 让多线程不再坑爹的线程池

简介: Java并发 之 线程池系列 (1) 让多线程不再坑爹的线程池


背景

线程池的来由

服务端的程序,例如数据库服务器和Web服务器,每次收到客户端的请求,都会创建一个线程来处理这些请求。

创建线程的方式又很多,例如继承Thread类、实现Runnable或者Callable接口等。

通过创建新的线程来处理客户端的请求,这种看起来很容易的方法,其实是有很大弊端且有很高的风险的。

俗话说,简单的路越走越困难,困难的路越走越简单,就是这个道理。

创建和销毁线程,会消耗大量的服务器资源,甚至创建和销毁线程消耗的时间比线程本身处理任务的时间还要长。

由于启动线程需要消耗大量的服务器资源,如果创建过多的线程会造成系统内存不足(run out of memory),因此限制线程创建的数量十分必要。

dogs_multithread_programming

什么是线程池

线程池通俗来讲就是一个取出和放回提前创建好的线程的池子,概念上,类似数据库的连接池。

那么线程池是如何发挥作用的呢?

实际上,线程池是通过重用之前创建好线程来处理当前任务,来达到大大降低线程频繁创建和销毁导致的资源消耗的目的。

A thread pool reuses previously created threads to execute current tasks and offers a solution to the problem of thread cycle overhead and resource thrashing. Since the thread is already existing when the request arrives, the delay introduced by thread creation is eliminated, making the application more responsive.

Thread Pool

背景总结

下面总结一下开篇对于线程池的一些介绍。

  1. 线程是程序的组成部分,可以帮助我们搞事情。
  2. 多个线程同时帮我们搞事情,可以通过更大限度地利用服务器资源,用来大大提高我们搞事情的效率。
  3. 我们创建的每个线程都不是省油的灯,线程越多就会占用越多的系统资源,因此小弟虽好使但不要贪多哦,在有限的系统资源下,线程并不是“韩信点兵,多多益善”的,要限制线程的数量。请记住这一条,因为下面“批判”Java提供的线程池创建解决方案的时候,这就是“罪魁祸首”。
  4. 创建和销毁线程会耗费大量系统资源,就像大佬招募和遣散小弟,都是要大费周章的。因此聪明的大佬就想到了“池”,把线程缓存起来,用的时候拿出来不用的时候还放回去,这就可以既享受多线程的乐趣,又可以避免使用多线程的痛苦了。

但到底怎么使用线程池呢?线程池真的这么简单好用吗?线程池使用的过程中有没有什么坑?

不要着急,下面就结合具体的示例,跟你讲解各种使用线程池的姿势,以及这些姿势爽在哪里,痛在哪里。

准备好纸巾,咳咳...,是笔记本,涛哥要跟你开讲啦!

用法

通过Executors创建线程池

Executors及其服务的类

java.util.concurrent.Executors是JDK的并发包下提供的一个工厂类(Factory)和工具类(Utility)。

Executors提供了关于Executor, ExecutorService, ScheduledExecutorService, ThreadFactoryCallable相关的工厂方法和工具方法。

Executor是一个执行提交的Runnable Tasks的对象,它有一个execute方法,参数是Runnable。当执行execute方法以后,会在未来某个时间,通过创建线程或者使用线程池中的线程的方式执行参数中的任务。用法如下:

Executor executor = anExecutor;
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());

ExecutorService继承了Executor,并提供了更多有意思的方法,比如shutdown方法会让ExecutorService拒绝创建新的线程来执行task。

Executors常用的几个方法


//创建固定线程数量的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);

//创建一个线程池,该线程池会根据需要创建新的线程,但如果之前创建的线程可以使用,会重用之前创建的线程
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

//创建一个只有一个线程的线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

一个线程池的例子

下面我就创建5个Task,并通过一个包含3个线程的线程池来执行任务。我们一起看下会发生什么。

Github 完整代码: 一个线程池的例子

ThreadPoolExample1就是我们的测试类,下面所有的内部类、常量和方法都写在这个测试类里。

package net.ijiangtao.tech.concurrent.jsd.threadpool;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample1 {

}

任务

Task内部类执行了两次for循环,并在每次循环执行结束以后 sleep 1秒钟。

// Task class to be executed (Step 1)
static class Task implements Runnable {

    private String name;

    public Task(String s) {
        name = s;
    }

    // Prints task name and sleeps for 1s
    // This Whole process is repeated 2 times
    public void run() {
        try {
            for (int i = 0; i <= 1; i++) {
                if (i == 0) {
                    //prints the initialization time for every task
                    printTimeMsg("Initialization");
                } else {
                    // prints the execution time for every task
                    printTimeMsg("Executing");
                }
                Thread.sleep(1000);
            }
            System.out.println(name + " complete");
        } catch (InterruptedException e) {
                e.printStackTrace();
        }
    }

    private void printTimeMsg(String state) {
        Date d = new Date();
        SimpleDateFormat ft = new SimpleDateFormat("hh:mm:ss");
        System.out.println(state+" Time for"+ " task name - " + name + " = " + ft.format(d));
    }
}

池子

创建一个固定线程数的线程池。

// Maximum number of threads in thread pool
static final int MAX_T = 3;
// creates a thread pool with MAX_T no. of
// threads as the fixed pool size(Step 2)
private static final ExecutorService pool = Executors.newFixedThreadPool(MAX_T);

测试

创建5个任务,并通过线程池的线程执行这些任务。

public static void main(String[] args) {
     // creates five tasks
     Runnable r1 = new Task("task 1");
     Runnable r2 = new Task("task 2");
     Runnable r3 = new Task("task 3");
     Runnable r4 = new Task("task 4");
     Runnable r5 = new Task("task 5");

     // passes the Task objects to the pool to execute (Step 3)
     pool.execute(r1);
     pool.execute(r2);
     pool.execute(r3);
     pool.execute(r4);
     pool.execute(r5);

     // pool shutdown ( Step 4)
     pool.shutdown();
}

执行结果如下。

Initialization Time for task name - task 1 = 12:39:44
Initialization Time for task name - task 2 = 12:39:44
Initialization Time for task name - task 3 = 12:39:44
Executing Time for task name - task 3 = 12:39:45
Executing Time for task name - task 1 = 12:39:45
Executing Time for task name - task 2 = 12:39:45
task 2 complete
Initialization Time for task name - task 4 = 12:39:46
task 3 complete
Initialization Time for task name - task 5 = 12:39:46
task 1 complete
Executing Time for task name - task 5 = 12:39:47
Executing Time for task name - task 4 = 12:39:47
task 5 complete
task 4 complete

说明

从输出的结果我们可以看到,5个任务在包含3个线程的线程池执行。

  1. 首先会有3个任务(task 1,task 2,task 3)获得线程资源并发执行;
  2. (task 2)执行成功以后,让出线程资源,(task 4)开始执行;
  3. (task 3)执行成功以后,让出线程资源,(task 5)开始执行;
  4. 最终,5个任务都执行结束,线程池将线程资源回收。

由于线程的执行有一定的随机性,以及不同机器的资源情况不同,每次的执行结果,可能会有差异。

下面是我第二次执行的结果。

Initialization Time for task name - task 1 = 12:46:33
Initialization Time for task name - task 3 = 12:46:33
Initialization Time for task name - task 2 = 12:46:33
Executing Time for task name - task 2 = 12:46:34
Executing Time for task name - task 3 = 12:46:34
Executing Time for task name - task 1 = 12:46:34
task 3 complete
task 2 complete
task 1 complete
Initialization Time for task name - task 4 = 12:46:35
Initialization Time for task name - task 5 = 12:46:35
Executing Time for task name - task 4 = 12:46:36
Executing Time for task name - task 5 = 12:46:36
task 5 complete
task 4 complete

task 1 2 3 获得线程资源,task 4 5排队等待:

task 1 2 3 获得线程资源,task 4 5排队等待

task 1 2 3 执行结束,task 4 5获得线程资源,线程池中有一个线程处于空闲状态:

task 1 2 3 执行结束,task 4 5获得线程资源

但规律是相同的,那就是线程池会将自己的线程资源贡献出来,如果任务数超出了线程池的线程数,就会阻塞并排队等待有可用的线程资源以后执行。

也就是线程池会保证你的task在将来(Future)的某个时间执行,但并不能保证什么时间会执行。

相信你现在对于ExecutorServiceinvokeAll方法,可以执行一批task并返回一个Future集合,就会有更深入的理解了。

List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException

通过ExecutorService线程池执行task的过程如下图所示,超出线程池线程数量的task将会在BlockingQueue排队等待获得线程资源的机会。

image

关于并发编程中的Futrue,笔者有一篇文章(Java并发编程-Future系列之Future的介绍和基本用法)专门介绍,请通过下面任意的链接移步欣赏:

总结

本教程带领大家了解了线程池的来由、概念和基本用法,相信大家看完,以后就不再只会傻傻地new Thread了。

本节只是线程池的入门,下面会介绍关于线程池的更多武功秘籍,希望大家持续关注,有所获益。

喜欢请点赞转发,如果大家对这个系列感兴趣,我会继续更新的。
?


Links

文章友链

相关资源

Concurrent-ThreadPool-线程池拒绝策略RejectedExecutionHandler

Concurrent-ThreadPool-ThreadPoolExecutor里面4种拒绝策略

Concurrent-ThreadPool-线程池ThreadPoolExecutor构造方法和规则

Concurrent-ThreadPool-线程池的成长之路

Concurrent-ThreadPool-LinkedBlockingQueue和ArrayBlockingQueue的异同

Concurrent-ThreadPool-最佳线程数总结

Concurrent-ThreadPool-最佳线程数

Concurrent-ThreadPool-Thread Pools in Java

Concurrent-ThreadPool-java-thread-pool

Concurrent-ThreadPool-thread-pool-java-and-guava

Concurrent-ThreadPool-ijiangtao.net

目录
相关文章
|
3天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
9天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
32 9
|
12天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
9天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
12天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
27 3
|
11天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
12天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
13天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
40 1
|
16天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
17天前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
44 4
下一篇
无影云桌面