Java线程池的使用

线程池的产生

(1)线程池的产生来源于new Thread的弊端

在程序中创建一个线程,通常如下形式:

new Thread(new Runnable() {
    @Override
    public void run() {}
}).start();

但这种方式的弊端很明显:

  • 每次都new Thread新建对象,性能差。
  • 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,占用过多系统资源,最终导致ANR或OOM。
  • 缺乏更多功能,如定时执行、定期执行、线程中断等。

(2)使用线程池的好处

  • 减少了创建和销毁线程的次数,重用存在的线程,在执行大量异步任务时增强了程序性能。
  • 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
  • 提供定时执行、定期执行和线程控制等功能。

(3)线程池的作用

  • 线程池作用就是限制系统中执行线程的数量。
  • 要配置一个线程池是比较复杂的,而且很有可能配置的线程池不是较优的,于是Executors类里面提供了一些静态工厂,生成一些常用的线程池。

(4)线程池的类图

(5)线程池的工作原理

Java中线程池共由四个部分组成(和Android中的Handler机制有相似之处):线程池管理器(ThreadPool)、工作线程(PoolWorker)、任务接口(Task)、任务队列(taskQueue)。本文不对此做详细探讨。

Executors提供四种线程池

Java通过Executors提供四种线程池,分别为:newCachedThreadPool、newFixedThreadPool、newScheduledThreadPool、newSingleThreadExecutor。

(1)newCachedThreadPool

  • 创建一个可缓存且无限大的线程池。
  • 调用 execute 将重用以前构造的线程(如果线程可用),如果现有线程没有可用的,则创建一个新线程并添加到池中。
  • 终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。

源码:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
            60L, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>());
}

例子:

private void myThreadPoolExecutor() {
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < 20; i++) {
        int finalI = i;
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000 + 100 * finalI);
                    Log.i(TAG, Thread.currentThread().getName() + "_run: " + finalI);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

Log:

pool-2-thread-1_run: 0
pool-2-thread-2_run: 1
pool-2-thread-3_run: 2
pool-2-thread-4_run: 3
pool-2-thread-1_run: 4
pool-2-thread-5_run: 5
pool-2-thread-6_run: 6
pool-2-thread-7_run: 7
pool-2-thread-8_run: 8
pool-2-thread-9_run: 9
pool-2-thread-10_run: 10
pool-2-thread-11_run: 11
pool-2-thread-12_run: 12
pool-2-thread-13_run: 13
pool-2-thread-14_run: 14
pool-2-thread-15_run: 15
pool-2-thread-16_run: 16
pool-2-thread-17_run: 17
pool-2-thread-18_run: 18
pool-2-thread-19_run: 19

(2)newFixedThreadPool

  • 创建固定大小的线程池。
  • 可控制线程最大并发数,超出的线程会在队列中等待。
  • 在某个线程被显式地关闭之前,池中的线程将一直存在。如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

源码:

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

(3)newScheduledThreadPool

创建一个线程池,支持定时和周期性地执行任务。

源码:

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
            DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
            new DelayedWorkQueue());
}

方法:

  • schedule():创建并执行在给定延迟后的操作。
  • scheduleAtFixedRate():创建并执行一个在给定初始延迟后,首次启用的定期操作,后续操作具有给定的周期。
  • scheduleWithFixedDelay():创建并执行一个在给定初始延迟后,首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟。

scheduleAtFixedRate与scheduleWithFixedDelay的区别:

  • scheduleAtFixedRate不管任务是否完成,只要间隔的时间到了,就会执行下一次,它不受计划执行时间的影响。另外,如果程序的执行时间大于间隔时间的话就会按照程序的执行时间当作间隔,如果程序的执行时间小于间隔时间的话就会按照间隔时间来执行。
  • scheduleWithFixedDelay要等到任务执行完成之后才会执行间隔,它受计划执行时间的影响。

例子:

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
Log.i(TAG, "myThreadPoolExecutor: ");
scheduledExecutorService.schedule(new Runnable() {
    @Override
    public void run() {
        Log.i(TAG, "run: ");
    }
},2, TimeUnit.SECONDS); // 延迟两秒执行

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
Log.i(TAG, "myThreadPoolExecutor: ");
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        Log.i(TAG, "run: ");
    }
},2,1,TimeUnit.SECONDS); // 延迟两秒执行,以后每隔1秒执行一次。

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
Log.i(TAG, "myThreadPoolExecutor: ");
scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
    @Override
    public void run() {
        Log.i(TAG, "run: ");
    }
},2,1, TimeUnit.SECONDS); // 延迟两秒执行,以后在任务执行完成之后,每隔1秒执行一次。

(4)newSingleThreadExecutor

  • 创建一个单线程的线程池,永远会维护存在一条线程。
  • 它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行,相当于单线程串行执行所有任务。
  • 如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。 
  • 和newFixedThreadPool一样,在线程池中没有任务执行时,也不会释放系统资源的,所以需要shudown()方法。

源码:

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                    0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>()));
}

例子:

ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 20; i++) {
    int finalI = i;
    executorService.execute(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
                Log.i(TAG, Thread.currentThread().getName() + "_run: " + finalI);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
} // 结果依次输出,相当于顺序执行各个任务。

5)核心和最大池大小

  • ThreadPoolExecutor将根据corePoolSize和maximumPoolSize设置的边界自动调整池大小。
  • 当新任务在方法execute(java.lang.Runnable)中提交时,如果运行的线程少于corePoolSize,则创建新线程来处理请求,即使其他辅助线程是空闲的。如果运行的线程多于corePoolSize而少于maximumPoolSize,则仅当队列满时才创建新线程。e.g. newScheduledThreadPool
  • 如果设置的corePoolSize和maximumPoolSize相同,则创建了固定大小的线程池。e.g. newFixedThreadPool
  • 如果将 maximumPoolSize 设置为基本的无界值(如 Integer.MAX_VALUE),则允许池适应任意数量的并发任务。e.g. newCachedThreadPool
  • 在大多数情况下,核心和最大池大小仅基于构造来设置,不过也可以使用 setCorePoolSize(int) 和 setMaximumPoolSize(int) 进行动态更改。

(6)合理使用线程池

  • 一般推荐使用newCachedThreadPool,对于执行很多短期异步任务的程序而言,可提高程序性能,而且会终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
  • 当需要固定线程数时可使用newFixedThreadPool。
  • newScheduledThreadPool支持定时和周期性地执行任务。
  • Android的UI线程就是一种单线程。

一道面试题

现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?

(1)可以通过join方法来确保t1、t2、t3的执行顺序。

private void myJoin() {
    Log.i(TAG, Thread.currentThread().getName() + "正在执行");
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            Log.i(TAG, Thread.currentThread().getName() + "执行完成");
        }
    };
    Thread thread1 = new Thread(runnable, "thread1");
    Thread thread2 = new Thread(runnable, "thread2");
    Thread thread3 = new Thread(runnable, "thread3");
    try {
        thread1.start();
        thread1.join(); // thread1线程插入正在执行的main线程,直到thread1线程执行完毕后,继续执行main线程。
        thread2.start();
        thread2.join();
        thread3.start();
        thread3.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    Log.i(TAG, Thread.currentThread().getName() + "正在执行");
}

(2)Log:

main正在执行
thread1执行完成
thread2执行完成
thread3执行完成
main正在执行

(3)Thread中的join方法:共重载了三个

  • 主要作用:就是同步,它可以使得线程之间的并行执行变为串行执行。
  • join(long millis):等待该线程终止的时间最长为 millis 毫秒。millis为 0 意味着要一直等下去,直到该线程运行结束。
  • join(long millis, int nanos):等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
  • join():等待该线程运行结束。内部调用join(0);
  • join方法必须在线程start方法调用之后调用才有意义。如果一个线程都没有start,那么线程都还没创建,那它也就无法同步了。
<think>好的,我需要回答用户关于如何在Java使用线程池的问题,包括示例代码和最佳实践。首先,我应该回顾用户提供的引用内容,看看有没有相关的信息可以利用。 根据引用[1],提到了Java线程池使用实例、最佳实践以及代码示例。引用[2]讨论了为什么使用线程池、如何使用以及遇到的坑。引用[3]则集中在最佳实践和异常处理方面。这些引用应该能提供足够的信息来构建回答。 首先,我需要解释线程池的基本概念和为什么要使用线程池,比如资源复用、控制并发数、管理线程生命周期等。然后,介绍Java中创建线程池的方法,特别是通过Executors工厂类提供的几种常见线程池类型,比如FixedThreadPool、CachedThreadPool、SingleThreadExecutor等,但也要提到推荐直接使用ThreadPoolExecutor来创建,以便更灵活地控制参数,避免资源耗尽的风险,这点在引用[3]中有提到。 接下来,给出一个示例代码。根据引用[1]中的示例,可能需要展示如何通过ThreadPoolExecutor来创建线程池,设置核心线程数、最大线程数、队列容量等参数,并演示如何提交任务。同时,需要强调在阿里巴巴开发规范中推荐手动创建线程池,而不是使用Executors,这点可能来自引用[3]中的最佳实践。 然后,讨论最佳实践,比如合理配置线程数、使用有界队列、处理未捕获异常、正确关闭线程池等。这些内容在引用[3]中有涉及,比如异常处理和分析的重要性,以及关闭线程池的方法。 此外,要注意线程池的任务拒绝策略,如AbortPolicy、CallerRunsPolicy等,并说明在不同场景下如何选择合适的策略。这部分可能需要结合引用[2]中提到的使用过程中遇到的坑。 最后,生成相关问题,可能包括线程池的参数配置、不同线程池类型的区别、如何监控线程池状态、常见的线程池问题排查等,确保问题与用户查询内容相关,并帮助深化理解。 需要确保回答结构清晰,分步骤说明,代码正确,并且引用正确的位置。例如,在提到线程池参数配置时,引用[3]中的最佳实践;在提到任务拒绝策略时,引用[2]中的实际应用经验。 现在需要将以上思考整理成中文回答,确保符合用户要求的格式,使用正确的LaTeX语法,并在适当的位置添加引用标识。</think>### 如何在Java使用线程池? #### 1. **线程池的作用与优势** - **资源复用**:避免频繁创建/销毁线程,提升性能[^2]。 - **控制并发**:通过核心线程数、最大线程数限制资源占用[^3]。 - **任务管理**:使用队列缓冲任务,支持拒绝策略处理过载[^1]。 #### 2. **创建线程池** Java推荐通过`ThreadPoolExecutor`手动配置参数(而非`Executors`工厂类,避免潜在风险)[^3]: ```java int corePoolSize = 5; // 核心线程数 int maxPoolSize = 10; // 最大线程数 long keepAliveTime = 60L; // 空闲线程存活时间(秒) BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(100); // 有界队列 ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.SECONDS, queue, new ThreadPoolExecutor.AbortPolicy() // 默认拒绝策略:直接抛出异常 ); ``` #### 3. **提交任务** - 使用`execute()`提交`Runnable`任务: ```java executor.execute(() -> { System.out.println("Task executed by " + Thread.currentThread().getName()); }); ``` - 使用`submit()`提交`Callable`任务(可获取返回值): ```java Future<String> future = executor.submit(() -> { return "Result from thread " + Thread.currentThread().getName(); }); System.out.println(future.get()); // 阻塞获取结果 ``` #### 4. **最佳实践** - **参数配置**: - CPU密集型任务:核心线程数 ≈ CPU核心数[^3]。 - I/O密集型任务:核心线程数可适当增加(如2倍CPU核心数)。 - 使用有界队列避免内存溢出[^3]。 - **异常处理**: ```java executor.setRejectedExecutionHandler((r, executor) -> { // 自定义拒绝策略,如记录日志或降级处理 }); // 任务内捕获异常 executor.execute(() -> { try { // 业务逻辑 } catch (Exception e) { // 记录异常 } }); ``` - **关闭线程池**: ```java executor.shutdown(); // 等待已提交任务完成 // executor.shutdownNow(); // 立即终止所有任务(慎用) ``` #### 5. **常用线程池类型** - **FixedThreadPool**:固定线程数,适用于负载较重的场景。 ```java ExecutorService fixedPool = Executors.newFixedThreadPool(5); ``` - **CachedThreadPool**:弹性线程数,适合短时异步任务[^1]。 - **ScheduledThreadPool**:支持定时/周期性任务。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值