欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

java 多个线程同时写同一个文件

程序员文章站 2024-03-13 12:31:57
...

话不多说,先直接上代码:

主方法:

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();
                }
            }
        }
    }
}

运行结果:

java 多个线程同时写同一个文件

 

涉及到的相关知识点:

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.概念

  • countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
  • 是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

2.源码

  • countDownLatch类中只提供了一个构造器:
//参数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();

注释掉

如下所示:

java 多个线程同时写同一个文件

我们可以看到跟之前的输出结果相比,我们的主方法里面输出的:全部下载结束的输出信息,已经打印到我们执行文件下载的线程输出信息的前面了,说明主线程先执行完。这从而说明,await() 方法具有阻塞作用

 我们在把latch.await();放开,把文件下载线程里的latch.countDown();注释掉,

如下:

java 多个线程同时写同一个文件

我们可以看到,主程序里的的输出;全部下载结束的输出信息,一直未输出,程序也一直未结束,由此可得,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