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

多线程 专栏收录该内容
6 篇文章 1 订阅

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

主方法:

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

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

注释掉

如下所示:

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

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

如下:

我们可以看到,主程序里的的输出;全部下载结束的输出信息,一直未输出,程序也一直未结束,由此可得,countDown() 方法具有唤醒阻塞线程的作用。

那么如何让 CountdownLatch 尽早结束

假如我们的程序执行到countDown()之前就抛出异常,这就可能导致一整情况,CountdownLatch 计数永远不会达到零并且 await() 永远不会终止。

为了解决这个问题,我们在调用 await() 时添加一个超时参数。

 

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

  • 3
    点赞
  • 4
    评论
  • 27
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值