一份针对于新手的多线程实践(下)

简介: 空余时间写了一个工具: github.com/crossoverJi… 利用 SpringBoot 只需要一行命令即可统计自己写了多少个字。 java -jar nows-0.0.1-SNAPSHOT.jar /xx/Hexo/source/_posts 传入需要扫描的文章目录即可输出结果(目前只支持 .md 结尾 Markdown 文件)

多线程模式


在我本地一共就几十篇博客的条件下执行一次还是很快的,但如果我们的文件是几万、几十万甚至上百万呢。


虽然功能可以实现,但可以想象这样的耗时绝对是成倍的增加。


这时多线程就发挥优势了,由多个线程分别去读取文件最后汇总结果即可。


这样实现的过程就变为:


  • 读取某个目录下的所有文件。


  • 将文件路径交由不同的线程自行处理。


  • 最终汇总结果。


多线程带来的问题


也不是使用多线程就万事大吉了,先来看看第一个问题:共享资源。


简单来说就是怎么保证多线程和单线程统计的总字数是一致的。


基于我本地的环境先看看单线程运行的结果:



总计为:414142 字。


接下来换为多线程的方式:


List<String> allFile = scannerFile.getAllFile(strings[0]);
logger.info("allFile size=[{}]",allFile.size());
for (String msg : allFile) {
  executorService.execute(new ScanNumTask(msg,filterProcessManager));
}
public class ScanNumTask implements Runnable {
    private static Logger logger = LoggerFactory.getLogger(ScanNumTask.class);
    private String path;
    private FilterProcessManager filterProcessManager;
    public ScanNumTask(String path, FilterProcessManager filterProcessManager) {
        this.path = path;
        this.filterProcessManager = filterProcessManager;
    }
    @Override
    public void run() {
        Stream<String> stringStream = null;
        try {
            stringStream = Files.lines(Paths.get(path), StandardCharsets.UTF_8);
        } catch (Exception e) {
            logger.error("IOException", e);
        }
        List<String> collect = stringStream.collect(Collectors.toList());
        for (String msg : collect) {
            filterProcessManager.process(msg);
        }
    }
}


使用线程池管理线程,更多线程池相关的内容请看这里:《如何优雅的使用和理解线程池》


执行结果:



我们会发现无论执行多少次,这个值都会小于我们的预期值。


来看看统计那里是怎么实现的。


@Component
public class TotalWords {
    private long sum = 0 ;
    public void sum(int count){
        sum += count;
    }
    public long total(){
        return sum;
    }
}


可以看到就是对一个基本类型进行累加而已。那导致这个值比预期小的原因是什么呢?


我想大部分人都会说:多线程运行时会导致有些线程把其他线程运算的值覆盖。


但其实这只是导致这个问题的表象,根本原因还是没有讲清楚。


内存可见性


核心原因其实是由 Java 内存模型(JMM)的规定导致的。


这里引用一段之前写的《你应该知道的 volatile 关键字》一段解释:


由于 Java 内存模型(JMM)规定,所有的变量都存放在主内存中,而每个线程都有着自己的工作内存(高速缓存)。


线程在工作时,需要将主内存中的数据拷贝到工作内存中。这样对数据的任何操作都是基于工作内存(效率提高),并且不能直接操作主内存以及其他线程工作内存中的数据,之后再将更新之后的数据刷新到主内存中。


这里所提到的主内存可以简单认为是堆内存,而工作内存则可以认为是栈内存


如下图所示:



所以在并发运行时可能会出现线程 B 所读取到的数据是线程 A 更新之前的数据。


更多相关内容就不再展开了,感兴趣的朋友可以翻翻以前的博文。


直接来说如何解决这个问题吧,JDK 其实已经帮我们想到了这些问题。


java.util.concurrent 并发包下有许多你可能会使用到的并发工具。


这里就非常适合 AtomicLong,它可以原子性的对数据进行修改。


来看看修改后的实现:


@Component
public class TotalWords {
    private AtomicLong sum = new AtomicLong() ;
    public void sum(int count){
        sum.addAndGet(count) ;
    }
    public  long total(){
        return sum.get() ;
    }
}


只是使用了它的两个 API 而已。再来运行下程序会发现结果居然还是不对



甚至为 0 了。


线程间通信


这时又出现了一个新的问题,来看看获取总计数据是怎么实现的。


List<String> allFile = scannerFile.getAllFile(strings[0]);
logger.info("allFile size=[{}]",allFile.size());
for (String msg : allFile) {
  executorService.execute(new ScanNumTask(msg,filterProcessManager));
}
executorService.shutdown();
long total = totalWords.total();
long end = System.currentTimeMillis();
logger.info("total sum=[{}],[{}] ms",total,end-start);


不知道大家看出问题没有,其实是在最后打印总数时并不知道其他线程是否已经执行完毕了。


因为 executorService.execute() 会直接返回,所以当打印获取数据时还没有一个线程执行完毕,也就导致了这样的结果。


关于线程间通信之前我也写过相关的内容:《深入理解线程通信》


大概的方式有以下几种:



这里我们使用线程池的方式:


在停用线程池后加上一个判断条件即可:


executorService.shutdown();
while (!executorService.awaitTermination(100, TimeUnit.MILLISECONDS)) {
  logger.info("worker running");
}
long total = totalWords.total();
long end = System.currentTimeMillis();
logger.info("total sum=[{}],[{}] ms",total,end-start);


这样我们再次尝试,发现无论多少次结果都是正确的了:



效率提升


可能还会有朋友问,这样的方式也没见提升多少效率啊。


这其实是由于我本地文件少,加上一个文件处理的耗时也比较短导致的。


甚至线程数开的够多导致频繁的上下文切换还是让执行效率降低。


为了模拟效率的提升,每处理一个文件我都让当前线程休眠 100 毫秒来模拟执行耗时。


先看单线程运行需要耗时多久。



总共耗时:[8404] ms


接着在线程池大小为 4 的情况下耗时:



总共耗时:[2350] ms


可见效率提升还是非常明显的。


更多思考


这只是多线程其中的一个用法,相信看到这里的朋友应该多它的理解更进一步了。


再给大家留个阅后练习,场景也是类似的:


在 Redis 或者其他存储介质中存放有上千万的手机号码数据,每个号码都是唯一的,需要在最快的时间内把这些号码全部都遍历一遍。


有想法感兴趣的朋友欢迎在文末留言参与讨论🤔🤨。


总结


希望看完的朋友心中能对文初的几个问题能有自己的答案:


  • 为什么需要多线程?


  • 怎么实现一个多线程程序?


  • 多线程带来的问题及解决方案?


文中的代码都在此处。


github.com/crossoverJi…



相关文章
|
2月前
|
并行计算 Java 数据处理
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
221 0
|
19天前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
20天前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
73 6
|
20天前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
54 1
|
29天前
|
Java 开发者
Java多线程编程的艺术与实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的技术文档,本文以实战为导向,通过生动的实例和详尽的代码解析,引领读者领略多线程编程的魅力,掌握其在提升应用性能、优化资源利用方面的关键作用。无论你是Java初学者还是有一定经验的开发者,本文都将为你打开多线程编程的新视角。 ####
|
1月前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
1月前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
1月前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
1月前
|
缓存 Java 调度
Java中的多线程编程:从基础到实践
【10月更文挑战第24天】 本文旨在为读者提供一个关于Java多线程编程的全面指南。我们将从多线程的基本概念开始,逐步深入到Java中实现多线程的方法,包括继承Thread类、实现Runnable接口以及使用Executor框架。此外,我们还将探讨多线程编程中的常见问题和最佳实践,帮助读者在实际项目中更好地应用多线程技术。
27 3
|
1月前
|
监控 安全 Java
Java多线程编程的艺术与实践
【10月更文挑战第22天】 在现代软件开发中,多线程编程是一项不可或缺的技能。本文将深入探讨Java多线程编程的核心概念、常见问题以及最佳实践,帮助开发者掌握这一强大的工具。我们将从基础概念入手,逐步深入到高级主题,包括线程的创建与管理、同步机制、线程池的使用等。通过实际案例分析,本文旨在提供一种系统化的学习方法,使读者能够在实际项目中灵活运用多线程技术。
下一篇
DataWorks