所用和需要学习的知识点

  • http
  • ScheduledExecutorService生成打印线程,按时间隔输出信息
  • 利用ThreadPoolExecutor线程池进行多线程分片下载
  • 使用原子类保证数据在线程中安全性

利用scanner获取控制台输入

Scanner sc =new Scanner(www.sxzhongrui.com);

String url=null;
while (true){System.out.println("请输入下载地址:");Scanner sc =new Scanner(www.sxzhongrui.com);url = www.sxzhongrui.com();break;
}

HttpURLConnection

 // 获取http链接public static HttpURLConnection getHttpURLConnection(String url) throws IOException {URL httpUrl = new URL(url);HttpURLConnection httpUrlConnection = (HttpURLConnection)httpUrl.openConnection();// 向文件所在服务器 伪造请求信息httpUrlConnection.addRequestProperty("User-Agent","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1");return httpUrlConnection;}// 获取文件名称public static String getFileName(String url){int index = url.lastIndexOf("/");String fileName = url.substring(index);return fileName;}

获取文件IO流进行下载

其中下载利用到try(){} catch(){}

IO流

  1. 利用上述的http链接获取输入流
  2. 获取保存路径
  3. 获取输出流,保存文件
public static void downLoad(String url){String fileName = HttpUtils.getFileName(url);String savePath = Constant.PATH + fileName;HttpURLConnection httpURLConnection = null;try {httpURLConnection = HttpUtils.getHttpURLConnection(url);} catch (IOException e) {throw new RuntimeException(e);}// 利用文件IO流进行下载try (// 执行完自动关闭IO流InputStream inputStream = httpURLConnection.getInputStream();BufferedInputStream bis = new BufferedInputStream(inputStream);FileOutputStream fos = new FileOutputStream(savePath);BufferedOutputStream bos = new BufferedOutputStream(fos);){int len=-1;while ((len = www.sxzhongrui.com())!=-1){bos.write(len);}} catch (FileNotFoundException e) {System.out.println("下载的文件不存在");} catch (Exception e){System.out.println("下载失败");}finally {if(httpURLConnection!=null){httpURLConnection.disconnect();}}}

flush()可以强制将缓冲区的内容全部写入输出流

其中 bos.flush() 其实可以忽略这个刷新缓冲流。只调用 bis.close() 和 bos.close() 就可以。

进一步使用try(){} catch{} 连关闭流可以省略

Logger工具类

编写logger工具类

使用了Java 可变参数,同时构建了打印主函数print

其次构建 info和error函数

public class LogUtils {public static void info(String msg, Object... args){print(msg,"-info-",args);}public static void error(String msg, Object... args){print(msg,"-error-",args);}public static void print(String msg, String level, Object... args){// 如果包含额外信息if (args!=null && args.length>0){msg = msg.replace("{}",(String)args[0]);}System.out.println(www.sxzhongrui.com().format(DateTimeFormatter.ofPattern("hh:mm:ss"))+level+msg);}
}

获取打印信息

文件下载的时候最好能够展示出下载的速度,已下载文件大小等信息。这里可以每隔一段时间来获取文件的下载信息,比如间隔1秒获取一次,然后将信息打印到控制台。文件下载是一个独立的线程,另外还需要再开启一个线程来间隔获取文件的信息。java.util.concurrent. ScheduledExecutorService,这个类可以帮助我们来实现此功能。

ScheduledExecutorService

在该类中提供了一些方法可以帮助开发者实现间隔执行的效果,下面列出一些常见的方法及其参数说明。我们可以通过下面方式来获取该类的对象,其中1标识核心线程的数量

ScheduledExecutorService s = Executors.newScheduledThreadPool(1);

schedule方法

该方法是重载的,这两个重载的方法都是有3个形参,只是第一个形参不同。

  • Runnable / Callable 可以传入这两个类型的任务
  • long delay 时间数量
  • TimeUnit unit 时间单位

该方法的作用是让任务按照指定的时间延时执行

public static void main(String[] args) {ScheduledExecutorService ses = Executors.newScheduledThreadPool(1);ses.schedule(new Runnable() {@Overridepublic void run() {System.out.println(System.currentTimeMillis());}},2, TimeUnit.SECONDS);ses.shutdown();}

scheduleAtFixedRate方法

该方法的作用是按照指定的时间延时执行,并且每隔一段时间再继续执行

  • Runnable command 执行的任务
  • long initialDelay 延时的时间数量
  • long period 间隔的时间数量

l TimeUnit unit   时间单位

倘若在执行任务的时候,耗时超过了间隔时间,则任务执行结束之后直接再次执行,而不是再等待间隔时间执行。 意思就是执行任务的时间 与 间隔的时间相比,这里间隔的时间包含执行任务的时间

scheduleWithFixedDelay方法

该方法的作用是按照指定的时间延时执行,并且每隔一段时间再继续执行

  • Runnable command 执行的任务
  • long initialDelay 延时的时间数量
  • long period 间隔的时间数量
  • TimeUnit unit 时间单位

在执行任务的时候,无论耗时多久,任务执行结束之后都会等待间隔时间之后再继续下次任务。

构建下载信息类

首先构建下载信息类实现Runnable接口,然后我们利用scheduleWithFixedDelay新建该进程方法,每间隔一秒执行run()方法更新并显示下载信息。

主要是存储文件下载信息,同时由于我们使用字节流进行下载,我们如果使用常见的MB显示,记得进行单位转换Constant.MB=1024d * 1024d

package com.yqyang.down;import com.yqyang.constant.Constant;public class DownLoadInfo implements Runnable{// 文件总大小double fileSize;// 剩余文件大小double lastFileSize;// 当前下载的总大小 多个线程 使用volatile强制从主内存读取volatile double downSize;// 前1s下载的总大小double predownSize;public DownLoadInfo(double fileSize) {this.fileSize = fileSize;lastFileSize = fileSize;}@Overridepublic void run() {// 下载使用的是字节流 单位换算 同时为了方便打印转化为字符串String fileSizeMB = String.format("%.2f",fileSize / Constant.MB);//  1s 内下载文件大小double secondDownSize= downSize -predownSize;predownSize = downSize;// 1s 下载速度 kb/s 同时为了方便打印转化为字符串String downSpeed = String.format("%.2f",secondDownSize / 1024d);;// 剩余下载大小 同时为了方便打印转化为字符串lastFileSize = (fileSize - downSize);String lastFileSizeMB = String.format("%.2f",lastFileSize/Constant.MB);// 剩余时间String lastTime = String.format("%.0f",lastFileSize/secondDownSize);String downInfo = String.format("剩余文件大小%sMB 文件大小 %sMB 下载速度 %sKB/s 剩余时间%ss",lastFileSizeMB,fileSizeMB,downSpeed,lastTime);// 打印下载信息System.out.print("\r");System.out.print(downInfo);}
}
  • 实现Runnable接口
  • 对于会在多个线程使用的变量,使用volatile声明,使其强制从内存读取
  • 使用 System.out.print("\r"); 不会换行,刷新当前显示字符串

更新DownLoad

我们需要更新DownLoad代码

首先我们可以先获取,要保存的地方是否存在文件,并且与要下载的文件大小是否相同,

编写FileUtils

package com.yqyang.utils;import java.io.File;public class FileUtils {public static double getLocalFileSize(String path){File file = new File(path);// 首先看文件是否存在 其次看是不是文件, 是的话返回文件长度return file.exists() && file.isFile() ? file.length() : 0;}
}
 public static void downLoad(String url){String fileName = HttpUtils.getFileName(url);String savePath = Constant.PATH + '/' +fileName;HttpURLConnection httpURLConnection = null;DownLoadInfo downLoadInfo;// 获取本地文件大小double localFileSize = FileUtils.getLocalFileSize(savePath);try {httpURLConnection = HttpUtils.getHttpURLConnection(url);// long contentLength = httpURLConnection.getContentLengthLong();String temp= String.valueOf(contentLength);// 本地是否已经曾经下载完成if (localFileSize*Math.pow(10, temp.length())>=contentLength) {www.sxzhongrui.com("文件已下载");return;}// 构建下载信息类downLoadInfo=new DownLoadInfo(contentLength);} catch (IOException e) {throw new RuntimeException(e);}// 新建打印信息线程 记得关闭ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);scheduledExecutorService.scheduleWithFixedDelay(downLoadInfo, 1, 1, TimeUnit.SECONDS);try (// 执行完自动关闭IO流InputStream inputStream = httpURLConnection.getInputStream();BufferedInputStream bis = new BufferedInputStream(inputStream);FileOutputStream fos = new FileOutputStream(savePath);BufferedOutputStream bos = new BufferedOutputStream(fos);){byte[] buff = new byte[1024*100];int len=-1;while ((len = www.sxzhongrui.com(buff))!=-1){bos.write(buff,0,len);downLoadInfo.downSize+=len;}} catch (FileNotFoundException e) {LogUtils.error("未找到{}文件", fileName);} catch (Exception e){LogUtils.error("下载失败");}finally {if(httpURLConnection!=null){httpURLConnection.disconnect();}// 关闭scheduledExecutorService.shutdownNow();}}/

线程池-ThreadPoolExecutor

线程在创建,销毁的过程中会消耗一些资源,为了节省这些开销,jdk添加了线程池。线程池节省了开销,提高了线程使用的效率。阿里巴巴开发文档中建议在编写多线程程序的时候使用线程池

ThreadPoolExecutor 构造方法

  • corePoolSize
    • 线程池中核心线程的数量
  • maximumPoolSize
    • 线程池中最大线程的数量,是核心线程数量和非核心线程数量之和
  • keepAliveTime
    • 非核心线程空闲的生存时间
  • unit
    • keepAliveTime的生存时间单位
  • workQueue
    • 当没有空闲的线程时,新的任务会加入到workQueue中排队等待
  • threadFactory
    • 线程工厂,用于创建线程
  • handler
    • 拒绝策略,当任务太多无法处理时的拒绝策略

线程池工作状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9mwkpmwI-1688108197533)(https://www.sxzhongrui.com/static/kMwHZjzDBe23Npnwj9HsV1/image.png?auth_key=1688098905-n5hidAYpRmNMPE1mqBss8X-0-3eff8ea5d93f40cbec6654930b9b0404)]

对于怎么安排非核心线程,和阻塞队列有关

比如使用ArrayBlockingQueue, 核心线程满之后,会先进阻塞队列,阻塞队列满之后会调用非核心线程

线程池的状态

线程池中有5个状态,分别是:

· RUNNING

创建线程池之后的状态是RUNNING

SHUTDOWN

该状态下,线程池就不会接收新任务,但会处理阻塞队列剩余任务,相对温和。

STOP

该状态下会中断正在执行的任务,并抛弃阻塞队列任务,相对暴力。

· TIDYING

任务全部执行完毕,活动线程为 0 即将进入终止

· TERMINATED

线程池终止

线程池的关闭

线程池使用完毕之后需要进行关闭,提供了以下两种方法进行关闭

  • shutdown()

该方法执行后,线程池状态变为 SHUTDOWN,不会接收新任务,但是会执行完已提交的任务,此方法不会阻塞调用线程的执行。

  • shutdownNow()

该方法执行后,线程池状态变为 STOP,不会接收新任务,会将队列中的任务返回,并用 interrupt 的方式中断正在执行的任务。

工作队列

jdk中提供的一些工作队列workQueue

  • SynchronousQueue

直接提交队列

·* ArrayBlockingQueue

有界队列,可以指定容量

· LinkedBlockingDeque

无界队列

· PriorityBlockingQueue

优先任务队列,可以根据任务优先级顺序执行任务

    public static void main(String[] args) {// 新建线程池ThreadPoolExecutor executor=new ThreadPoolExecutor(3, 4,10,  TimeUnit.SECONDS, new ArrayBlockingQueue<>(2));// 设置任务Runnable r=()->{System.out.println(Thread.currentThread().getName());};int i=5;// 打印线程池信息System.out.println(executor);for (int i1 = 0; i1 < i; i1++) {// 线程执行任务executor.execute(r);}System.out.println(executor);}

实现分片下载

完善HttpUtils方法-分片下载

重写getHttpURLConnection方法, 实现分片下载

下载url 中startPos - endPos 字节的数据,如果endPos不写表示从startPos下载到最后。

// https://www.sxzhongrui.com/qqfile/qq/PCQQ9.7.9/QQ9.7.9.29065.exe// 获取http链接public static HttpURLConnection getHttpURLConnection(String url) throws IOException {URL httpUrl = new URL(url);HttpURLConnection httpUrlConnection = (HttpURLConnection)httpUrl.openConnection();// 向文件所在服务器 伪造请求信息httpUrlConnection.addRequestProperty("User-Agent","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1");return httpUrlConnection;}/*     获取分片下载链接 重写方法** */public static HttpURLConnection getHttpURLConnection(String url,long startPos,long endPos) throws IOException {// 获取链接HttpURLConnection httpURLConnection = getHttpURLConnection(url);if (endPos==0)// 最后一个分片httpURLConnection.setRequestProperty("RANGE","bytes="+startPos+"-");elsehttpURLConnection.setRequestProperty("RANGE","bytes="+startPos+"-"+endPos);return httpURLConnection;}

完善分片下载任务

因为我们要实现分片下载,所以我们需要用到多线程下载,要构建线程任务。

这里我们实现Callable接口,因为Callable接口可以有返回值,但是Runnable接口没有返回值

public class DownLoadTask implements Callable {public DownLoadTask(String url, long startPos, long endPos, int part) {this.url = url;this.startPos = startPos;this.endPos = endPos;this.part = part;}private String url;// 下载起始位置private long startPos;// 下载结束位置private long endPos;// 分片索引private int part;@Overridepublic Boolean call() throws IOException {// 获取文件名字String fileName = HttpUtils.getFileName(url);// 分块的文件名String saveName = Constant.PATH +"/"+fileName+".temp"+part;// 获取分块下载的链接HttpURLConnection httpURLConnection=HttpUtils.getHttpURLConnection(url, startPos, endPos);System.out.println(Thread.currentThread()+String.valueOf(startPos)+"-"+ String.valueOf(endPos));try(    // 实现自动关闭InputStream is = httpURLConnection.getInputStream();BufferedInputStream bis = new BufferedInputStream(is);RandomAccessFile rw = new RandomAccessFile(saveName, "rw");){int len=-1;byte[] buff = new byte[1024 * 100];while ((len=www.sxzhongrui.com(buff))!=-1){rw.write(buff, 0, len);DownLoadInfo.downSize.add(len);}}catch (FileNotFoundException e){LogUtils.error("下载文件不存在");return false;}catch (Exception e){LogUtils.error("出错了");return false;}finally {// 记得关闭啊if (httpURLConnection!=null)httpURLConnection.disconnect();}www.sxzhongrui.com("下载完成!");return true;}
}

实现分片下载

其次创建线程池对象

public  ThreadPoolExecutor executor=new ThreadPoolExecutor(Constant.THREAD_NUM, Constant.THREAD_NUM,1,TimeUnit.SECONDS,new ArrayBlockingQueue<>(5));

写文件分片方法并调用执行任务,这里传入的Future泛型的list,我们可以利用future泛型保证进程都执行结束

      // 文件切分public  void split(String url, ArrayList list, long contentLength){long size = contentLength / Constant.THREAD_NUM;for (int i = 0; i < Constant.THREAD_NUM; i++) {long startPos = i * size;long endPos;if (i==Constant.THREAD_NUM-1) endPos=0;endPos = startPos + size;if (i!=0) startPos+=1;DownLoadTask downLoadTask = new DownLoadTask(url, startPos, endPos, i);Future future = executor.submit(downLoadTask);list.add(future);}}

利用原子类 LongAdder

保证变量downSize的安全


public class DownLoadInfo implements Runnable{// 文件总大小double fileSize;// 当前下载的总大小 多个线程 使用volatile强制从主内存读取static volatile LongAdder downSize=new LongAdder();// 前1s下载的总大小double predownSize;public DownLoadInfo(double fileSize) {this.fileSize = fileSize;}@Overridepublic void run() {// 下载使用的是字节流 单位换算 同时为了方便打印转化为字符串String fileSizeMB = String.format("%.2f",fileSize / Constant.MB);//  1s 内下载文件大小double secondDownSize= downSize.doubleValue() -predownSize;predownSize = downSize.doubleValue();// 1s 下载速度 kb/s 同时为了方便打印转化为字符串String downSpeed = String.format("%.2f",secondDownSize / 1024d);;// 剩余下载大小 同时为了方便打印转化为字符串double lastFileSize = (fileSize - downSize.doubleValue());String lastFileSizeMB = String.format("%.2f",lastFileSize/Constant.MB);// 剩余时间String lastTime = String.format("%.0f",lastFileSize/secondDownSize);String downInfo = String.format("剩余文件大小%sMB 文件大小 %sMB 下载速度 %sKB/s 剩余时间%ss",lastFileSizeMB,fileSizeMB,downSpeed,lastTime);System.out.print("\r");System.out.print(downInfo);}
}

合并分片文件并删除缓存

    public Boolean mergeFile(String fileName){www.sxzhongrui.com("正在合并文件");try (RandomAccessFile accessFile = new RandomAccessFile(fileName,"rw")) {for (int i = 0; i < Constant.THREAD_NUM; i++) {int len=-1;try ( BufferedInputStream bis=new BufferedInputStream(new FileInputStream(fileName+".temp"+i));){byte[] buff = new byte[1024 * 100];while ((len=www.sxzhongrui.com(buff))!=-1){accessFile.write(buff);}}}} catch (FileNotFoundException e) {return false;} catch (IOException e) {return false;}www.sxzhongrui.com("合并文件完成");return true;}public Boolean deleteTempFile(String fileName){for (int i = 0; i < Constant.THREAD_NUM; i++) {File file=new File(fileName+".temp"+i);file.delete();}return true;}

CountDownLaunch

累减器等待所有进程执行完毕