[Java] FileOutputStream 原理(Windows)

前言

前幾天幫公司新人解決了一個多線程問題,問題很簡單。這位同事想用多線程提高文件寫入速度結(jié)果使用同一個文件在多個線程創(chuàng)建的多個 FileOutPutStream 然后使用這個FileOutPutStream 分別向文件中寫入內(nèi)容,可想而知結(jié)果肯定是不正確的。

問題雖然簡單但其實(shí)排查的過程也并非一帆風(fēng)順,究其原因可能還是自己對API不夠熟悉,當(dāng)問題發(fā)生時沒能堅(jiān)信自己的理論導(dǎo)致方向逐漸跑偏浪費(fèi)了很多時間镜沽,所以解決問題后我就對FileOutputStream 進(jìn)行了更為深入的研究。

正文

首先我寫了一個demo復(fù)現(xiàn)當(dāng)時的問題demo如下:

       File file = new File("E:/Tmp/1.txt");

        new Thread(()->{
            try(FileOutputStream outputStream = new FileOutputStream(file);) {
                for (int i = 0; i < 1000_0; i++) {
                    outputStream.write("Thread 1 write 1\n".getBytes(StandardCharsets.UTF_8));
                }

            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }).start();
        new Thread(()->{
            try(FileOutputStream outputStream = new FileOutputStream(file);) {
                for (int i = 0; i < 1000_0; i++) {
                    outputStream.write("Thread 2 write 2\n".getBytes(StandardCharsets.UTF_8));
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }).start();

運(yùn)行結(jié)果: 文本文件中交替出現(xiàn) Thread 1 write 1Thread 2 write 2
在 Debug 的過程當(dāng)中可以發(fā)現(xiàn) FileOutputStream 中有一個屬性名為 FileDescriptor 直譯即為文件描述符,此時可以猜測他應(yīng)該會是輸出流的關(guān)鍵贱田。


我們可以繼續(xù)深入到FileOutputStream 類中尋找 FileDescriptor 是合適被創(chuàng)建的根據(jù)構(gòu)造方法我們很容易找到缅茉,在其中一個構(gòu)造方法中找到

   public FileOutputStream(File file, boolean append)
        throws FileNotFoundException
    {
        String name = (file != null ? file.getPath() : null);
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkWrite(name);
        }
        if (name == null) {
            throw new NullPointerException();
        }
        if (file.isInvalid()) {
            throw new FileNotFoundException("Invalid file path");
        }
       //  ------------------------------------------------------------------------------------
       //  ----------------------------在此處創(chuàng)建文件描述符--------------------------------------
       //  ------------------------------------------------------------------------------------
        this.fd = new FileDescriptor();
        fd.attach(this);
        this.append = append;
        this.path = name;

        open(name, append);
    }

但此處的 FileDescriptor 并沒有完全初始化完成,對比上文截圖可以發(fā)現(xiàn)其handle屬性并沒有被賦值(后文可以知道它一定是一個大于零的整數(shù))


繼續(xù)Debug 不難看到當(dāng)調(diào)用open函數(shù)后handle被賦值男摧,所以查看open函數(shù)代碼,open會繼續(xù)調(diào)用open0方法蔬墩,此時到達(dá)native函數(shù)。

    private native void open0(String name, boolean append) throws FileNotFoundException;

注意:低版本Jdk 可能沒有 open0 調(diào)用過程 open即為 native函數(shù)所以見到直接調(diào)用 native open 也是正常的

接下來需要下載 jdk 源碼(這里下載的是 openjdk 的源碼這些基礎(chǔ)類庫的實(shí)現(xiàn)jdk和 openjdk 基本不會有差別)
jdk8u60 下載地址
其他版本可進(jìn)入openjdk自行選擇下載
Git用戶也可使用Git

git clone -b jdk8-b120 https://github.com/openjdk/jdk.git

下載完畢后使用任意IDE打開耗拓,這里我使用VS Code拇颅,定位到src\windows\native\java\io\FileOutputStream_md.c文件,對應(yīng)的 c 代碼如下

JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_open0(JNIEnv *env, jobject this,
                                    jstring path, jboolean append) {
    fileOpen(env, this, path, fos_fd,
             O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC));
}

可以看到open0 調(diào)用的是 fileOpen函數(shù),繼續(xù)查看fileOpen函數(shù)(VS Code 需要安裝 C/C++ Extension 才可以函數(shù)導(dǎo)航)該函數(shù)位于src\windows\native\java\io\io_util_md.c

void
fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags)
{
    FD h = winFileHandleOpen(env, path, flags);
    if (h >= 0) {
        SET_FD(this, h, fid);
    }
}

同文件下找到 winFileHandleOpen(env, path, flags)

FD
winFileHandleOpen(JNIEnv *env, jstring path, int flags)
{
    // 準(zhǔn)備 CreateFileW 函數(shù)參數(shù)
    // 訪問權(quán)限
    const DWORD access =
        (flags & O_WRONLY) ?  GENERIC_WRITE :
        (flags & O_RDWR)   ? (GENERIC_READ | GENERIC_WRITE) :
        GENERIC_READ;
    //共享模式
    const DWORD sharing =
        FILE_SHARE_READ | FILE_SHARE_WRITE;
    //該 文件\設(shè)備是否存在
    const DWORD disposition =
        /* Note: O_TRUNC overrides O_CREAT */
        (flags & O_TRUNC) ? CREATE_ALWAYS :
        (flags & O_CREAT) ? OPEN_ALWAYS   :
        OPEN_EXISTING;
    const DWORD  maybeWriteThrough =
        (flags & (O_SYNC | O_DSYNC)) ?
        FILE_FLAG_WRITE_THROUGH :
        FILE_ATTRIBUTE_NORMAL;
    const DWORD maybeDeleteOnClose =
        (flags & O_TEMPORARY) ?
        FILE_FLAG_DELETE_ON_CLOSE :
        FILE_ATTRIBUTE_NORMAL;
    //文件的屬性和操作標(biāo)志位乔询,例如是否為壓縮文件樟插,是否隱藏,是否在釋放資源時自動刪除等
    const DWORD flagsAndAttributes = maybeWriteThrough | maybeDeleteOnClose;
    HANDLE h = NULL;

    WCHAR *pathbuf = pathToNTPath(env, path, JNI_TRUE);
    if (pathbuf == NULL) {
        /* Exception already pending */
        return -1;
    }
       //  ------------------------------------------------------------------------------------
       //  ------------------------------------關(guān)鍵--------------------------------------------
       //  ------------------------------------------------------------------------------------
    h = CreateFileW(
        pathbuf,            /* Wide char path name */
        access,             /* Read and/or write permission */
        sharing,            /* File sharing flags */
        NULL,               /* Security attributes */
        disposition,        /* creation disposition */
        flagsAndAttributes, /* flags and attributes */
        NULL);
    free(pathbuf);

    if (h == INVALID_HANDLE_VALUE) {
        throwFileNotFoundException(env, path);
        return -1;
    }
    return (jlong) h;
}

到這里我們可以看到該函數(shù)調(diào)用 CreateFileW 函數(shù)從函數(shù)名看來是創(chuàng)建了一個文件竿刁。實(shí)際上它是Windows API中的一個函數(shù)感興趣的可以看下CreateFileW API黄锤,鏈接指向的是 Windows系統(tǒng) API CreateFileW 函數(shù)的文檔。該函數(shù)的作用是打開一個 文件或者 IO設(shè)備并返回一個句柄(句柄是 Windows編程中的一個概念它可以指代 窗口食拜、資源鸵熟、文件等)通過該句柄我們就可以訪問該句柄指向的資源了也就是我們的文件。

其中幾個重要的參數(shù)我也在上文中進(jìn)行了注釋负甸。重點(diǎn)看下 sharing

 const DWORD sharing =
        FILE_SHARE_READ | FILE_SHARE_WRITE;

他是一個 64 bit 數(shù)據(jù) 每個bit代表不同的模式流强,不同模式間可共存,例如可以同時共享寫和共享讀以下為該參數(shù)的可選值扎抄自微軟官網(wǎng)文檔(就是上邊 CreateFileW API的鏈接)

Value Meaning
0 0x00000000 Prevents subsequent open operations on a file or device if they request delete, read, or write access.
FILE_SHARE_DELETE 0x00000004 Enables subsequent open operations on a file or device to request delete access. Otherwise, no process can open the file or device if it requests delete access. If this flag is not specified, but the file or device has been opened for delete access, the function fails.
Note Delete access allows both delete and rename operations.
FILE_SHARE_READ 0x00000001 Enables subsequent open operations on a file or device to request read access. Otherwise, no process can open the file or device if it requests read access. If this flag is not specified, but the file or device has been opened for read access, the function fails.
FILE_SHARE_WRITE 0x00000002 Enables subsequent open operations on a file or device to request write access. Otherwise, no process can open the file or device if it requests write access. If this flag is not specified, but the file or device has been opened for write access or has a file mapping with write access, the function fails.

我們可以看到共享寫和共享讀是寫死的呻待,每個IO流都是默認(rèn)共享讀寫的打月,這也就解釋了為什么我們可以在不同線程使用同一個文件創(chuàng)建多個 FileOutputStream

也許我們都會碰到用其他軟件打開文件并沒關(guān)閉的情況下我們開發(fā)過程中的默寫操作是會失敗的,典型的當(dāng)我們使用壓縮軟件打開maven構(gòu)建的 jar 包時執(zhí)行 maven clean 是會失敗的蚕捉。我想這可能是因?yàn)樗麄兇蜷_文件的方式是非共享模式奏篙,當(dāng)然可能不是 CreateFileW 函數(shù)還可能是 CreateFIle 也是Windows API 中的一個函數(shù)且和 CreateFileW 有著類似的方法簽名

最后可以看到該函數(shù)返回的即使 CreateFileW 創(chuàng)建的句柄,所以讓我們回到 fileOpen 看看句柄返回后如何處理

void
fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags)
{
    FD h = winFileHandleOpen(env, path, flags);
    if (h >= 0) {
        SET_FD(this, h, fid);
    }
}

當(dāng)句柄創(chuàng)建成功即大于零時鱼冀,會執(zhí)行 SET_FD(this, h, fid); 代碼段报破,還記得上文中說過 FileDescriptor 中 handle 一定會大于零的整數(shù)嗎,此處已經(jīng)初露端倪了千绪。繼續(xù)定位到SET_FD

/*
 * Macros to set/get fd from the java.io.FileDescriptor.
 * If GetObjectField returns null, SET_FD will stop and GET_FD
 * will simply return -1 to avoid crashing VM.
 */
#define SET_FD(this, fd, fid) \
    if ((*env)->GetObjectField(env, (this), (fid)) != NULL) \
        (*env)->SetLongField(env, (*env)->GetObjectField(env, (this), (fid)), IO_handle_fdID, (fd))

此處是一個宏定義展開的等效形式如下

if((*env)->GetObjectField(env, (this), (fid)) != NULL)
{
    (*env)->SetLongField(env, (*env)->GetObjectField(env, (this), (fid)), IO_handle_fdID, (fd))
}

通過注釋也可以知道其功能是為Java 中 Class 實(shí)例的某個屬性賦值充易,轉(zhuǎn)到 IO_handle_fdID 的定義

/* field id for jlong 'handle' in java.io.FileDescriptor */
jfieldID IO_handle_fdID;

注釋同樣標(biāo)注的很清楚此屬性代表了 FileDescriptor 實(shí)例 handle 的屬性ID。接下來我們回到 Java 中定位到 FileDescriptor,可以很輕松找到如下代碼


    static {
        initIDs();
    }

initIDs 也是一個native方法

private static native void initIDs();

所以按照相似的方法定位到src\windows\native\java\io\FileDescriptor_md.c

JNIEXPORT void JNICALL
Java_java_io_FileDescriptor_initIDs(JNIEnv *env, jclass fdClass) {
    CHECK_NULL(IO_fd_fdID = (*env)->GetFieldID(env, fdClass, "fd", "I"));
    CHECK_NULL(IO_handle_fdID = (*env)->GetFieldID(env, fdClass, "handle", "J"));
}

到這里可以看到 IO_handle_fdID 的初始化過程荸型。到這里我們就完全可以解釋 open0 是如何為每個OutputStream 的文件描述符 fd屬性的handle屬性進(jìn)行賦值的盹靴。

  1. 調(diào)用fileOpen()
  2. fileOpen 調(diào)用 winFileHandleOpen
  3. winFileHandleOpen 解析參數(shù)并調(diào)用Windows API CreateFileW 并返回文件句柄給
  4. fileOpen中判斷文件句柄是否合法若合法,將文件句柄通過SetLongField寫入到該FileOutputStream的 fd屬性(即那個文件描述符)的handle屬性

好了瑞妇,至此算是大致了解了 FileOutputStream 的創(chuàng)建過程稿静,其本質(zhì)是打開了一個文件或IO設(shè)備并保存了打開文件\設(shè)備的句柄 handle 那接下來就可以探究 FileOutputStream 是如何向這個打開的文件寫入數(shù)據(jù)的

回到 FileOutputStream 查看幾個 write API 很容易可以看到最終調(diào)用的總會是一下兩個函數(shù)之一

private native void write(int b, boolean append) throws IOException;

private native void writeBytes(byte b[], int off, int len, boolean append) throws IOException;

按照同樣的方法找到 native 函數(shù)在 jdk 中的定義,定位到jdk\src\windows\native\java\io\FileOutputStream_md.c

JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_write(JNIEnv *env, jobject this, jint byte, jboolean append) {
    writeSingle(env, this, byte, append, fos_fd);
}

找到 writeSingle 代碼,文件位置*jdk\src\share\native\java\io\io_util.c

void
writeSingle(JNIEnv *env, jobject this, jint byte, jboolean append, jfieldID fid) {
    // Discard the 24 high-order bits of byte. See OutputStream#write(int)
    char c = (char) byte;
    jint n;
    FD fd = GET_FD(this, fid);
    if (fd == -1) {
        JNU_ThrowIOException(env, "Stream Closed");
        return;
    }
    if (append == JNI_TRUE) {
        n = IO_Append(fd, &c, 1);
    } else {
        n = IO_Write(fd, &c, 1);
    }
    if (n == -1) {
        JNU_ThrowIOExceptionWithLastError(env, "Write error");
    }
}

可以看到首先會將傳入進(jìn)來的數(shù)據(jù)轉(zhuǎn)換為 char 類型(只保留低8位數(shù)據(jù))然后關(guān)鍵的一步FD fd = GET_FD(this, fid); 取出上文構(gòu)造open0 中寫入到類信息中的文件句柄。GET_FD 也是一個宏定義

#define GET_FD(this, fid) \
    (*env)->GetObjectField(env, (this), (fid)) == NULL ? \
        -1 : (*env)->GetIntField(env, (*env)->GetObjectField(env, (this), (fid)), IO_fd_fdID)

等效宏展開

(*env)->GetObjectField(env, (this), (fid)) == NULL ? -1 : (*env)->GetIntField(env, (*env)->GetObjectField(env, (this), (fid)), IO_fd_fdID)

隨后判斷打開文件時指定的打開模式是否為追加模式辕狰,單個參數(shù) FileOutputStream 的構(gòu)造方法默認(rèn)以復(fù)寫模式打開 IO 流

   public FileOutputStream(File file) throws FileNotFoundException {
        this(file, false);
    }

如果想使用追加模式打開 IO 流可使用 FileOutputStream 的另一個重載構(gòu)造

public FileOutputStream(File file, boolean append)throws FileNotFoundException

可以看到單參數(shù)構(gòu)造調(diào)用的就是此構(gòu)造且指定 append 為 false
繼續(xù)看 writeSingle 函數(shù)改备,確定了文件打開模式后可分別執(zhí)行 n = IO_Append(fd, &c, 1);n = IO_Write(fd, &c, 1); 代碼段。
IO_Append 是一個宏定義

#define IO_Append handleAppend

所以繼續(xù)查看 handleAppend定義蔓倍,這里注意 jdk 中可能會對不同的操作系統(tǒng)有不同的實(shí)現(xiàn)悬钳,代碼閱讀工具自動導(dǎo)航可能會導(dǎo)航錯,導(dǎo)航到 solaris 系統(tǒng)看到的會是 #define IO_Append handleWrite 選擇 Windows 系統(tǒng)實(shí)現(xiàn) jdk\src\solaris\native\java\io\io_util_md.h

jint handleAppend(FD fd, const void *buf, jint len) {
    return writeInternal(fd, buf, len, JNI_TRUE);
}

handleWrite 會調(diào)用 writeInternal 繼續(xù)查看 writeInterna 定義

static jint writeInternal(FD fd, const void *buf, jint len, jboolean append)
{
    BOOL result = 0;
    DWORD written = 0;
    HANDLE h = (HANDLE)fd;
    if (h != INVALID_HANDLE_VALUE) {
        OVERLAPPED ov;
        LPOVERLAPPED lpOv;
        if (append == JNI_TRUE) {
            ov.Offset = (DWORD)0xFFFFFFFF;
            ov.OffsetHigh = (DWORD)0xFFFFFFFF;
            ov.hEvent = NULL;
            lpOv = &ov;
        } else {
            lpOv = NULL;
        }
        result = WriteFile(h,                /* File handle to write */
                           buf,              /* pointers to the buffers */
                           len,              /* number of bytes to write */
                           &written,         /* receives number of bytes written */
                           lpOv);            /* overlapped struct */
    }
    if ((h == INVALID_HANDLE_VALUE) || (result == 0)) {
        return -1;
    }
    return (jint)written;
}

這里可以看到 最終調(diào)用 WriteFile API 寫入數(shù)據(jù)偶翅,WriteFile 文檔 WriteFile 通過文檔得知當(dāng)使用追加模式寫入時 LPOVERLAPPED 的 Offset 和 OffsetHigh 要設(shè)置為 0xFFFFFFFF

To write to the end of file, specify both the Offset and OffsetHigh members of the OVERLAPPED structure as 0xFFFFFFFF. This is functionally equivalent to previously calling the CreateFile function to open hFile using FILE_APPEND_DATA access.

WriteFile 的第一個參數(shù) h 即為文件描述符默勾,至此寫入過程大致也比較清晰了。另一個 writeBytes 實(shí)現(xiàn)會比 write 更加復(fù)雜聚谁。

void
writeBytes(JNIEnv *env, jobject this, jbyteArray bytes,
           jint off, jint len, jboolean append, jfieldID fid)
{
    jint n;
    char stackBuf[BUF_SIZE];
    char *buf = NULL;
    FD fd;

    if (IS_NULL(bytes)) {
        JNU_ThrowNullPointerException(env, NULL);
        return;
    }

    if (outOfBounds(env, off, len, bytes)) {
        JNU_ThrowByName(env, "java/lang/IndexOutOfBoundsException", NULL);
        return;
    }

    if (len == 0) {
        return;
    } else if (len > BUF_SIZE) {
        buf = malloc(len);
        if (buf == NULL) {
            JNU_ThrowOutOfMemoryError(env, NULL);
            return;
        }
    } else {
        buf = stackBuf;
    }

    (*env)->GetByteArrayRegion(env, bytes, off, len, (jbyte *)buf);

    if (!(*env)->ExceptionOccurred(env)) {
        off = 0;
        while (len > 0) {
            fd = GET_FD(this, fid);
            if (fd == -1) {
                JNU_ThrowIOException(env, "Stream Closed");
                break;
            }
            if (append == JNI_TRUE) {
                n = IO_Append(fd, buf+off, len);
            } else {
                n = IO_Write(fd, buf+off, len);
            }
            if (n == -1) {
                JNU_ThrowIOExceptionWithLastError(env, "Write error");
                break;
            }
            off += n;
            len -= n;
        }
    }
    if (buf != stackBuf) {
        free(buf);
    }
}

writeBytes 會多出不少邊界檢測代碼母剥,同時源碼中還可以看見老朋友 java/lang/IndexOutOfBoundsException 但最終 會執(zhí)行同樣的邏輯,獲取 class 實(shí)例中句柄,根據(jù)append 選擇是 IO_Append 還是 IO_Wrtie 這兩個實(shí)現(xiàn)一樣區(qū)別在于最后調(diào)用 writeInternal 最后一個參數(shù)是 ture 還是 fase (1 或 0)

至此寫入操作流程也比較清晰了形导,關(guān)閉操作就不詳細(xì)展開了环疼。感興趣的可以查看 close0 對應(yīng)源碼最終調(diào)用 Window API CloseHandle 關(guān)閉句柄。

完結(jié)撒花6涓G乇!


最后:大幻夢森羅萬象狂氣斷罪眼~

搬家驗(yàn)證:3e70d467-3718-47b6-a6bb-e1dd84a2f145

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末憔披,一起剝皮案震驚了整個濱河市等限,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌芬膝,老刑警劉巖望门,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異锰霜,居然都是意外死亡筹误,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門癣缅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來厨剪,“玉大人哄酝,你說我怎么就攤上這事〉簧牛” “怎么了陶衅?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長直晨。 經(jīng)常有香客問我搀军,道長,這世上最難降的妖魔是什么勇皇? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任罩句,我火速辦了婚禮,結(jié)果婚禮上敛摘,老公的妹妹穿的比我還像新娘门烂。我一直安慰自己,他們只是感情好兄淫,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布诅福。 她就那樣靜靜地躺著,像睡著了一般拖叙。 火紅的嫁衣襯著肌膚如雪氓润。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天薯鳍,我揣著相機(jī)與錄音咖气,去河邊找鬼。 笑死挖滤,一個胖子當(dāng)著我的面吹牛崩溪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播斩松,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼伶唯,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了惧盹?” 一聲冷哼從身側(cè)響起乳幸,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎钧椰,沒想到半個月后粹断,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡嫡霞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年瓶埋,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡养筒,死狀恐怖曾撤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情晕粪,我是刑警寧澤挤悉,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站兵多,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏橄仆。R本人自食惡果不足惜剩膘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望盆顾。 院中可真熱鬧怠褐,春花似錦、人聲如沸您宪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宪巨。三九已至磷杏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間捏卓,已是汗流浹背极祸。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留怠晴,地道東北人遥金。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像蒜田,于是被迫代替她去往敵國和親稿械。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344