一文搞懂Java多线程基础内容

一、首先什么是线程和进程?

进程好比我们电脑里开的程序,比如dota、英雄联盟、网易云音乐,每个程序都是一个独立的进程;然后每个程序中,会有多个线程,比如英雄联盟游戏里有画面、有音乐,这就是不同的线程。

二、java中实现多线程的三种方法

1、继承Thread类,重写run方法
2、实现Runnable接口,重写run方法
3、实现Callbale接口,重写call方法

手撕代码:

package com.xusujun.thread_finish;
import static java.util.concurrent.Executors.newFixedThreadPool;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        DemoClass demoClass = new DemoClass();
        demoClass.setName("通过继承thread类实现");
        demoClass.start();// 继承实现的可以直接调用start方法启动线程

        DemoRunnable demoRunnable = new DemoRunnable();
        // 通过runnable接口实现的,需要通过thread类进行代理实现
        new Thread(demoRunnable, "通过runnable接口实现").start();

        DemoCallable demoCallable = new DemoCallable();
        // 创建服务
        ExecutorService ser = newFixedThreadPool(1);
        // 提交服务
        Future<Boolean> r = ser.submit(demoCallable);
        // 获取结果
        boolean result = r.get();
        // 关闭服务
        ser.shutdown();

    }
}

// 通过继承thread类实现,重写run方法
class DemoClass extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "执行了");
    }
}

// 通过实现Runnable接口实现,重写run方法
class DemoRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "执行了");
    }

}

// 通过callable接口实现,重写call方法
class DemoCallable implements Callable<Boolean> {

    @Override
    public Boolean call() throws Exception {
        Thread.currentThread().setName("通过callbale接口实现");
        System.out.println(Thread.currentThread().getName() + "执行了");
        return true;
    }

}

以上代码执行结果如下:


image.png

三种方法个人粗略总结:
1、通过继承Thread类实现,对象可以直接调用Thread类静态方法,入start、sleep等。由于单继承的原因,灵活性不够,Thread类其本质也是实现runnable接口。
2、通过实现runnable接口方法更灵活,但是需要Thread类来进行代理实现
3、callable重写的call方法有返回值,run方法没有返回值。call可以抛出异常,run方法无法抛出异常

三、线程的生命周期

通过上面的例子,大概能感觉出来线程的生命周期包括:

  1. 创建 对象实例化
  2. 就绪 .start() 启动不代表立即执行,由cpu自行调度
  3. 运行 .run() 获取系统资源开始运行
  4. 等待 .sleep() 休眠状态 .wait() 等待状态
  5. 结束 运行结束,线程一旦运行结束无法重新启动。推荐使用外部标志位进行停止,不推荐使用stop等方法。

关于sleep,一条重点,执行sleep不会导致解锁

四、龟兔赛跑案例

假设龟兔分别代表两个线程,我们让他们进行50米比赛,假设我们不人工干预让他们两个线程自己跑,你们想下会有什么结果?
看代码:

//龟兔赛跑
public class ThreadDemo {

    static class Mythread implements Runnable {
        int raceway = 50; //赛道设置50米
        Boolean flag = false; //比赛是否结束标志
        String winner; //获胜者

        @Override
        public void run() {
            while (raceway >= 0) {
                if (flag == true) {
                    break;
                }
                raceway--;
                System.out.println(Thread.currentThread().getName() + "距离终点" + raceway + "米 !");
                gameOver(raceway);

            }
        }

        //是否获胜方法
        public void gameOver(int distance) {
            if (winner != null) {
                this.flag = true;
            }
            if (distance == 0) {
                this.flag = true;
                winner = Thread.currentThread().getName();
                System.out.println(winner + "获得了胜利");
            }

        }
    }

    public static void main(String[] args) {
        Mythread mythread = new Mythread();
        new Thread(mythread, "兔子").start(); //兔子线程
        new Thread(mythread, "乌龟").start(); //乌龟线程
    }
}

如果我们按照上面的程序跑,结果是不是可能乌龟获胜,也可能兔子获胜啊,因为之前说过线程的启动是cpu自行调度的。那么龟兔赛跑的故事是不是有兔子睡觉啊,那么我们加入兔子睡觉再来看,在run()方法中加入如下代码:

if (Thread.currentThread().getName().equals("兔子")) {
    try {
        Thread.sleep(200);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

以上代码先找到兔子的线程,然后通过sleep()方法让他睡觉200毫秒,这样一来是不是每次都是乌龟获胜了呀!

image.png
image.png

五、sleep、yield、join 一起

sleep ---线程休眠
yield ---线程礼让
join ---线程插队

直接上例子更好理解

1、sleep例子:
class ThreadDemoread {

    static class Mythread implements Runnable {

        @Override
        public void run() {

            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("当前时间:" + printDate());
            }
        }

        // 每隔一秒打印当前时间方法
        public String printDate() {
            // 获取系统当前时间
            Date now = new Date(System.currentTimeMillis());
            String formatDate = new SimpleDateFormat("HH:mm:ss").format(now);
            return formatDate;
        }

    }

    public static void main(String[] args) {
        Mythread mythread = new Mythread();
        new Thread(mythread).start();
    }
}

执行结果如下:


image.png
2、yeild例子:
public class ThreadDemo {

    static class Mythread implements Runnable {

        @Override
        public void run() {
            if (Thread.currentThread().getName().equals("A")) {
                 Thread.yield();
             }
            System.out.println(Thread.currentThread().getName() + "执行了");
            System.out.println(Thread.currentThread().getName() + "结束了");
        }
    }

    public static void main(String[] args) {
        new Thread(new Mythread(), "A").start();
        new Thread(new Mythread(), "B").start();
    }
}

在run方法中添加yield后,线程A会进行线程礼让,但是注意,礼让不一定成功,依旧是看cpu心情。也就是说可能礼让成功,也可能礼让失败。

3、join

join是强制执行,他就比较霸道了,类似于vip,vip来了,其他人都要让位子

public class ThreadDemo {

    static class Mythread implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i < 200; i++) {
                System.out.println(Thread.currentThread().getName() + "执行了" + i + "次");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Mythread mythread = new Mythread();
        Thread thB = new Thread(mythread, "VIP");
        thB.start();
        for (int i = 0; i < 600; i++) {
            if(i==10){
                thB.join();//main线程阻塞
            }
            System.out.println("main线程执行了" + i + "次");
        }
    }
}

以上代码当main线程打印到第10次的时候,vip线程就会插入强制优先执行,main线程会被阻塞,等待vip执行完才能继续执行。结果如下:


image.png

六、线程状态和优先级

1、线程状态

既然线程有生命周期,那就可以观测线程的状态 Thread.State

1、new
2、runnable
3、blocked
4、waiting
5、timed_waiting
6、terminated
上代码:

class ThreadDemo {
    static class Mythread implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("******");
        }

    }

    public static void main(String[] args) throws InterruptedException {
        Mythread mythread = new Mythread();
        Thread thread = new Thread(mythread, "线程A");
        Thread.State state = thread.getState();
        //观察状态
        System.out.println(state);// new

        thread.start();
        state = thread.getState();
        System.out.println(state); // runnable

        while (state != Thread.State.TERMINATED) {
            Thread.sleep(100);
            state = thread.getState();
            System.out.println(state);
        }

    }
}

运行结果如下:

image.png

2、线程优先级
每个线程都有一个优先级,从1到10上升,数字越大优先级越高,如果我们在创建一个线程的手不设置优先级,那么默认的优先级是5。
设置优先级使用thread.setPriority(value)方法进行设置。通过thread.getPriority()可以获得线程优先级数值。

七、Lamada表达式

lamada表达式是函数式编程的思想,看过js的我看到这个第一反应就是卧槽 这不就是es6里的箭头函数吗??
先看这么一句话:

任何一个接口如果只包含一个抽象方法,那么这个接口就可以称作函数式接口,对于函数式接口,我们就可以使用lamada表达式来创建对象。
如:
new Thread(()->System.out.println(”lamada表达式创建的线程"));

现在可能有的朋友看起来会比较懵逼,下面我们手撕代码,顺便复习下javase中的各种不同名称的类,大家脑子里赶紧想下有哪些类的写法?
我们先把头写好,主函数以及自定义一个函数是接口,也就是只有一个抽象方法的接口say。

public class ThreadDemo{
    public static void main(String[] args) {
        
    }
}

//定义一个函数式接口
interface Say{
    void say(); //只有一个说话的方法
}

1、常规写法:

//接口的实现类
class Person implements Say{

    @Override
    public void say() {
        System.out.println("我在学lamada表达式-常规写法");
    }
}

补全主函数内容:
public class ThreadDemo{
    public static void main(String[] args) {
        Person person = new Person();
        person.say();
    }
}

运行后应该会打印我在学lamada表达式-常规写法
2、实现类写在外面是不是不好看啊,我们可以把它放到主类里面,就构成静态内部类,改造:

public class ThreadDemo {

    // 接口的实现类-静态内部类
    static class Person implements Say {

        @Override
        public void say() {
            System.out.println("我在学lamada表达式-静态内部类");
        }
    }

    public static void main(String[] args) {
        Person person = new Person();
        person.say();
    }
}

3、这时候有的同学可能要说了我能不能把实现类写到主函数方法里?这就是局部内部类

public class ThreadDemo {

    public static void main(String[] args) {
        // 接口的实现类-局部内部类
        class Person implements Say {
            @Override
            public void say() {
                System.out.println("我在学lamada表达式-局部内部类");
            }
        }
        Person person = new Person();
        person.say();
    }
}

4、匿名内部类
以上这些写法是不是实现类都有名字啊,也就是Person,我们能不能不要名字,再作简化?直接通过接口来创建对象

public class ThreadDemo {

    public static void main(String[] args) {

        Say person = new Say() {

            @Override
            public void say() {
                System.out.println("我在学lamada表达式-匿名内部类");
            }
        };
        person.say();
    }
}

5、还想要简化?那就要请出lamada表达式了
之前说过,只有一个抽象方法的接口可以采用lamada表达式,我们这个say接口是不是只有一个say方法啊,如何写?赶紧自己想下

public class ThreadDemo {

    public static void main(String[] args) {

        Say person = ()->System.out.println("我在学lamada表达式-lamada写法");
        person.say();
    }
}

和上面那些写法相比是不是减少了大量代码,更简洁了呀。那么有的同学要问了,我只能写一句代码吗?我想写多条内容可以吗?当然可以,加上大括号就可以了:

Say person = () -> {
            System.out.println("我在学lamada表达式-lamada写法");
            System.out.println("多输出一句话也OK拉");
        };
        person.say();

而且lamada表达式也支持参数,具体写法这里就不多说了,感兴趣的自己去了解吧!

八、暴露问题-重要

前面学了很多相关的基础内容,感觉写的还挺顺利,多线程也挺厉害,可以同时执行多个任务,那么多线程会出问题吗?我们先来看一个例子,抢购火车票:

class ThreadDemo {

    static class BuyTicket implements Runnable {

        private int tickets = 10; // 总共有10张票
        boolean flag = true; // 抢购是否结束标志

        @Override
        public void run() {
            while (flag) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                buy();

            }

        }

        // 购票
        synchronized void buy() {
            if (tickets == 0) {
                this.flag = false;
            } else {
                System.out.println(Thread.currentThread().getName() + "抢到了第" + tickets-- + "票!");
                if (tickets == 0) {
                    this.flag = false;
                }
            }
        }
    }

    public static void main(String[] args) {
        BuyTicket buy = new BuyTicket();
        new Thread(buy, "小明").start();
        new Thread(buy, "小红").start();
        new Thread(buy, "可恶的黄牛").start();
    }
}

大家看下上面代码,先想想以上代码会输出什么,会有问题吗?

直接运行看下结果:
image.png

是不是发现很多问题啊,抢到同一张票,出现负数的票等,为什么会出现这个问题?因为多线程他们是同时进入开始操作是吧,这个问题出现于当多个对象使用同一份资源的时候,因为多线程对资源的操作是如何操作的?是把资源拷贝到自己的内存空间然后进行操作吧,也就是说小红、小明、黄牛这3个线程可能会同时分别把同一个资源拷贝到自己的内存空间进行操作,也就出现同时抢到了,但是实际服务器里并没有,当他们都抢完后,服务器会显示负数张票对吧。

如何解决这个问题呢?就要引入线程同步机制了,通过同步方法和同步块来解决,关键词:synchronized 本质就是加锁让他们排队。

举个栗子:大家去热门景点游玩,都肚子疼,但是厕所只有一个,大家想象一下,大家同时进去会出现怎样的场景,是不是会打架啊,排队是不是就能解决了?一个个去嘛

我们上代码看如何解决上面这个问题?

1、同步方法:

// 购票
        synchronized void buy() {
            if (tickets == 0) {
                this.flag = false;
            } else {
                System.out.println(Thread.currentThread().getName() + "抢到了第" + tickets-- + "票!");
                if (tickets == 0) {
                    this.flag = false;
                }
            }
        }

在run方法前加上synchronized,就可以了,再次运行,结果如下:


image.png

加入同步机制,让他们进行排队购票,就不会再出现上面那种问题了吧。

2、同步块

我们再来看另一个例子,你有一张银行卡,卡里有10万,今天同一时间你和你媳妇同时用钱,你用网银消费8W,你老婆呢刷卡消费8W,代码如下:

class ThreadDemo {
    public static void main(String[] args) {
        Back back = new Back(new Account("8888", 10), 8);
        new Thread(back, "你自己").start();
        new Thread(back, "你媳妇").start();
    }
}

// 账户
class Account {
    String id; // 卡号
    int money; // 卡里的钱

    public Account(String id, int money) {
        this.id = id;
        this.money = money;
    }
}
//银行
class Back implements Runnable {

    Account account;
    int takeMoney; // 取多少钱

    public Back(Account account, int takeMoney) {
        this.account = account;
        this.takeMoney = takeMoney;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
            takeoutMoney();
    }

    // 取钱
    void takeoutMoney() {
        System.out.println(Thread.currentThread().getName() + " 你好,当前账户剩余:" + account.money + "万");
        if (account.money < takeMoney) {
            System.out.println(Thread.currentThread().getName() + ": 当前账户余额不足");
        } else {
            int leftMoney = account.money - takeMoney;
            System.out.println(Thread.currentThread().getName() + "消费成功!当前余额:" + leftMoney);
            account.money = leftMoney;
        }
    }
}

大家看下代码后先想想会输出什么内容?
是不是你自己和媳妇能分别消费8万啊


image.png

这种情况是我们想看到的吗?当然是了呀。但是我估计很快银行就都倒闭了。
虽然你跟你媳妇是同时操作,但是是不是应该让他们排队啊,肯定有个先后顺序,我们看用同步块来解决这个问题:

//添加同步块,将账号信息锁定
        synchronized(account){
            takeoutMoney(); 
        }

以上什么意思呢,同步块可以锁定一个对象,让多个对象操作这个对象的时候进行排队,也就是我用的时候给他锁住,其他人用不了,我用完了解锁,我媳妇才能再用。
现在你和媳妇再去操作结果就会这样了:


image.png

记住同步方法的默认锁定对象是this,而同步块是具体你指定的对象

3、Lock锁

前面说的同步方法和同步块都是隐式的进行加锁和解锁吧,我们是看不到锁的,但是lock锁是可以自己定义和自己释放的,想锁哪里就锁哪里,我们看代码
还是那个买火车票的例子:

class ThreadDemo {
    static class BuyTicket implements Runnable {
        int num = 10;

        @Override
        public void run() {
            while (num > 0) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "买到了第" + num-- + "张票");
            }
        }

    }

    public static void main(String[] args) {
        BuyTicket buy = new BuyTicket();
        new Thread(buy, "小红").start();
        new Thread(buy, "张三").start();
        new Thread(buy, "黄牛").start();
    }
}

输出结果现在大家应该都清晰了吧,会有问题是吧,现在我们通过显示的添加lock锁来解决同步问题,修改部分代码:

        // 创建一把锁
        private final ReentrantLock lock = new ReentrantLock();

        @Override
        public void run() {
            try {
                lock.lock(); // 在需要锁定的区域添加锁
                while (num > 0) {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "买到了第" + num-- + "张票");
                }
            } finally {
                lock.unlock(); // 释放锁
            }
        }

记住这么一句话:锁lock.lock 必须紧跟在try代码块,且unlock必须写在finally代码块的第一行。

关于死锁

知道了同步方法和同步块后,我们来想这么一个问题,比如有两个小孩分别有汽车和飞机,同时呢他们又想去拿对方的玩具,自己的玩具又不肯松手,这会导致什么问题?是不是两边就僵持住了呀,在多线程中,互相抱着对方的资源,又不肯释放自己资源的现象叫死锁,我们应该避免死锁的出现。具体这里就不展开了,同学们可以自行查阅相关资料了解学习。

九、生产者-消费者问题-重点

什么是生产者-消费者模式?还是举个栗子:你去肯德基买吃的,你跟前台小姐姐说你要一个汉堡,她是不是会看橱窗里还有没有货啊,有的话她就会卖给你,没有她是不是会喊后面的阿姨做啊,然后会喊你等一会是吧。如果橱窗的容量只有10个汉堡,而里面已经有10个了,她是不是会喊阿姨先别做了呀,这个就是一个简单的生产者-消费者模式,你和阿姨之间存在一个小姐姐作为你们的缓冲。这样做有什么好处啊?你和阿姨是不是都是独立的呀,由前台小姐姐负责你们之间的业务呀,而且不会出现汉堡放不下了阿姨还在一直做,或者橱窗已经没东西了,还在不断让消费者购买。

废话不多说,撸个代码:

public class ThreadDemo {
    public static void main(String[] args) {
        Container container = new Container();
        new Thread(new Product(container)).start();
        new Thread(new Cunsumer(container)).start();
    }
}

// 缓冲区容器类
class Container {

    int goods = 0;

    //生产商品方法
    synchronized void put() {
        while (goods >= 5) {
            System.out.println("产品已满");
            try {
                this.wait(); //通知生产者等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("生产了第" + goods++ + "个产品");
        notifyAll(); //通知消费者消费
    };

    //消费东西方法
    synchronized void pop() {
        while (goods <= 0) {
            System.out.println("已经没有产品了");
            try {
                this.wait(); //通知消费者等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("消费了第" + goods-- + "个产品");
        notify(); //通知生产者生产

    }

}

// 生产者
class Product implements Runnable {
    Container container;

    public Product(Container container) {
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            container.put();
        }
    }

}

// 消费者
class Cunsumer implements Runnable {
    Container container;

    public Cunsumer(Container container) {
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            container.pop();
        }
    }

}

我们运行后看下结果:


image.png

现在可以看到生产多少才会消费多少,不会过度生产也不会过度消费。

十、总结

多线程中还有守护线程、线程池等其他内容和概念,这里就不多展开说了,有兴趣的同学可以自行查阅相关资料。而且关于上面讲的内容我这里都只是粗浅的个人见解和认识,尤其是多线程的进阶还有juc等高阶内容,我还没学到所以这篇文章更多的是对多线程基础知识的梳理,正所谓温故而知新,希望这篇文章可以帮助到你。

感谢您的阅读,有出错的地方还望见谅和指出。
我们下一篇文章见!

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

推荐阅读更多精彩内容