時(shí)間撿拾(上篇-應(yīng)用層)

目錄:


image.png

什么是時(shí)間?這是一個(gè)物理概念和哲學(xué)問題岛抄。物理學(xué)認(rèn)為時(shí)間是一種尺度,一個(gè)標(biāo)量狈茉,借著時(shí)間夫椭,事件發(fā)生之先后可以按過去-現(xiàn)在-未來之序列得以確定(時(shí)間點(diǎn)),進(jìn)而事件之間的間隔長(zhǎng)短亦得以衡量(時(shí)間段)氯庆。哲學(xué)上認(rèn)為時(shí)間是宇宙的基本結(jié)構(gòu)蹭秋,是一個(gè)會(huì)依序列方式出現(xiàn)的維度〉棠欤或主張時(shí)間“本身并不存在仁讨,而是我們表達(dá)事物方式的產(chǎn)物”。


故宮日晷

計(jì)算機(jī)科學(xué)是建立在現(xiàn)實(shí)物理世界的基礎(chǔ)上的实昨,要盡量匹配地球自轉(zhuǎn)公轉(zhuǎn)的結(jié)果洞豁,同時(shí)要匹配一系列人為規(guī)定的概念(如時(shí)區(qū)、夏令時(shí))荒给。這就帶來了一系列問題:計(jì)算機(jī)如何描述及存儲(chǔ)時(shí)間點(diǎn)和時(shí)間段丈挟、如何匹配不同時(shí)區(qū)和計(jì)時(shí)方式、如何轉(zhuǎn)換時(shí)間的表示方法志电、如何獲取當(dāng)前時(shí)間曙咽、如何控制時(shí)間精度、如何感知時(shí)間流逝等一系列問題挑辆。本篇文章盡筆者能力清晰深入地探究這個(gè)問題例朱。

一. 常識(shí)知識(shí)

1. 時(shí)區(qū)

時(shí)區(qū)是地球上的同一塊區(qū)域使用的同一個(gè)時(shí)間定義。世界各個(gè)國(guó)家位于地球不同位置上鱼蝉,因此不同國(guó)家洒嗤,特別是東西跨度大的國(guó)家日出、日落時(shí)間必定有所偏差魁亦。這些偏差就是所謂的時(shí)差渔隶。

2. 夏令時(shí)

所謂“夏令時(shí)”(Daylight Saving Time,簡(jiǎn)稱D.S.T.)吉挣,是指在夏天太陽(yáng)升起的比較早時(shí)派撕,將時(shí)鐘撥快一小時(shí),以提早日光的使用睬魂。這個(gè)構(gòu)想于1784年由美國(guó)班杰明·富蘭克林提出來终吼,1915年德國(guó)成為第一個(gè)正式實(shí)施夏令日光節(jié)約時(shí)間的國(guó)家,以削減燈光照明和耗電開支氯哮。 進(jìn)夏令時(shí)時(shí)間要撥快一小時(shí)际跪,出夏令時(shí)時(shí)間再撥回來。但這跟UTC或GMT完全沒有關(guān)系喉钢,完全是人為行為姆打。

3. UTC和GMT

  • UTC是“協(xié)調(diào)世界時(shí)”(Universal Time Coordinated)的英文縮寫,是由國(guó)際無線電咨詢委員會(huì)規(guī)定和推薦肠虽,并由國(guó)際時(shí)間局(BIH)負(fù)責(zé)保持的以秒為基礎(chǔ)的時(shí)間標(biāo)度幔戏。個(gè)人理解為按規(guī)定的統(tǒng)一計(jì)量單位延伸的時(shí)間標(biāo)度。
  • GMT(Greenwich Mean Time)是格林尼治平均時(shí)間税课。由于地球軌道并非圓形闲延,其運(yùn)行速度又隨著地球與太陽(yáng)的距離改變而出現(xiàn)變化。在格林尼治子午線上的平太陽(yáng)時(shí)稱為世界時(shí)(UT0)韩玩,又叫格林尼治平時(shí)(GMT)垒玲。 個(gè)人理解為實(shí)際觀測(cè)計(jì)算不受人為控制的太陽(yáng)運(yùn)行周期的時(shí)間標(biāo)度。
  • 若以“世界標(biāo)準(zhǔn)時(shí)間”的角度來說找颓,UTC比GMT來得更加精準(zhǔn)合愈。兩者誤差值必須保持在0.9秒以內(nèi),若大于0.9秒則由位于巴黎的國(guó)際地球自轉(zhuǎn)事務(wù)中央局發(fā)布閏秒击狮,使UTC與地球自轉(zhuǎn)周期一致佛析。

二. Java關(guān)于日期時(shí)間的獲取、表示及格式轉(zhuǎn)換

時(shí)間的表示可以分為時(shí)間點(diǎn)和時(shí)間段彪蓬。時(shí)間點(diǎn)又可以分為“相對(duì)時(shí)間”和“絕對(duì)時(shí)間”(不是相對(duì)論里那個(gè))说莫,人們一般理解表述的“現(xiàn)在幾點(diǎn)”、“掛鐘上顯示什么時(shí)間”是“相對(duì)時(shí)間”寞焙,即本地時(shí)間储狭,是沒有時(shí)區(qū)屬性的。但是如果要表示一個(gè)客觀發(fā)生的時(shí)間點(diǎn)就要用到“絕對(duì)時(shí)間”捣郊,這個(gè)時(shí)間點(diǎn)在每個(gè)時(shí)區(qū)的掛鐘上顯示的都不同辽狈。
時(shí)間的表示還關(guān)系到精度問題,如精確到天呛牲、秒還是毫秒刮萌、納秒,都有不同的表示方法娘扩。
下面以Java為例着茸,較為詳細(xì)地介紹關(guān)于日期時(shí)間的獲取壮锻、表示及格式轉(zhuǎn)換方法。

1. System.currentTimeMillis()

這是我們最常用的獲取當(dāng)前時(shí)間的方法涮阔,靜態(tài)方法System.currentTimeMillis() 返回UTC時(shí)間從1970年1月1日00:00到現(xiàn)在的總毫秒數(shù)猜绣,返回類型為long。我們所有需要做的就是一行代碼:

Long time = System.currentTimeMillis();

ps:為什么是從1970年1月1日開始敬特?
Unix是1969年發(fā)布的雛形掰邢,最早是基于硬件60Hz的時(shí)間計(jì)數(shù)。1971年底出版的《Unix Programmer’s Manual》里定義的Unix Time是以1971年1月1日00:00:00作為起始時(shí)間伟阔,每秒增長(zhǎng)60辣之。之后考慮到32位整數(shù)的范圍,如果每秒60個(gè)數(shù)字皱炉,則兩年半就會(huì)循環(huán)一輪怀估。于是改成了以秒為計(jì)數(shù)單位。這個(gè)循環(huán)周期有136年之長(zhǎng)合搅,就不在乎起始時(shí)間是1970還是1971年了奏夫,于是就改成了人工記憶、計(jì)算比較方便的1970年历筝。
“The date was programmed into the system sometime in the early 70s only because it was convenient to do so, according to Dennis Ritchie, one the engineers who worked on Unix at Bell Labs at its inception.”

趣聞:32位Unix時(shí)間戳的范圍是 1971年1月1日00:00:00 ~ 2038年1月19日03:14:07(UTC)酗昼,超過這一范圍則會(huì)越界。2016年出現(xiàn)過蘋果用戶將手機(jī)時(shí)間設(shè)為1971年之前梳猪,然后iPhone變磚了÷橄鳎現(xiàn)在iPhone的解決方法是不允許手動(dòng)設(shè)置年份 :P

注意,java.lang包在該方法的注釋中提到春弥,當(dāng)返回值的時(shí)間單位是毫秒時(shí)呛哟,值的粒度取決于底層操作系統(tǒng),可能粒度會(huì)大于1ms匿沛。同時(shí)高并發(fā)場(chǎng)景下要小心該方法的性能消耗扫责。為什么會(huì)這樣?什么時(shí)候會(huì)出現(xiàn)這種情況逃呼?下篇會(huì)從該方法的源碼入手深入探究鳖孤。

2. System.nanoTime()

Java7的API文檔中說明:該方法返回正在運(yùn)行的Java虛擬機(jī)的高分辨率時(shí)間源的當(dāng)前值,以納秒為單位抡笼。此方法只能用于測(cè)量經(jīng)過的時(shí)間苏揣,與系統(tǒng)或鐘表時(shí)間等任何其他概念無關(guān)。在同一個(gè)Java虛擬機(jī)實(shí)例中推姻,此方法的所有調(diào)用都使用相同的時(shí)間原點(diǎn)平匈,其他虛擬機(jī)實(shí)例可能使用不同的時(shí)間原點(diǎn)。此方法提供納秒級(jí)精度,但不一定是納秒級(jí)分辨率增炭,但是最少和 currentTimeMillis() 方法的分辨率一樣高忍燥。

也就是說,nanoTime() 方法返回的數(shù)字絕對(duì)值沒有意義隙姿,僅當(dāng)計(jì)算在Java虛擬機(jī)的同一實(shí)例中獲得的兩個(gè)此值之間的差異時(shí)梅垄,此方法返回的值才有意義。常用的方法是:

Long startTime = System.nanoTime();
doSomething();
Long estimatedTime = System.nanoTime() - startTime;

那所謂的“隨機(jī)起點(diǎn)”在不同平臺(tái)上是如何實(shí)現(xiàn)的孟辑?System.nanoTime() 和 System.currentTimeMillis() 有沒有什么關(guān)系哎甲?也會(huì)在下篇中一并提及蔫敲。

3. java.util.Date

Date是Java最早提供的用來封裝日期時(shí)間的類饲嗽,由于不易于國(guó)際化且很多參數(shù)計(jì)算不符合日常認(rèn)知或不正確(具體可以見源碼),很多獲取年奈嘿、月貌虾、日、小時(shí)等數(shù)據(jù)的方法都過時(shí)了不推薦使用(@Deprecated)裙犹,被Calendar類的方法代替尽狠。這里選一些還在使用的關(guān)鍵字段和方法進(jìn)行說明。
Date類有兩個(gè)關(guān)鍵的成員變量:

// 記錄當(dāng)前時(shí)間戳
private transient long fastTime;

/*
 * cdate對(duì)象是 BaseCalendar.Date類叶圃,繼承自sun.util.calendar.CalendarDate袄膏。
 * 包含很多已計(jì)算好的日期時(shí)間相關(guān)變量,如 dayOfWeek(所在星期的第幾天)掺冠、leapYear(是否是閏年)等沉馆。
 * 如果 cdate 對(duì)象為空,用 fastTime 變量代表精確到毫秒的時(shí)間德崭。
 * 如果 cdate.isNormalized() 方法返回 true斥黑,則 fastTime 和 cdate 已經(jīng)同步過。
 * 如果 cdate.isNormalized() 方法返回 false眉厨,則忽略 fastTime 的值锌奴,使用 cdate 代表時(shí)間。
 */
private transient BaseCalendar.Date cdate;

Date類提供的兩個(gè)構(gòu)造函數(shù)憾股,看源碼清晰明了:

// 無參構(gòu)造方法鹿蜀,創(chuàng)建當(dāng)前時(shí)間的Date類
public Date() {
    this(System.currentTimeMillis());
}
// 傳入一個(gè)Unix時(shí)間戳,創(chuàng)建特定時(shí)間的Date類
public Date(long date) {
    fastTime = date;
}
// 其他通過年月日創(chuàng)建的構(gòu)造方法已被 Calendar.set() 和 DateFormat.parse() 等方法替代服球,不再展示

Date類型存儲(chǔ)日期時(shí)間實(shí)際存儲(chǔ)的是Unix時(shí)間戳耻姥,所以可以表示絕對(duì)時(shí)間,支持絕對(duì)時(shí)間的比較有咨。典型的Date類型數(shù)據(jù)結(jié)構(gòu)如下圖:


Date類型的數(shù)據(jù)結(jié)構(gòu)舉例

一個(gè)小問題:上文我們看到構(gòu)造方法中并沒有賦值 cdate 變量琐簇,那么調(diào)試的時(shí)候顯示的 cdate 是如何被初始化的呢?
答案是:IDE調(diào)試的時(shí)候?yàn)榱孙@示變量值,調(diào)用了 toString 方法婉商,至于為什么會(huì)初始化似忧,參考該類 toString() 方法源碼。

Date類還有很多常用的成員方法丈秩,可以用 long getTime( ) 和 void setTime(long time) 進(jìn)行該Date對(duì)象日期時(shí)間的獲取和設(shè)定(毫秒級(jí)別)盯捌;可以用 boolean after(Date date)、boolean before(Date date)蘑秽、int compareTo(Date date)饺著、boolean equals(Object date)等方法比較兩個(gè)日期時(shí)間的先后順序。具體的比較簡(jiǎn)單肠牲,不展開詳述幼衰。

4. java.sql.Date、java.sql.Time 和 java.sql.Timestamp

java.sql.Date缀雳、java.sql.Time 和 java.sql.Timestamp 都繼承自 java.util.Date 類渡嚣,是專門用于數(shù)據(jù)庫(kù)連接的。由于繼承關(guān)系肥印,從數(shù)據(jù)結(jié)構(gòu)來看和它們的父類區(qū)別不大识椰。最主要的區(qū)別在于 Timestamp 類可以表示至納秒級(jí),其 fastTime 字段從秒之后被截掉深碱,毫秒至納秒精度保存在特有的 nanos 字段中腹鹉。可參考下圖:


java.sql.Date敷硅、java.sql.Time 和 java.sql.Timestamp的時(shí)間表示

但是要注意 Timestamp 類的納秒精度可能是“假的”功咒,構(gòu)造方法源碼如下:

public Timestamp(long time) {
    super((time/1000)*1000);
    nanos = (int)((time%1000) * 1000000);
    if (nanos < 0) {
        nanos = 1000000000 + nanos;
        super.setTime(((time/1000)-1)*1000);
    }
}

可以看出,在將 fastTime 字段強(qiáng)行截掉之后竞膳,進(jìn)行 毫秒值直接乘1,000,000 的操作后賦給了 nanos 字段航瞭,成為了“只能表示到毫秒的納秒級(jí)精確度”。當(dāng)然坦辟,還可以通過 setNanos(int n) 方法給納秒數(shù)賦精確值刊侯。

雖然數(shù)據(jù)結(jié)構(gòu)看來沒什么特別,但是如果涉及到Timestamp類的父子類型轉(zhuǎn)換或時(shí)間的比較锉走,就要小心一些“坑”滨彻。

  1. equals() 方法的不對(duì)稱性
    java.sql.Timestamp 類和其父類 java.util.Date 的 equals() 方法是不符合對(duì)稱性的。舉例如下:


    equals() 方法的不對(duì)稱性

    這是由于java.sql.Timestamp 類的 equals() 方法對(duì)于非本類的實(shí)例直接返回false挪蹭,jdk中給出了解釋:

The Timestamp.equals(Object) method never returns true when passed an object that isn't an instance of java.sql.Timestamp, because the nanos component of a date is unknown. As a result, the Timestamp.equals(Object) method is not symmetric with respect to the java.util.Date.equals(Object) method. Also, the hashCode method uses the underlying java.util.Date implementation and therefore does not include nanos in its computation.
意為:傳遞一個(gè)不是java.sql.Timestamp實(shí)例的對(duì)象時(shí)亭饵,Timestamp.equals(Object)方法永遠(yuǎn)不會(huì)返回true,因?yàn)槿掌诘膎anos組件是未知的梁厉。因此辜羊,Timestamp.equals(Object)方法與java.util.Date.equals(Object)方法不對(duì)稱踏兜。此外,hashCode方法使用底層的java.util.Date實(shí)現(xiàn)八秃,因此在其計(jì)算中不包括nanos碱妆。

equals() 源碼如下:

public boolean equals(java.lang.Object ts) {
    if (ts instanceof Timestamp) {
        return this.equals((Timestamp)ts);
    } else {
        // 非Timestamp類型直接返回false
        return false;
    }
}
// Timestamp類型的equals判斷
public boolean equals(Timestamp ts) {
    if (super.equals(ts)) {
        if  (nanos == ts.nanos) {
            return true;
        } else {
            return false;
        }
    } else {
        return false;
    }
}
  1. 時(shí)間比較類方法的“異常”
    現(xiàn)象舉例如下昔驱,兩個(gè)有毫秒之差的時(shí)間點(diǎn)疹尾,after() 方法返回不符合客觀事實(shí):


    compareTo() 和 after() 方法返回不同

    探究其原因。
    父類 java.util.Date 中 after() 方法的實(shí)現(xiàn)如下:

public boolean after(Date when) {
    return getMillisOf(this) > getMillisOf(when);
}

java.sql.Timestamp 類沒有重寫 after(Date d) 方法骤肛,只寫了after(Timestamp t) 方法纳本,如下:

public boolean after(Timestamp ts) {
    return compareTo(ts) > 0;
}

所以上圖傳參為 java.util.Date 類,程序走的是父類的 after() 方法腋颠,而 java.sql.Timestamp 類也沒有重寫 getMillisOf() 方法繁成,所以也是使用父類的:

static final long getMillisOf(Date date) {
    if (date.cdate == null || date.cdate.isNormalized()) {
        return date.fastTime;
    }
    BaseCalendar.Date d = (BaseCalendar.Date) date.cdate.clone();
    return gcal.getTime(d);
}

上文有提到,java.util.Date 會(huì)對(duì) fastTime 和 cdate 進(jìn)行同步秕豫,由于 Timestamp 類在其繼承父類的 fastTime 和 cdate 變量中不存儲(chǔ)毫秒數(shù)據(jù)朴艰,所以調(diào)用父類的 after() 方法時(shí)观蓄, 只有毫秒差異的時(shí)間調(diào)用 getMillisOf() 方法返回的結(jié)果是相同的混移。所以,java.sql.Timestamp 向父類 java.util.Date轉(zhuǎn)型時(shí)會(huì)丟失毫秒侮穿。
JDK文檔中對(duì)此的說明為:

Due to the differences between the Timestamp class and the java.util.Date class mentioned above, it is recommended that code not view Timestamp values generically as an instance of java.util.Date. The inheritance relationship between Timestamp and java.util.Date really denotes implementation inheritance, and not type inheritance.
意為:建議代碼不要將 Timestamp 值一般視為java.util.Date的實(shí)例歌径。 Timestamp 和 java.util.Date 之間的繼承關(guān)系實(shí)際上表示實(shí)現(xiàn)繼承,而不是類型繼承亲茅。

如果不確定類型的情況下要進(jìn)行時(shí)間的比較回铛,盡量使用 compareTo() 方法,可以保證正確性克锣。

5. java.util.Calendar

Calendar類是一個(gè)日歷抽象類茵肃,提供了一組對(duì)年月日時(shí)分秒星期等日期信息的操作的函數(shù),并針對(duì)不同國(guó)家和地區(qū)的日歷提供了相應(yīng)的子類袭祟,即本地化验残。比如公歷 GregorianCalendar ,佛歷(泰國(guó)使用)BuddhistCalendar巾乳,日本歷 JapaneseImperialCalendar 等(沒有中國(guó)農(nóng)歷太不友好了=_=)您没。從JDK1.1版本開始,在處理日期和時(shí)間時(shí)系統(tǒng)推薦使用Calendar類進(jìn)行實(shí)現(xiàn)胆绊。在設(shè)計(jì)上氨鹏,Calendar類的功能要比Date類強(qiáng)大很多,而且在實(shí)現(xiàn)方式上也比Date類要復(fù)雜一些压状。
首先我們來直觀地看一下Calendar類能表示些什么仆抵,打印一個(gè)新建的Calendar實(shí)例:

// 代碼:
Calendar calendar = Calendar.getInstance();
System.out.println(calendar);

// 打印結(jié)果,字段含義都是字面意思:
java.util.GregorianCalendar[
    time=1564912275912,
    areFieldsSet=true, 
    areAllFieldsSet=true, 
    lenient=true, 
    zone=sun.util.calendar.ZoneInfo[
        id="Asia/Shanghai", 
        offset=28800000, 
        dstSavings=0, 
        useDaylight=false, 
        transitions=19, 
        lastRule=null
    ], 
    firstDayOfWeek=1, 
    minimalDaysInFirstWeek=1, 
    ERA=1, 
    YEAR=2019, 
    MONTH=7, 
    WEEK_OF_YEAR=32, 
    WEEK_OF_MONTH=2, 
    DAY_OF_MONTH=4, 
    DAY_OF_YEAR=216, 
    DAY_OF_WEEK=1, 
    DAY_OF_WEEK_IN_MONTH=1, 
    AM_PM=1, 
    HOUR=5, 
    HOUR_OF_DAY=17, 
    MINUTE=51, 
    SECOND=15, 
    MILLISECOND=912, 
    ZONE_OFFSET=28800000, 
    DST_OFFSET=0
]

Calendar類可以通過靜態(tài)工廠方法或new子類的方式來獲得實(shí)例:

  1. getInstance()方法,有四個(gè)重載方法镣丑,參數(shù)是時(shí)區(qū)和地區(qū)还栓,如果不傳會(huì)取服務(wù)器默認(rèn)的時(shí)區(qū)和地區(qū)。(地區(qū)現(xiàn)在是專門為了區(qū)分泰國(guó)和日本)
    1.1 getInstance()
    1.2 getInstance(TimeZone zone)
    1.3 getInstance(Locale aLocale)
    1.4 getInstance(TimeZone zone,Locale aLocale)
  2. 新建子類對(duì)象
Calendar calendar = new GregorianCalendar();

Calendar類可以實(shí)現(xiàn)帶時(shí)區(qū)的年月日時(shí)分秒星期等對(duì)Unix時(shí)間戳的轉(zhuǎn)換传轰,內(nèi)部通過子類復(fù)雜的 computeTime() 方法進(jìn)行計(jì)算剩盒。可以使用 getTime() 方法返回 java.util.Date 類型的時(shí)間慨蛙,可以使用 getTimeInMillis() 方法返回當(dāng)前Unix時(shí)間戳辽聊,也可以通過 get(int field) 方法獲取其他年月日等單獨(dú)信息,部分可用 field 列表如下:

常量 含義
Calendar.YEAR 年份
Calendar.MONTH 月份
Calendar.DATE 日期
Calendar.DAY_OF_MONTH 日期期贫,和上面的字段意義完全相同
Calendar.HOUR 12小時(shí)制的小時(shí)
Calendar.HOUR_OF_DAY 24小時(shí)制的小時(shí)
Calendar.MINUTE 分鐘
Calendar.SECOND
Calendar.DAY_OF_WEEK 星期幾
Calendar.DAY_OF_YEAR 今年的第幾天

也可以通過多個(gè) set 重載方法設(shè)定各種值跟匆。
同時(shí), add() 方法支持對(duì)單個(gè)值的加減通砍,從而實(shí)現(xiàn)時(shí)間推移的計(jì)算玛臂,傳入負(fù)數(shù)即為減,示例如下:

Calendar類時(shí)間推移計(jì)算

GregorianCalendar 對(duì)象可以直接使用 isLeapYear(int year) 接口判斷是否閏年封孙。
要注意兩個(gè)設(shè)定上的問題:在 Calendar 中 MONTH 這個(gè)域并不是從1到12的迹冤,而是0表示一月,11表示十二月虎忌。 DAY_OF_WEEK 域星期天是1泡徙,星期一是2,依次類推膜蠢。為了避免用錯(cuò)堪藐,Calendar 類已經(jīng)為我們定義好了常量,如一月可以直接 Calendar.JANUARY挑围。

6. java.text.SimpleDateFormat

SimpleDateFormat 是一個(gè)以語言環(huán)境敏感的方式來格式化和分析日期的類礁竞。SimpleDateFormat 允許選擇任何用戶自定義的日期時(shí)間格式來運(yùn)行。如:


SimpleDateFormat日期時(shí)間格式化

還有更多可表示的模式杉辙,對(duì)應(yīng)符號(hào)不在此給出模捂。

值得一提的是,在后端接口開發(fā)時(shí)奏瞬,接口返回的日期時(shí)間格式可能是和框架序列化方式有關(guān)的枫绅。如 springboot 中使用 jackson 作為默認(rèn)的 json 工具,不同版本 jackson 對(duì)于日期時(shí)間的默認(rèn)序列化方式不同硼端。1.5.10.RELEASE 版本的 springboot 默認(rèn) 2.8.10 版本的 jackson并淋,Date類返回的默認(rèn)格式是Unix時(shí)間戳;2.0.5.RELEASE 版本的 springboot 默認(rèn) 2.9.6 版本的 jackson珍昨,Date類返回的默認(rèn)格式類似 "2019-08-04T13:43:21.535+0000" 县耽。如果想規(guī)定返回格式可以在 spring 中配置句喷,或直接使用 SimpleDateFormat 格式化成 String 后再返回。

7. Java7中日期時(shí)間類的線程安全問題

癥狀如下圖兔毙,開多個(gè)線程使用同一個(gè) SimpleDateFormat 實(shí)例唾琼,會(huì)出現(xiàn)解析失敗:


線程安全問題舉例

說明在多線程場(chǎng)景下 SimpleDateFormat 是有線程安全問題的澎剥。究其原因锡溯,SimpleDateFormat 類繼承自 DateFormat 類,DateFormat 實(shí)例中維護(hù)了一個(gè) Calendar 對(duì)象哑姚,parse() 方法會(huì)調(diào)用 Calendar 對(duì)象的方法去根據(jù)給定格式設(shè)置屬性值祭饭,而 Calendar 對(duì)象的 fields、time叙量、zone 等表示字段都是線程不安全的倡蝙。如果 SimpleDateFormat 是單例,Calendar 對(duì)象一定也是多線程共用一個(gè)的绞佩。
解決方法:

  1. 使用局部變量
    這也是我們常用的方法寺鸥,每次請(qǐng)求新建一個(gè) SimpleDateFormat 的實(shí)例。雖然常用品山,但是實(shí)際開銷是較大的胆建;
  2. 給 parse() 方法加 synchronized
    既然是由于調(diào)用 Calendar 設(shè)置時(shí)出的線程安全問題,加鎖當(dāng)然可以解決谆奥。但是系統(tǒng)性能會(huì)下降眼坏,權(quán)衡利弊個(gè)人認(rèn)為還不如1方法拂玻;
  3. 使用 ThreadLocal 為每個(gè)線程維護(hù)一個(gè) SimpleDateFormat 實(shí)例酸些,起碼同一線程內(nèi)可以共享一個(gè)實(shí)例減少了不少開銷,上述代碼可修改如下:
public class Main {
    private static ThreadLocal<DateFormat> sdfThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static void main(String[] args) {
        for (int i = 0; i < 100; ++i) {
            Thread thread = new Thread(() -> {
                try {
                    System.out.println(sdfThreadLocal.get().parse("2019-08-04 22:17:27"));
                } catch (Exception e) {
                    System.out.println("解析失敗");
                }
            });
            thread.start();
        }
    }
}

8. Java8 中的新類型

由于舊版 Java 中的日期時(shí)間 API 存在線程不安全檐蚜、某些設(shè)計(jì)不符合日常直覺魄懂、時(shí)區(qū)處理復(fù)雜等問題,Java8 中提供了一些新的 API闯第。包括Instant市栗、LocalDate、LocalTime咳短、LocalDateTime填帽、ZonedDateTime、Period咙好、Duration篡腌、DateTimeFormatter等。
首先直觀看一下這些類里都有什么:


Java8 中新日期時(shí)間類概覽

8.1. Instant

Instant勾效,中文可譯為“瞬間”嘹悼,表示了時(shí)間線上一個(gè)確切的點(diǎn)叛甫,可以表示納秒級(jí)別的時(shí)刻(雖然 now() 構(gòu)造方法得出的納秒數(shù)和 java.sql.Timestamp 類一樣也是“假的”,是從 System.currentTimeMillis() 得來的)杨伙。Instant是時(shí)區(qū)無關(guān)的其监,如何理解這個(gè)“時(shí)區(qū)無關(guān)”?即始終是對(duì)標(biāo)協(xié)調(diào)世界時(shí)(UTC)即格林尼治零時(shí)區(qū)的限匣,個(gè)人覺得可以理解為“Unix時(shí)間戳的更精確表示形式”抖苦。
Instant 類有四種實(shí)例化方法:


Instant 類的四種實(shí)例化方法

由上上圖可知,Instant 對(duì)象中保存了 seconds(距離初始時(shí)間的秒數(shù))和 nanos(當(dāng)前秒的第幾納秒)米死,可以通過以下get開頭的方法獲取睛约,傳入 field 也可以獲取毫秒、微秒級(jí)的時(shí)間哲身。


Instant的多種get方法

8.2. LocalDate辩涝、LocalTime 和 LocalDateTime

字面含義,LocalDate 表示本地日期勘天,LocalTime 表示本地時(shí)間怔揩,LocalDateTime 表示日期加時(shí)間。Java8中支持日期和時(shí)間的分別表示脯丝。

API都較為簡(jiǎn)單商膊,來講兩個(gè)需要理解的注意點(diǎn):

  • 為什么叫“Local”?
    Local 表示“本地時(shí)間”宠进,即和時(shí)區(qū)沒有關(guān)系晕拆。比如“你的生日是哪天”,并沒有人會(huì)說“格林尼治時(shí)間的幾月幾日”材蹬,而只是像日歷頁(yè)上的一格实幕,“幾月幾日”的概念;再比如“新年的鐘聲幾點(diǎn)敲響”堤器,也不會(huì)全球在同一時(shí)間過新年昆庇,而是當(dāng)?shù)貟扃娚系牧泓c(diǎn),沒有時(shí)區(qū)屬性闸溃。那什么樣的時(shí)間不是“Local”的整吆?就是時(shí)間線上的一個(gè)固定時(shí)間點(diǎn),事情就在那一刻發(fā)生了辉川,雖然地球上每個(gè)角落的太陽(yáng)位置不同表蝙,墻上掛鐘顯示的數(shù)字也不同,但都是時(shí)間這個(gè)坐標(biāo)軸上的同一點(diǎn)乓旗。比如北京時(shí)間2003年10月15日9時(shí)00分03秒497毫秒府蛇,神舟五號(hào)成功發(fā)射,就不是一個(gè)“Local”的時(shí)間寸齐。
  • 什么叫“分別表示日期和時(shí)間”欲诺?
    LocalDate 類只表示日期抄谐,而不是這個(gè)日期所在的時(shí)間(如java.util.Date中的 2019-08-05 表示的實(shí)際是這一天的00:00這個(gè)瞬間)。

以 LocalDate 為例說明API扰法,剩余兩個(gè)類大同小異蛹含。
LocalDate 可以通過三種方法創(chuàng)建實(shí)例:


LocalDate 創(chuàng)建實(shí)例

可以通過各種get方法得到日期相關(guān)字段,如字面意思:


LocalDate 的多種 get 方法

可以增減字段值:
LocalDate 支持增減字段值

以及一些原來要很復(fù)雜代碼的操作塞颁,現(xiàn)在可以簡(jiǎn)化:
取各種關(guān)聯(lián)日期的操作

還可以獲取指定時(shí)區(qū)的當(dāng)前日期時(shí)間浦箱,或添加時(shí)區(qū)屬性,轉(zhuǎn)化成下面要介紹的 ZonedDateTime祠锣,注意這里沒有進(jìn)行時(shí)間的時(shí)區(qū)變換酷窥,而是僅僅添加了時(shí)區(qū)屬性,更印證了上文說的“Local”的含義伴网。拿 LocalDateTime 舉例:


LocalDateTime 轉(zhuǎn)化成 ZonedDateTime

8.3. ZonedDateTime

ZonedDateTime 可以被理解為 LocalDateTime 的外層封裝蓬推,它的內(nèi)部存儲(chǔ)了一個(gè) LocalDateTime 的實(shí)例,專門用于普通的日期時(shí)間處理澡腾,此外它還定義了 ZoneId 實(shí)例和 ZoneOffset 實(shí)例來描述時(shí)區(qū)的概念滩字。調(diào)試信息顯示如下:


ZonedDateTime 數(shù)據(jù)結(jié)構(gòu)

產(chǎn)生 ZonedDateTime 實(shí)例的幾種方法如下烘豌,如字面意思較好理解:

public static ZonedDateTime now();
public static ZonedDateTime now(ZoneId zone);
public static ZonedDateTime of(LocalDate date, LocalTime time, ZoneId zone)
public static ZonedDateTime of(LocalDateTime localDateTime, ZoneId zone)
public static ZonedDateTime ofInstant(Instant instant, ZoneId zone)
public static ZonedDateTime of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond, ZoneId zone)

其他方法操作和 LocalDateTime 類似,不多贅述拳亿。

8.4. DateTimeFormatter

DateTimeFormatter 類作為 Java8 中用于表示日期時(shí)間的類栗竖,與原有DateFormat 類最大的不同就在于它是線程安全的腰池,其他使用上的操作基本類似富拗。舉例如下:


DateTimeFormatter 進(jìn)行時(shí)間格式轉(zhuǎn)換

8.5. Period 和 Duration

Java8 添加了處理時(shí)間差的功能萍歉,用 Period 處理兩個(gè)日期之間的差值,用 Duration 處理兩個(gè)時(shí)間之間的差值坟乾。between() 方法等大大簡(jiǎn)化了計(jì)算兩個(gè)日期時(shí)間之間差值的操作迹辐,舉例如下:


方便的差值計(jì)算

8.6. Java8 日期時(shí)間小結(jié)

簡(jiǎn)單介紹了 Java8 的一些處理日期時(shí)間的新API,可以說對(duì)比之前的版本是有很大的改進(jìn)的糊渊。

  • 首先右核,原有的 Date、Calendar 等類過于泛泛渺绒,既可以表示日期又可以表示時(shí)間,還能進(jìn)行時(shí)區(qū)轉(zhuǎn)換菱鸥,結(jié)果就是各方面都差點(diǎn)意思宗兼。Java8 區(qū)分了日期和時(shí)間的分別表示,使得不同的業(yè)務(wù)需求有專門對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)進(jìn)行設(shè)計(jì)氮采;
  • 其次殷绍,由于 java.sql.Date、java.sql.Time鹊漠、java.sql.Timestamp 都繼承自 java.util.Date主到,所以本質(zhì)上他們都是時(shí)區(qū)相關(guān)的茶行。Java8 區(qū)分了本地時(shí)間和帶時(shí)區(qū)的時(shí)間的表示,ZonedDateTime 的時(shí)區(qū)轉(zhuǎn)換也非常方便登钥;
  • 再次畔师,提供了時(shí)間差的直接計(jì)算方法,不用先換算成Unix時(shí)間戳再做減法再做除法等麻煩的步驟牧牢;
  • 最重要的是看锉,他們都是不可變類!Kⅰ伯铣!線程安全!B秩摇腔寡!

三. 對(duì)時(shí)間的存儲(chǔ)

講完表示再來看日期時(shí)間的存儲(chǔ)方法。以MySQL數(shù)據(jù)庫(kù)為例掌唾,介紹數(shù)據(jù)存儲(chǔ)的方式蹬蚁,以及與Java程序的交互。

1. MySQL的日期時(shí)間類型介紹

將MySQL提供的幾種日期時(shí)間數(shù)據(jù)結(jié)構(gòu)列表如下:

類型名稱 占用空間 展示格式 表示范圍
YEAR 1 bytes YYYY 1901——2155
DATE 4 bytes YYYY-MM-DD 1000-01-01——9999-12-31
TIME 3 bytes HH:MM:SS -838:59:59——838:59:59
DATETIME 8 bytes YYYY-MM-DD HH:MM:SS 1000-01-01 00:00:00——9999-12-31 23:59:59
TIMESTAMP 4 bytes YYYY-MM-DD HH:MM:SS 1970-01-01 00:00:01——2038-01-19 03:14:07 (UTC)

1.1. YEAR 類型用于表示年份郑兴,默認(rèn)是4位犀斋,可以直接插入4位數(shù)字或字符串。由于YEAR類型占用空間很小情连,如果只想表示年份叽粹,并在其表示范圍內(nèi),不失是一種很好的選擇却舀。
1.2. DATE 類型用于表示日期虫几,以 YYYY-MM-DD 格式顯示。指“日歷頁(yè)上的日期”挽拔,沒有時(shí)區(qū)概念辆脸,類似于 Java8 中的 LocalDate。
1.3. TIME 類型用于表示時(shí)間螃诅,以 HH:MM:SS 格式顯示啡氢,精度為秒。指“掛鐘顯示的時(shí)間”术裸,沒有時(shí)區(qū)概念倘是,類似于 Java8 中的 LocalTime。
1.4. DATETIME 類型是 DATE 和 TIME 的結(jié)合袭艺,占8位搀崭,它把日期和時(shí)間封裝到格式為 “YYYYMMDDHHMMSS” 的整數(shù)中,可以記錄較 TIMESTAMP 更長(zhǎng)的時(shí)間猾编。沒有時(shí)區(qū)概念瘤睹,類似于 Java8 中的 LocalDateTime升敲。
1.5. TIMESTAMP 類型也是表示日期加時(shí)間,但是表示的時(shí)間較短轰传,和32位 Unix 時(shí)間戳相同驴党。TIMESTAMP 類型表示的時(shí)間與時(shí)區(qū)有關(guān),MySQL服務(wù)器绸吸、操作系統(tǒng)鼻弧、客戶端連接等都有時(shí)區(qū)設(shè)置,插入日期時(shí)會(huì)先轉(zhuǎn)換為本地時(shí)區(qū)后再存放锦茁,查詢?nèi)掌跁r(shí)會(huì)將日期轉(zhuǎn)換為本地時(shí)區(qū)后再顯示攘轩。如果插入時(shí)沒有指定 TIMESTAMP 列的值,則系統(tǒng)默認(rèn)設(shè)置為 '0000-00-00 00:00:00'码俩,也可以手動(dòng)設(shè)置為添加當(dāng)前時(shí)間度帮。

2. MySQL的日期時(shí)間類型比較與選擇

YEAR、DATE稿存、TIME 三種類型都功能不同笨篷,YEAR 存年份,DATE 存日期瓣履,TIME 存時(shí)間率翅,按業(yè)務(wù)需求進(jìn)行挑選即可。
主要比較 DATETIME 和 TIMESTAMP 類型:

  • 時(shí)區(qū)屬性不同:DATETIME 無時(shí)區(qū)屬性袖迎,TIMESTAMP支持時(shí)區(qū)變換冕臭;
  • 表示范圍不同:DATETIME 表示范圍更大,為1000-01-01 00:00:00——9999-12-31 23:59:59燕锥,TIMESTAMP 只能表示32位Unix時(shí)間戳的范圍辜贵;
  • 空間占用不同:TIMESTAMP 只要 4 bytes,效率更高归形。
  • 綜上:若有明確的需要時(shí)區(qū)轉(zhuǎn)換或不需要時(shí)區(qū)轉(zhuǎn)換的問題托慨,則根據(jù)業(yè)務(wù)需求選擇對(duì)應(yīng)的,否則會(huì)出現(xiàn)邏輯錯(cuò)誤暇榴;else if 32位Unix時(shí)間戳的范圍夠用則推薦選擇 TIMESTAMP 類型厚棵,因?yàn)榭臻g效率更高。

還有一種可選項(xiàng):每次涉及日期時(shí)間時(shí)全部用Unix時(shí)間戳表示跺撼,Java中用long窟感,MySQL中用INT類型,詳見如何正確地處理時(shí)間-廖雪峰歉井。好處是體現(xiàn)了“存儲(chǔ)與顯示分離”的原則,且易于比較哈误。但是肉眼無法快速識(shí)別時(shí)間戳確實(shí)帶來了很大的麻煩哩至,況且Java和MySQL開發(fā)出那么多類型就是為了方便使用(不然上文全都白講了)躏嚎,也可以解決大多數(shù)問題,所以個(gè)人并不推薦這種做法(也可能是開發(fā)經(jīng)驗(yàn)不夠菩貌,沒有理解到廖老師這個(gè)點(diǎn)的精髓)卢佣。

3. 與Java的交互

筆者自己總結(jié)了Java 和 MySQL 日期時(shí)間數(shù)據(jù)類型的一種映射關(guān)系:

Java類型 MySQL映射
java.sql.Date DATE
java.sql.Time TIME
java.sql.Timestamp TIMESTAMP
java.time.LocalDate DATE
java.time.LocalTime TIME
java.time.LocalDateTime DATETIME
java.time.ZonedDateTime TIMESTAMP
java.time.Instant TIMESTAMP

在MySQL數(shù)據(jù)庫(kù)創(chuàng)建表包含各種類型的字段用于測(cè)試:

  • DATETIME、TIMESTAMP類型默認(rèn)精確到秒箭阶,如需毫秒或更高精度虚茶,需手動(dòng)指定字段長(zhǎng)度,如下:
CREATE TABLE `test_time` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `time1` date DEFAULT NULL,
  `time2` time DEFAULT NULL,
  `time3` year(4) DEFAULT NULL,
  -- 長(zhǎng)度為3精確到毫秒  
  `time4` datetime(3) DEFAULT NULL,
  -- 長(zhǎng)度為6精確到微秒  
  `time5` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=latin1;
image
手動(dòng)指定時(shí)間精度
  • 使用spring mybatis generator 插件創(chuàng)建 model仇参,自動(dòng)創(chuàng)建的數(shù)據(jù)格式都是 java.util.Date嘹叫。將數(shù)據(jù)讀出時(shí),無關(guān)聯(lián)時(shí)區(qū)的數(shù)據(jù)格式都會(huì)加上當(dāng)前系統(tǒng)默認(rèn)時(shí)區(qū)诈乒,缺少的數(shù)據(jù)會(huì)用缺省值填充罩扇。這是一種非常浪費(fèi)且繁瑣且容易出錯(cuò)的方式。如下圖:


    java.util.Date 類缺少的數(shù)據(jù)用缺省值填充
  • MySQL 版本在5.1.37以上的怕磨,驅(qū)動(dòng)在4.2以上的喂饥,可以使用Java8中的新類型,幾乎可以說完美匹配肠鲫。


    MySQL數(shù)據(jù)結(jié)構(gòu)和Java8新日期時(shí)間數(shù)據(jù)結(jié)構(gòu)完美匹配

四. 時(shí)區(qū)轉(zhuǎn)換的操作

需要時(shí)區(qū)轉(zhuǎn)換的時(shí)間一定不是“掛鐘上的時(shí)間”员帮,而是時(shí)間軸上確定的一個(gè)“絕對(duì)時(shí)間”。所以時(shí)區(qū)轉(zhuǎn)換分為兩個(gè)方面:由被展示的字符串添加某時(shí)區(qū)信息后轉(zhuǎn)為Java對(duì)象导饲,或由固定時(shí)區(qū)的Java對(duì)象轉(zhuǎn)換時(shí)區(qū)后展示捞高。下面各種方式實(shí)現(xiàn)這兩個(gè)轉(zhuǎn)換:

1. 無腦加減操作

根據(jù)目標(biāo)時(shí)區(qū)和原時(shí)區(qū)的時(shí)差直接加減,“硬核轉(zhuǎn)換”帜消,極不推薦棠枉。

2. Date + SimpleDateFormat

如下圖(注意,轉(zhuǎn)為Date對(duì)象的時(shí)候自動(dòng)變?yōu)榱讼到y(tǒng)時(shí)區(qū)):
Date + SimpleDateFormat 時(shí)區(qū)轉(zhuǎn)換

或者更簡(jiǎn)單的利用“z”這個(gè)域:
Date + SimpleDateFormat 時(shí)區(qū)轉(zhuǎn)換

3. ZonedDateTime + DateTimeFormatter

ZonedDateTime + DateTimeFormatter 時(shí)區(qū)轉(zhuǎn)換

4. 用時(shí)間戳處理

用各種方法得到該時(shí)間點(diǎn)的時(shí)間戳泡挺,然后轉(zhuǎn)化為Java對(duì)象辈讶,添加時(shí)區(qū)信息,輸出娄猫。

5. 與MySQL的交互轉(zhuǎn)換

按照上述MySQL與Java交互中所述贱除,將MySQL存儲(chǔ)的時(shí)間轉(zhuǎn)換為Java對(duì)象,然后按照2媳溺,3方法轉(zhuǎn)換即可月幌。

五. 總結(jié)

本篇文章全面貼近實(shí)際開發(fā),首先從日常代碼遇到的問題出發(fā)悬蔽,介紹了一些常識(shí)和會(huì)遇到的問題扯躺。
隨后介紹了Java中日期時(shí)間的獲取、數(shù)據(jù)格式表示及格式轉(zhuǎn)換方法。其中深入源碼詳細(xì)介紹了Java7中的日期時(shí)間數(shù)據(jù)結(jié)構(gòu)录语,拆解了可能會(huì)遇到的線程安全問題及解決辦法倍啥,并在使用層面介紹了Java8中日期時(shí)間新API及其優(yōu)點(diǎn),源碼中的復(fù)雜計(jì)算方法有待今后研究澎埠。
接著在存儲(chǔ)方面介紹了MySQL的日期時(shí)間類型及如何選擇的建議虽缕,并給出了與Java各種日期時(shí)間類型的轉(zhuǎn)換示例。
最后根據(jù)時(shí)區(qū)轉(zhuǎn)換的需求給出各種數(shù)據(jù)結(jié)構(gòu)的時(shí)區(qū)轉(zhuǎn)換操作方法蒲稳。

本篇為上篇-應(yīng)用篇氮趋,下篇中會(huì)詳細(xì)解釋一些底層日期時(shí)間的處理,如為什么不同操作系統(tǒng)獲取當(dāng)前時(shí)間的速度有數(shù)量級(jí)差異江耀;高并發(fā)場(chǎng)景用 System.currenTimeMillis() 會(huì)出現(xiàn)什么問題及怎么解決剩胁;Linux中有哪些時(shí)間相關(guān)系統(tǒng)調(diào)用及他們的區(qū)別;系統(tǒng)對(duì)于類似 Thread.sleep(long millis) 的“時(shí)間段”長(zhǎng)度是如何控制的决记;以上這些底層問題如何影響我們的程序設(shè)計(jì)等摧冀。

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市系宫,隨后出現(xiàn)的幾起案子索昂,更是在濱河造成了極大的恐慌,老刑警劉巖扩借,帶你破解...
    沈念sama閱讀 222,378評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件椒惨,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡潮罪,警方通過查閱死者的電腦和手機(jī)康谆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嫉到,“玉大人沃暗,你說我怎么就攤上這事『味瘢” “怎么了孽锥?”我有些...
    開封第一講書人閱讀 168,983評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)细层。 經(jīng)常有香客問我惜辑,道長(zhǎng),這世上最難降的妖魔是什么疫赎? 我笑而不...
    開封第一講書人閱讀 59,938評(píng)論 1 299
  • 正文 為了忘掉前任盛撑,我火速辦了婚禮,結(jié)果婚禮上捧搞,老公的妹妹穿的比我還像新娘抵卫。我一直安慰自己狮荔,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,955評(píng)論 6 398
  • 文/花漫 我一把揭開白布陌僵。 她就那樣靜靜地躺著轴合,像睡著了一般创坞。 火紅的嫁衣襯著肌膚如雪碗短。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,549評(píng)論 1 312
  • 那天题涨,我揣著相機(jī)與錄音偎谁,去河邊找鬼。 笑死纲堵,一個(gè)胖子當(dāng)著我的面吹牛巡雨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播席函,決...
    沈念sama閱讀 41,063評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼铐望,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了茂附?” 一聲冷哼從身側(cè)響起正蛙,我...
    開封第一講書人閱讀 39,991評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎营曼,沒想到半個(gè)月后乒验,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,522評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蒂阱,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,604評(píng)論 3 342
  • 正文 我和宋清朗相戀三年锻全,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片录煤。...
    茶點(diǎn)故事閱讀 40,742評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鳄厌,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出妈踊,到底是詐尸還是另有隱情了嚎,我是刑警寧澤,帶...
    沈念sama閱讀 36,413評(píng)論 5 351
  • 正文 年R本政府宣布响委,位于F島的核電站新思,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏赘风。R本人自食惡果不足惜夹囚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,094評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望邀窃。 院中可真熱鬧荸哟,春花似錦假哎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至劣砍,卻和暖如春惧蛹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背刑枝。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工香嗓, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人装畅。 一個(gè)月前我還...
    沈念sama閱讀 49,159評(píng)論 3 378
  • 正文 我出身青樓靠娱,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親掠兄。 傳聞我的和親對(duì)象是個(gè)殘疾皇子像云,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,747評(píng)論 2 361