一、多线程介绍

  在编程中,我们不可逃避的会遇到多线程的编程问题,因为在大多数的业务系统中需要并发处理,如果是在并发的场景中,多线程就非常重要了。另外,我们在面试的时候,面试官通常也会问到我们关于多线程的问题,如:如何创建一个线程?我们通常会这么回答,主要有两种方法,第一种:继承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;
}

接着,模拟业务处理,创建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"));

                   }

         }

}

测试结果如下:

可以看到,生成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);
}
} }

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;

         }

         @Override

         public void run() {

                   for(T t:list){

                            try {

                                     Thread.sleep(1);

                            } catch (InterruptedException e) {

                                     e.printStackTrace();

                            }

                            countDownLatch.countDown();

                   }

         }

}

测试结果如下:

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方法
*/
@Override
protected 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();
}
}
}

测试结果如下:

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"));

           }

}

}

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

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

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

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

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

三、总结

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

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

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

Java多线程优化方法及使用方式的更多相关文章

  1. Java多线程的三种实现方式

    java多线程的三种实现方式 一.继承Thread类 二.实现Runnable接口 三.使用ExecutorService, Callable, Future 无论是通过继承Thread类还是实现Ru ...

  2. Java多线程-run方法与start方法的区别

    package com.interview; /** * java多线程的两种实现方式以及run.start方法的区别 * @author MEI.LIU * */ public class Thre ...

  3. 如何实现有返回值的多线程 JAVA多线程实现的三种方式

    可返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnable接口.执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable ...

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

    java多线程的几种实现方式 1.继承Thread类,重写run方法2.实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target3.通 ...

  5. JAVA多线程实现的四种方式

    Java多线程实现方式主要有四种:继承Thread类.实现Runnable接口.实现Callable接口通过FutureTask包装器来创建Thread线程.使用ExecutorService.Cal ...

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

    JAVA多线程实现方式主要有三种:继承Thread类.实现Runnable接口.使用ExecutorService.Callable.Future实现有返回结果的多线程.其中前两种方式线程执行完后都没 ...

  7. JAVA多线程实现的两种方式

    java多线程实现方式主要有两种:继承Thread类.实现Runnable接口 1.继承Thread类实现多线程 继承Thread类的方法尽管被我列为一种多线程实现方式,但Thread本质上也是实现了 ...

  8. 【转】JAVA多线程实现的四种方式

    原文地址:http://www.cnblogs.com/felixzh/p/6036074.html Java多线程实现方式主要有四种:继承Thread类.实现Runnable接口.实现Callabl ...

  9. JAVA多线程实现的四种方式(转自https://www.cnblogs.com/felixzh/p/6036074.html)

    Java多线程实现方式主要有四种:继承Thread类.实现Runnable接口.实现Callable接口通过FutureTask包装器来创建Thread线程.使用ExecutorService.Cal ...

随机推荐

  1. React Native随笔——警告处理方法(持续更新)

    一.警告propTypes was defined as an instance property on commonTabar. Use a static property to define pr ...

  2. Python模块之信号学习(signal)

    信号概述 在学习Python前应该学习下Linux下的信号,软中断信号(signal,又简称为信号)用来通知进程发生了异步事件.进程之间可以互相通过系统调用kill发送软中断信号.内核也可以因为内部事 ...

  3. python机器学习工具包

    1. scikit-learn: Machine Learning in Python scikit-learn是一个基于NumPy, SciPy, Matplotlib的开源机器学习工具包,主要涵盖 ...

  4. zoj 3494:BCD Code

    Description Binary-coded decimal (BCD) is an encoding for decimal numbers in which each digit is rep ...

  5. Ultra-QuickSort(树状数组求逆序对数)

    Ultra-QuickSort 题目链接:http://poj.org/problem?id=2299 Time Limit: 7000MS   Memory Limit: 65536K Total ...

  6. Sql Server——约束

    约束是什么: 每个人都在网站或者APP上注册过账号,在注册账号时会限制用户名.密码等格式,如果格式不对就不能注册.在数据库中我们可以通过约束来进行限制,超过约束范围的数据就不能写入. 约束的种类: 主 ...

  7. 快速搭建appium自动测试环境

    首先申明本文是基本于Python与Android来快速搭建Appium自动化测试环境: 主要分为以下几个步骤: 前提条件: 1)安装与配置python环境,打开 Python官网,找到"Do ...

  8. Thinkphp5.0+Vue2.0前后端分离框架Vuethink

    VueThink是一套基于Vue全家桶(Vue2.x + Vue-router2.x + Vuex)+ Thinkphp的前后端分离框架. 脚手架构建也可以通过vue官方的vue-cli脚手架工具构建 ...

  9. 数据库复习总结(20)-存储过程以及.net调用存储过程

    一.存储过程(注意区分将一段select语句进行封装叫做视图)(1)将一段t-sql脚本进行封装,以完成一个逻辑操作(2)创建存储过程:            create proc 名称      ...

  10. 【开发技术】 使用JSP开发WEB应用系统-------笔记

    1.主机IP地址是:localhost     or    127.0.0.1    or     实际的IP地址 2.Tomcat 服务器是一个免费的开放源代码的Web 应用服务器 3.WebRoo ...