synchronized与ReentrantLock(Lock的默认实现)类

synchronized详解

  • 公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权。
  • 非公平锁则随机分配这种使用权。
synchronized的三种使用方式
  • 修饰实例方法(所谓实例对象锁就是用synchronized修饰实例对象的实例方法,注意是实例方法,不是静态方法)
  • 修饰静态方法(当synchronized作用于静态方法时,锁的对象就是当前类的Class对象)
  • 修饰代码块(手动指定锁定对象,也可是是this,也可以是自定义的锁)

// 修饰实例方法
public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence = new SynchronizedObjectLock();

    @Override
    public void run() {
        method();
    }

    public synchronized void method() {
        System.out.println("我是线程" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "结束");
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instence);
        Thread t2 = new Thread(instence);
        t1.start();
        t2.start();
    }
}输出结果:
  我是线程Thread-0  Thread-0结束  我是线程Thread-1  Thread-1结束
// 修饰静态方法
public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence1 = new SynchronizedObjectLock();
    static SynchronizedObjectLock instence2 = new SynchronizedObjectLock();

    @Override
    public void run() {
        method();
    }

    // synchronized用在普通方法上,默认的锁就是this,当前实例
    public synchronized void method() {
        System.out.println("我是线程" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "结束");
    }

    public static void main(String[] args) {
        // t1和t2对应的this是两个不同的实例,所以代码不会串行
        Thread t1 = new Thread(instence1);
        Thread t2 = new Thread(instence2);
        t1.start();
        t2.start();
    }
}输出结果:
  我是线程Thread-0  我是线程Thread-1  Thread-1结束  Thread-0结束
//修饰代码块
public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence = new SynchronizedObjectLock();

    @Override
    public void run() {
        // 同步代码块形式——锁为this,两个线程使用的锁是一样的,线程1必须要等到线程0释放了该锁后,才能执行
        synchronized (this) {
            System.out.println("我是线程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "结束");
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instence);
        Thread t2 = new Thread(instence);
        t1.start();
        t2.start();
    }
}输出结果:  我是线程Thread-0  Thread-0结束  我是线程Thread-1  Thread-1结束
synchronized的性质

1.可重入性
概念:指同一个线程外层函数获取到锁之后,内层函数可以直接使用该锁

好处:避免死锁,提升封装性(如果不可重入,假设method1拿到锁之后,在method1中又调用了method2,如果method2没办法使用method1拿到的锁,那method2将一直等待,但是method1由于未执行完毕,又无法释放锁,就导致了死锁,可重入正好避免这这种情况)但是synchronized的使用也有可能会出现死锁,这里的避免死锁的更底层的概念。
2.不可中断性

概念:如果这个锁被B线程获取,如果A线程想要获取这把锁,只能选择等待或者阻塞,直到B线程释放这把锁,如果B线程一直不释放这把锁,那么A线程将一直等待。

相比之下,未来的Lock类,可以拥有中断的能力(如果一个线程等待锁的时间太长了,有权利中断当前已经获取锁的线程的执行,也可以退出等待)

synchronized的原理
public class SynchronizedDemo2 {
    Object object = new Object();

    public void method1() {
        synchronized (object) {

        }
    }
}
使用javac命令进行编译生成.class文件

>javac SynchronizedDemo2.java

使用javap命令反编译查看.class文件的信息

>javap -verbose SynchronizedDemo2.class
得到如下的信息:
关注红色方框里的monitorenter和monitorexit即可。
image.png

Monitorenter和Monitorexit指令,会让对象在执行,使其锁计数器加1或者减1。每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得,一个对象在尝试获得与这个对象相关联的Monitor锁的所有权的时候,monitorenter指令会发生如下3中情况之一:

1)monitor计数器为0,意味着目前还没有被获得,那这个线程就会立刻获得然后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待

2)如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加

3)这把锁已经被别的线程获取了,等待锁释放

monitorexit指令:释放对于monitor的所有权,释放过程很简单,就是讲monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁。

2.可重入原理:加锁次数计数器

jvm会负责跟踪对象被加锁的次数

线程第一次获得所,计数器+1,当锁重入的时候,计数器会递增

当任务离开的时候(一个同步代码块的代码执行结束),计数器会减1,当减为0的时候,锁被完全释放。

synchronized的缺陷
  • 效率低:锁的释放情况少,只有代码执行完毕或者异常结束才会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock可以中断和设置超时

  • 不够灵活:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活

  • 无法知道是否成功获得锁,相对而言,Lock可以拿到状态,如果成功获取锁,....,如果获取失败,.....

3.可见性原理:

  • 在加锁前会将工作内存的值全部重新加载一遍,保证最新;释放锁前将工作内存的值全部更新到主存;由于在带锁期间,没有其他线程能访问本线程正在使用的共享变量,这样就保证了可见性
  • 由于Synchronized修饰的代码块都是原子性执行的,即一旦开始做,就会一直执行完毕,期间有其他线程不可以访问本线程所使用的共享变量,这样,即便指令重排了也不会出现问题。

4.synchronized非公平锁原理:synchronized底层使用操作系统mutext锁实现的,而mutext锁并不保证公平性,这样做的目的就是为了提高执行性能,当然缺点是有可能会产生线程饥饿。

synchronized和@Transactional一起使用时出现了synchronized失效的原因

1.@Transactional 注解是Spring 提供来进行控制事务的注解,当注解标明在方法上时, 则会对 该方法进行做AOP 增强(动态代理),然后在方法执行前,开启事务,执行后提交事务。
2.是因为Synchronized锁定的是当前调用方法对象,而Spring AOP 处理事务会进行生成一个代理对象,并在代理对象执行方法前的事务开启,方法执行完的事务提交,所以说,事务的开启和提交并不是在 Synchronized 锁定的范围内。出现同步锁失效的原因是:当A(线程) 执行完insert()方法,会进行释放同步锁,去做提交事务,但在A(线程)还没有提交完事务之前,B(线程)进行执行insert() 方法,执行完毕之后和A(线程)一起提交事务, 这时候就会出现线程安全问题,就会插入重复数据。

cglib代理synchronized的方法,同步关键字是否还在?

在动态生成的子类中不存在synchronized,但synchronized会有效果。因为动态代理回去调用父类的方法,父类的方法是被锁住的。

Synchronized本质上是通过什么保证线程安全的?分三个方面回答:加锁和释放锁的原理,可重入原理,保证可见性原理。
Synchronized由什么样的缺陷?Java Lock是怎么弥补这些缺陷的。
  • 1.当线程尝试获取锁的时候,如果获取不到锁会一直阻塞,这个阻塞的过程,用户无法控制
  • 2.如果获取锁的线程进入休眠或者阻塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待
Synchronized和Lock的对比,和选择?
Synchronized在使用时有何注意事项?
Synchronized修饰的方法在抛出异常时,会释放锁吗?
  • 线程在执行同步方法时抛出异常,会自动释放锁,以便其他线程可以拿到锁继续执行。
多个线程等待同一个synchronized锁的时候,JVM如何选择下一个获取锁的线程?
  • 这个问题就涉及到内部锁的调度机制,线程获取 synchronized 对应的锁,也是有具体的调度算法的,这个和具体的虚拟机版本和实现都有关系,所以下一个获取锁的线程是事先没办法预测的。
Synchronized使得同时只有一个线程可以执行,性能比较差,有什么提升的方法?
  • 优化 synchronized 的使用范围,让临界区的代码在符合要求的情况下尽可能的小。
  • 使用其他类型的 lock(锁),synchronized 使用的锁经过 jdk 版本的升级,性能已经大幅提升了,但相对于更加轻量级的锁(如读写锁)还是偏重一点,所以可以选择更合适的锁。
我想更加灵活地控制锁的释放和获取(现在释放锁和获取锁的时机都被规定死了),怎么办?
  • 可以根据需要实现一个 Lock 接口,这样锁的获取和释放就能完全被我们控制了。
什么是锁的升级和降级?什么是JVM里的偏斜锁、轻量级锁、重量级锁?
  • JDK6 之后,不断优化 synchronized,提供了三种锁的实现,分别是偏向锁、轻量级锁、重量级锁,还提供自动的升级和降级机制。对于不同的竞争情况,会自动切换到合适的锁实现。当没有竞争出现时,默认使用偏斜锁,也即是在对象头的 Mark Word 部分设置线程ID,来表示锁对象偏向的线程,但这并不是互斥锁;当有其他线程试图锁定某个已被偏斜过的锁对象,JVM 就撤销偏斜锁,切换到轻量级锁,轻量级锁依赖 CAS 操作对象头的 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则进一步升级为重量级锁。锁的降级发生在当 JVM 进入安全点后,检查是否有闲置的锁,并试图进行降级。锁的升级和降级都是出于性能的考虑。
    偏向锁:在线程竞争不激烈的情况下,减少加锁和解锁的性能损耗,在对象头中保存获得锁的线程ID信息,如果这个线程再次请求锁,就用对象头中保存的ID和自身线程ID对比,如果相同,就说明这个线程获取锁成功,不用再进行加解锁操作了,省去了再次同步判断的步骤,提升了性能。

  • 轻量级锁:再线程竞争比偏向锁更激烈的情况下,在线程的栈内存中分配一段空间作为锁的记录空间(轻量级锁对应的对象的对象头字段的拷贝),线程通过CAS竞争轻量级锁,试图把对象的对象头字段改成指向锁记录的空间,如果成功就说明获取轻量级锁成功,如果失败,则进入自旋(一定次数的循环,避免线程直接进入阻塞状态)试图获取锁,如果自旋到一定次数还不能获取到锁,则进入重量级锁。

  • 自旋锁:获取轻量级锁失败后,避免线程直接进入阻塞状态而采取的循环一定次数去尝试获取锁。(线程进入阻塞状态和非阻塞状态都是涉及到系统层面的,需要在用户态到内核态之间切换,非常消耗系统资源)实验证明,锁的持有时间一般是非常短的,所以一般多次尝试就能竞争到锁。

  • 重量级锁:在 JVM 中又叫做对象监视器(monitor),锁对象的对象头字段指向的是一个互斥量,多个线程竞争锁,竞争失败的线程进入阻塞状态(操作系统层面),并在锁对象的一个等待池中等待被唤醒,被唤醒后的线程再次竞争锁资源。

ReentrantLock详解

  • 1.可重入锁:可重入锁是指同一个线程可以多次获得同一把锁;ReentrantLock和关键字Synchronized都是可重入锁
  • 2.可中断锁:可中断锁时子线程在获取锁的过程中,是否可以相应线程中断操作。synchronized是不可中断的,ReentrantLock是可中断的
  • 3.公平锁和非公平锁:公平锁是指多个线程尝试获取同一把锁的时候,获取锁的顺序按照线程到达的先后顺序获取,而不是随机插队的方式获取。synchronized是非公平锁,而ReentrantLock是两种都可以实现,不过默认是非公平锁
ReentrantLock的基本使用
public class Demo3 {

    private static int num = 0;

    private static ReentrantLock lock = new ReentrantLock();

    private static void add() {
        lock.lock();
        try {
            num++;
        } finally {
//注意lock.unlock()一定要放在finally中,否则,
//若程序出现了异常,锁没有释放,那么其他线程就再也没有机会获取这个锁了。
            lock.unlock();
        }

    }

    public static class T extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                Demo3.add();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        T t1 = new T();
        T t2 = new T();
        T t3 = new T();

        t1.start();
        t2.start();
        t3.start();

        t1.join();
        t2.join();
        t3.join();

        System.out.println(Demo3.num);
    }
}
ReentrantLock的使用过程:

1.创建锁:ReentrantLock lock = new ReentrantLock();
2.获取锁:lock.lock()
3.释放锁:lock.unlock();

ReentrantLock的方法
无参构造器(默认为非公平锁)
public ReentrantLock() {
        sync = new NonfairSync();//默认是非公平的
    }
带布尔值的构造器(是否公平)

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();//fair为true,公平锁;反之,非公平锁
    }
Sync的lock方法是抽象的,实际的lock会代理到FairSync或是NonFairSync上(根据用户的选择来决定,公平锁还是非公平锁)
public void lock() {
        sync.lock();//代理到Sync的lock方法上
    }
  
此方法响应中断,当线程在阻塞中的时候,若被中断,会抛出InterruptedException异常 
public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);//代理到sync的相应方法上,同lock方法的区别是此方法响应中断
    }
tryLock,尝试获取锁,成功则直接返回true,不成功也不耽搁时间,立即返回false。
public boolean tryLock() {
        return sync.nonfairTryAcquire(1);//代理到sync的相应方法上
    }
ReentrantLock的实现原理:
  • 它的设计基于AQS框架
  • 在 Lock 中,用到了一个同步队列 AQS,全称 AbstractQueuedSynchronizer,它 是一个同步工具也是Lock用来实现线程同步的核心组件。如果你搞懂了AQS,那 么J.U.C中绝大部分的工具都能轻松掌握

AQS 的两种功能

  • 从使用层面来说,AQS的功能分为两种:独占和共享
  • 独占锁(写锁),每次只能有一个线程持有锁,比如前面给大家演示的ReentrantLock就是 以独占方式实现的互斥锁
  • 共 享 锁(读锁) , 允 许 多 个 线 程 同 时 获 取 锁 , 并 发 访 问 共 享 资 源 , 比 如 ReentrantReadWriteLock

AQS 的内部实现

  • AQS 队列内部维护的是一个 FIFO (先进先出)的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。
  • 所以双向链表可以从任 意一个节点开始很方便的访问前驱和后继。每个 Node 其实是由线程封装,当线 程争抢锁失败后会封装成Node加入到ASQ队列中去;当获取锁的线程释放锁以 后,会从队列中唤醒一个阻塞的节点(线程)。


    image.png
从源码看ReentrantLock非公平锁实现原理

简单总结下流程:

1.先获取state值,若为0,意味着此时没有线程获取到资源,CAS将其设置为1,设置成功则代表获取到排他锁了;
2.若state大于0,肯定有线程已经抢占到资源了,此时再去判断是否就是自己抢占的,是的话,state累加,返回true,重入成功,state的值即是线程重入的次数;
3.其他情况,则获取锁失败。

NonFairSync(非公平可重入锁)
static final class NonfairSync extends Sync {//继承Sync
        private static final long serialVersionUID = 7316153563782823691L;
        /** 获取锁 */
        final void lock() {
            if (compareAndSetState(0, 1))//CAS设置state状态,若原值是0,将其置为1
                setExclusiveOwnerThread(Thread.currentThread());//将当前线程标记为已持有锁
            else
                acquire(1);//若设置失败,调用AQS的acquire方法,acquire又会调用我们下面重写的tryAcquire方法。这里说的调用失败有两种情况:1当前没有线程获取到资源,state为0,但是将state由0设置为1的时候,其他线程抢占资源,将state修改了,导致了CAS失败;2 state原本就不为0,也就是已经有线程获取到资源了,有可能是别的线程获取到资源,也有可能是当前线程获取的,这时线程又重复去获取,所以去tryAcquire中的nonfairTryAcquire我们应该就能看到可重入的实现逻辑了。
        }
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);//调用Sync中的方法
        }
    }


nonfairTryAcquire()
final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();//获取当前线程
            int c = getState();//获取当前state值
            if (c == 0) {//若state为0,意味着没有线程获取到资源,CAS将state设置为1,并将当前线程标记我获取到排他锁的线程,返回true
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {//若state不为0,但是持有锁的线程是当前线程
                int nextc = c + acquires;//state累加1
                if (nextc < 0) // int类型溢出了
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);//设置state,此时state大于1,代表着一个线程多次获锁,state的值即是线程重入的次数
                return true;//返回true,获取锁成功
            }
            return false;//获取锁失败了
        }
从源码看ReentrantLock公平锁实现原理
FairSync

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);//直接调用AQS的模板方法acquire,acquire会调用下面我们重写的这个tryAcquire
        }

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();//获取当前线程
            int c = getState();//获取state值
            if (c == 0) {//若state为0,意味着当前没有线程获取到资源,那就可以直接获取资源了吗?NO!这不就跟之前的非公平锁的逻辑一样了嘛。看下面的逻辑
                if (!hasQueuedPredecessors() &&//判断在时间顺序上,是否有申请锁排在自己之前的线程,若没有,才能去获取,CAS设置state,并标记当前线程为持有排他锁的线程;反之,不能获取!这即是公平的处理方式。
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {//重入的处理逻辑,与上文一致,不再赘述
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

可以看到,公平锁的大致逻辑与非公平锁是一致的,不同的地方在于有了!hasQueuedPredecessors()这个判断逻辑,即便state为0,也不能贸然直接去获取,要先去看有没有还在排队的线程,若没有,才能尝试去获取,做后面的处理。反之,返回false,获取失败。

看看这个判断是否有排队中线程的逻辑

hasQueuedPredecessors()
public final boolean hasQueuedPredecessors() {
        Node t = tail; // 尾结点
        Node h = head;//头结点
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());//判断是否有排在自己之前的线程
    }
注意
  • 1.ReentrantLock可以实现公平锁和非公平锁
  • 2.ReentrantLock默认实现的是非公平锁
  • 3.ReentrantLock的获取锁和释放锁必须成对出现,锁了几次,也要释放几次
  • 4.释放锁的操作必须放在finally中执行
  • 5.lockInterruptibly()实例方法可以相应线程的中断方法,调用线程的- interrupt()方法时,lockInterruptibly()方法会触发InterruptedException异常
  • 6.关于InterruptedException异常说一下,看到方法声明上带有 throws InterruptedException,表示该方法可以相应线程中断,调用线程的interrupt()方法时,这些方法会触发InterruptedException异常,触发InterruptedException时,线程的中断中断状态会被清除。所以如果程序由于调用interrupt()方法而触发InterruptedException异常,线程的标志由默认的false变为ture,然后又变为false
  • 7.实例方法tryLock()会尝试获取锁,会立即返回,返回值表示是否获取成功
  • 8.实例方法tryLock(long timeout, TimeUnit unit)会在指定的时间内尝试获取锁,指定的时间内是否能够获取锁,都会返回,返回值表示是否获取锁成功,该方法会响应线程的中断
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352

推荐阅读更多精彩内容