[翻译]双重检查加锁损坏宣言(The "Double-Checked Locking is Broken" Declaration)

前言:

这是一篇关于java中双重检查加锁的翻译,原文见The "Double-Checked Locking is Broken" Declaration ,这篇文章讲述了java双重检查加锁的失效原因以及解决办法,从宣言的签名来看,是非常权威的文章。
我会一段一段地给出原文,然后在下面给出翻译,翻译这篇文章,旨在学习交流,并不保留任何权利,也不承担任何责任。

文章中的idiom ,我翻译成惯用语法了。


原文:

The "Double-Checked Locking is Broken" Declaration

Signed by: David Bacon (IBM Research) Joshua Bloch (Javasoft), Jeff Bogda, Cliff Click (Hotspot JVM project), Paul Haahr, Doug Lea, Tom May, Jan-Willem Maessen, Jeremy Manson, John D. Mitchell (jGuru) Kelvin Nilsen, Bill Pugh, Emin Gun Sirer

译文:

双重检查加锁损坏宣言

签署人:(译注,人名就不翻译了,里面有一些我们熟悉的人,例如java的concurrent包的作者Doug Lea,但可以看出这篇文章的权威性和可靠性)

原文:

Double-Checked Locking is widely cited and used as an efficient method for implementing lazy initialization in a multithreaded environment.

译文:

双重检查加锁,作为一个在多线程环境中实现懒初始化的有效办法,被广泛地引用和使用。

原文:

Unfortunately, it will not work reliably in a platform independent way when implemented in Java, without additional synchronization. When implemented in other languages, such as C++, it depends on the memory model of the processor, the reorderings performed by the compiler and the interaction between the compiler and the synchronization library. Since none of these are specified in a language such as C++, little can be said about the situations in which it will work. Explicit memory barriers can be used to make it work in C++, but these barriers are not available in Java.

译文:

不幸的是,当用java实现的时候,(如果)没有附加的同步措施,它将无法以平台独立的方式真正地工作。用其他语言实现的时候,例如C++,它取决于处理器的内存模型、编译器执行的指令重排序以及编译器和同步库的交互方式,由于在编程语言例如C++中,这几点里面没有一个是明确的。可以说很少有它能够工作的场景。在C++中可以用显式的内存屏障使它工作,但这些屏障在Java中是不可用的。

原文:

To first explain the desired behavior, consider the following code:

译文:

首先说明预期的行为,考虑下面的代码

// Single threaded version
class Foo { 
  private Helper helper = null;
  public Helper getHelper() {
    if (helper == null) 
        helper = new Helper();
    return helper;
    }
  // other functions and members...
  }

原文:

If this code was used in a multithreaded context, many things could go wrong. Most obviously, two or more Helper objects could be allocated. (We'll bring up other problems later). The fix to this is simply to synchronize the getHelper() method:

译文:

如果这个代码在一个多线程的环境中使用,很多事情就会出错,最明显的是,两个或者多个Helper 对象会被分配。(我们在后面将会提出其他问题)。这个问题的解决办法是简单地同步(synchronize )getHelper()方法。

// Correct multithreaded version
class Foo { 
  private Helper helper = null;
  public synchronized Helper getHelper() {
    if (helper == null) 
        helper = new Helper();
    return helper;
    }
  // other functions and members...
  }

原文:

The code above performs synchronization every time getHelper() is called. The double-checked locking idiom tries to avoid synchronization after the helper is allocated:

译文:

上面的代码每次调用getHelper() 都会执行同步。双重检查加锁惯用语法在helper分配以后尝试避免同步。

// Broken multithreaded version
// "Double-Checked Locking" idiom
class Foo { 
  private Helper helper = null;
  public Helper getHelper() {
    if (helper == null) 
      synchronized(this) {
        if (helper == null) 
          helper = new Helper();
      }    
    return helper;
    }
  // other functions and members...
  }

原文:

Unfortunately, that code just does not work in the presence of either optimizing compilers or shared memory multiprocessors.

译文:

很不幸,这个代码在现有的优化编译器或者共享内存的多处理器中都无法工作。

原文:

It doesn't work

There are lots of reasons it doesn't work. The first couple of reasons we'll describe are more obvious. After understanding those, you may be tempted to try to devise a way to "fix" the double-checked locking idiom. Your fixes will not work: there are more subtle reasons why your fix won't work. Understand those reasons, come up with a better fix, and it still won't work, because there are even more subtle reasons.

译文:

无法工作

无法工作的原因有很多,我们首先要描述的是几个更为明显的原因。明白这些以后,你可能会被尝试设计一个修复双重检查加锁惯用语法所吸引。你的修复将无法工作:有更加微妙的原因使得你的修复不能工作。理解那些原因,想到一个更好的修复,它仍然不能工作,因为有更加微妙的原因。

原文:

Lots of very smart people have spent lots of time looking at this. There is no way to make it work without requiring each thread that accesses the helper object to perform synchronization.

译文:

很多聪明的人已经花费了很多时间研究这个问题,没有办法使它工作,除了要求每个访问helper 对象的线程执行同步。

原文:

The first reason it doesn't work

The most obvious reason it doesn't work it that the writes that initialize the Helper object and the write to the helper field can be done or perceived out of order. Thus, a thread which invokes getHelper() could see a non-null reference to a helper object, but see the default values for fields of the helper object, rather than the values set in the constructor.

译文:

无法工作的首要原因

无法工作最为明显的原因是,初始化Helper 对象的赋值和给helper成员变量赋值可以不按次序地完成或感知,因此,一个调用getHelper() 的线程可以看见一个非空的helper 对象引用,但是看见的是helper对象的成员变量的默认值,而不是构造方法中设置的值。(译注:这个是由于编译器指令重排序引起的)

原文:

If the compiler inlines the call to the constructor, then the writes that initialize the object and the write to the helper field can be freely reordered if the compiler can prove that the constructor cannot throw an exception or perform synchronization.

译文:

如果编译器内联构造方法调用,如果编译器可以证明构造器不会抛出一个异常或者执行一个同步,那么初始化对象的赋值和helper成员变量的赋值可以可以自由地重新排序。

原文:

Even if the compiler does not reorder those writes, on a multiprocessor the processor or the memory system may reorder those writes, as perceived by a thread running on another processor.

译文:

即使如果编译器重排序这些赋值,在一个多处理器上,处理器或者内存系统可能重新排序这些赋值,让一个线程感知从而运行在另一个处理器中。

原文:

Doug Lea has written a more detailed description of compiler-based reorderings.

译文:

Doug Lea 写过一篇基于编译器重排序的更详细描述

原文:

A test case showing that it doesn't work

Paul Jakubik found an example of a use of double-checked locking that did not work correctly. A slightly cleaned up version of that code is available here.

译文:

一个展示它无法工作的测试用例
Paul Jakubik 发现一个使用双重检查加锁不能正确工作例子。一个稍微整理的代码版本可以在这里找到.

原文:

When run on a system using the Symantec JIT, it doesn't work. In particular, the Symantec JIT compiles

译文:

当运行在一个使用Symantec (译注:不知道这个名词具体所指,可能是赛门铁克) 即时编译(JIT)(译注:JIT, 即时编译,Just In Time的简写,简单说就是把java的字节码编译成CPU指令来提升运行效率。)的系统上的时候,它无法工作。通常Symantec JIT 编译

singletons[i].reference = new Singleton();

原文:

to the following (note that the Symantec JIT using a handle-based object allocation system).

译文:

成下面(注意 Symantec JIT 使用一个handle-based 对象分配系统)

0206106A   mov         eax,0F97E78h
0206106F   call        01F6B210                  ; allocate space for
                                                 ; Singleton, return result in eax
02061074   mov         dword ptr [ebp],eax       ; EBP is &singletons[i].reference 
                                                ; store the unconstructed object here.
02061077   mov         ecx,dword ptr [eax]       ; dereference the handle to
                                                 ; get the raw pointer
02061079   mov         dword ptr [ecx],100h      ; Next 4 lines are
0206107F   mov         dword ptr [ecx+4],200h    ; Singleton's inlined constructor
02061086   mov         dword ptr [ecx+8],400h
0206108D   mov         dword ptr [ecx+0Ch],0F84030h

原文:

As you can see, the assignment to singletons[i].reference is performed before the constructor for Singleton is called. This is completely legal under the existing Java memory model, and also legal in C and C++ (since neither of them have a memory model).

译文:

你可以看见,singletons[i].reference的分配在 Singleton 构造函数调用之前执行。这在现存的java内存模型下是完全合法的,并且在C和C++中也是合法的(因为它们两个都没有内存模型)。

原文:

A fix that doesn't work

Given the explanation above, a number of people have suggested the following code:

译文:

一个不能工作的修复

给出了上面的解释,一些人建议了下面的代码:

// (Still) Broken multithreaded version
// "Double-Checked Locking" idiom
class Foo { 
  private Helper helper = null;
  public Helper getHelper() {
    if (helper == null) {
      Helper h;
      synchronized(this) {
        h = helper;
        if (h == null) 
            synchronized (this) {
              h = new Helper();
            } // release inner synchronization lock
        helper = h;
        } 
      }    
    return helper;
    }
  // other functions and members...
  }

原文:

This code puts construction of the Helper object inside an inner synchronized block. The intuitive idea here is that there should be a memory barrier at the point where synchronization is released, and that should prevent the reordering of the initialization of the Helper object and the assignment to the field helper.

译文:

这个代码把Helper 对象的构造放到了一个内部同步块里面。这里直觉的观点是,在同步被释放的点,应该有一个内存屏障,它应该阻止Helper对象初始化和helper成员变量赋值的重排序。

原文:

Unfortunately, that intuition is absolutely wrong. The rules for synchronization don't work that way. The rule for a monitorexit (i.e., releasing synchronization) is that actions before the monitorexit must be performed before the monitor is released. However, there is no rule which says that actions after the monitorexit may not be done before the monitor is released. It is perfectly reasonable and legal for the compiler to move the assignment helper = h; inside the synchronized block, in which case we are back where we were previously. Many processors offer instructions that perform this kind of one-way memory barrier. Changing the semantics to require releasing a lock to be a full memory barrier would have performance penalties.

译文:

不幸的是,这个直觉完全是错误的,这个同步规则不是那样工作的。一个退出监视指令(monitorexit)(译注:monitorexit 是一个jvm指令,这里翻译成"退出监视指令"了,具体参见monitorexit)(例如:释放同步)的规则是:退出监视指令之前的动作必须在释放监视器之前执行。然而,没有规则说明退出监视指令之后的动作可能不会在释放监视器之前完成。这对编译器移动 helper = h 赋值是极好的原因,并且是合法的,在同步块里面,在这种情况下我们回到了我们之前的问题。很多处理器提供指令来执行这种类型的单向内存屏障。改变语义来要求释放一个锁是一个完整的内存屏障,会有性能的不足。

原文:

More fixes that don't work

There is something you can do to force the writer to perform a full bidirectional memory barrier. This is gross, inefficient, and is almost guaranteed not to work once the Java Memory Model is revised. Do not use this. In the interests of science, I've put a description of this technique on a separate page. Do not use it.

译文:

更多无法工作的修复

有些你能够做的事情是强制要求写操作执行一个完整的双向内存屏障。这是粗放的,低效的,并且一旦Java内存模型被修订就几乎无法保证还能工作的。不要用这个,出于学术兴趣,我在一个单独的页面发布了一篇关于这个技术的描述。不要使用它。

原文:

However, even with a full memory barrier being performed by the thread that initializes the helper object, it still doesn't work.

译文:

然而,即使这个初始化helper 的线程执行了一个完整的内存屏障,它仍旧无法工作。

原文:

The problem is that on some systems, the thread which sees a non-null value for the helper field also needs to perform memory barriers.

译文:

问题在于在一些操作系统上,看见helper 成员变量非空值的线程还是需要执行内存屏障。

原文:

Why? Because processors have their own locally cached copies of memory. On some processors, unless the processor performs a cache coherence instruction (e.g., a memory barrier), reads can be performed out of stale locally cached copies, even if other processors used memory barriers to force their writes into global memory.

译文:

为什么呢?因为处理器有他们自己内存备份的本地缓存。在一些处理器上,除非处理器执行一个缓存一致性指令(例如:内存屏障),否则读操作可以在旧的本地缓存备份之外执行,即便其他处理器强制它们的全局内存写入使用了内存屏障。

原文:

I've created a separate web page with a discussion of how this can actually happen on an Alpha processor.

译文:

我已经创建了一个单独的web页面 讨论这在一个Alpha 处理器上实际会发生什么。

原文:

Is it worth the trouble?

For most applications, the cost of simply making the getHelper() method synchronized is not high. You should only consider this kind of detailed optimizations if you know that it is causing a substantial overhead for an application.

译文:

值得这样做吗

对于绝大多数应用,简单地使 getHelper()方法同步的成本不是很高,如果你知道它给一个应用带来了一个实质的负担,你才应该考虑这种精细的优化。

原文:

Very often, more high level cleverness, such as using the builtin mergesort rather than handling exchange sort (see the SPECJVM DB benchmark) will have much more impact.

译文:

通常,更高级别的聪明,例如使用内置的归并排序,而不是处理交换排序(查看SPECJVM DB基准),将会有很多影响。

(译注:聪明的办法有时候容易弄巧成拙)

原文:

Making it work for static singletons

If the singleton you are creating is static (i.e., there will only be one Helper created), as opposed to a property of another object (e.g., there will be one Helper for each Foo object, there is a simple and elegant solution.

译文:

用静态单例使它工作

如果你创建的单例是静态的(例如,只有一个Helper 对象被创建),而不是其他对象的一个属性(例如,对于每个Foo对象都有一个Helper对象),有一个简单而且优雅的方案。

原文:

Just define the singleton as a static field in a separate class. The semantics of Java guarantee that the field will not be initialized until the field is referenced, and that any thread which accesses the field will see all of the writes resulting from initializing that field.

译文:

仅仅在一个独立的类中定义一个静态变量作为单例。Java的语义确保这个静态变量不会被初始化,直到这个变量被引用,并且任何访问这个静态变量的线程都会看见初始化这个变量的所有写入结果。

class HelperSingleton {
  static Helper singleton = new Helper();
  }

原文:

It will work for 32-bit primitive values

Although the double-checked locking idiom cannot be used for references to objects, it can work for 32-bit primitive values (e.g., int's or float's). Note that it does not work for long's or double's, since unsynchronized reads/writes of 64-bit primitives are not guaranteed to be atomic.

译文:

对于32位的基本类型数据它可以工作(译注: primitive values,指的是Java中的基本数据类型,例如int, long,float, double,与之相对的是引用数据类型)

尽管双重检查加锁语法习惯对对象引用不能使用,但对于对32位基本类型数据(例如int或者float)它可以工作。注意它对于long或者double却是不能工作的。(译注:JVM规范要求对32为数值的赋值操作必须是原子的,也就是线程安全的,可以查看《深入JAVA虚拟机》第五章“The Java Virtual Machine”)是因为64位基本数据的非同步读写不能保证是原子性的。

// Correct Double-Checked Locking for 32-bit primitives
class Foo { 
  private int cachedHashCode = 0;
  public int hashCode() {
    int h = cachedHashCode;
    if (h == 0) 
    synchronized(this) {
      if (cachedHashCode != 0) return cachedHashCode;
      h = computeHashCode();
      cachedHashCode = h;
      }
    return h;
    }
  // other functions and members...
  }

原文:

In fact, assuming that the computeHashCode function always returned the same result and had no side effects (i.e., idempotent), you could even get rid of all of the synchronization.

译文:

事实上,假设computeHashCode 方法通常返回相同的结果并且没有副作用(例如幂值),你可以摆脱所有的同步。

// Lazy initialization 32-bit primitives
// Thread-safe if computeHashCode is idempotent
class Foo { 
  private int cachedHashCode = 0;
  public int hashCode() {
    int h = cachedHashCode;
    if (h == 0) {
      h = computeHashCode();
      cachedHashCode = h;
      }
    return h;
    }
  // other functions and members...
  }

(译注:对比前面的代码,去掉了同步块,但这和我们常说的单例不一样,我们更侧重于引用类型)

原文:

Making it work with explicit memory barriers

It is possible to make the double checked locking pattern work if you have explicit memory barrier instructions. For example, if you are programming in C++, you can use the code from Doug Schmidt et al.'s book:

译文:

用显式的内存屏障使它工作

如果你有显式的内存屏障指令,有可能使得双重检查加锁模式正常工作。如果你用C++编程,你可以用来自Doug Schmidt et al.'s的书里面的代码:

// C++ implementation with explicit memory barriers
// Should work on any platform, including DEC Alphas
// From "Patterns for Concurrent and Distributed Objects",
// by Doug Schmidt
template <class TYPE, class LOCK> TYPE *
Singleton<TYPE, LOCK>::instance (void) {
    // First check
    TYPE* tmp = instance_;
    // Insert the CPU-specific memory barrier instruction
    // to synchronize the cache lines on multi-processor.
    asm ("memoryBarrier");
    if (tmp == 0) {
        // Ensure serialization (guard
        // constructor acquires lock_).
        Guard<LOCK> guard (lock_);
        // Double check.
        tmp = instance_;
        if (tmp == 0) {
                tmp = new TYPE;
                // Insert the CPU-specific memory barrier instruction
                // to synchronize the cache lines on multi-processor.
                asm ("memoryBarrier");
                instance_ = tmp;
        }
    return tmp;
    }

原文:

Fixing Double-Checked Locking using Thread Local Storage

Alexander Terekhov (TEREKHOV@de.ibm.com) came up clever suggestion for implementing double checked locking using thread local storage. Each thread keeps a thread local flag to determine whether that thread has done the required synchronization.

译文:

用Thread Local Storage修复双重检查加锁(译注:Thread Local Storage 详情见 java的实现ThreadLocal

Alexander Terekhov (TEREKHOV@de.ibm.com) 给出了聪明的建议,用thread local storage 来实现双重检查加锁。每一个线程保存一个thread local标记,来决定这个线程是否已经完成了所需的同步。

class Foo {
     /** If perThreadInstance.get() returns a non-null value, this thread
        has done synchronization needed to see initialization
        of helper */
         private final ThreadLocal perThreadInstance = new ThreadLocal();
         private Helper helper = null;
         public Helper getHelper() {
             if (perThreadInstance.get() == null) createHelper();
             return helper;
         }
         private final void createHelper() {
             synchronized(this) {
                 if (helper == null)
                     helper = new Helper();
             }
         // Any non-null value would do as the argument here
             perThreadInstance.set(perThreadInstance);
         }
    }

(译注:个人感觉,这个写法虽然线程安全,但和我们常见的单例模式差别也太大了)

原文:

The performance of this technique depends quite a bit on which JDK implementation you have. In Sun's 1.2 implementation, ThreadLocal's were very slow. They are significantly faster in 1.3, and are expected to be faster still in 1.4. Doug Lea analyzed the performance of some techniques for implementing lazy initialization.

译文:

这个技术的性能,对你的JDK的实现依赖不小。在Sun 的1.2实现,ThreadLocal非常慢,在1.3以后它们显著变快。在1.4期待变得更快。Doug Lea 分析了一些延迟初始化技术的性能.

原文:

Under the new Java Memory Model

As of JDK5, there is a new Java Memory Model and Thread specification.

译文:

在JDK5, 有一个新的java内存模型和线程规范.

原文:

Fixing Double-Checked Locking using Volatile

JDK5 and later extends the semantics for volatile so that the system will not allow a write of a volatile to be reordered with respect to any previous read or write, and a read of a volatile cannot be reordered with respect to any following read or write. See this entry in Jeremy Manson's blog for more details.

译文:

使用Volatile修复双重检查加锁

JDK5和以后的JKD扩展了volatile语义,因此系统不允许一个volatile 的写操作前面的读或者写操作重排序,一个volatile的读操作不能和后面的读或者写操作重排序。见Jeremy Manson博客中的这一篇 了解更多.

原文:

With this change, the Double-Checked Locking idiom can be made to work by declaring the helper field to be volatile. This does not work under JDK4 and earlier.

译文:

有了这个修改,双重检查加锁语法习惯可以通过申明 helper 变量为 volatile 来使它工作,但这个不会在JDK4及其以下工作。

// Works with acquire/release semantics for volatile
// Broken under current semantics for volatile
  class Foo {
        private volatile Helper helper = null;
        public Helper getHelper() {
            if (helper == null) {
                synchronized(this) {
                    if (helper == null)
                        helper = new Helper();
                }
            }
            return helper;
        }
    }

原文:

Double-Checked Locking Immutable Objects

If Helper is an immutable object, such that all of the fields of Helper are final, then double-checked locking will work without having to use volatile fields. The idea is that a reference to an immutable object (such as a String or an Integer) should behave in much the same way as an int or float; reading and writing references to immutable objects are atomic.

译文:

双重检查加锁不可变对象

如果Helper 是一个不可变对象,这样Helper 的所有成员变量是final的,那么就算没有使用volatile 变量,双重检查加锁还会工作。观点是,不可变对象(例如String或者Integer)的引用应当和int或者float大致表现相同。不可变对象引用的读和写是原子的。

原文:

Descriptions of double-check idiom

译文:

双重检查语法习惯的说明

(译注:这里列举的是参考文献,无需翻译)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容