带你认识多线程、线程池,并简单使用

线程池和多线程的介绍

在多线程编程中,线程池和多线程都是比较常用的概念。线程池是一组预先创建的线程,用于执行不同的并发任务,从而减少线程创建和销毁的开销。而多线程则是指一个程序有多个线程同时执行,这些线程可以在同一时间或交替执行,并发地执行程序中的多个操作,从而提高程序的效率。

线程池和多线程都具有其优点和缺点。在使用过程中,我们需要权衡利弊,选择适合自己应用场景的方法。

线程池和多线程的优点

线程池的优点

  1. 减少线程的创建和销毁开销:创建一个线程需要消耗大量的系统资源,并且销毁线程也需要一定的时间,而线程池中的线程已经创建好,并且可以重复利用,避免了创建和销毁线程的开销。
  2. 控制并发数量:通过定义线程池的大小,可以控制并发数量,避免因为线程过多导致系统负载过高。
  3. 提供更好的响应速度:线程池可以提高系统的响应速度,当有新的任务需要处理时,可以直接使用已经创建好的线程,避免了等待新线程创建的时间。

多线程的优点

  1. 提高程序的效率:多个线程可以并发执行程序中的多个任务,从而提高程序的效率。
  2. 提升系统的吞吐量:多线程可以提升系统的吞吐量,因为在等待某些操作完成时,可以切换到执行其他操作,从而充分利用 CPU 时间。
  3. 改善用户体验:多线程可以使得程序能够更加及时地响应用户操作,从而改善用户体验。

线程池和多线程的缺点

线程池的缺点

  1. 线程数难以控制:如果线程池中的线程数量设置不当,会导致系统负荷过高或者资源浪费等问题。
  2. Task 队列可能会满:任务队列的容量有限,如果任务的数量超过了队列的容量,那么可能会导致某些任务无法执行。
  3. 过多的锁竞争:在线程池的实现过程中,需要加锁进行线程同步,如果加锁不当,可能会产生过多的锁竞争,从而影响程序的性能。

多线程的缺点

  1. 程序的复杂性增加:多线程编程需要考虑线程同步、线程安全等因素,增加了程序的复杂性。
  2. 线程死锁:如果线程在获取资源时发生死锁,就会一直阻塞下去,从而导致系统不能正常工作。
  3. 程序调试难度增加:由于程序的多线程并发执行特性,程序出错时难以找出错误发生的位置,增加了程序调试的难度。

线程池和多线程的原理

在 Java 中,线程池一般是通过 Executor 和 ExecutorService 接口实现的。这两个接口分别表示线程池和线程池服务,其中,Executor 接口中只包含一个方法 execute,用于提交一个 Runnable 实例,而 ExecutorService 接口中则包含了很多方法,比如 submitinvokeAnyinvokeAll等,用于提交任务、执行任务和获取任务执行结果等操作。

在使用线程池时,需要通过 Executors 工厂类创建一个线程池。这个工厂类提供了一些静态方法可以方便地创建不同类型的线程池,比如:

ExecutorService executorService = Executors.newFixedThreadPool(10); // 创建固定大小的线程池
ExecutorService executorService = Executors.newSingleThreadExecutor(); // 创建只有一个线程的线程池
ExecutorService executorService = Executors.newCachedThreadPool(); // 创建一个可以根据需要创建新线程的池

通过 execute 或 submit 方法提交任务,任务可以是 Runnable 或 Callable 的实例。简单来说,Runnable 和 Callable 接口都是用于描述一个任务的,但 Callable 接口还可以返回任务的执行结果。

线程池有多种工作队列供选择,其中最常见的是有界队列和无界队列。如果使用有界队列,当线程池中的任务数达到上限时,新的任务将会被放入队列中等待被执行。而如果使用无界队列,那么任何时候都可以添加任务到队列中,不会因为队列满而被拒绝。但是使用无界队列也会带来风险,因为队列可能会无限增长,从而导致内存溢出等问题。

多线程的实现原理基于操作系统的 CPU 调度机制,操作系统在运行多个线程的时候,根据不同的调度算法来分配 CPU 时间片给不同的线程,从而实现多线程的并发执行。

如何使用线程池和多线程

线程池的使用可以在多种场景下提高程序的效率。下面通过几个示例来介绍如何使用线程池和多线程。

场景一:多线程下载图片并保存

在这个场景下,我们需要下载多张图片,并将其保存到磁盘上。我们可以在 ExecutorService 中提交多个下载任务,这些下载任务将会在多个线程中并发执行,提高下载速度。下面是示例代码

   public static void downloadImages(List<String> urls) {// 创建固定数量的线程池ExecutorService executorService = Executors.newFixedThreadPool(10);List<Future<Boolean>> futures = new ArrayList<>();// 提交多个下载任务for (String url : urls) {Future<Boolean> future = executorService.submit(() -> {// 下载图片并保存boolean result = downloadAndSaveImage(url);return result;});futures.add(future);}// 等待所有下载任务完成for (Future<Boolean> future : futures) {try {boolean result = future.get();if (!result) {System.out.println(“下载图片失败”);}} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}}// 关闭线程池executorService.shutdown();}

场景二:多线程读取多个文档的内容

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class DocumentGenerator {// 定义文档生成方法,返回文档内容public static String generateDocument(int id) {return "这是文档 " + id + " 的内容。";}public static void main(String[] args) {// 创建固定数量的线程池int threadPoolSize = 10;ExecutorService executorService = Executors.newFixedThreadPool(threadPoolSize);// 定义任务总数int taskNum = 16;// 创建存储文档内容的列表List<String> documentList = new ArrayList<>(taskNum);// 提交任务for (int i = 0; i < taskNum; i++) {final int taskId = i;executorService.execute(() -> {String document = generateDocument(taskId);// 将文档内容添加到列表中documentList.add(document);});}// 关闭线程池executorService.shutdown();// 等待线程池执行结束while (!executorService.isTerminated()) {Thread.yield();}// 输出文档内容for (String document : documentList) {System.out.println(document);}}
}

在这个示例中,我们将 generateDocument 方法修改为返回一个 String 类型的结果,表示文档的内容。在主函数中,我们创建一个 List<String> 对象 documentList,用于存储所有生成的文档内容。在 executorService.execute() 方法中,我们将生成的文档内容添加到 documentList 中。在所有任务执行完成后,我们通过遍历 documentList 将所有文档的内容输出到控制台。

需要注意的是,在使用线程池时,我们需要确保所有任务都执行完成后再关闭线程池。为此,我们使用了一个 while 循环,调用 Thread.yield() 方法让出 CPU 时间,等待线程池中的所有任务执行结束。

场景三:多线程执行计算密集型任务

在这个场景下,我们需要执行一些计算密集型的任务,比如计算斐波那契数列

这时,可以使用多个线程并发执行,提高计算速度。下面是示例代码 :

   public static BigInteger fibonacci(int n){if(n< 0){throw new IllegalArgumentException("n must be positive");}if(n==0||n==1){return BigInteger.valueOf(n);}// 递归计算斐波那契数列BigInteger a=fibonacci(n-1);BigInteger b=fibonacci(n-2);return a.add(b);}public static void calculateFibonacci(){// 使用多个线程并发执行计算int threadNum=10;ExecutorService executorService= Executors.newFixedThreadPool(threadNum);List<Future<BigInteger>> futures=new ArrayList<>();for(int i=0;i<threadNum; i++){int start=i*10; // 每个线程计算10个数Future<BigInteger> future=executorService.submit(()->{BigInteger result= BigInteger.valueOf(0);for(int j=start;j<start +10;j++){result=result.add(fibonacci(j));}return result;});futures.add(future);}// 统计所有线程的计算结果BigInteger total=BigInteger.valueOf(0);for(Future<BigInteger> future:futures){try{total=total.add(future.get());}catch(InterruptedException|ExecutionException e){e.printStackTrace();}}// 输出计算结果System.out.println(total);// 关闭线程池executorService.shutdown();}

小结

线程池和多线程都是常用的并发处理技术,它们可以提高程序的效率和响应速度。线程池可以减少线程创建和销毁的开销,并控制并发数量,从而提高系统的性能。多线程可以并发执行程序中的不同任务,提高程序的效率和系统的吞吐量。

在使用线程池和多线程时,需要权衡它们的优缺点并选择适合自己应用场景的方法。使用线程池前需要使用 `Executors` 工厂类创建一个线程池,然后通过 `execute` 或 `submit` 方法向线程池提交任务。使用多线程时,需要注意线程同步和线程安全等问题。

通过上面的示例,我们可以看到线程池和多线程的使用方法很简单,但需要注意一些细节问题,比如线程数、任务队列、线程同步等。只有正确地使用线程池和多线程,才能充分地发挥它们的优点,提高程序的效率和响应速度。
 

本文链接:https://my.lmcjl.com/post/13681.html

展开阅读全文

4 评论

留下您的评论.