一、volatile和sychronized

1 volatile

1.1 volatile的应用

Java语言规范第3版中对volatile的定义如下:Java编程语言允许线程访问共享变量,为了确保共享变量能够被准确和一致的更新,线程应该确保通过排他锁单独获取这个变量。

也就是说,使用volatile修饰的变量,可以保证其“可见性”

  • 何为“可见性”?
    就是一个线程的修改,可以让另一个线程”感知“到。

  • 为什么需要“可见性”?
    所谓“共享变量”就是所有线程都可以使用的变量,存储于内存中,每个使用到该变量的线程,都要保存一份变量的副本到自己对应的CPU高速缓存中,每个CPU高速缓存都是相互独立的,当CPU修改共享变量后(其实只是修改的共享变量的副本),需要写回到内存中,而写回到内存中这个操作,时机是不确定的,所以就可能造成该共享变量已经修改(但并未写回内存),但其他高速缓存中仍然保存着旧值的副本的情况。

1.2 volatile 的作用及原理

使用volatile修饰的共享变量,会在编译时增加一个“Lock”的前缀指令,该指令会引发两件事情:

  • 1)将当前处理器缓存行的数据立即写回系统内存。
  • 2)这个写回内存的操作会使其他CPU里缓存了该内存地址的数据无效。

为了提高运行速度,CPU是不和内存直接通信的,而是把系统内存的数据缓存到内部缓存再进行操作,操作后,并不确定何时写回内存,而使用volatile修饰的变量会让CPU将当前缓存行立即写回内存。但即使在写回内存后,其他CPU里缓存的数据仍然可能是旧值,所以,在多处理器下,就会实现缓存一致性协议来避免这个问题。每个处理器通过嗅探在总线上传播的数据来检查自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改,就会将当前缓存行设置为无效状态,当处理器需要对这个数据进行操作时,再重新从内存中读取。

2. synchronized的实现原理及应用

synchronized是多线程并发编程中的元老,亦可称为”重量级锁“

以下例子展示synchronized的用法:

实例一:

package com.lipeng.second;
 
import java.util.concurrent.TimeUnit;
/**
 * 使用Synchronized修饰方法,同一时间只能有一个线程访问被同步的代码
 * SyncDemo1-Thread-1、SyncDemo1-Thread-2访问同步代码
 * SyncDemo1-Thread-3、SyncDemo1-Thread-4访问非同步代码
 * @author promi
 *
 */
public class SyncDemo1 {
    static Sync1 sync=new Sync1();
    public static void main(String[] args) {
        Thread thread1=new Thread(new Runnable() {
            @Override
            public void run() {
                sync.syncAction();
            }
        },"SyncDemo1-Thread-1");
        Thread thread2=new Thread(new Runnable() {
            @Override
            public void run() {
                sync.syncAction();
            }
        },"SyncDemo1-Thread-2");
        Thread thread3=new Thread(new Runnable() {
            @Override
            public void run() {
                sync.noSyncAction();
            }
        },"SyncDemo1-Thread-3");
        Thread thread4=new Thread(new Runnable() {
            @Override
            public void run() {
                sync.noSyncAction();
            }
        },"SyncDemo1-Thread-4");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}
class Sync1{
    public synchronized void syncAction(){
        System.out.println(Thread.currentThread().getName()+" 执行syncAction方法 ,TimeStrap:"+System.currentTimeMillis());
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public void noSyncAction(){
        System.out.println(Thread.currentThread().getName()+"执行noSyncAction方法,TimeStrap:"+System.currentTimeMillis());
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果:


通过结果可以看到线程1和2在执行同步代码时,线程1先获取到锁,直到3秒后释放锁,线程2才可以获取到锁并执行。

实例二:

package com.lipeng.second;
 
import java.util.concurrent.TimeUnit;
/**
 * 使用synchronized封装代码块获取"对象锁"
 * SyncDemo1-Thread-1、SyncDemo1-Thread-2访问同步代码
 * SyncDemo1-Thread-3、SyncDemo1-Thread-4访问非同步代码
 * @author promi
 *
 */
public class SyncDemo2 {
    static Sync2 sync=new Sync2();
    public static void main(String[] args) {
        Thread thread1=new Thread(new Runnable() {
            @Override
            public void run() {
                sync.syncAction();
            }
        },"SyncDemo2-Thread-1");
        Thread thread2=new Thread(new Runnable() {
            @Override
            public void run() {
                sync.syncAction();
            }
        },"SyncDemo2-Thread-2");
        Thread thread3=new Thread(new Runnable() {
            @Override
            public void run() {
                sync.noSyncAction();
            }
        },"SyncDemo2-Thread-3");
        Thread thread4=new Thread(new Runnable() {
            @Override
            public void run() {
                sync.noSyncAction();
            }
        },"SyncDemo2-Thread-4");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}
class Sync2{
    public void syncAction(){
        synchronized(this){
            System.out.println(Thread.currentThread().getName()+" 执行syncAction方法 ,TimeStrap:"+System.currentTimeMillis());
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public void noSyncAction(){
        System.out.println(Thread.currentThread().getName()+" 执行noSyncAction方法 ,TimeStrap:"+System.currentTimeMillis());
    }
}

运行结果:


Synchronized(this)表示在执行该代码块之前需要获取到对象锁。在线程1获取到对象锁后,其他线程仍然可访问其他未同步的代码。

实例三:

package com.lipeng.second;
 
import java.util.concurrent.TimeUnit;
/**
 * 使用synchronized封装代码块获取"类锁"
 * SyncDemo1-Thread-1、SyncDemo1-Thread-2访问同步代码
 * SyncDemo1-Thread-3、SyncDemo1-Thread-4访问非同步代码
 * @author promi
 *
 */
public class SyncDemo3 {
    public static void main(String[] args) {
        Thread thread1=new Thread(new Runnable() {
            @Override
            public void run() {
                Sync3 sync=new Sync3();
                sync.syncAction();
            }
        },"SyncDemo3-Thread-1");
        Thread thread2=new Thread(new Runnable() {
            @Override
            public void run() {
                Sync3 sync=new Sync3();
                sync.syncAction();
            }
        },"SyncDemo3-Thread-2");
        Thread thread3=new Thread(new Runnable() {
            @Override
            public void run() {
                Sync3 sync=new Sync3();
                sync.noSyncAction();
            }
        },"SyncDemo3-Thread-3");
        Thread thread4=new Thread(new Runnable() {
            @Override
            public void run() {
                Sync3 sync=new Sync3();
                sync.noSyncAction();
            }
        },"SyncDemo3-Thread-4");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}
class Sync3{
    public void syncAction(){
        synchronized(Sync3.class){
            System.out.println(Thread.currentThread().getName()+" 执行syncAction方法 ,TimeStrap:"+System.currentTimeMillis());
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public void noSyncAction(){
        System.out.println(Thread.currentThread().getName()+" 执行noSyncAction方法 ,TimeStrap:"+System.currentTimeMillis());
    }
}

运行结果:


实例一和二的锁,都是对象锁即每个线程中使用的都是同一个Sync对象,若在每个线程中声明不同的Sync对象,则不会出现线程阻塞等待锁的情况,因为每个线程获取到及需要获取的对象锁并不是同一个。但实例三种同步代码块中需要获取“类锁”即使在每个线程中声明不同的Sync对象,也避免不了锁的等待。因为该锁是“属于类的”,是同一个

总结synchronized使用形式:
a),对于普通方法,锁是当前对象。
b),对于静态同步方法,锁是当前类的Class对象。
c),对于同步方法块,锁是synchronized括号里配置的对象。

2.1 synchronized在JVM里的实现

从JVM规范中可以看到Synchonized在JVM里的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter 和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明。但是,方法的同步同样可以使用这两个指令来实现。

monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结 束处和异常处,JVM要保证每个 monitorenter 必须有对应的 monitorexit 与之配对。任何对象都有 一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到 monitorenter 指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

我们先通过反编译下面的代码来看看Synchronized是如何实现对代码块进行同步的:

package com.paddx.test.concurrent;

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("Method 1 start");
        }
    }
}

反编译结果:

关于这两条指令的作用,我们直接参考JVM规范中描述:

monitorenter :

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

这段话的大概意思为:

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

monitorexit:

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

这段话的大概意思为:

执行monitorexit的线程必须是objectref所对应的monitor的所有者。

指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

我们再来看一下同步方法的反编译结果:

源代码:

package com.paddx.test.concurrent;

public class SynchronizedMethod {
    public synchronized void method() {
        System.out.println("Hello World!");
    }
}

反编译结果:


从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

2.2 synchronized用的锁是存在哪里的

synchronized用到的锁信息是存在Java对象头里的

3 Java的对象模型和对象头

3.1 Java的对象模型

3.1.1 在Java程序运行过程中,每创建一个新的对象,在JVM内部就会相应地创建一个对应类型的OOP对象。在HotSpot中,根据JVM内部使用的对象业务类型,具有多种oopDesc的子类。除了oppDesc类型外,opp体系中还有很多instanceOopDesc、arrayOopDesc 等类型的实例,他们都是oopDesc的子类。

这些OOPS在JVM内部有着不同的用途,例如,instanceOopDesc表示类实例,arrayOopDesc表示数组。也就是说,当我们使用new创建一个Java对象实例的时候,JVM会创建一个instanceOopDesc对象来表示这个Java对象。同理,当我们使用new创建一个Java数组实例的时候,JVM会创建一个arrayOopDesc对象来表示这个数组对象。

3.1.2 JVM在运行时,需要一种用来标识Java内部类型的机制。在HotSpot中的解决方案是:为每一个已加载的Java类创建一个instanceKlass对象,用来在JVM层表示Java类。

3.1.3 关于一个Java对象,他的存储是怎样的,一般很多人会回答:对象存储在堆上。稍微好一点的人会回答:对象存储在堆上,对象的引用存储在栈上。今天,再给你一个更加显得牛逼的回答:

对象的实例(instantOopDesc)保存在堆上,对象的元数据(instantKlass)保存在方法区,对象的引用保存在栈上。

其实如果细追究的话,上面这句话有点故意卖弄的意思。因为我们都知道。方法区用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 所谓加载的类信息,其实不就是给每一个被加载的类都创建了一个 instantKlass对象么。

class Model
{
    public static int a = 1;
    public int b;
 
    public Model(int b) {
        this.b = b;
    }
}
 
public static void main(String[] args) {
    int c = 10;
    Model modelA = new Model(2);
    Model modelB = new Model(3);
}

存储结构如下:


3.2 Java的对象头

每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据。

这里提到的对象头到底是什么呢?



instanceOopDesc表示类实例,arrayOopDesc表示数组。也就是说,当我们使用new创建一个Java对象实例的时候,JVM会创建一个instanceOopDesc对象来表示这个Java对象。同理,当我们使用new创建一个Java数组实例的时候,JVM会创建一个arrayOopDesc对象来表示这个数组对象。

class oopDesc {
  friend class VMStructs;
 private:
  volatile markOop  _mark;
  union _metadata {
    wideKlassOop    _klass;
    narrowOop       _compressed_klass;
  } _metadata;
}

HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头、实例数据和对齐填充。在虚拟机内部,一个Java对象对应一个instanceOopDesc的对象.上面代码中的_mark和_metadata其实就是对象头的定义。关于_metadata,_metadata是一个联合体,这个字段被称为元数据指针。指向描述类型Klass对象的指针。由于这个专题主要想介绍和JAVA并发相关的知识,所以本文展开介绍一下_mark ,即mark word

对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。

对markword的设计方式上,非常像网络协议报文头:将mark word划分为多个比特位区间,并在不同的对象状态下赋予比特位不同的含义。下图描述了在32位虚拟机上,在对象不同状态时 mark word各个比特位区间的含义。


从上图中可以看出,对象的状态一共有五种,分别是无锁态、轻量级锁、重量级锁、GC标记和偏向锁。在32位的虚拟机中有两个Bits是用来存储锁的标记位的,但是我们都知道,两个bits最多只能表示四种状态:00、01、10、11,那么第五种状态如何表示呢 ,就要额外依赖1Bit的空间,使用0和1来区分

在32位的HotSpot虚拟机 中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志位,1Bit固定为0,表示非偏向锁。

4 锁的升级与对比

4.1 偏向锁

HotSpot [1] 的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程

上文中黑体字部分,写得太简略,以致于很多初学者,对这个过程有点不明白,这个过程是怎么实现锁的升级、释放的?下面一一分析

  1. 线程2来竞争锁对象;
  2. 判断当前对象头是否是偏向锁;
  3. 判断拥有偏向锁的线程1是否还存在;
  4. 线程1不存在,直接设置偏向锁标识为0(线程1执行完毕后,不会主动去释放偏向锁);
  5. 使用cas替换偏向锁线程ID为线程2,锁不升级,仍为偏向锁;
  6. 线程1仍然存在,暂停线程1;
  7. 设置锁标志位为00(变为轻量级锁),偏向锁为0;
  8. 从线程1的空闲monitor record中读取一条,放至线程1的当前monitor record中;
  9. 更新mark word,将mark word指向线程1中monitor record的指针;
  10. 继续执行线程1的代码;
  11. 锁升级为轻量级锁;
  12. 线程2自旋来获取锁对象;
偏向锁争夺过程以及升级过程

4.2 轻量级锁

(1)轻量级锁加锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
(2)轻量级锁解锁
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。下图是两个线程同时争夺锁,导致锁膨胀的流程图。


轻量级锁及膨胀

因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。


5 Java的原子操作

在Java中可以通过循环CAS的方式来实现原子操作

5.1 使用循环CAS实现原子操作

CAS的全称是Compare And Swap 即比较交换,其算法核心思想如下
执行函数:CAS(V,E,N)
其包含3个参数
V表示要更新的变量
E表示预期值
表示新值

自旋CAS实现的基本 思路就是循环进行CAS操作直到成功为止

5.2 CAS实现原子操作的三大问题

1)ABA问题。因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化 则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它 的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面 追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。从 Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个 类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

2)循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

3)只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循 环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来 操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始, JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对 象里来进行CAS操作。

5.3 使用锁机制实现原子操作

锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。有意思的是除了偏向锁,JVM实现锁的方式都用了循环 CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时 候使用循环CAS释放锁。

.

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

推荐阅读更多精彩内容