并發(fā)編程之ThreadLocal祟身、Volatile、Synchronized物独、Atomic關(guān)鍵字

前言

工作中用到ThreadLocal袜硫、Volatile、Synchronized挡篓、Atomic婉陷、LongAdder這五個關(guān)鍵字,是時候?qū)λ鼈兛偨Y(jié)一波了官研。

共享對象

使用Java編寫線程安全的程序關(guān)鍵在于正確的使用共享對象秽澳,以及安全的對其進行訪問管理。Java的內(nèi)置鎖可以保障線程安全,對于其他的應(yīng)用來說并發(fā)的安全性是使用內(nèi)置鎖保障了線程變量使用的邊界。談到線程的邊界問題,隨之而來的是Java內(nèi)存模型另外的一個重要的含義,可見性。Java對可見性提供的原生支持是volatile關(guān)鍵字晴叨。

Atomic

作用

對于原子操作類,Java的concurrent并發(fā)包中主要為我們提供了這么幾個常用的:AtomicInteger、AtomicLong捞挥、AtomicBoolean、AtomicReference<T>忧吟。
對于原子操作類砌函,最大的特點是在多線程并發(fā)操作同一個資源的情況下,使用Lock-Free算法來替代鎖溜族,這樣開銷小讹俊、速度快,對于原子操作類是采用原子操作指令實現(xiàn)的煌抒,從而可以保證操作的原子性仍劈。
通常情況下,在Java里面寡壮,++i或者--i不是線程安全的贩疙,這里面有三個獨立的操作:獲得變量當(dāng)前值,為該值+1/-1况既,然后寫回新的值这溅。在沒有額外資源可以利用的情況下,只能使用加鎖才能保證讀-改-寫這三個操作是“原子性”的棒仍。
Java 5新增了AtomicInteger類悲靴,該類包含方法getAndIncrement()以及getAndDecrement(),這兩個方法實現(xiàn)了原子加以及原子減操作莫其,但是比較不同的是這兩個操作沒有使用任何加鎖機制癞尚,屬于無鎖操作。
它會在這步操作都完成情況下才允許其它線程再對它進行操作榜配,而這個實現(xiàn)則是通過Lock-Free+原子操作指令來確定的
AtomicInteger類中:

public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }
public final int get() {  
    return value;  
}  
private volatile int value;
public final boolean compareAndSet(int expect, int update) {  
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
}  

可以看到是一個cas原子操作否纬。
unsafe是java用來在CPU級別的操作CAS指令的類,對于程序員來說蛋褥,此類是不可用临燃。
由于是cpu級別的指令,其開銷比需要操作系統(tǒng)參與的鎖的開銷小烙心。
對于多個線程進入時膜廊,會先比較現(xiàn)在的value 是否與expect相等,如果不相等淫茵,則進入下一個循環(huán)爪瓜。如果相等,則會更新成update值匙瘪。
之后再進入的線程則會死循環(huán)铆铆。這樣就保證了操作的原子性蝶缀。
這樣一個方法中 即包含了原子性,又包含了可見性

而關(guān)于Lock-Free算法薄货,則是一種新的策略替代鎖來保證資源在并發(fā)時的完整性的翁都,Lock-Free的實現(xiàn)有三步:

  1. 循環(huán)(for(;;)、while)
  2. CAS(CompareAndSet)
  3. 回退(return谅猾、break)

用法

比如在多個線程操作一個count變量的情況下柄慰,則可以把count定義為AtomicInteger,如下:

public class Counter {
    private AtomicInteger count = new AtomicInteger();
    public int getCount() {
        return count.get();
    }
    public void increment() {
        count.incrementAndGet();
    }

在每個線程中通過increment()來對count進行計數(shù)增加的操作税娜,或者其它一些操作坐搔。這樣每個線程訪問到的將是安全、完整的count敬矩。

內(nèi)部實現(xiàn)

采用Lock-Free算法替代鎖+原子操作指令實現(xiàn)并發(fā)情況下資源的安全概行、完整、一致性

ABA問題(AtomicStampedReference的使用)

public class ABA {
    
    // 普通的原子類谤绳,存在ABA問題
    AtomicInteger a1 = new AtomicInteger(10);
    // 帶有時間戳的原子類占锯,不存在ABA問題,第二個參數(shù)就是默認(rèn)時間戳缩筛,這里指定為0
    AtomicStampedReference<Integer> a2 = new AtomicStampedReference<Integer>(10, 0);
    
    public static void main(String[] args) {
        ABA a = new ABA();
        a.test();
    }
    
    public void test() {
        new Thread1().start();
        new Thread2().start();
        new Thread3().start();
        new Thread4().start();
    }
    
    class Thread1 extends Thread {
        @Override
        public void run() {
            a1.compareAndSet(10, 11);
            a1.compareAndSet(11, 10);
        }
    }
    class Thread2 extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(200);  // 睡0.2秒消略,給線程1時間做ABA操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("AtomicInteger原子操作:" + a1.compareAndSet(10, 11));
        }
    }
    class Thread3 extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(500);  // 睡0.5秒,保證線程4先執(zhí)行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int stamp = a2.getStamp();
            a2.compareAndSet(10, 11, stamp, stamp + 1);
            stamp = a2.getStamp();
            a2.compareAndSet(11, 10, stamp, stamp + 1);
        }
    }
    class Thread4 extends Thread {
        @Override
        public void run() {
            int stamp = a2.getStamp();
            try {
                Thread.sleep(1000);  // 睡一秒瞎抛,給線程3時間做ABA操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("AtomicStampedReference原子操作:" + a2.compareAndSet(10, 11, stamp, stamp + 1));
        }
    }
}

LongAdder

繼承自Striped64艺演,這個類包裝了一些很重要的內(nèi)部類和操作
LongAdder類與AtomicLong類的區(qū)別在于高并發(fā)時前者將對單一變量的CAS操作分散為對數(shù)組cells中多個元素的CAS操作,取值時進行求和桐臊;
而在并發(fā)較低時僅對base變量進行CAS操作胎撤,與AtomicLong類原理相同

Volatile

作用

Volatile可以看做是一個輕量級的synchronized,它可以在多線程并發(fā)的情況下保證變量的“可見性”断凶,
什么是可見性伤提?
就是在一個線程的工作內(nèi)存中修改了該變量的值,該變量的值立即能回顯到主內(nèi)存中认烁,從而保證所有的線程看到這個變量的值是一致的肿男,其二 volatile 禁止了指令重排,所以在處理同步問題上它大顯作用却嗡,而且它的開銷比synchronized小舶沛、使用成本更低。
雖然 volatile 變量具有可見性和禁止指令重排序窗价,但是并不能說 volatile 變量能確保并發(fā)安全如庭。

舉個栗子:在寫單例模式中,除了用靜態(tài)內(nèi)部類外撼港,還有一種寫法也非常受歡迎坪它,就是Volatile+DCL:

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

這樣單例不管在哪個線程中創(chuàng)建的骤竹,所有線程都是共享這個單例的。
雖說這個Volatile關(guān)鍵字可以解決多線程環(huán)境下的同步問題往毡,不過這也是相對的瘤载,因為它不具有操作的原子性,也就是它不適合在對該變量的寫操作依賴于變量本身自己卖擅。舉個最簡單的栗子:在進行計數(shù)操作時count++,實際是count=count+1;墨技,count最終的值依賴于它本身的值惩阶。所以使用volatile修飾的變量在進行這么一系列的操作的時候,就有并發(fā)的問題 .

volatile只能確保操作的是同一塊內(nèi)存扣汪,并不能保證操作的原子性断楷。所以volatile一般用于聲明簡單類型變量,使得這些變量具有原子性崭别,即一些簡單的賦值與返回操作將被確保不中斷冬筒。但是當(dāng)該變量的值由自身的上一個決定時,volatile的作用就將失效茅主,這是由volatile關(guān)鍵字的性質(zhì)所決定的舞痰。
所以在volatile時一定要謹(jǐn)慎,千萬不要以為用volatile修飾后該變量的所有操作都是原子操作诀姚,不再需要synchronized關(guān)鍵字了响牛。

用法

因為volatile不具有操作的原子性,所以如果用volatile修飾的變量在進行依賴于它自身的操作時赫段,就有并發(fā)問題呀打,如:count,像下面這樣寫在并發(fā)環(huán)境中是達不到任何效果的:

public class Counter {
    private volatile int count;

    public int getCount(){
        return count;
    }
    public void increment(){
        count++;
    }
}

而要想count能在并發(fā)環(huán)境中保持?jǐn)?shù)據(jù)的一致性糯笙,則可以在increment()中加synchronized同步鎖修飾贬丛,改進后的為:

public class Counter {
    private volatile/無 int count;

    public int getCount(){
        return count;
    }
    public synchronized void increment(){
        count++;
    }
}

內(nèi)部實現(xiàn)

匯編指令實現(xiàn)

Synchronized

作用

synchronized關(guān)鍵字是Java利用鎖的機制自動實現(xiàn)的,一般有同步方法和同步代碼塊兩種使用方式给涕。Java中所有的對象都自動含有單一的鎖(也稱為監(jiān)視器)豺憔,當(dāng)在對象上調(diào)用其任意的synchronized方法時,此對象被加鎖(一個任務(wù)可以多次獲得對象的鎖稠炬,計數(shù)會遞增)焕阿,同時在線程從該方法返回之前,該對象內(nèi)其他所有要調(diào)用類中被標(biāo)記為synchronized的方法的線程都會被阻塞首启。當(dāng)然針對每個類也有一個鎖(作為類的Class對象的一部分)暮屡,所以你懂的.
正因為它基于這種阻塞的策略毅桃,所以它的性能不太好褒纲,但是由于操作上的優(yōu)勢准夷,只需要簡單的聲明一下即可,而且被它聲明的代碼塊也是具有操作的原子性莺掠。
最后需要注意的是synchronized是同步機制中最安全的一種方式衫嵌,其他的任何方式都是有風(fēng)險的,當(dāng)然付出的代價也是最大的彻秆。

用法

    public synchronized void increment(){
            count++;
    }
    public void increment(){
        synchronized (Counte.class){
            count++;
        }
    }

內(nèi)部實現(xiàn)

synchronized 關(guān)鍵字原理

ThreadLocal

作用

而ThreadLocal的設(shè)計楔绞,并不是解決資源共享的問題,而是用來提供線程內(nèi)的局部變量唇兑,這樣每個線程都自己管理自己的局部變量酒朵,別的線程操作的數(shù)據(jù)不會對我產(chǎn)生影響,互不影響扎附,所以不存在解決資源共享這么一說蔫耽,如果是解決資源共享,那么其它線程操作的結(jié)果必然我需要獲取到留夜,而ThreadLocal則是自己管理自己的匙铡,相當(dāng)于封裝在Thread內(nèi)部了,供線程自己管理碍粥,這樣做其實就是以空間換時間的方式(與synchronized相反)鳖眼,以耗費內(nèi)存為代價,單大大減少了線程同步(如synchronized)所帶來性能消耗以及減少了線程并發(fā)控制的復(fù)雜度嚼摩。

用法

ThreadLocal實例通常來說都是private static類型的具帮,用于關(guān)聯(lián)線程和線程的上下文
一般使用ThreadLocal,官方建議我們定義為private static 低斋,至于為什么要定義成靜態(tài)的蜂厅,這和內(nèi)存泄露有關(guān),后面再討論膊畴。
它有三個暴露的方法掘猿,set、get唇跨、remove稠通。

public class TestThreadLocal {
    private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new MyThread(i)).start();
        }
    }

    static class MyThread implements Runnable {
        private int index;

        public MyThread(int index) {
            this.index = index;
        }

        public void run() {
            System.out.println("線程" + index + "的初始value:" + value.get());
            for (int i = 0; i < 10; i++) {
                value.set(value.get() + i);
            }
            System.out.println("線程" + index + "的累加value:" + value.get());
        }
    }
}

運行結(jié)果如下,這些ThreadLocal變量屬于線程內(nèi)部管理的买猖,互不影響:

線程0的初始value:0
線程3的初始value:0
線程2的初始value:0
線程2的累加value:45
線程1的初始value:0
線程3的累加value:45
線程0的累加value:45
線程1的累加value:45
線程4的初始value:0
線程4的累加value:45

對于get方法改橘,在ThreadLocal沒有set值得情況下,默認(rèn)返回null玉控,所有如果要有一個初始值我們可以重寫initialValue()方法飞主,在沒有set值得情況下調(diào)用get則返回初始值。

內(nèi)部實現(xiàn)

ThreadLocal內(nèi)部有一個靜態(tài)類ThreadLocalMap,使用到ThreadLocal的線程會與ThreadLocalMap綁定碌识,維護著這個Map對象碾篡,而這個ThreadLocalMap的作用是映射當(dāng)前ThreadLocal對應(yīng)的值,它key為當(dāng)前ThreadLocal的弱引用:WeakReference

內(nèi)存泄露問題

對于ThreadLocal筏餐,一直涉及到內(nèi)存的泄露問題开泽,即當(dāng)該線程不需要再操作某個ThreadLocal內(nèi)的值時,應(yīng)該手動的remove掉魁瞪,為什么呢穆律?我們來看看ThreadLocal與Thread的聯(lián)系圖:
此圖來自網(wǎng)絡(luò):


image

其中虛線表示弱引用,從該圖可以看出导俘,一個Thread維持著一個ThreadLocalMap對象众旗,而該Map對象的key又由提供該value的ThreadLocal對象弱引用提供,所以這就有這種情況:
如果ThreadLocal不設(shè)為static的趟畏,由于Thread的生命周期不可預(yù)知,這就導(dǎo)致了當(dāng)系統(tǒng)gc時將會回收它滩租,而ThreadLocal對象被回收了赋秀,此時它對應(yīng)key必定為null,這就導(dǎo)致了該key對應(yīng)得value拿不出來了律想,而value之前被Thread所引用猎莲,所以就存在key為null、value存在強引用導(dǎo)致這個Entry回收不了技即,從而導(dǎo)致內(nèi)存泄露著洼。

所以避免內(nèi)存泄露的方法,是對于ThreadLocal要設(shè)為static靜態(tài)的而叼,
這樣的話ThreadLocal的生命周期就更長身笤,由于一直存在ThreadLocal的強引用,所以ThreadLocal也就不會被回收葵陵,也就能保證任何時候都能根據(jù)ThreadLocal的弱引用訪問到Entry的value值液荸,然后remove它,防止內(nèi)存泄露脱篙。除了這個娇钱,還必須在線程不使用它的值是手動remove掉該ThreadLocal的值,這樣Entry就能夠在系統(tǒng)gc的時候正嘲砝В回收文搂,而關(guān)于ThreadLocalMap的回收,會在當(dāng)前Thread銷毀之后進行回收秤朗。

但需要注意的是煤蹭,雖然ThreadLocal和Synchonized都用于解決多線程并發(fā)訪問,ThreadLocal與synchronized還是有本質(zhì)的區(qū)別。synchronized是利用鎖的機制疯兼,使變量或代碼塊在某一時該只能被一個線程訪問然遏。而ThreadLocal為每一個線程都提供了變量的副本,使得每個線程在某一時間訪問到的并不是同一個對象吧彪,這樣就隔離了多個線程對數(shù)據(jù)的數(shù)據(jù)共享待侵。而Synchronized卻正好相反,它用于在多個線程間通信時能夠獲得數(shù)據(jù)共享姨裸。即Synchronized用于線程間的數(shù)據(jù)共享秧倾,而ThreadLocal則用于線程間的數(shù)據(jù)隔離。所以ThreadLocal并不能代替synchronized傀缩,Synchronized的功能范圍更廣(同步機制)那先。

  • 補充
InheritableThreadLocal

ThreadLocal類固然很好,但是子線程并不能取到父線程的ThreadLocal類的變量赡艰,InheritableThreadLocal類就是解決這個問題的售淡。

/**
 *TODO 驗證線程變量間的隔離性
 */
public class Test3 {

    public static void main(String[] args) {
        try {
            for (int i = 0; i < 10; i++) {
                System.out.println("       在Main線程中取值=" + Tools.tl.get());
                Thread.sleep(100);
            }
            Thread.sleep(5000);
            ThreadA a = new ThreadA();
            a.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    /*static public class Tools {
        public static ThreadLocalExt tl = new ThreadLocalExt();
    }
    static public class ThreadLocalExt extends ThreadLocal {
        @Override
        protected Object initialValue() {
            return new Date().getTime();
        }
    }*/
static public class Tools {
        public static InheritableThreadLocalExt tl = new InheritableThreadLocalExt();
    }
    static public class InheritableThreadLocalExt extends InheritableThreadLocal {
        @Override
        protected Object initialValue() {
            return new Date().getTime();
        }

        @Override
        protected Object childValue(Object parentValue) {
            return parentValue + " 我在子線程加的~!";
        }
    }

    static public class ThreadA extends Thread {

        @Override
        public void run() {
            try {
                for (int i = 0; i < 10; i++) {
                    System.out.println("在ThreadA線程中取值=" + Tools.tl.get());
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

    }
}

在使用InheritableThreadLocal類需要注意的一點是:如果子線程在取得值的同時,主線程將InheritableThreadLocal中的值進行更改慷垮,那么子線程取到的還是舊值揖闸。

總結(jié)

關(guān)于Volatile關(guān)鍵字具有可見性,但不具有操作的原子性料身,而synchronized比volatile對資源的消耗稍微大點汤纸,但可以保證變量操作的原子性,保證變量的一致性芹血,最佳實踐則是二者結(jié)合一起使用贮泞。

  1. 對于synchronized的出現(xiàn),是解決多線程資源共享的問題幔烛,同步機制采用了“以時間換空間”的方式:訪問串行化啃擦,對象共享化。同步機制是提供一份變量饿悬,讓所有線程都可以訪問议惰。

  2. 對于Atomic的出現(xiàn),是通過原子操作指令+Lock-Free完成乡恕,從而實現(xiàn)非阻塞式的并發(fā)問題言询。

  3. 對于Volatile,為多線程資源共享問題解決了部分需求傲宜,在非依賴自身的操作的情況下运杭,對變量的改變將對任何線程可見。

  4. ThreadLocal的作用是提供線程內(nèi)的局部變量函卒,這種變量在線程的生命周期內(nèi)起作用辆憔,減少同一個線程內(nèi)多個函數(shù)或者組件之間一些公共變量的傳遞的復(fù)雜度。

其他:
談?wù)刵otify()、notifyAll()虱咧、wait()熊榛、sleep()
線程間通信實例


技術(shù)討論 & 疑問建議 & 個人博客

版權(quán)聲明: 本博客所有文章除特別聲明外,均采用 CC BY-NC-SA 3.0 許可協(xié)議腕巡,轉(zhuǎn)載請注明出處玄坦!

參考:
http://blog.csdn.net/z69183787/article/details/51490129
https://yq.aliyun.com/articles/582139

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市绘沉,隨后出現(xiàn)的幾起案子煎楣,更是在濱河造成了極大的恐慌,老刑警劉巖车伞,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件择懂,死亡現(xiàn)場離奇詭異,居然都是意外死亡另玖,警方通過查閱死者的電腦和手機困曙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谦去,“玉大人慷丽,你說我怎么就攤上這事∧慕危” “怎么了?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵翔怎,是天一觀的道長窃诉。 經(jīng)常有香客問我,道長赤套,這世上最難降的妖魔是什么飘痛? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮容握,結(jié)果婚禮上宣脉,老公的妹妹穿的比我還像新娘。我一直安慰自己剔氏,他們只是感情好塑猖,可當(dāng)我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著谈跛,像睡著了一般羊苟。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上感憾,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天蜡励,我揣著相機與錄音,去河邊找鬼。 笑死凉倚,一個胖子當(dāng)著我的面吹牛兼都,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播稽寒,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼扮碧,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了瓦胎?” 一聲冷哼從身側(cè)響起芬萍,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎搔啊,沒想到半個月后柬祠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡负芋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年漫蛔,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片旧蛾。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡莽龟,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出锨天,到底是詐尸還是另有隱情毯盈,我是刑警寧澤,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布病袄,位于F島的核電站搂赋,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏益缠。R本人自食惡果不足惜脑奠,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望幅慌。 院中可真熱鬧宋欺,春花似錦、人聲如沸胰伍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽骂租。三九已至掌挚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間菩咨,已是汗流浹背吠式。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工陡厘, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人特占。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓糙置,卻偏偏與公主長得像,于是被迫代替她去往敵國和親是目。 傳聞我的和親對象是個殘疾皇子谤饭,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內(nèi)容