【死磕NIO】— 跨進程文件鎖:FileLock


大家好,我是大明哥,一個專注於【死磕 Java】系列創作的程序員。
死磕 Java 】系列為作者「chenssy」 傾情打造的 Java 系列文章,深入分析 Java 相關技術核心原理及源碼
死磕 Java :https://www.cmsblogs.com/group/1420041599311810560


上篇文章(【死磕 NIO】— 深入分析Channel和FileChannel)已經詳細介紹了 FileChannel的核心原理及相關API,了解了FileChannel是用來讀寫和映射一個系統文件的 Channel,其實他還有很牛逼的功能就是:跨進程文件鎖。

說一個場景有多個進程同時操作某一個文件,並行往文件中寫數據,請問如何保證寫入文件的內容是正確的?可能有小伙伴說加分布式鎖,可以解決問題,但是有點兒重了。

有沒有更加輕量級的方案呢? 多進程文件鎖:FileLock

FileLock

FileLock是文件鎖,它能保證同一時間只有一個進程(程序)能夠修改它,或者都只可以讀,這樣就解決了多進程間的同步文件,保證了安全性。但是需要注意的是,它進程級別的,不是線程級別的,他可以解決多個進程並發訪問同一個文件的問題,但是它不適用於控制同一個進程中多個線程對一個文件的訪問。這也是為什么它叫做 多進程文件鎖,而不是 多線程文件鎖

FileLock一般都是從FileChannel 中獲取,FileChannel 提供了三個方法用以獲取 FileLock。

    public abstract FileLock lock(long position, long size, boolean shared) throws IOException;

    public final FileLock lock() throws IOException;

    public abstract FileLock tryLock(long position, long size, boolean shared) throws IOException;
    
    public final FileLock tryLock() throws IOException;
  • lock() 是阻塞式的,它要阻塞進程直到鎖可以獲得,或調用lock()的線程中斷,或調用lock()的通道關閉。
  • tryLock()是非阻塞式的,它設法獲取鎖,但如果不能獲得,例如因為其他一些進程已經持有相同的鎖,而且不共享時,它將直接從方法調用返回。

lock()tryLock()方法有三個參數,如下:

  • position:鎖定文件中的開始位置
  • size:鎖定文件中的內容長度
  • shared:是否使用共享鎖。true為共享鎖;false為獨占鎖。

共享鎖和獨占鎖的區別,大明哥就不解釋了。

示例

不使用文件鎖來讀寫文件

首先我們不使用文件鎖來進行多進程間文件讀寫,進程1往文件中寫數據,進程2讀取文件的大小。

  • 進程1
        RandomAccessFile randomAccessFile = new RandomAccessFile("/Users/chenssy/Downloads/filelock.txt","rw");
        FileChannel fileChannel = randomAccessFile.getChannel();
        // 這里是獨占鎖
        //FileLock fileLock = fileChannel.lock();
        System.out.println("進程 1 開始寫內容:" + LocalTime.now());
        for(int i = 1 ; i <= 10 ; i++) {
            randomAccessFile.writeChars("chenssy_" + i);
            // 等待兩秒
            TimeUnit.SECONDS.sleep(2);
        }
        System.out.println("進程 1 完成寫內容:" + LocalTime.now());
        // 完成后要釋放掉鎖
        //fileLock.release();
        fileChannel.close();
        randomAccessFile.close();
  • 進程2
        RandomAccessFile randomAccessFile = new RandomAccessFile("/Users/chenssy/Downloads/filelock.txt","rw");
        FileChannel fileChannel = randomAccessFile.getChannel();
        // 這里是獨占鎖
        //FileLock fileLock = fileChannel.lock();
        System.out.println("開始讀文件的時間:" + LocalTime.now());

        for(int i = 0 ; i < 10 ; i++) {
            // 這里直接讀文件的大小
            System.out.println("文件大小為:" + randomAccessFile.length());
            // 這里等待 1 秒
            TimeUnit.SECONDS.sleep(1);
        }

        System.out.println("結束讀文件的時間:" + LocalTime.now());
        // 完成后要釋放掉鎖
        //fileLock.release();
        fileChannel.close();
        randomAccessFile.close();

運行結果

  • 進程1

  • 進程2

從這個結果可以非常清晰看到,進程1和進程2是同時執行的。進程1一邊往文件中寫,進程2是一邊在讀的

使用文件鎖讀寫文件

這里我們使用文件鎖來進行多進程間文件讀寫,依然使用上面的程序,只需要將對應的注釋放開即可。執行結果

  • 進程1

  • 進程2

從這里可以看到,進程2是等進程1釋放掉鎖后才開始執行的。同時由於進程1已經將數據全部寫入文件了,所以進程2讀取文件的大小是一樣的。從這里可以看出 ** FileLock確實是可以解決多進程訪問同一個文件的並發安全問題。**

同進程不同線程進行文件讀寫

在開始就說到,FileLock是不適用同一進程不同線程之間文件的訪問。因為你根本無法在一個進程中不同線程同時對一個文件進行加鎖操作,如果線程1對文件進行了加鎖操作,這時線程2也來進行加鎖操作的話,則會直接拋出異常:java.nio.channels.OverlappingFileLockException

當然我們可以通過另外一種方式來規避,如下:

            FileLock fileLock;
            while (true){
                try{
                    fileLock = fileChannel.tryLock();
                    break;
                } catch (Exception e) {
                    System.out.println("其他線程已經獲取該文件鎖了,當前線程休眠 2 秒再獲取");
                    TimeUnit.SECONDS.sleep(2);
                }
            }

將上面獲取鎖的部分用這段代碼替換,執行結果又如下兩種:

  • 線程1先獲取文件鎖

  • 線程2先獲取文件鎖

這種方式雖然也可以實現多線程訪問同一個文件,但是不建議這樣操作!!!

源碼分析

下面我們以 FileLock lock(long position, long size, boolean shared)為例簡單分析下文件鎖的源碼。lock()方法是由FileChannel的子類 FileChannelImpl來實現的。

public FileLock lock(long position, long size, boolean shared) throws IOException {
        // 確認文件已經打開 , 即判斷open標識位
        ensureOpen();
        if (shared && !readable)
            throw new NonReadableChannelException();
        if (!shared && !writable)
            throw new NonWritableChannelException();
        // 創建 FileLock 對象
        FileLockImpl fli = new FileLockImpl(this, position, size, shared);
        // 創建 FileLockTable 對象
        FileLockTable flt = fileLockTable();
        flt.add(fli);
        boolean completed = false;
        int ti = -1;
        try {
            // 標記開始IO操作 , 可能會導致阻塞
            begin();
            ti = threads.add();
            if (!isOpen())
                return null;
            int n;
            do {
                // 開始鎖住文件
                n = nd.lock(fd, true, position, size, shared);
            } while ((n == FileDispatcher.INTERRUPTED) && isOpen());
            if (isOpen()) {
                // 如果返回結果為RET_EX_LOCK的話
                if (n == FileDispatcher.RET_EX_LOCK) {
                    assert shared;
                    FileLockImpl fli2 = new FileLockImpl(this, position, size,
                                                         false);
                    flt.replace(fli, fli2);
                    fli = fli2;
                }
                completed = true;
            }
        } finally {
            // 釋放鎖
            if (!completed)
                flt.remove(fli);
            threads.remove(ti);
            try {
                end(completed);
            } catch (ClosedByInterruptException e) {
                throw new FileLockInterruptionException();
            }
        }
        return fli;
    }

首先會判斷文件是否已打開,然后創建FileLock和FileLockTable 對象,其中FileLockTable是用於存放 FileLock的table。

  • 調用 begin()設置中斷觸發
protected final void begin() {
        if (interruptor == null) {
            interruptor = new Interruptible() {
                    public void interrupt(Thread target) {
                        synchronized (closeLock) {
                            if (!open)
                                return;
                            open = false;
                            interrupted = target;
                            try {
                                AbstractInterruptibleChannel.this.implCloseChannel();
                            } catch (IOException x) { }
                        }
                    }};
        }
        blockedOn(interruptor);
        Thread me = Thread.currentThread();
        if (me.isInterrupted())
            interruptor.interrupt(me);
    }
  • 調用 FileDispatcher.lock()開始鎖住文件
int lock(FileDescriptor fd, boolean blocking, long pos, long size,
             boolean shared) throws IOException
    {
        BlockGuard.getThreadPolicy().onWriteToDisk();
        return lock0(fd, blocking, pos, size, shared);
    }

lock0()的實現是在 FileDispatcherImpl.c 中,源碼如下:

JNIEXPORT jint JNICALL
FileDispatcherImpl_lock0(JNIEnv *env, jobject this, jobject fdo,
                                      jboolean block, jlong pos, jlong size,
                                      jboolean shared)
{
    // 通過fdval函數找到fd
    jint fd = fdval(env, fdo);
    jint lockResult = 0;
    int cmd = 0;
    // 創建flock對象
    struct flock64 fl;

    fl.l_whence = SEEK_SET;
    // 從position位置開始
    if (size == (jlong)java_lang_Long_MAX_VALUE) {
        fl.l_len = (off64_t)0;
    } else {
        fl.l_len = (off64_t)size;
    }
    fl.l_start = (off64_t)pos;
    // 如果是共享鎖 , 則只讀
    if (shared == JNI_TRUE) {
        fl.l_type = F_RDLCK;
    } else {
        // 否則可讀寫
        fl.l_type = F_WRLCK;
    }
    // 設置鎖參數
    // F_SETLK : 給當前文件上鎖(非阻塞)。
    // F_SETLKW : 給當前文件上鎖(阻塞,若當前文件正在被鎖住,該函數一直阻塞)。
    if (block == JNI_TRUE) {
        cmd = F_SETLKW64;
    } else {
        cmd = F_SETLK64;
    }
    // 調用fcntl鎖住文件
    lockResult = fcntl(fd, cmd, &fl);
    if (lockResult < 0) {
        if ((cmd == F_SETLK64) && (errno == EAGAIN || errno == EACCES))
            // 如果出現錯誤 , 返回錯誤碼
            return sun_nio_ch_FileDispatcherImpl_NO_LOCK;
        if (errno == EINTR)
            return sun_nio_ch_FileDispatcherImpl_INTERRUPTED;
        JNU_ThrowIOExceptionWithLastError(env, "Lock failed");
    }
    return 0;
}

所以,其實文件鎖的核心就是調用Linux的fnctl來從內核對文件進行加鎖。

關於Linux 文件鎖,大明哥推薦這兩篇博客,小伙伴可以了解下:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM