一、多线程介绍

  在编程中,我们不可逃避的会遇到多线程的编程问题,因为在大多数的业务系统中需要并发处理,如果是在并发的场景中,多线程就非常重要了。另外,我们在面试的时候,面试官通常也会问到我们关于多线程的问题,如:如何创建一个线程?我们通常会这么回答,主要有两种方法,第一种:继承Thread类,重写run方法;第二种:实现Runnable接口,重写run方法。那么面试官一定会问这两种方法各自的优缺点在哪,不管怎么样,我们会得出一个结论,那就是使用方式二,因为面向对象提倡少继承,尽量多用组合。

这个时候,我们还可能想到,如果想得到多线程的返回值怎么办呢?根据我们多学到的知识,我们会想到实现Callable接口,重写call方法。那么多线程到底在实际项目中怎么使用呢,他有多少种方式呢?

首先,我们来看一个例子:

  这是一种创建多线程的简单方法,很容易理解,在例子中,根据不同的业务场景,我们可以在Thread()里边传入不同的参数实现不同的业务逻辑,但是,这个方法创建多线程暴漏出来的问题就是反复创建线程,而且创建线程后还得销毁,如果对并发场景要求低的情况下,这种方式貌似也可以,但是高并发的场景中,这种方式就不行了,因为创建线程销毁线程是非常耗资源的。所以根据经验,正确的做法是我们使用线程池技术,JDK提供了多种线程池类型供我们选择,具体方式可以查阅jdk的文档。

  这里代码我们需要注意的是,传入的参数代表我们配置的线程数,是不是越多越好呢?肯定不是。因为我们在配置线程数的时候要充分考虑服务器的性能,线程配置的多,服务器的性能未必就优。通常,机器完成的计算是由线程数决定的,当线程数到达峰值,就无法在进行计算了。如果是耗CPU的业务逻辑(计算较多),线程数和核数一样就到达峰值了,如果是耗I/O的业务逻辑(操作数据库,文件上传、下载等),线程数越多一定意义上有助于提升性能。

  线程数大小的设定又一个公式决定:

Y=N*((a+b)/a),其中,N:CPU核数,a:线程执行时程序的计算时间,b:线程执行时,程序的阻塞时间。有了这个公式后,线程池的线程数配置就会有约束了,我们可以根据机器的实际情况灵活配置。

二、多线程优化及性能比较

最近的项目中用到了所线程技术,在使用过程中遇到了很多的麻烦,趁着热度,整理一下几种多线程框架的性能比较。目前所掌握的大致分三种,第一种:ThreadPool(线程池)+CountDownLatch(程序计数器),第二种:Fork/Join框架,第三种JDK8并行流,下面对这几种方式的多线程处理性能做一下比较总结。

首先,假设一种业务场景,在内存中生成多个文件对象,这里暂定30000,(Thread.sleep(时间))线程睡眠模拟业务处理业务逻辑,来比较这几种方式的多线程处理性能。

1) 单线程

  这种方式非常简单,但是程序在处理的过程中非常的耗时,使用的时间会很长,因为每个线程都在等待当前线程执行完才会执行,和多线程没有多少关系,所以效率非常低。

首先创建文件对象,代码如下:

public class FileInfo {private String fileName;//文件名private String fileType;//文件类型private String fileSize;//文件大小private String fileMD5;//MD5码private String fileVersionNO;//文件版本号public FileInfo() {super();}public FileInfo(String fileName, String fileType, String fileSize, String fileMD5, String fileVersionNO) {super();this.fileName = fileName;this.fileType = fileType;this.fileSize = fileSize;this.fileMD5 = fileMD5;this.fileVersionNO = fileVersionNO;}public String getFileName() {return fileName;}public void setFileName(String fileName) {this.fileName = fileName;}public String getFileType() {return fileType;}public void setFileType(String fileType) {this.fileType = fileType;}public String getFileSize() {return fileSize;}public void setFileSize(String fileSize) {this.fileSize = fileSize;}public String getFileMD5() {return fileMD5;}public void setFileMD5(String fileMD5) {this.fileMD5 = fileMD5;}public String getFileVersionNO() {return fileVersionNO;}public void setFileVersionNO(String fileVersionNO) {this.fileVersionNO = fileVersionNO;}

View Code

接着,模拟业务处理,创建30000个文件对象,线程睡眠1ms,之前设置的1000ms,发现时间很长,整个Eclipse卡掉了,所以将时间改为了1ms。

public class Test {private static List<FileInfo> fileList= new ArrayList<FileInfo>();public static void main(String[] args) throws InterruptedException {createFileInfo();long startTime=System.currentTimeMillis();for(FileInfo fi:fileList){Thread.sleep(1);}long endTime=System.currentTimeMillis();System.out.println("单线程耗时:"+(endTime-startTime)+"ms");}private static void createFileInfo(){for(int i=0;i<30000;i++){fileList.add(new FileInfo("身份证正面照","jpg","101522","md5"+i,"1"));}}}

View Code

测试结果如下:

可以看到,生成30000个文件对象消耗的时间比较长,接近1分钟,效率比较低。

2) ThreadPool(线程池)+CountDownLatch(程序计数器)

  顾名思义,CountDownLatch为线程计数器,他的执行过程如下:首先,在主线程中调用await()方法,主线程阻塞,然后,将程序计数器作为参数传递给线程对象,最后,每个线程执行完任务后,调用countDown()方法表示完成任务。countDown()被执行多次后,主线程的await()会失效。实现过程如下:

public class Test2 {private static ExecutorService executor=Executors.newFixedThreadPool(100);private static CountDownLatch countDownLatch=new CountDownLatch(100);private static List<FileInfo> fileList= new ArrayList<FileInfo>();private static List<List<FileInfo>> list=new ArrayList<>();public static void main(String[] args) throws InterruptedException {createFileInfo();addList();long startTime=System.currentTimeMillis();int i=0;for(List<FileInfo> fi:list){executor.submit(new FileRunnable(countDownLatch,fi,i));i++;}countDownLatch.await();long endTime=System.currentTimeMillis();executor.shutdown();System.out.println(i+"个线程耗时:"+(endTime-startTime)+"ms");}private static void createFileInfo(){for(int i=0;i<30000;i++){fileList.add(new FileInfo("身份证正面照","jpg","101522","md5"+i,"1"));}}private static void addList(){for(int i=0;i<100;i++){list.add(fileList);}}}

View Code

FileRunnable类:

/*** 多线程处理* @author wangsj** @param <T>*/public class FileRunnable<T> implements Runnable {private CountDownLatch countDownLatch;private List<T> list;private int i;public FileRunnable(CountDownLatch countDownLatch, List<T> list, int i) {super();this.countDownLatch = countDownLatch;this.list = list;this.i = i;}@Overridepublic void run() {for(T t:list){try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}countDownLatch.countDown();}}}

View Code

测试结果如下:

3) Fork/Join框架

  Jdk从版本7开始,出现了Fork/join框架,从字面来理解,fork就是拆分,join就是合并,所以,该框架的思想就是。通过fork拆分任务,然后join来合并拆分后各个人物执行完毕后的结果并汇总。比如,我们要计算连续相加的几个数,2+4+5+7=?,我们利用Fork/join框架来怎么完成呢,思想就是拆分子任务,我们可以把这个运算拆分为两个子任务,一个计算2+4,另一个计算5+7,这是Fork的过程,计算完成后,把这两个子任务计算的结果汇总,得到总和,这是join的过程。

  Fork/Join框架执行思想:首先,分割任务,使用fork类将大任务分割为若干子任务,这个分割过程需要按照实际情况来定,直到分割出的任务足够小。然后,join类执行任务,分割的子任务在不同的队列里,几个线程分别从队列里获取任务并执行,执行完的结果放到一个单独的队列里,最后,启动线程,队列里拿取结果并合并结果。

  使用Fork/Join框架要用到几个类,关于类的使用方式可以参考JDK的API,使用该框架,首先需要继承ForkJoinTask类,通常,只需要继承他的子类RecursiveTask或RecursiveAction即可,RecursiveTask,用于有返回结果的场景,RecursiveAction用于没有返回结果的场景。ForkJoinTask的执行需要用到ForkJoinPool来执行,该类用于维护分割出的子任务添加到不同的任务队列。

下面是实现代码:

public class Test3 {private static List<FileInfo> fileList= new ArrayList<FileInfo>();//    private static ForkJoinPool forkJoinPool=new ForkJoinPool(100);//    private static Job<FileInfo> job=new Job<>(fileList.size()/100, fileList);public static void main(String[] args) {createFileInfo();long startTime=System.currentTimeMillis();ForkJoinPool forkJoinPool=new ForkJoinPool(100);//分割任务Job<FileInfo> job=new Job<>(fileList.size()/100, fileList);//提交任务返回结果
ForkJoinTask<Integer> fjtResult=forkJoinPool.submit(job);
//阻塞while(!job.isDone()){System.out.println("任务完成!");}long endTime=System.currentTimeMillis();System.out.println("fork/join框架耗时:"+(endTime-startTime)+"ms");}private static void createFileInfo(){for(int i=0;i<30000;i++){fileList.add(new FileInfo("身份证正面照","jpg","101522","md5"+i,"1"));}}
}/*** 执行任务类* @author wangsj**/
public class Job<T> extends RecursiveTask<Integer> {private static final long serialVersionUID = 1L;private int count;private List<T> jobList;public Job(int count, List<T> jobList) {super();this.count = count;this.jobList = jobList;}/*** 执行任务,类似于实现Runnable接口的run方法*/@Overrideprotected Integer compute() {//拆分任务if(jobList.size()<=count){executeJob();return jobList.size();}else{//继续创建任务,直到能够分解执行List<RecursiveTask<Long>> fork = new LinkedList<RecursiveTask<Long>>();//拆分子任务,这里采用二分法int countJob=jobList.size()/2;List<T> leftList=jobList.subList(0, countJob);List<T> rightList=jobList.subList(countJob, jobList.size());//分配任务Job leftJob=new Job<>(count,leftList);Job rightJob=new Job<>(count,rightList);//执行任务
            leftJob.fork();rightJob.fork();return Integer.parseInt(leftJob.join().toString())+Integer.parseInt(rightJob.join().toString());}}/*** 执行任务方法*/private void executeJob() {for(T job:jobList){try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}}}

View Code

测试结果如下:

4) JDK8并行流

  并行流是jdk8的新特性之一,思想就是将一个顺序执行的流变为一个并发的流,通过调用parallel()方法来实现。并行流将一个流分成多个数据块,用不同的线程来处理不同的数据块的流,最后合并每个块数据流的处理结果,类似于Fork/Join框架。

并行流默认使用的是公共线程池ForkJoinPool,他的线程数是使用的默认值,根据机器的核数,我们可以适当调整线程数的大小。线程数的调整通过以下方式来实现。

System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "100");

以下是代码的实现过程,非常简单:

public class Test4 {private static List<FileInfo> fileList= new ArrayList<FileInfo>();public static void main(String[] args) {//                System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "100");
createFileInfo();long startTime=System.currentTimeMillis();fileList.parallelStream().forEach(e ->{try {Thread.sleep(1);} catch (InterruptedException f) {f.printStackTrace();}});long endTime=System.currentTimeMillis();System.out.println("jdk8并行流耗时:"+(endTime-startTime)+"ms");}private static void createFileInfo(){for(int i=0;i<30000;i++){fileList.add(new FileInfo("身份证正面照","jpg","101522","md5"+i,"1"));}}}

View Code

下面是测试,第一次没有设置线程池的数量,采用默认,测试结果如下:

我们看到,结果并不是很理想,耗时较长,接下来设置线程池的数量大小,即添加如下代码:

System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "100");

接着进行测试,结果如下:

这次耗时较小,比较理想。

三、总结

  综上几种情况来看,以单线程作为参考,耗时最长的还是原生的Fork/Join框架,这里边尽管配置了线程池的数量,但效果较精确配置了线程池数量的JDK8并行流较差。并行流实现代码简单易懂,不需要我们写多余的for循环,一个parallelStream方法全部搞定,代码量大大的减少了,其实,并行流的底层还是使用的Fork/Join框架,这就要求我们在开发的过程中灵活使用各种技术,分清各种技术的优缺点,从而能够更好的为我们服务。

  技术水平有限,欢迎各位批评指导!

源码地址:https://files.cnblogs.com/files/10158wsj/threadsDemo.zip

转载于:https://www.cnblogs.com/10158wsj/p/8338367.html

Java多线程优化方法及使用方式相关推荐

  1. 深入探索多线程优化方法

    前言 什么是线程的概念我就不在介绍,不懂的自行百度,我想百分之九十九的人都是知道的,至于多线程,通俗的就是有很多的线程在一起工作从而完成某一件事,从而提升效率.这就是使用多线程的好处之一,举个列子,一 ...

  2. 探索 Android 多线程优化方法

    前言 1. 基本介绍 在我学习 Android 多线程优化方法的过程中,发现我对多线程优化的了解太片面. 写这篇文章的目的是完善我对 Android 多线程优化方法的认识,分享这篇文章的目的是希望大家 ...

  3. Java多线程的4种实现方式

    ** Java多线程的4种实现方式 ** 1:继承Thread并重写run方法,并调用start方法 /*** Java实现多线程的方式1* 继承Thread类,重写run方法* @author ho ...

  4. Java多线程优化都不会,怎么拿Offer?

    " 随着业务量的增加,多线程处理成为家常便饭.于是,多线程优化成了摆在我们面前的问题.Java 作为当今主流的应用开发语言,也会有同样的问题. 转自:51CTO技术栈 图片来自 Pexels ...

  5. Java多线程实现的两种方式

    Java多线程实现方式:1.实现Runnable接口2.继承Thread类,虽然是比较基础的知识点,作为学习记录写下来,高手略过! 1.实现Runnable接口 1) RunnableDemo类 pa ...

  6. java多线程实现的几种方式

    基本的线程机制 并发编程时我们可以将程序或分为多个分离的.独立运行的任务.通过多线程机制,这些独立任务中的每一个都由执行线程来驱动. 线程可以驱动任务,因此需要一种描述任务的方式,这可以通过 Runn ...

  7. Java:简述Java多线程的四种实现方式

    关联文章:<Java:简述Java中的多线程编程> Java多线程实现方式主要有四种:继承Thread类.实现Runnable接口.实现Callable接口通过FutureTask包装器来 ...

  8. JAVA多线程实现的三种方式

    文章目录 继承Thread类实现多线程 实现Runnable接口方式实现多线程 区别 ExecutorService/Callable/Future实现有返回结果的多线程 步骤 概念 实例(可忽略) ...

  9. java多线程的6种实现方式详解

    多线程的形式上实现方式主要有两种,一种是继承Thread类,一种是实现Runnable接口.本质上实现方式都是来实现线程任务,然后启动线程执行线程任务(这里的线程任务实际上就是run方法).这里所说的 ...

最新文章

  1. Jquery滚动监听和附加导航
  2. python编程和plc哪个好-plc和python
  3. Nancy 寄宿IIS
  4. MySQL高级or索引失效情况
  5. 电脑SSH登陆树莓派Raspberry的两种方式
  6. KlayGE 4.4中渲染的改进(五):OpenGL 4.4和OpenGLES 3
  7. 【英语学习】【WOTD】scrumptious 释义/词源/示例
  8. 1052. 卖个萌 (20)-PAT乙级真题
  9. 【前端】设置好CSS样式动态添加元素会按照样式显示
  10. visio for android,iPad版的Visio Viewer发布:移动端也能查看Visio文档啦
  11. 不加群提取群成员_使用itchat分析指定微信群男女比例等成员数据
  12. Flink 常见问题总结
  13. 怎么把图片的边缘弄圆_ps里面照片怎么把边缘变成椭圆形
  14. Codeforces Round #609 (Div. 2) C. Long Beautiful Integer
  15. 【毕业设计】基于java web的医院预约挂号系统
  16. STM8 串口接收字符串问题
  17. 不改HOST,另类打开谷歌搜索的方法
  18. 如何把汉字转成五笔与拼音(首字母或全部字母)
  19. postgresql 计算时间差
  20. Tomcat两个项目,一个可以正常访问,另一个报错404

热门文章

  1. c语言快速排序_Damp;C思想-快速排序算法
  2. php7单独运行,如何让PHP 7运行更加神速
  3. ioswebview混编_iOS与H5混编--优秀的第三方框架WebViewJavascriptBridge
  4. testng 检查异常_TestNG异常– ExpectedExceptions,ExpectedExceptionsMessageRegExp
  5. aws lambda_AWS API Gateway和AWS Lambda示例
  6. spring事务 jdbc_Spring事务管理示例JDBC
  7. 深入了解什么是服务网格
  8. 关于生活工作学习之感悟-第一篇
  9. MSP430学习小结3-MSP430基本时钟模块
  10. 深入学习JavaScript: apply 方法 详解