Java编程之多线程&并发编程(中)

3. 创建新线程

创建一个线程有两种方法:

  1. 从Thread类继承并重写run()方法。创建一个实例,调用start()方法,这会在新线程上调用run()。如下:
Thread t = new Thread() {   // Create an instance of an anonymous inner class that extends Thread
    @Override
    public void run() {      // Override run() to specify the running behaviors
        for (int i = 0; i < 100000; ++i) {
            if (stop) break;
            tfCount.setText(count + "");
            ++count;
            // Suspend itself and yield control to other threads for the specified milliseconds
            // Also provide the necessary delay
            try {
                sleep(10); // milliseconds
            } catch (InterruptedException ex) {}
        }
    }
};
t.start();  // Start the thread. Call back run() in a new thread
  1. 创建一个实现Runnable 接口的类并为抽象方法 run()提供具体实现。用带Runnable对象的构造函数构造一个新线程实例并调用 start()方法,这会在新线程上调用run()。
// Create an anonymous instance of an anonymous inner class that implements Runnable
// and use the instance as the argument of Thread's constructor.
Thread t = new Thread(new Runnable() {
    // Provide implementation to abstract method run() to specify the running behavior
    @Override
    public void run() {
        for (int i = 0; i < 100000; ++i) {
            if (stop) break;
            tfCount.setText(count + "");
            ++count;
            // Suspend itself and yield control to other threads
            // Also provide the necessary delay
            try {
                Thread.sleep(10);  // milliseconds
            } catch (InterruptedException ex) {}
        }
    }
});
t.start();  // call back run() in new thread

因为Java不支持多重继承,一般选择第二种方法。如果类已经继承了某一超类,它便不能继承Thread,必须实现 Runnable 接口。第二种方法也能用来提供针对JDK 1.1的兼容。应该注意的是Thread类本身实现Runnable接口。

run()方法制定线程的任务。不能直接从程序中调用run()方法,而是先要创建线程实例并调用start()方法,之后start()方法在新线程上调用run()。

3.1 Runnable接口

接口java.lang.Runnable 声明一个抽象方法run()用来制定线程的执行任务:public void run();

3.2 Thread类

类 java.lang.Thread 有以下构造函数:

public Thread();
public Thread(String threadName);
public Thread(Runnable target);
public Thread(Runnable target, String threadName);

前面两个构造函数用来通过子类化Thread 类创建线程,下面两个用来创建带实现Runnable 接口类实例的线程。

Thread类实现Runnable接口,如图所示:


如上面所提到的,run()方法制定线程的执行任务。不能直接调用run()方法而应该调用Thread类的 start()方法。如果线程是通过继承Thread 类创建的,方法start()将调用继承类中重写的run()方法;如果线程是通过给线程构造函数提供Runnable 对象创建的,方法start()将调用Runnable对象的run()方法 (而非Thread的run())。

3.3 通过继承 Thread 并重写run()创建新线程

通过继承Thread类来创建并运行一个新线程:

  1. 定义一个继承父类Thread的子类(命名或者匿名);
  2. 在子类中重写run()方法来制定线程的操作,(并提供其他如构造函数,变量和方法的实现);
  3. Client类创建了一个该新类的实例, 该实例为Runnable对象(因为 Thread类本身实现Runnable接口);
  4. Client类调用Runnable对象的start()方法。结果是两个线程并发执行-调用start()之后的当前线程和执行Runnable对象 run()方法的新线程。
class MyThread extends Thread {
    // override the run() method
    @Override
    public void run() {
        // Thread's running behavior
    }
    // constructors, other variables and methods
    ......
}
public class Client {
    public static void main(String[] args) {
        ......
        // Start a new thread
        MyThread t1 = new MyThread();
        t1.start();  // Called back run()
        ......
        // Start another thread
        new MyThread().start();
        ......
    }
}

通常使用内部类 (命名或匿名)而非普通子类,这是为了可读性并获取对外部类私有变量和方法的访问。例如:

public class Client {
    ......
    public Client() {
        Thread t = new Thread() { // Create an anonymous inner class extends Thread
            @Override
            public void run() {
                // Thread's running behavior
                // Can access the private variables and methods of the outer class
            }
        };
        t.start();
        ...
        // You can also used a named inner class defined below
        new MyThread().start();
    }

    // Define a named inner class extends Thread   
    class MyThread extends Thread {
        public void run() {
            // Thread's running behavior
            // Can access the private variables and methods of the outer class
        }
    }
}
public class MyThread extends Thread {
    private String name;
 
    public MyThread(String name) {   // constructor
        this.name = name;
    }
 
    // Override the run() method to specify the thread's running behavior
    @Override
    public void run() {
        for (int i = 1; i <= 5; ++i) {
            System.out.println(name + ": " + i);
            yield();
        }
    }
}

通过继承Thread类创建一个MyThead类并重写run()方法。定义一个带字符串参数的构造函数,该字符串用来标识此线程的名字。run()方法打印数字1 到5, 调用yield()让线程在打印出每个数字之后自动将资源控制权交与其他线程。

public class TestMyThread {
    public static void main(String[] args) {
        Thread[] threads = {
            new MyThread("Thread 1"),
            new MyThread("Thread 2"),
            new MyThread("Thread 3")
        };
        for (Thread t : threads) {
            t.start();
        }
    }
}

测试类分配并启动三个线程。输出如下:

Thread 1: 1
Thread 3: 1
Thread 1: 2
Thread 2: 1
Thread 1: 3
Thread 3: 2
Thread 2: 2
Thread 3: 3
Thread 1: 4
Thread 1: 5
Thread 3: 4
Thread 3: 5
Thread 2: 3
Thread 2: 4
Thread 2: 5

注意输出是不确定的(每次运行都可能产生不同输出), 因为没有完全控制线程的执行方式。

3.4 通过实现Runnable接口创建新线程

通过实现Runnable接口创建并运行新线程:
1.定义一个实现Runnable接口的类;
2.在类中为抽象方法run()提供实现来制定线程的操作,(并提供其他如构造函数,变量和方法的实现);
3.Client类创建了一个该新类的实例,该实例称作Runnable对象;
4.Client类接着创建一个新线程对象,构造函数以Runnable对象为参数,然后调用start()方法。Start()调用Runnable对象(而不是Thread类)中的 run()方法。

class MyRunnable extends SomeClass implements Runnable {
    // provide implementation to abstract method run()
    @Override
    public void run() {
        // Thread's running behavior
    }
    ......
    // constructors, other variables and methods
}
public class Client {
    ......
    Thread t = new Thread(new MyRunnable());
    t.start();
    ...
}

同样地,内部类 (命名或匿名) 通常是为了可读性并能获取对外部类私有变量和方法的访问。例如:

Thread t = new Thread(new Runnable() { // Create an anonymous inner class that implements Runnable interface
    public void run() {
        // Thread's running behavior
        // Can access the private variables and methods of the outer class
    }
});
t.start();

3.5 Thread类中的方法

Thread类中的方法包括:

  • public void start(): 开始一个新线程。JRE 调用该类run()方 法,当前线程继续运行;
  • public void run(): 制定新线程的执行任务。当run()完成时,线程终止;
  • 中断函数
public static sleep(long millis)throws InterruptedException
public static sleep(long millis, int nanos)throws InterruptedException
public void interrupt()

暂停当前线程并将资源控制权交与其他线程以给定的时间。Sleep()方法是线程安全的因为它不会释放自己的监视器。通过调用 interrupt()方法能在给定时间到达之前唤醒阻塞线程。被唤醒线程会抛出InterruptedException并在继续操作之前执行其InterruptedException 处理函数。这是个静态方法且广泛用来暂停当前线程(通过Thread.sleep()),这样其他线程就有机会执行。 这也在许多应用中提供了必要的延迟。例如:

try {
    // Suspend the current thread and give other threads a chance to run
    // Also provide the necessary delay
    Thread.sleep(100);  // milliseconds
} catch (InterruptedException ex) {}
  • public static void yield(): 暗示调度程序当前线程愿意将当前处理器的使用权交与其他线程,但是该调度程序可以随意忽略这条暗示,因此很少使用;
  • public boolean isAlive(): 如果线程是新建的或死亡返回false;如果线程是"就绪" 或 "阻塞"返回true;
  • public void setPriority(sint p): 设置线程的优先级,依赖于运行情况。

stop(),suspend(),和resume()方法在JDK 1.4中已经被废弃了,它们因monitor的释放而非线程安全,详见JDK API文档。

3.6 守护线程

有两种线程,分别是守护线程和用户线程。守护线程能通过方法setDaemon(boolean on)设置。守护线程是基础线程,如:垃圾回收线程及 GUI的事件分发线程。JVM在运行线程都是守护线程的时候退出。换言之,当不再有用户线程且所有存留线程都是基础线程时,JVM 认为其任务已完成。

3.7 线程的生命周期

线程刚创建的时候处于"新建"(NEW)状态。在该状态的时候,它只是堆栈中的一个对象,而没有分配任何系统资源来执行。从"新建"状态开始,能做的唯一事情就是调用start()方法,这将线程置于"就绪"(RUNNABLE)状态。调用除start()之外的任何方法会导致IllegalThreadStateException。

start()方法分配必需的系统资源来执行线程,安排线程运行并调用run()。 这将线程置于"就绪"状态。由于大多数计算机只有一个CPU且通过CPU时间片来支持多线程,因此在"就绪"状态下,线程可能运行或等待CPU时间片。

线程不能被启动两次,否则会造成 IllegalThreadStateException。

当以下事件出现时线程会进入"阻塞"状态:

  1. 调用sleep()挂起线程一段时间以将控制权交给其他线程,也可以调用 yield()来示意调度函数当前线程愿意交出当前处理器的使用权。只是调度函数会随意忽略该暗示;
  2. 调用wait()方法等待满足特定条件;
  3. 线程阻塞,并等待I/O操作的完成。

线程再次从"阻塞"状态变成 "就绪":

  1. 如果线程休眠,特定的休眠时间结束或休眠通过interrupt()方法中断;
  2. 如果线程通过wait()处于等待状态,调用notify()或者notifyAll()方法通知等待线程条件已经满足并结束等待;
  3. 阻塞线程的I/O操作完成。

只有当run()方法自然终止并退出时线程才处于“终止”状态。

方法isAlive()能被用来检测线程是否存活。如果线程是"新建" 或 "终止",isAlive()返回false;如果线程是"就绪"或"阻塞"则返回true。

JDK 1.5引入了一新方法getState(),该方法返回 (封装) Thread.State枚举类型,为五种状态中一种 - {NEW, BLOCKED, RUNNABLE, TERMINATED, WAITING}。

  • NEW: 线程尚未启动;
  • RUNNABLE:
  • WAITING:
  • BLOCKED: 线程阻塞等待监视器锁;
  • TIMED_WAITING: 线程等待一定时间;
  • TERMINATED:

4. 线程调度及优先级

JVM 实现固定的线程调度机制。每个线程都会设置一个优先级(在Thread.MIN_PRIORITY和Thread.MAX_PRIORITY之间)。数字越大,线程优先级越高。当创建新线程,它会继承创建线程的优先级。可以调用方法setPriority()来改变线程的优先级:public void setPriority(int priority);

参数priority取值范围为1(优先级最低)to 10。

JVM选择执行优先级最高的线程。如果有一个以上线程都有最高优先级,JVM会遵循轮询调度。

JVM实行优先权调度机制,在这种环境下如果任何时候有一更高优先级的线程状态转为"就绪",当前低优先级线程将立即让步与更高优先级线程。

如果有两个以上线程处于同一优先级且均处于就绪状态,线程可能会一直运行直至完成而不让步与其他同优先级线程,这样会造成线程饥饿。因此通过sleep()或 yield()让步与其他同级线程是个不错的做法。但是,永远都不可能将控制权交与低优先级线程。

在一些操作系统如Windows中,每一个运行线程都被分配一定量的CPU时间片,该时间片用以阻止其他同优先级线程出现饥饿。但不要依赖时间片,因为它也依赖于运行情况。

因此,正在运行的线程在出现以下情况时会停止执行:

  • 更高优先级线程状态为"就绪";
  • 正在运行线程通过调用如:sleep(),yield()和wait()等方法主动放弃控制;
  • 正在运行线程终止,即其run()方法退出;
  • 在执行时间片的系统上,运行线程消耗完自己的CPU时间片;

需要注意的一点是线程调度和优先级都依赖于JVM,因为JVM是虚拟机并需要原生操作系统资源来支持多线程。大部分JVM不保证优先级最高的线程一直在运行,有时为了避免饥饿会选择执行较低优先级线程,因此不应该依赖算法中的优先次序。

5. 监视器锁 & 同步

监视器(monitor)是用来阻塞并唤醒线程的对象,在 java.lang.Object中通过以下机制支持:

  1. 每个对象都有一个锁;
  2. 关键字synchronized用来获取对象锁;
  3. java.lang.Object中的方法wait(),notify()和notifyAll()方法控制线程。

每个java对象都有锁。任何时候,锁最多被一单线程控制。可以对方法或某段代码使用关键字synchronized。要执行对象同步代码的线程必须先要获取该对象的锁。如果该锁为其他线程所控制,那么尝试的线程会进入“找锁”状态,且仅当锁可以访问的时候才准备完成。占用锁的线程执行完同步代码之后便会放弃锁。

5.1 关键词"synchronized"

如下:

public synchronized void methodA() { ...... }  // synchronized a method based on this object
 
public void methodB() {
    synchronized(this) {      // synchronized a block of codes based on this object
        ......
    }
 
    synchronized(anObject) {  // synchronized a block of codes based on another object
        ......
    }
    ......
}

同步能在方法层面或区块层面进行控制。变量不能同步,需要同步所有访问那个变量的方法。

private static int counter = 0;

public static synchronized void increment() {
    ++counter;
}

public static synchronized void decrement() {
    --counter;
}

也可以对静态方法使用synchronized。这情况下,需要获取类锁 (而不是实例锁) 来执行方法。

public class SynchronizedCounter {
    private static int count = 0;
 
    public synchronized static void increment() {
        ++count;
        System.out.println("Count is " + count + " @ " + System.nanoTime());
    }
 
    public synchronized static void decrement() {
        --count;
        System.out.println("Count is " + count + " @ " + System.nanoTime());
    }
}
public class TestSynchronizedCounter {
    public static void main(String[] args) {
        Thread threadIncrement = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 10; ++i) {
                    SynchronizedCounter.increment();
                    try {
                        sleep(1);
                    } catch (InterruptedException e) {}
                }
            }
        };
 
        Thread threadDecrement = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 10; ++i) {
                    SynchronizedCounter.decrement();
                    try {
                        sleep(1);
                    } catch (InterruptedException e) {}
                }
            }
        };
 
        threadIncrement.start();
        threadDecrement.start();
    }
}
Count is -1 @ 71585106672577
Count is 0 @ 71585107040916
Count is -1 @ 71585107580661
Count is 0 @ 71585107720865
Count is 1 @ 71585108577488
Count is 0 @ 71585108715261
Count is 1 @ 71585109590928
Count is 0 @ 71585111400613
Count is 1 @ 71585111640095
Count is 0 @ 71585112581002
Count is 1 @ 71585112748760
Count is 2 @ 71585113580259
Count is 1 @ 71585113729378
Count is 2 @ 71585114579922
Count is 1 @ 71585114712832
Count is 2 @ 71585115578775
Count is 1 @ 71585115722626
Count is 2 @ 71585116578843
Count is 1 @ 71585116719452
Count is 0 @ 71585117583368

需要注意的重要一点是当对象被锁住的时候,同步的方法和代码都会被阻塞。而非同步方法不受锁影响继续执行,因此有必要同步所有操作共享资源的方法。比如,如需要对一变量访问进行同步,那么所有指向该变量方法都应被同步。否则,非同步方法不需先获取锁而直接访问会影响该变量的状态。

5.2 wait(), notify() & notifyAll() 实现线程间同步

这些方法都定义在java.lang.Object 类中(而不是java.land.Thread),这些只能在同步代码中调用。

方法wait()和notify()为共享对象提供了一种暂停线程,在合适的时候继续该线程的方法。

例子: 消费者和生产者

在本例中,生产者生产信息(通过方法putMessage()),该信息在下一条信息生产出来之前被消费者消费 (通过方法getMessage())。在这样一个producer-consumer模式中,一个线程调用wait()将自己挂起(并释放锁),直到另一线程调用notify()或notifyAll()将其唤醒。

// Testing wait() and notify()
public class MessageBox {
    private String message;
    private boolean hasMessage;
 
    // producer
    public synchronized void putMessage(String message) {
        while (hasMessage) {
            // no room for new message
            try {
                wait();  // release the lock of this object
            } catch (InterruptedException e) { }
        }
        // acquire the lock and continue
        hasMessage = true;
        this.message = message + " Put @ " + System.nanoTime();
        notify();
    }
 
    // consumer
    public synchronized String getMessage() {
        while (!hasMessage) {
            // no new message
            try {
                wait();  // release the lock of this object
            } catch (InterruptedException e) { }
        }
        // acquire the lock and continue
        hasMessage = false;
        notify();
        return message + " Get @ " + System.nanoTime();
    }
}
public class TestMessageBox {
    public static void main(String[] args) {
        final MessageBox box = new MessageBox();
 
        Thread producerThread = new Thread() {
            @Override
            public void run() {
                System.out.println("Producer thread started...");
                for (int i = 1; i <= 6; ++i) {
                    box.putMessage("message " + i);
                    System.out.println("Put message " + i);
                }
            }
        };
 
        Thread consumerThread1 = new Thread() {
            @Override
            public void run() {
                System.out.println("Consumer thread 1 started...");
                for (int i = 1; i <= 3; ++i) {
                    System.out.println("Consumer thread 1 Get " + box.getMessage());
                }
            }
        };
 
        Thread consumerThread2 = new Thread() {
            @Override
            public void run() {
                System.out.println("Consumer thread 2 started...");
                for (int i = 1; i <= 3; ++i) {
                    System.out.println("Consumer thread 2 Get " + box.getMessage());
                }
            }
        };
 
        consumerThread1.start();
        consumerThread2.start();
        producerThread.start();
    }
}
Consumer thread 1 started...
Producer thread started...
Consumer thread 2 started...
Consumer thread 1 Get message 1 Put @ 70191223637589 Get @ 70191223680947
Put message 1
Put message 2
Consumer thread 2 Get message 2 Put @ 70191224046855 Get @ 70191224064279
Consumer thread 1 Get message 3 Put @ 70191224164772 Get @ 70191224193543
Put message 3
Put message 4
Consumer thread 2 Get message 4 Put @ 70191224647382 Get @ 70191224664401
Put message 5
Consumer thread 2 Get message 5 Put @ 70191224939136 Get @ 70191224965070
Consumer thread 1 Get message 6 Put @ 70191225071236 Get @ 70191225101222
Put message 6

输出信息 (System.out)可能是乱序的,仔细检查put/get时间戳可确证正确的操作顺序。

同步的生产者方法putMessage()获取对象锁,检查之前信息是否已被清理。如果没有清理则调用wait(),释放对象锁并进入WAITING状态,将该线程置于对象的"等待"组。另一方面,同步的消费者方法getMessage()获取该对象锁并检查新信息。如果有一条新信息,清理之并调用notify(),这会在该对象的"等待"组上任意选择一个线程(在本例中刚好是生产者线程)并将其置于BLOCKED状态。消费者线程相应地进入WAITING状态并将自己置于对象的"等待"组(在wait()方法之后)。生产者线程之后获取锁并继续执行后面操作。


notify()和notifyAll()的区别在于:notify()从该对象的等待池中随意选择一个线程并将其置于找锁状态;而notifyAll()唤醒对象等待池中所有线程。然后被唤醒的线程以正常方式竞争执行。

多线程在类java.lang.Object处编入Java语言,同步锁保存在Object中,用来协调线程的方法wait(),notify(),notifyAll()正好都在在类Object中。

带超时参数的wait()

下面几种形式的wait()方法,带一个超时参数:

public final void wait() throws InterruptedException
public final void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException

超过超时时间线程将也进入BLOCKED状态。


5.3 饥饿与死锁

饥饿(Starvation )指的是一个(或更多)线程不能访问对象的状态。此问题可通过对所有线程设置正确的优先级加以解决。

死锁(Deadlock)指的是一个线程正在等待一个条件,但是程序的其他地方阻止了该条件的满足,因而阻止了线程的执行。典型的例子,也称"死亡拥抱":线程1持有对象A的锁,线程2持有对象B的锁。线程1等待获取对象B的锁,而线程2则等待获取对象A的锁。两个线程都处于死锁状态而不能继续执行。如果两个线程都以相同顺序找锁,这种情况就不会出现。但是在程序中实现这种安排较为复杂。可以选择的方法是要么在另一对象(而不是对象A或B)上同步;要么只同步方法的一部分,而非整个方法。死锁可能会很复杂,因为它可能涉及到很多线程和对象且难于检测。

翻译自:
https://www3.ntu.edu.sg/home/ehchua/programming/java/j5e_multithreading.html

Java编程之多线程&并发编程(上)
Java编程之多线程&并发编程(下)

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

推荐阅读更多精彩内容