话不多说,先直接上代码:
主方法:
import java.util.concurrent.CountDownLatch;
/**
* @ProjectName: emp_customer
* @Package: PACKAGE_NAME
* @ClassName: Test
* @Author: Administrator
* @Description: ${description}
* @Date: 2019/10/11 14:10
* @Version: 1.0
*/
public class Test {
public static void main(String args[]){
//线程数
int threadSize=4;
//源文件地址
String sourcePath = "E:\\1\\4.txt";
//目标文件地址
String destnationPath = "E:\\2\\4.txt";
//
CountDownLatch latch = new CountDownLatch(threadSize);
MultiDownloadFileThread m = new MultiDownloadFileThread(threadSize, sourcePath, destnationPath, latch);
long startTime = System.currentTimeMillis();
try {
m.excute();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println("全部下载结束,共耗时" + (endTime - startTime) / 1000 + "s");
}
}
线程类:
import java.io.*;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.concurrent.CountDownLatch;
/**
* @ProjectName: emp_customer
* @Package: PACKAGE_NAME
* @ClassName: MultiDownloadFileThread
* @Author: Administrator
* @Description: ${description}
* @Date: 2019/10/11 15:03
* @Version: 1.0
*/
public class MultiDownloadFileThread {
private int threadCount;
private String sourcePath;
private String targetPath;
private CountDownLatch latch;
public MultiDownloadFileThread(int threadCount, String sourcePath, String targetPath, CountDownLatch latch) {
this.threadCount = threadCount;
this.sourcePath = sourcePath;
this.targetPath = targetPath;
this.latch = latch;
}
public void excute() {
File file = new File(sourcePath);
int fileLength = (int) file.length();
//分割文件
int blockSize = fileLength / threadCount;
for (int i = 1; i <= threadCount; i++) {
//第一个线程下载的开始位置
int startIndex = (i - 1) * blockSize;
int endIndex = startIndex + blockSize - 1;
if (i == threadCount) {
//最后一个线程下载的长度稍微长一点
endIndex = fileLength;
}
System.out.println("线程" + i + "下载:" + startIndex + "字节~" + endIndex + "字节");
new DownLoadThread(i, startIndex, endIndex).start();
}
}
public class DownLoadThread extends Thread {
private int i;
private int startIndex;
private int endIndex;
public DownLoadThread(int i, int startIndex, int endIndex) {
this.i = i;
this.startIndex = startIndex;
this.endIndex = endIndex;
}
@Override
public void run() {
File file = new File(sourcePath);
FileInputStream in = null;
RandomAccessFile raFile = null;
FileChannel fcin = null;
FileLock flin = null;
try {
in = new FileInputStream(file);
in.skip(startIndex);
//给要写的文件加锁
raFile = new RandomAccessFile(targetPath, "rwd");
fcin =raFile.getChannel();
while(true){
try {
flin = fcin.tryLock();
break;
} catch (Exception e) {
System.out.println("有其他线程正在操作该文件,当前线程休眠1000毫秒,当前进入的线程为:"+i);
sleep(1000);
}
}
//随机写文件的时候从哪个位置开始写
raFile.seek(startIndex);
int len = 0;
byte[] arr = new byte[1024];
//获取文件片段长度
int segLength = endIndex - startIndex + 1;
while ((len = in.read(arr)) != -1) {
if (segLength > len) {
segLength = segLength - len;
raFile.write(arr, 0, len);
} else {
raFile.write(arr, 0, segLength);
break;
}
}
System.out.println("线程" + i + "下载完毕");
//计数值减一
latch.countDown();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
} finally {
try {
if (in != null) {
in.close();
}
if (raFile != null) {
raFile.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
运行结果:
涉及到的相关知识点:
1.CountDownLatch
2.RandomAccessFile
3.FileLock
下面我们具体讲解下
一、FileLock :文件锁
FileLock是java 1.4 版本后出现的一个类,它可以通过对一个可写文件(w)加锁,保证同时只有一个进程可以拿到文件的锁,这个进程从而可以对文件做访问;而其它拿不到锁的进程要么选择被挂起等待,要么选择去做一些其它的事情, 这样的机制保证了众进程可以顺序访问该文件。
1. 概念
2. lock()和tryLock()的区别:
lock()阻塞的方法,锁定范围可以随着文件的增大而增加。无参lock()默认为独占锁;有参lock(0L, Long.MAX_VALUE, true)为共享锁。
tryLock()非阻塞,当未获得锁时,返回null.
3. FileLock的生命周期:在调用FileLock.release(),或者Channel.close(),或者JVM关闭
4. FileLock是线程安全的
二、RandomAccessFile
java除了File类之外,还提供了专门处理文件的类,即RandomAccessFile(随机访问文件)类。该类是Java语言中功能最为丰富的文件访问类,它提供了众多的文件访问方法。RandomAccessFile类支持“随机访问”方式,这里“随机”是指可以跳转到文件的任意位置处读写数据。在访问一个文件的时候,不必把文件从头读到尾,而是希望像访问一个数据库一样“随心所欲”地访问一个文件的某个部分,这时使用RandomAccessFile类就是最佳选择。
RandomAccessFile对象类有个位置指示器,指向当前读写处的位置,当前读写n个字节后,文件指示器将指向这n个字节后面的下一个字节处。刚打开文件时,文件指示器指向文件的开头处,可以移动文件指示器到新的位置,随后的读写操作将从新的位置开始。RandomAccessFile类在数据等长记录格式文件的随机(相对顺序而言)读取时有很大的优势,但该类仅限于操作文件,不能访问其他的I/O设备,如网络、内存映像等。RandomAccessFile类的构造方法如下所示:
RandomAccessFile(File file , String mode)
//创建随机存储文件流,文件属性由参数File对象指定
RandomAccessFile(String name , String mode)
//创建随机存储文件流,文件名由参数name指定
这两个构造方法均涉及到一个String类型的参数mode,它决定随机存储文件流的操作模式,其中mode值及对应的含义如下:
“r”:以只读的方式打开,调用该对象的任何write(写)方法都会导致IOException异常
“rw”:以读、写方式打开,支持文件的读取或写入。若文件不存在,则创建之。
“rws”:以读、写方式打开,与“rw”不同的是,还要对文件内容的每次更新都同步更新到潜在的存储设备中去。这里的“s”表示synchronous(同步)的意思
“rwd”:以读、写方式打开,与“rw”不同的是,还要对文件内容的每次更新都同步更新到潜在的存储设备中去。使用“rwd”模式仅要求将文件的内容更新到存储设备中,而使用“rws”模式除了更新文件的内容,还要更新文件的元数据(metadata),因此至少要求1次低级别的I/O操作
三、CountDownLatch
1.概念
2.源码
//参数count为计数值
public CountDownLatch(int count) { };
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };
//将count值减1
public void countDown() { };
假如在我们的代码里面,我们把main方法里面的
latch.await();
注释掉
如下所示:
我们可以看到跟之前的输出结果相比,我们的主方法里面输出的:全部下载结束的输出信息,已经打印到我们执行文件下载的线程输出信息的前面了,说明主线程先执行完。这从而说明,await() 方法具有阻塞作用
我们在把latch.await();放开,把文件下载线程里的latch.countDown();注释掉,
如下:
我们可以看到,主程序里的的输出;全部下载结束的输出信息,一直未输出,程序也一直未结束,由此可得,countDown() 方法具有唤醒阻塞线程的作用。
CountDownLatch总结:
1、CountDownLatch end = new CountDownLatch(N); //构造对象时候 需要传入参数N
2、end.await() 能够阻塞线程 直到调用N次end.countDown() 方法才释放线程
3、end.countDown() 可以在多个线程中调用 计算调用次数是所有线程调用次数的总和
对于,本demo而言,加不加文件锁的意义不大,因为在进入线程写的时候,就已经告诉单个线程需要写的内容是哪一块到哪一块,不加锁,也会正常写入,切经本人测试无误,但若是对同一个文件,即要写,又要读话,就必须加锁,不然程序执行可能不完整,具体情况可以查看下面的这个博客:https://blog.csdn.net/gxy3509394/article/details/7435993