Java文件鎖以及併發讀寫中的應用

引言

​ 在項目中,如果遇到需要併發讀寫文件的問題,那麼對文件上鎖分開訪問是十分有必要的。因此這篇博文主要介紹文件鎖的相關知識。有誤之處,希望指出。

什麼是文件鎖

​ 文件鎖就如同編程概念中其他鎖的意義一樣。通過對文件上鎖,只有擁有鎖的進程才能擁有對應鎖權限的操作權限。而沒有鎖的進程只能掛起或者處理其他的事務直到擁有鎖。從而在併發的場景下,我們才能對文件的讀寫進行控制。

分類

  • 共享鎖:獲取到共享鎖,會阻止獲取獨佔鎖,但不會阻止獲取共享鎖。獲取到共享鎖可以同時讀,但只能有一個在寫。
  • 獨佔鎖:獲取到獨佔鎖後,會阻止其他進程獲得任何鎖。只能一個讀或者一個寫。

注意項和異常

  • 首先務必注意,獲取的文件鎖是進程之間的鎖。因此獲得鎖時,進程下的線程都會獲得這個鎖。由於在一個進程內,鎖沒有被釋放的情況下,再次使用lock()獲取鎖的話,會拋出java.nio.channels.OverlappingFileLockException異常。所以多線程處理文件時,可以使用lock()方法捕獲異常,使得線程之間依次操作文件。

  • 雖然常規都是使用new RandomAccessFile(file,”rw”).getChannel().lock();獲得鎖。

    但是如果使用new FileInputStream(file).getChannel().lock();獲得鎖時。會拋出NonWritableChannelException異常。

  • 如果使用new FileOutputStream(file).getChannel().lock()時,會先將原文件的內容清空,因此最好使用

    new FileOutputStream(file,true).getChannel().lock()

  • lock()和tryLock()的區別:

    lock()方法當無法獲得鎖時會阻塞。

    tryLock()方法當無法獲得鎖時會獲得null值。

代碼實現


public class FileLockDemo {


    public static void main(String []args){
        File file = new File("d:/test/test.txt");
        RandomAccessFile raf = null;
        FileChannel fc = null;
        FileLock fl = null;
        try{
            raf = new RandomAccessFile(file, "rw");
            fc = raf.getChannel();
            //此處主要是針對多線程獲取文件鎖時輪詢鎖的狀態。如果只是單純獲得鎖的話,直接fl = fc.tryLock();即可
            while(true){
                try{
                    //無參獨佔鎖
                    fl = fc.tryLock();
                    //採用共享鎖
                    //fl = fc.tryLock(0,Long.MAX_VALUE,true);
                    if(fl!=null){
                        System.out.println("get the lock");
                        break;
                    }
                }catch(Exception e){
                    //如果是同一進程的多線程,重複請求tryLock()會拋出OverlappingFileLockException異常
                    System.out.println("current thread is block");
                }
            }
            //獲得文件鎖權限後,進行相應的操作
            fl.release();
            fc.close();
            raf.close();
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            if(fl!=null&&fl.isValid()){
                try {
                    fl.release();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }

    }
}

以下是一些使用文件鎖控制文件併發讀寫的測試代碼,如果感興趣的話,可以繼續往下看:

  1. 獲得文件鎖後寫文件代碼

    
    public class WriteThread extends Thread{
    
    private String sourceFile;
    private String targetFile;
    private String threadName;
    
    public WriteThread(String sourceFile,String targetFile,String ThreadName){
        this.sourceFile = sourceFile;
        this.targetFile = targetFile;
        this.threadName = ThreadName;
    }
    
    @Override
    public void run(){
        RandomAccessFile raf = null;
        FileChannel fc = null;
        FileLock fl = null;
        FileInputStream in = null;
        try {
            raf = new RandomAccessFile(targetFile, "rw");
            fc = raf.getChannel();
            while(true){
                try {
                    fl = fc.tryLock();
                    System.out.println(fl.isShared());
                    System.out.println(this.threadName+" : get the lock");
                    try {
                        sleep(1000);
                    } catch (InterruptedException e1) {
                        // TODO Auto-generated catch block
                        e1.printStackTrace();
                    }
                    break;
                } catch (Exception e) {
                    System.out.println(this.threadName+" is block");
                    try {
                        sleep(1000);
                    } catch (InterruptedException e1) {
                        e1.printStackTrace();
                    }
                }
    
            }
            in = new FileInputStream(sourceFile);
            byte[] b = new byte[1024];
            int len = 0;
            ByteBuffer bb = ByteBuffer.allocate(1024);
            while((len=in.read(b))!=-1){
                bb.clear();
                bb.put(b, 0, len);
                bb.flip();
                fc.write(bb);
    
            }
            System.out.println(this.threadName+" : write success");
            fl.release();
                System.out.println(this.threadName+" : release lock");
            raf.close();
            fc.close();
            in.close();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            //e.printStackTrace();
            System.out.println(this.threadName+" : write failed");
        }finally{
            if(fl!=null&&fl.isValid()){
                try {
                    fl.release();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }
    }
  2. 獲得文件鎖後讀文件代碼

    public class ReadThread extends Thread{
    
    private String sourceFile;
    private String threadName;
    
    public ReadThread(String sourceFile,String threadName){
        this.sourceFile = sourceFile;
        this.threadName = threadName;
    }
    
    @Override
    public void run(){
        RandomAccessFile raf = null;
        FileChannel fc = null;
        FileLock fl = null;
        try {
            raf = new RandomAccessFile(sourceFile, "rw");
            fc = raf.getChannel();
            while(true){
                try {
                    fl = fc.tryLock(0,Long.MAX_VALUE,true);
                    System.out.println(fl.isShared());
                    System.out.println(this.threadName+" : get the lock");
                    try {
                        sleep(1000);
                    } catch (InterruptedException e1) {
                        e1.printStackTrace();
                    }
                    break;
                } catch (Exception e) {
                    // TODO Auto-generated catch block
                    //e.printStackTrace();
                    System.out.println(this.threadName+" is block");
                    try {
                        sleep(1000);
                    } catch (InterruptedException e1) {
                        e1.printStackTrace();
                    }
                }
    
            }
            StringBuffer sb = new StringBuffer();
            sb.append("Read from "+this.threadName+":");
            ByteBuffer bb = ByteBuffer.allocate(1024);
            while((fc.read(bb))!=-1){
                //sb.append(new String(bb.array()));
                //System.out.println(new String(bb.array()));
                bb.clear();
            }
            System.out.println(this.threadName+" : read success!");
            fl.release();
                System.out.println(this.threadName+" : release lock");
            raf.close();
            fc.close();
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    }
  3. 測試客戶端代碼

    public class Client {
    
    public static void main(String []args){
        new WriteThread("d:/test/test_1.txt", "d:/test/test.txt", "write_thread-1").start();
        new WriteThread("d:/test/logs", "d:/test/test.txt", "write_thread-2").start();  
    
        new ReadThread("d:/test/test.txt", "read_thread-1").start();
        new ReadThread("d:/test/test.txt", "read_thread-2").start();
    
    }
    }
    輸出結果:
    true
    read_thread-1 : get the lock
    read_thread-2 is block
    write_thread-1 is block
    write_thread-2 is block
    read_thread-2 is block
    write_thread-1 is block
    write_thread-2 is block
    read_thread-1 : read success!
    read_thread-1 : release lock
    true
    write_thread-2 is block
    read_thread-2 : get the lock
    write_thread-1 is block
    write_thread-2 is block
    write_thread-1 is block
    read_thread-2 : read success!
    read_thread-2 : release lock
    false
    write_thread-1 is block
    write_thread-2 : get the lock
    write_thread-1 is block
    write_thread-2 : write success
    write_thread-2 : release lock
    false
    write_thread-1 : get the lock
    write_thread-1 : write success
    write_thread-1 : release lock
  4. 可以看到,四個併發的線程依次訪問原文件,依次獲得鎖的權限。不因爲併發問題導致文件讀寫出錯。


再增加兩組不獲取鎖直接進行讀寫的線程,可以看到共享鎖和獨佔鎖的區別:

  1. 不加鎖的讀線程
public class NormalReadThread extends Thread{

    private String sourceFile;
    private String threadName;

    public NormalReadThread(String sourceFile,String threadName){
        this.sourceFile = sourceFile;
        this.threadName = threadName;
    }

    @Override
    public void run(){
        RandomAccessFile raf = null;
        try {
            raf = new RandomAccessFile(sourceFile, "rw");
            FileChannel fc = raf.getChannel();
            StringBuffer sb = new StringBuffer();
            sb.append("Read from "+this.threadName+":");
            ByteBuffer bb = ByteBuffer.allocate(1024);
            while((fc.read(bb))!=-1){
                //sb.append(new String(bb.array()));
                bb.clear();
            }
            System.out.println(this.threadName+" : read success!");
            raf.close();
            fc.close();
        } catch(Exception e){
            e.printStackTrace();
            System.out.println(this.threadName+" : read failed!!");
        }
    }

}
  1. 不加鎖的寫線程
public class NormalWriteThread extends Thread{

    private String sourceFile;
    private String targetFile;
    private String threadName;

    public NormalWriteThread(String sourceFile,String targetFile,String ThreadName){
        this.sourceFile = sourceFile;
        this.targetFile = targetFile;
        this.threadName = ThreadName;
    }

    @Override
    public void run(){
        RandomAccessFile raf = null;
        FileInputStream in = null;
        try {
            raf = new RandomAccessFile(targetFile, "rw");
            FileChannel fc = raf.getChannel();
            in = new FileInputStream(sourceFile);
            ByteBuffer bb = ByteBuffer.allocate(1024);
            byte[] b = new byte[1024];
            int len = 0;
            while((len=in.read(b))!=-1){
                bb.clear();
                bb.put(b, 0, len);
                fc.write(bb);
            }

            System.out.println(this.threadName+" : write success");
            fc.close();
            raf.close();
            in.close();
        } catch(Exception e){
            //e.printStackTrace();
            System.out.println(this.threadName+" : write   failed!!");
        }
    }

}

測試代碼:

  • 共享鎖測試
  public class Client {

    public static void main(String []args){
        /*new WriteThread("d:/test/test_1.txt", "d:/test/test.txt", "write_thread-1").start();
        new WriteThread("d:/test/logs", "d:/test/test.txt", "write_thread-2").start();*/    

        new ReadThread("d:/test/test.txt", "read_thread-1").start();
        new ReadThread("d:/test/test.txt", "read_thread-2").start();

        new NormalWriteThread("d:/test/test_copy1.txt", "d:/test/test.txt", "write_thread-3").start();
        new NormalWriteThread("d:/test/test_copy2.txt","d:/test/test.txt",  "write_thread-4").start();
        new NormalReadThread("d:/test/test.txt", "read_thread-3").start();
        new NormalReadThread("d:/test/test.txt", "read_thread-4").start();

    }
  }
測試結果:
true
read_thread-2 : get the lock
read_thread-4 : read success!
read_thread-3 : read success!
read_thread-1 is block
write_thread-4 : write   failed!!
write_thread-3 : write   failed!!
read_thread-1 is block
read_thread-2 : read success!
read_thread-2 : release lock
true
read_thread-1 : get the lock
read_thread-1 : read success!
read_thread-1 : release lock
可以看出,讀線程2先獲得鎖,此時另外兩個讀線程34可以併發進行讀操作,而線程1因爲需要持有鎖才能操作,因此阻塞中。而寫線程34因爲共享鎖不支持同時讀寫,因此寫入失敗。
  • 獨佔鎖測試
public class Client {

    public static void main(String []args){
        new WriteThread("d:/test/test_1.txt", "d:/test/test.txt", "write_thread-1").start();
        new WriteThread("d:/test/logs", "d:/test/test.txt", "write_thread-2").start();  

        /*new ReadThread("d:/test/test.txt", "read_thread-1").start();
        new ReadThread("d:/test/test.txt", "read_thread-2").start();*/

        new NormalWriteThread("d:/test/test_copy1.txt", "d:/test/test.txt", "write_thread-3").start();
        new NormalWriteThread("d:/test/test_copy2.txt","d:/test/test.txt",  "write_thread-4").start();
        new NormalReadThread("d:/test/test.txt", "read_thread-3").start();
        new NormalReadThread("d:/test/test.txt", "read_thread-4").start();

    }
}
測試結果:
false
write_thread-2 : get the lock
read_thread-4 : read failed!!
read_thread-3 : read failed!!
write_thread-1 is block
write_thread-4 : write   failed!!
write_thread-3 : write   failed!!
write_thread-1 is block
write_thread-2 : write success
write_thread-2 : release lock
false
write_thread-1 : get the lock
write_thread-1 : write success
write_thread-1 : release lock
從結果上看,寫線程2先獲得鎖,完成寫入後,寫線程1再獲得鎖,完成寫入。其他不加鎖的線程,無論是寫還是讀統統處理失敗。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章