Java 虚拟机详解

一、Java内存模型(JMM)

堆(线程共享)

  • Java程序中通过创建出来的对象存放在堆内存中,其成员变量和数据存放在对象的内部,堆内存中的对象不可以共享,空间不是连续的。
  • 堆是Java垃圾收集器管理的主要区域,JVM会在CPU空闲时或内存空间不足时会进行垃圾回收,也可以通过代码调用System.gc()方法请求JVM进行垃圾回收,但JVM不一定立刻执行垃圾回收,可以通过代码调用System.runFinalization()方法强制调用失去引用的对象的finalize()方法,加速JVM垃圾回收速度。
  • 堆由三部分组成,分别是新生区、老年区、永久区,所有新建的对象都会存放在新生区,随着对象的创建越来越多,新生区会执行轻GC,清理掉大量失去引用的局部变量,当新生区满了时会把部分对象转移到老年区,老年区会执行重GC,当新生区、老年区同时满了时会抛出OOM异常。

Java栈(线程私有)

  • Java栈的特点是先进后出,后进先出,生命周期和线程同步,线程结束时释放栈内存,不存在垃圾回收问题。
  • Java程序中的八大基本类型的数据、堆中实例对象的引用地址、实例对象的方法调用都存放在栈中,引用是栈指向堆的过程,也称Java的指针。

本地方法栈(线程私有)

  • 本地方法栈主要存放被native关键字修饰的方法,本地方法可以调用本地方法接口JNI(JavaNativeInterface),可以实现对本地方法库中底层语言C、C++方法的调用。

方法区(线程共享)

  • Java程序中已经被虚拟机加载的类信息(构造方法、接口)、常量(final)、静态变量(static)、运行时常量池,都存放在方法区中,可被线程共享。
  • 运行时常量池用来存放Java程序运行时产生的字符串常量(String类的intern()方法)和八大基本数据类型的常量,常量在编译期以HashSet的形式放入常量池,常量可以实现对象共享。

程序计数器(线程私有)

  • 程序计数器占据非常小的内存空间,用来存储即将执行指令的地址,便于执行引擎读取下一条指令,这样就能理解代码为何能有序地从上往下执行。

JVM参数设置

-Xmx128m -> Java堆内存的最大值,默认为物理内存的1/4
-Xms64m -> Java堆内存的初始值
-Xmn32m -> Java堆内存新生代的大小
-Xss16m -> Java线程的栈内存大小

二、Java垃圾回收算法

  • 标记-清除算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1。当引用失效时,计数器值就减1。任何时刻计数器都为0的对象就是不再被使用的,垃圾收集器将回收该对象使用的内存。
  • 复制-清除算法:将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就把还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况。
  • 标记-压缩算法:结合了标记-清除算法和复制-清除算法,通过标记得到存活的对象,让对象都紧挨在一起,从而避免内存碎片的产生,同时保证内存的高速分配。

三、对象的四种引用方式

  • 强引用,只要引用存在,垃圾回收器永远不会进行垃圾回收,当内存开销不足时,抛出OutOfMemory运行时异常,即常说的内存泄漏或内存溢出。强引用是开发中最常用的方式,如:Object obj = new Object(),直到obj对象的引用被释放后,new Object()才会被垃圾回收器回收,如:obj = null,obj = new Object(),前者没有指向任何引用,后者指向新的对象。
  • 软引用,当内存开销不足时,垃圾回收器会对该引用类型进行回收。常用在内存缓存的场景中,内存足够时在软引用对象中获取数据,内存不足时从数据库里获取实时数据。如:SoftReference<Object> softReference = new SoftReference<>(new Object()),Object obj = softReference.get(),当对象被回收后获取的结果为Null。
  • 弱引用,垃圾回收器进行垃圾回收时会对该引用类型进行回收。如:
    WeakReference<Object> weakReference = new WeakReference<>(new Object()),Object obj = weakReference.get(),可以通过weakReference.isEnqueued()查询对象是否被垃圾回收器标记,当对象被回收后获取的结果为Null。
  • 虚引用,也被称为幽灵引用或者幻影引用,不会对对象的生命周期造成任何影响,也无法获取到对象的引用实例,常用于跟踪垃圾回收的过程和监听回收的频率,垃圾回收器进行垃圾回收时会对该引用类型进行回收。
Object object = new Object();
ReferenceQueue<Object> queue = new ReferenceQueue<>(); -> 保存对象被回收的虚引用
PhantomReference<Object> phantomReference = new PhantomReference<>(object,queue);

object = null; -> 切断对象的引用
System.gc(); -> 请求垃圾回收
System.runFinalization(); -> 强制调用对象的finalize()方法

Reference<? extends Object> reference = queue.poll(); -> 取出虚引用
phantomReference == reference; -> true
  • 编译期:编译器是一种计算机程序,它能将程序的源码(.java)编译成计算机可以执行的文件(.class),在翻译过程中会检查代码语法错误,不会把代码放入内存中运行,从中会产生一些指令,如:分配内存大小、存放位置等。
  • 运行期:程序编译后的文件(.class)交由计算机执行开始,直至计算机不执行结束,按照编译期生成的指令运行,类的加载也发生在该时期。
  • 类加载机制:加载阶段,JVM将字节码文件转化为二进制字节流加载到内存中,接为这个类在方法区创建对应的 Class 对象,这个对象就是各种数据的访问入口。验证阶段,包括JVM规范校验和代码逻辑校验。准备阶段,分配类变量内存和变量类型的初始化。解析阶段,针对类、接口、方法、字段、限定符等解析。初始化阶段,用户的代码执行初始化。使用阶段,程序启动完成供用户使用。卸载阶段,程序的字节码在JVM内存中卸载停止使用。

四、包装与解包

包装类 基础类型 包装 解包 缓存
Byte byte Byte b = (byte) 1 b.byteValue() -128~127
Boolean boolean Boolean b = true b.booleanValue() TRUE FLASE
Short short Short s = (short) 1 s.shortValue() -128~127
Character char Character c = (char) 1 c.charValue() 0~127
Integer int Integer i = 1 i.intValue() -128~127
Long long Long l = Long.valueOf(1L) l.longValue() -128~127
Float float Float f = Float.valueOf(1f) f.floatValue()
Double double Double d = Double.valueOf(1d) d.doubleValue()
int int1 = 10,int2 = 10;
Integer in1 = 127,in2 = 127;
Integer out1 = 128,out2 = 128;
Integer integer1 = new Integer(10);
Integer integer2 = new Integer(10);
Integer integer3 = new Integer(127);

int1 == int2; -> true,基本类型比较数值
int1 == integer1; -> true,包装类自动拆包,本质是基本类型相比较
integer1 == integer2; -> false,两个引用对象比较的是堆内存的地址
integer3 == in1; -> false,引用对象和常量的存放地址不同
in1 == in2; -> true,数值在包装类的缓存区间中
out1 == out2; -> false,数值不在包装类的缓存区间中,本质是两个引用对象相比较
  • 当基本类型和包装类型比较时,包装类型会自动拆包生成其对应的基本类型,基本类型之间的比较,使用"=="即可。
  • 当包装类型的对象是通过new关键字创建,对象实例存在放堆内存中,引用对象持有堆内存的指针,两个对象之间通过"=="比较的是对象的堆内存地址,比较结果不会相等,两个对象之间通过equals()方法比较的是它们的值,数据存放在栈内存中,栈的特点是数据共享,所以相同的数据仅有一份,比较结果可能相等也可能不相等。
  • 当包装类型中的一个对象是直接赋予确定的数值,另一个对象是通过new关键字创建时,直接定义的对象属于常量,存放在常量池中,new关键字创建的对象属于实例对象,存放在堆内存中,两个对象通过"=="比较的结果显然是不相等的,需要比较它们的值是否相等可以通过equals()方法。
  • 当包装类型中的两个对象同时直接赋予确定的数值,以Integer为例子,包装类会默认缓存 -128~127区间中的数值,当直接赋予的常量值在此区间内,两个对象通过"=="比较结果会相等,反之超出区间通过"=="比较结果不会相等。
String name = new String("wjx"); -> 这个过程会创建1~2个对象(堆内存和常量池)
String wjx = "wjx";
String w = "w";
String j = "j";
String x = "x";

name == wjx; -> false,引用对象和常量比较,内存地址不一样
wjx == "w"+"j"+"x"; -> true,常量在编译期已经确定
wjx == w+j+x; -> false,变量在运行期才能确定
wjx == (w+j+x).intern(); -> true,运行期设置常量

五、常见面试题

运算符“==”和equals()方法的区别

  • “==”在比较Java基本数据类型(byte、int、long、short、double、float、boolean、char)时是比较它们的值,在比较引用对象类型时是比较它们在堆内存中的地址。
  • equals的本质是==,Object类中的equals()方法默认是比较两个对象在堆内存中的地址,重写后比较它们两个的值是否相等。

equals()和hashCode()的区别

  • equals()相等的两个对象其hashCode()肯定相等。
  • hashCode()相等的两个对象其equals()不一定相等。
  • equals()的本质是==,比较的是对象在栈中的引用地址。
  • hashCode()的本质是对象在哈希表中的索引,通过索引找到对象。
  • 在自定义比较两个对象时需要重写equals()和hashCode()方法。

变量命名规范

  • 首字母为英文(a~z)、美元符($)、下划线(_)。
  • 英文、美元符、下划线、数字随机搭配,单词间建议驼峰格式。
  • 命名直至中文,但不建议使用。
  • 不允许使用java中的关键字命名。

定义 float、double、long 变量的规范

  • JVM的小数默认解析为double类型。
  • float单精度,double双精度,double可以接收float。
  • float和double都可以接收任意长度的小数并截取指定精度。
  • 以float、double、long的形式接收整型值时不需添加后缀。

String、StringBuffer、StringBuilder的区别

  • String修饰不可变的对象,每次操作都会生成新的对象,指针也随之发生改变。
  • StringBuffer、StringBuilder修饰可变的对象,可以对原有的对象进行操作。
  • StringBuffer是线程安全的,其内部的所有方法都添加了synchronized关键字进行修饰,而StringBuilder是非线程安全的。
  • 三者性能间比较:StringBuilder > StringBuffer > String 。

普通类和抽象类的区别

  • 普通类不含有抽象方法,可以直接实例化对象,可被final关键字修饰。
  • 抽象类可以含有抽象方法,不可以直接实例化对象,不可被final关键字修饰。

接口和抽象类的区别

  • 抽象类的子类通过extends关键字继承,一个实体类只能继承一个抽象类,抽象类可以有构造方法、普通成员方法、任意类型的成员变量,抽象类的设计目的是代码复用。
  • 接口的子类通过implements关键字实现,一个实体类可以继承多个接口,接口没有构造方法,只有抽象普通成员方法、final和static修饰的成员变量,接口的设计目的是约束类的行为。

深拷贝和浅拷贝的区别

  • 浅拷贝只是复制了对象的引用地址,两个对象指向同一个内存地址,修改其中一个对象的值,另一个对象的值也会随之发生改变。
String s1 = "wjx";
String s2 = s1;
  • 深拷贝实现Cloneable接口,重写clone()方法,把对象和属性值都复制了,两个对象间互不影响,都是独立的对象。
public class Node implements Cloneable {
    private String name;
    private Node next;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Node node = (Node) super.clone();
        if (this.next != null) {
            node.next = (Node) next.clone(); -> 手动拷贝对象成员
        }
        return node;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Node getNext() {
        return next;
    }

    public void setNext(Node next) {
        this.next = next;
    }
}

线性表、B-Tree、B+Tree

  • 线性表是最基本、最简单、最常用的一种数据结构,一个线性表包含n个相同特性的数据元素,存储结构是链式存储,第一个元素没有头,最后一个元素没有尾,中间的元素都是首尾连接的,有序。
  • B-Tree是多路平衡查找树,一棵m阶层的B树,每个节点里最多存放(m-1)个关键字,关键字以key和value形式存储,每个节点最多有m个子节点,结点中包含多个关键字、父结点的指针、子结点的指针,关键字不可重复出现在不同的结点中,左子结点的值会比右子结点的值小,B树会把关键字添加到最底层的非叶子结点中,如果不破坏m阶层树的结构则结束,反之结点会进行分裂使之恢复到m阶层树的结构。


    分裂步骤1.jpg

    分裂步骤2.jpg

    分裂步骤3.jpg

    分裂步骤4.jpg

    删除方式1.jpg

    删除方式1.jpg

    删除方式2.jpg

    删除方式2.jpg

    删除方式3

    删除方式3
  • B+Tree是在B-Tree基础上的一种优化,非叶子结点不存放value,使之可以存放更多的key,降低树的高度,最底层的叶子结点包含所有的key和value,key从小到大从左往右排列,相邻的两个结点通过指针关联。

什么是面向过程,什么是面向对象

  • 面向过程会把任务拆解为一系列的动作,比如洗衣服,包含步骤:1、打开洗衣机;2、往里面放衣服;3、往里面放洗衣粉;4、清洗;5、烘干;
  • 面向对象拥有面向过程的特点,会更注重于任务的参与者以及各自需要完成的步骤,比如洗衣服,包含参与者:人、洗衣机,包含步骤:1、人打开洗衣机;2、人往洗衣机里面放衣服;3、人往洗衣机里面放洗衣粉;4、洗衣机对衣服进行清洗;5、洗衣机对衣服进行烘干;
  • 面向过程直接高效,面向对象易于复用、扩展、维护。

JDK、JRE、JVM三者的区别和联系

  • JDK(Java Development Kit)是开发Java程序的工具。
  • JRE(Java Runtime Environment)是运行Java程序的环境。
  • JVM(Java Virtual Machine)是Java的虚拟机。
  • *.java文件通过JDK的javac命令编译成 *.class文件,JRE通过ClassLoader加载 *.class文件解析到JVM里,JDK中包含JRE,JRE中包含JVM。

final的作用

  • 修饰的类不可被继承,修饰的方法不可被覆盖。
  • 修饰基本类型的变量不可更改值,修饰引用类型的变量不可更改对象,引用值可以改变。
  • 局部内部类和匿名内部类只能访问final修饰的变量。

重载和重写的区别

  • 重载:在同一个类中,方法名相同,方法修饰符、方法返回值、参数类型、参数个数不同,发生在编译期。
  • 重写:在子类中,方法名、参数列表必须相同,返回值、抛出异常小于等于父类,修饰符大于等于父类(private除外)。

ArrayList和LinkList的区别

  • ArrayList基于动态数组,数组的特点是内存存储空间连续,适合使用下标访问数组里的元素,查询速度快,新增、修改、删除速度慢,每次扩容空间的量为原来的1.5倍,把原数组的元素迁移到新数组上。
  • LinkList基于链表,链表的特点是内存储存空间不连续,只能通过iterator迭代器去遍历,查询速度慢,新增、修改、删除速度快,不适合使用下标访问数组里的元素。

HashMap和HashTable的区别

  • 底层原理:数组+链表+红黑树,当数组的长度超过64,链表的长度达到8转变成红黑树,红黑树的长度小于等于6转变成链表,取出key的hashCode进行二次hash,然后对数组的长度进行取模,得到数组的存放下标,如果没有产生hash冲突(数组下标的位置为空),则创建节点存入数组,如果产生hash冲突(数组下标的位置存在链表),则遍历链表通过equals()方法判断元素是否在链表中,如果存在就覆盖原来的元素,如果不存在就创建节点添加到链表的尾部。
  • HashMap是线程不安全的,key和value都允许为null,key为null时,元素存放在数组的第一个位置,即下标是0。
  • HashTable是线程安全的,其内部的所有方法都添加了synchronized关键字进行修饰,key和value都不允许为null。

Thread.sleep(0)的作用

  • 由于大部分操作系统采用的是抢占式线程调度算法,因此可能会出现某条线程经常获取到CPU控制权的情况,为了让某些优先级低的线程也能获取到CPU的控制权,可以通过调用Thread.sleep(0)来手动触发操作系统重新给线程分配时间片。
  • 在大循环里写上Thread.sleep(0),间歇地给其他线程获取CPU的控制权,避免优先级低的线程出现假死的情况,是一种平衡CPU控制权的操作。

什么是字节码,采用字节码有什么好处

  • 不同操作系统(Windows、Linux、Mac)的解析器实现是不一样的,但它们的虚拟机实现是一致的,Java中引入了虚拟机的概念,Java编译器面向的对象是虚拟机,通过把源文件(*.java)编译成虚拟机可以理解的字节码( *.class),虚拟机中的解析器将每一条需要执行的字节码翻译成特定机器上的机器码,然后在机器上执行,这也解析了Java的编译与解析共存的特点。
  • 在一定程度上解决了传统解析型语言执行效率低的问题,同时又保留了解析型语言可移植的特点,而且字节码只面向虚拟机,所以Java程序只需要编译一次就可以在不同的操作系统上运行使用。

Java中的类加载器有哪些

  • BootStrapClassLoader,负责加载%JAVA_HOME%/lib文件夹下的*.jar文件和 *.class文件。
  • ExtClassLoader,是BootStrapClassLoader的子类,负责加载%JAVA_HOME%/lib/ext文件夹下的*.jar文件和 *.class文件。
  • AppClassLoader,是ExtClassLoader的子类,用于实现自定义的类加载,负责加载开发人员编写的文件。

Java中的异常体系

  • 异常的顶级父类是Throwable,下面有Exception和Error两个子类。
  • Error是程序无法处理的错误,一旦出现则会终止程序的运行,如内存溢出(OutOfMemoryError)、线程死锁(ThreadDeath)、虚拟机错误(VirtualMachineError)等等。
  • Exception不会终止程序的运行,它可以分为CheckedException和RuntimeException两种,CheckedException发生在程序编译过程中,导致程序编译不通过,RuntimeException发生在程序运行过程中,导致程序线程执行失败。

什么是线程安全

  • 多个线程对同一个对象进行访问,如果对象不需要进行同步控制或者其他协调操作,调用该对象的行为都可以获得正确的结果,就证明是线程安全。
  • 栈是每个线程独有的,生命周期和线程同步,线程切换时栈也会随之切换,所以栈是线程安全的。
  • 堆是线程共享的,多个线程可以对堆中的同一个对象进行访问,所以堆是线程不安全的。

串行、并行、并发的区别

  • 串行在时间上不会出现重叠,前一个任务完成才会执行后一个任务。
  • 并行在时间上会出现重叠,两个任务独立完成,互不干扰。
  • 并发在时间上会出现重叠,两个任务间存在干扰,干扰阶段串行执行。

并发编程的三要素

  • 原子性:不可分割的操作,多个步骤要保证同时成功或者同时失败。
  • 有序性:程序执行的顺序和代码的顺序保持一致。
  • 可用性:一个线程对共享变量的修改,另一个线程能马上看见。

线程池的线程复用原理是什么

  • 线程池把线程和任务进行解耦,摆脱了直接创建线程,线程和任务一对一的绑定限制。
  • 线程池中的线程会不断轮询堵塞队列,不断地检查和获取新的任务来执行。
  • 直至堵塞队列的任务全部执行完,调用线程的wait()方法,释放出CPU。

谈谈你对AOP的理解

  • 系统由诸多不同的组件构成,每个组件都负责特定的功能,除此以外还经常承担着额外的职责,例如操作日志、事务管理、权限控制等等的核心服务,当需要为分散的对象引入公共行为时,OOP则显得乏力,它导致大量重复的代码,不利于给各个组件复用公共行为。
  • 将公共行为封装成一个切面,然后注入到每个组件中,目的是增强组件的功能,常见的用法是在组件调用某个方法前、后做些额外的事情,例如:在操作前校验权限、在操作后记录日志等等。

谈谈你对IOC的理解

  • 控制反转,在没有IOC容器之前,对象A依赖对象B(A a = new B();),A必须主动创建对象B,控制权在A手里,在引入IOC容器之后,对象A和对象B失去了直接联系,当对象A需要依赖对象B时,IOC容器会主动创建对象B提供给对象A,控制权在容器手里。
  • 依赖注入,对象A和对象B的依赖关系由IOC容器动态地注入,使用到简单工厂设计模式和Java反射技术,原理是动态创建对象。

谈谈你对微服务的理解

  • 微服务是一种架构风格,通过将大型的单体应用划分为比较小的服务单元,从而降低整个系统的复杂度。
  • 优点是每个微服务都是独立的,应用性能提高,技术选择灵活,耦合性降低,代码复用强。
  • 缺点是微服务之间的通信难度提高了,如负载均衡、熔断降级、分布式锁、分布式事务等等。

如何实现一个IOC容器

  • 配置文件中指定需要扫描的包路径。
  • 定义一些注解,分别表示访问控制层、业务服务层、数据持久层、依赖注入、获取配置。
  • 递归扫描包下面的文件夹及文件信息,用Set集合存放所有以class结尾的文件。
  • 遍历Set集合,获取在类上有指定注解的类交给IOC容器,并在容器内部定义一个安全的Map集合来存放这些被容器管理的类。
  • 遍历Map集合,获取到每个类的实例对象,判断里面是否有依赖其他类的实例对象,然后进行递归的依赖注入。

BeanFactory和ApplicationContext的区别

  • BeanFactory采用懒加载的形式注入Bean,即只有调用getBean()方法时才会实例化,这样就不能及时发现Spring配置的问题,直到实例化的时候才抛出异常。
  • ApplicationContext在容器启动时会一次性创建好所有的Bean,这样能在容器启动时及时发现Spring配置的问题,在需要Bean对象时可以直接获取,无需等待。不足的是占用内存空间大,容器启动速度慢。
  • BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,区别是BeanFactory需要手动注册,而ApplicationContext是自动注册,ApplicationContext是BeanFactory的子接口,提供了更完成的功能,还可以访问资源文件、获取系统变量、支持国际化、注册事件发布等等。

Spring容器的启动流程

  • 首先扫描得到所有的BeanDefinition对象,并存放在Map中。
  • 筛选出非懒加载的单例Bean进行创建,步骤包括推断构造方法、实例化、属性填充、初始化前、初始化中、初始化后,其中AOP就发生在初始化后这一步骤里。
  • 所有的单例Bean创建完成后,Spring会发布一个容器启动事件。

Spring Bean的生命周期

  • Spring容器启动时解析类得到BeanDefinition,它主要存储Bean的定义信息,决定Bean的生产方式。
  • 当类存在多个构造方法时,需要推断用哪个构造方法实例化得到对象。
  • 对类中添加了@Autowired注解的成员变量进行依赖注入。
  • 回调Aware方法,例如BeanNameAware、BeanFactoryAware等等。
  • 调用BeanPostProcessor初始化前(AOP)、初始化、初始化后(AOP)的方法。
  • 如果Bean是单例,则放进单例池中,使用Bean。
  • Spring容器关闭前调用DisposableBean中的destory()方法。

Spring框架中的单例Bean线程安不安全

  • 不安全,Spring加载Bean默认使用单例模式,并没有对Bean进行多线程封装处理。
  • Controller层、Service层、Dao层,本身并不是线程安全的,如果需要定义实例变量,就需要用ThreadLocal把变量设为线程私有,如果需要多线程之间共享,就需要使用Lock实现同步。
  • 可以修改Bean的作用域,使每次请求Bean都重新实例化对象,保证线程安全。

Spring Bean有几种作用域

  • singleton:默认创建方式,由BeanFactory来维护,生命周期和IOC容器一样。
  • prototype:每次请求Bean都重新注入一个新的对象。
  • request:每个HTTP请求中都创建一个单例对象。
  • session:每次会话中都创建一个单例对象。
  • application:ServletContext生命周期中创建一个单例对象。
  • websocket:WebSocket生命周期中创建一个单例对象。

Spring事务的实现方式、原理、隔离级别

  • 类和方法上添加@Transactional注解便可以使用Spring的事务。
  • Spring的事务基于数据库的事务进行了扩展,为启用事务的Bean创建代理对象,代理对象会取消事务的自动提交,再执行原来的逻辑,过程没有异常再把事务提交,反之进行逻辑回滚,默认回滚RuntimeException和Error,也可以通过注解的rollbackFor属性自定义回滚的异常。
  • 未提交读(read uncommitted):事务的最低隔离等级,AB两个事务都开启的前提下,A方对表数据进行了修改,在未提交事务的情况下,B方对表进行查询时能读取到A方修改后的数据,存在脏读现象,安全性最低。
  • 已提交读(read committed):事务隔离等级比未提交读的高,AB两个事务都开启的前提下,A方在未提交事务的情况下对表做的任何修改B方都不会看到,A方提交事务后,B方在未提交事务的情况下能看到A方对表做的修改,A方对表数据进行更新则B方出现不可重复读的现象(相同的查询语句查出来的数据值不一样),A方对表数据进行新增、删除则B方出现幻读的现象(相同的查询语句查出来的数据行不一样)。
  • 可重复读(repeatable read):事务隔离等级比已提交读的高,AB两个事务都开启的前提下,各自会生成一个数据表的快照,A方对表数据进行了修改并提交了事务,B方在未提交事务的情况下,仍然看不到A方做的任何修改,解决了不可重复读的现象,B方提交事务后,如果A方做的操作是新增或删除,则此时会出现幻读现象。
  • 可串行化(serializable):事务的最高隔离等级,AB两个事务都开启的前提下,A方对表数据进行了修改,在未提交事务的情况下表会加锁,B方对表进行查询时会被堵塞,直到A方提交事务或者超时为止。

Spring事务的传播机制

  • REQUIRED:如果没有事务,则创建一个事务,反之加入当前事务。
  • SUPPORTS:当前存在事务,则加入事务,反之以非事务形式执行。
  • MANDATORY:当前存在事务,则加入事务,反之抛出异常。
  • REQUIRES_NEW:创建一个事务,如果已存在事务,则挂起当前事务。
  • NOT_SUPPORTED:以非事务方式执行,如果已存在事务,则挂起当前事务。
  • NEVER:不使用事务,如果已存在事务,则抛出异常。
  • NESTED:当前存在事务,则创建一个子事务进行嵌套,子事务发生异常不提交,但不会影响到父事务的提交,反之创建一个事务。

Spring事务的隔离级别

  • ISOLATION_DEFAULT:使用数据库默认的事务隔离级别。
  • ISOLATION_READ_UNCOMMITTED:读未提交,允许事务执行过程中读取其它事务未提交的数据。
  • ISOLATION_READ_COMMITTED:读已提交,允许事务执行过程中读取其它事务已提交的数据。
  • ISOLATION_REPEATABLE_READ:可重复读,在同一个事务内,任意时刻查询的数据结果都是一样的。
  • ISOLATION_SERIALIZABLE:可串行化,所有事务按顺序
    依次执行。

Spring事务什么时候会失效

  • 类方法的自调用,Spring事务的原理是AOP,自调用方法的对象不是代理类。
  • @Transactional注解只作用于public修饰的方法,非public修饰的方法不支持事务。
  • 数据库不支持事务、类没有被Spring容器管理、手动捕获处理了异常。

Spring使用到哪些设计模式

  • 单例模式:Bean对象默认的创建方式是Singleton。
  • 观察者模式:Spring的事件监听和订阅。
  • 代理模式:AOP的实现,为不同的功能引入公共行为。
  • 简单工厂模式:BeanFactory和ApplicationContext都是创建Bean的工厂。
  • 工厂方法模式:FactoryBean是一个工厂接口,需要通过不同具体的Bean来实现,对象初始化采用的是懒加载方式,Spring管理BeanFactory,BeanFactory根据使用场景创建FactoryBean。
  • 装饰设计模式:DataSource可以是不同的数据库,根据客户端请求动态更改属性达到连接不同数据源的目的。
  • 适配器模式:AOP的增强通知,如前置通知、后置通知、环绕通知等等。
  • 模板方法模式:定义流程的算法骨架,把一些步骤延迟到子类中执行,使得子类在不改变算法骨架的情况下重写算法。
  • 策略模式:定义一系列的算法并封装起来,使得算法独立也可以相互替换。

Spring、SpringMVC、SpringBoot的区别

  • Spring是一个IOC容器,用来集中管理Bean,支持饿汉式初始化和懒加载,通过依赖注入实现控制反转,降低了代码的耦合度,通过AOP面向切面编程为不同的功能引入公共行为,弥补了OOP面向对象代码冗余的问题,用最小的侵入性实现松散耦合,可以很方便整合各种框架。
  • SpringMVC是Spring对Web框架的一种解决方案,提供了前端控制器接收请求,通过定义的路由策略和视图解析技术展现给前端。
  • SpringBoot是Spring提供的快速开发工具,内置了Web容器,可以直接启动运行Web应用,管理了常用的第三方依赖包的版本,减少了版本依赖的冲突,提供了Java配置的方式,减少了大量的Xml配置,提供了很多的Starter结合自动配置,对主流框架无配置集成,提供了监控功能,可以查看应用程序的运行状况,如内存、线程池、网络请求统计等等。

SpringMVC的工作流程

  • 用户发送请求至前端控制器DispatcherServlet,DispatcherServlet调用HandlerMapping映射器,HandlerMapping通过XML配置、注解(@Controller)的方式找到后端控制器和拦截器,并返回给DispatcherServlet。
  • DispatcherServlet通过HandlerAdapter处理适配器执行Controller并返回结果ModelAndView。
  • DispatcherServlet将ModelAndView传给ViewReslover视图解析器,通过解析生成View视图,DispatcherServlet根据View进行渲染,响应用户。

CAP 理论

  • Consistency:一致性,即更新操作成功返回给客户端后,所有节点在同一时间的数据完全一致。
  • Availability:可用性,即服务一直可用,而且是正常响应时间。
  • Partition Tolerance:分区容错性,即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。
  • CP是强一致性,AP是可用性,分区容错时只能二选一。

BASE 理论

  • Basically Avaliable:基本可用,响应时间上的损失和系统功能上的损失。
  • Soft State:软状态,数据同步允许一定的延时。
  • Eventually Consistent:最终一致性,系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一致的状态。

GC如何判断对象可以被回收

  • 引用计数法:每个对象都有一个引用计数属性,新增一个引用时计数加一,释放一个引用时计数减一,计数为零时可以回收。
  • 可达性分析法:从GC的Roots开始向下搜索,当一个对象不存在引用链时可以回收,常见的GC Roots对象有虚拟机栈的引用对象、方法区的静态引用对象和常量引用对象、本地方法栈的JNI引用对象、正在运行的线程。不可达对象需要经过两次标记过程,第一次是不存在引用链时,第二次是执行对象的finalize()方法。

消息队列有哪些作用

  • 解耦:系统间的通讯依赖消息队列,单个系统独立。
  • 异步:上游不受下游执行结果影响时,下游操作放在消息队列中执行,提高上游返回的效率。
  • 流量削峰:在高并发的环境中,系统在同一时间点接收大量的网络请求容易引起宕机,把网络请求放在消息队列中排队消费,减轻了服务器承受的压力。
  • 使用场景:业务审批流的消息推送、系统功能错误日志的记录。

消息队列如何保证消息的可靠传输

  • 生产者不能重复生产消息,消费者不能重复消费消息。
  • 生产者发送成功的消息,消费者一定能消费成功。

死信队列和延时队列分别是什么

  • 死信队列中存放着消费失败的消息,可用来消息的消费重试。
  • 延时队列中存放着需要延时消费的消息,可用来处理具有过期性的业务,如订单在十分钟内未完成支付则取消订单。

Kafka吞吐量高的原因

  • 生产者采用异步发送消息机制,消息缓存起来后就直接返回成功的操作提示。
  • 当缓存的消息量达到某个阈值时,批量发送给Broker处理,减少了网络的IO,从而提高了吞吐量。
  • 但生产者宕机会导致消息丢失,通过降低可靠性来提高性能。

Netty和Tomcat的特点和区别

  • Netty是基于NIO的异步网络通讯框架,关注网络数据的传输,不关注传输的网络协议,可用于开发各种高效的网络服务器。
  • Tomcat是Web服务器,内部只运行Servlet的程序和处理http请求。

数据库的事务ACID是怎么保证的

  • A:原子性(Atomicity),事务需要回滚时从undo log中获取信息。
  • C:一致性,由其他三大特征保证,程序保证业务上的一致性。
  • I:隔离性(Isolation),由多版本并发控制(Multi-Version Concurrency Control)保证。
  • D:持久性(Duration),事务需要前滚时从redo log中获取信息。

MySQL中binlog、redolog、undolog的区别和作用

  • binlog是MySQL Service层的逻辑日志,记录了数据库事务提交后的增删改操作语句,日志文件写满后,会写入到另一个新的文件,适合用于备份,如主从复制。
  • redolog和undolog是InnoDB引擎的日志,用于支持事务。
  • 事务执行的完整过程,执行器先查询数据是否在InnoDB存储引擎的内存中,不在则从磁盘加载到内存去,有则直接返回数据,然后把数据的旧值写入undolog中,再把值更新到InnoDB存储引擎的内存中,再把数据的新值写入redolog中,此时状态为prepare,当事务发生回滚,先删除prepare状态的redolog,再根据undolog将数据还原,当事务提交后,先把redolog的状态改为commit,再把binlog日志写入磁盘。

MySQL 主从同步原理

  • binlog是数据库服务从启用起记录所有修改操作的文件。
  • 当主节点的binlog发生改变,log-dump线程会向从节点发送变化的binlog。
  • 从节点的IO线程接收到binlog后,写入到relaylog中。
  • 从节点的SQL线程读取relaylog文件对数据更新进行重放,确保了数据的一致性。

Redis过期键的删除策略

  • 惰性过期:只有访问key时,才判断是否过期,过期则删除,该策略可以节省CPU资源,但对内存不友好,如果大量的key没有被再次访问,就不会被删除,占用大量内存。
  • 定期过期:每隔一段时间就去扫描expires字典,字典中保存了所有的key和expire-time,从中清除已过期的key。

Redis内存不足的淘汰策略

  • volatile-lru:从设置了过期时间的数据集中筛选出最近使用最少的数据进行淘汰。
  • volatile-ttl:从设置了过期时间的数据集中筛选出即将要过期的数据进行淘汰。
  • volatile-random:从设置了过期时间的数据集中随机筛选出数据进行淘汰。
  • allkeys-lru:从数据集中筛选出最近使用最少的数据进行淘汰。
  • allkeys-random:从数据集中随机筛选出数据进行淘汰。
  • noeviction:默认配置,禁止驱逐数据,所有申请内存的命令都引起报错。

Redis单线程速度快的原因

  • 核心基于非堵塞的IO多路复用机制。
  • 单线程避免了多线程频繁切换上下文的性能开销。
  • 纯内存操作。

Redis集群方案

  • 哨兵模式:sentinel是redis集群非常重要的组件,本身就是分布式系统。
  • 集群监控:负责监控主(master)从(slave)结构中的每台服务器是否正常工作。
  • 消息通知:当某个redis实例发生故障,哨兵会向其它哨兵和redis实例发送通知。
  • 故障转移:当master服务挂掉后,哨兵会选举一个slave服务作为master服务,告知其它slave服务新的连接地址并重新连接。

zookeeper和eureka的区别

  • zookeeper的设计是强一致性,是分布式的协调系统,用于资源的统一管理,当节点crash后,需要进行leader的选举,这期间zookeeper的服务是不可用的。
  • eureka的设计是高可用性,是服务注册发现系统,用于微服务的注册和发现,各个节点都是平等的,当一个节点crash后,剩余的节点依然可以提供服务。

SpringCloud和Dubbo的区别

  • 底层协议:SpringCloud基于http协议,Dubbo基于tcp协议,决定了dubbo性能更好一些。
  • 注册中心:SpringCloud使用的是eureka,Dubbo使用的是zookeeper。
  • 模型定义:SpringCloud将一个应用定义为服务,Dubbo将一个接口定义为服务。SpringCloud是一个生态,而Dubbo是SpringCloud生态中关于服务治理的一种方案。

ThreadLocal的原理

  • ThreadLocal是Java中所提供的线程本地存储机制,可以把数据缓存到线程内部。
  • 每个Thread对象中都包含ThreadLocalMap,key为ThreadLocal对象,value为缓存数据,在线程池中使用ThreadLocal会造成内存泄漏,原因是线程池的线程不会回收,ThreadLocal的引用也不会释放,因此使用完需要手动调用ThreadLocal的remove方法。
  • ThreadLocal的应用场景是数据库连接管理,线程之间不共享连接对象,如Spring的事务管理就是基于单线程的,Connection存放在线程内部,因此Spring无法保证多线程事务的一致性。

ReentrantLock中的公平锁和非公平锁的原理

  • 公平锁:线程调用lock()方法时,先检查AQS队列中是否存在线程在排队,如果有则排队等待,没有则获取到锁。
  • 非公平锁:线程调用lock()方法时,直接和AQS队列的线程竞争锁,如果获取到锁就结束了,没有则加入AQS队列等待。

类加载器的双亲委派模型,有什么好处

  • JVM中存在三个类加载器:BootstrapClassLoader > ExtClassLoader > AppClassLoader。
  • JVM在加载一个类时会调用AppClassLoader的loadClass()方法,这里面又会调用ExtClassLoader的loadClass()方法,这里面又会调用BootstrapClassLoader的loadClass()方法,加载成功则直接返回,反之则往下执行加载。
  • 好处在于加载安全,避免用户编写的类覆盖了Java的核心类,也避免了类的重复加载。

Sychronized的偏向锁、轻量级锁、重量级锁

  • 偏向锁:Sychronized会在对象头存放当前线程的ID作为锁的信息,当线程下次再来访问对象时,可以直接获得锁,是可重入的概念。
  • 轻量级锁:由偏向锁升级而来,当一个线程来获取对象锁时,对象头已经存在另一个线程的锁信息时,会进行自旋来等待锁,这时不会堵塞线程。
  • 重量级锁:当轻量级锁自旋次数达到一定次数时仍没有获取到锁时,会升级成重量级锁,这时会堵塞线程。

CopyOnWriteArrayList的实现原理

  • 底层是对数组的封装,当需要add元素时,添加ReentrantLock锁。
  • 新增时通过复制旧数组来创建新数组,新数组比旧数组的长度多一,再把新元素放到新数组的末位。
  • 删除时先找出被移除元素的下标,然后把该元素后面的元素下标全部都往前进一,再通过复制旧数组来创建新数组,新数组比旧数组的长度少一。
  • 当需要get元素时,不会添加锁。

ConcurrentHashMap的实现原理

  • 底层是Segment对象的数组,Segment对象的结构是数组+链表。
  • 实现了锁分段技术,JDK7中锁的粒度为Segment对象,JDK8中锁的粒度为Segment对象的链表,降低了锁的粒度。
  • put()方法和get()方法都需要调用二次hash()方法,第一次是定位到Segment对象,第二次是定位到Segment对象的链表。

SpringMVC的九大核心组件

  • HandlerMapping:用于接收用户的请求,根据访问的url来找到对应的Handler作出响应。
  • HandlerAdapter:用于Handler适配Servlet的请求,Servlet相当固定,分为request和response,Handler则形式多样化。
  • HandlerExceptionResolver:异常处理解析器,通过捕获解析异常,返回ModelAndView对象。
  • LocaleResolver:区域解析器,从request请求中解析出Locale对象,用于配置国际化资源以及结合ViewResolver解析视图。
  • ViewResolver:视图解析器,结合Local对象使用,把String类型的视图名解析成View类型的视图,用于渲染页面。
  • MultipartResolver:文件解析器,把request请求封装为MultipartHttpServletRequest请求,用于接收文件上传请求并清理上传过程产生的临时资源。
  • ThemeResolver:主题解析器,用于解析*.properties文件,同时支持国际化。
  • FlashMapManager:用于管理FlashMap,FlashMap主要用在redirect中传递参数。
  • RequestToViewNameTranslator:从request请求中获取ViewName,交由ViewResolver查找View视图。

分布式事务的解决方案有哪些

  • 2PC(Prepare Commit):第一阶段,事务管理器(协调者)会执行SQL脚本,但不会提交事务,此时数据库资源被锁定,资源管理器(参与者)将undo和redo写入事务日志,第二阶段,资源管理器根据操作结果提交事务或者回滚事务,释放资源。缺点是协调者或者参与者宕机会引起数据不一致。
  • 3PC(Prepare Commit):第一阶段CanCommit,协调者询问参与者是否可以提交事务,第二阶段PreCommit,协调者执行SQL脚本但不提交事务,第三阶段DoCommit,如果二阶段中参与者宕机或者预提交操作失败则执行回滚,反之执行事务的提交,释放资源。优点是加入超时机制,解决2PC宕机时遇到的问题。
  • TCC(Try Confirm Cancel):通过系统提供的业务逻辑流程调度,分为三个步骤,首先是try(entity),这个过程会锁定资源,给所有服务提供更新的预校验,如库存更新时分为即时库存和可用库存,此阶段将会冻结部分可用库存,同时会记录即时库存、冻结库存、可用库存的日志,满足条件时再执行Confirm(entity),此阶段将会实际更新即时库存,同时会记录即时库存、冻结库存、可用库存的日志,当所有服务提交成功时释放资源,当某个服务提交失败时,再执行所有服务的Cancel(entity),把数据统一回滚,此阶段将会反冻结部分可用库存,同时会记录即时库存、冻结库存、可用库存的日志,保证了数据的一致性。
  • AT(Auto Transcation):业务无侵入二阶段提交,在一阶段中Seata会拦截业务SQL,解析其语义并找到需要更新的业务数据,在更新前把数据保存为前镜像(before-image),再执行业务SQL更新数据,在更新后把数据保存为后镜像(after-image),最后生成行锁(避免脏读),保证了一阶段的原子性。在二阶段提交时,只需将一阶段的镜像和行锁删除即可。在二阶段回滚时,先对比数据库当前的业务数据和后镜像的业务数据是否相同,如果相同则说明没有脏写,通过前镜像的业务数据还原到数据库,最后把一阶段的镜像和行锁删除,反之需要人工处理。
  • Seata框架:提供了AT 模式、TCC 模式、Saga 模式和 XA 模式四种分布式事务解决方案。

泛型中extends和super的区别

  • <? extends T> 表示包括T在内的任何子类。
  • <? super T> 表示包括T在内的任何父类。

TCP的三次握手和四次挥手

  • TCP协议是七层网络协议中的传输层协议,负责数据的可靠传输。
  • 在建立TCP连接时,需要通过三次握手,首先是客户端向服务端发送SYN(Synchronize Sequence Numbers),其次是服务端接收到SYN后给客户端返回SYN_ACK,最后是客户端接收到SYN_ACK后给服务端发送一个ACK(Acknowledge character)。
  • 在断开TCP连接时,需要通过四次挥手,首先是客户端向服务端发送FIN(finish),其次是服务端接收到FIN后向客户端发送ACK,意味着服务端接收到断开连接的请求,客户端可以不发送数据了,但服务端可能还有数据在处理,待数据完成处理后,服务端向客户端发送FIN,表示当前可以断开连接,最后是客户端接收到FIN后向服务端发送ACK,表示当前也可以断开连接。

HTTPS如何保证安全传输

  • 通过使用对称加密、非对称加密、数字证书等方式来保证数据的安全传输。
  • 客户端向服务端发送数据前,需要先建立TCP连接,建立完成后服务端会向客户端发送公钥,客户端通过公钥来对传输数据进行加密,服务端接收到传输数据后通过私钥进行解密,这种就是非对称加密。
  • 服务端向客户端发送非对称公钥时,可以先把非对称公钥和服务端相关信息通过hash算法生成消息摘要,再通过数字证书提供的私钥对消息摘要进行加密生成数字签名,再把服务端相关信息和数字签名一起形成数字证书发送到客户端,客户端通过数字证书提供的公钥进行解密,从而得到非对称加密的公钥。
  • 在服务端给客户端传输的过程中,数据包可能会被中间人截获,同时被解密出非对称的公钥,但是中间人是没办法伪造数字证书发送给客户端的,生成数字证书需要私钥,网站如果需要支持HTTPS,就需要申请数字证书的私钥,这样就确保安全了。

Dubbo的工作流程

  • Spring容器启动时,Dubbo的Provider会同时启动,向Zookeeper服务中心注册相关信息,如IP、端口、接口、版本、协议等。
  • Dubbo的Consumer启动时,会去Zookeeper中获取已注册服务的信息,当Provider的信息发生变化时,Zookeeper会向Consumer推送通知。
  • Consumer通过RPC的方式调用Provider的方法,每隔两分钟,Provider和Consumer都会向Monitor发送访问次数,由Monitor进行统计。

如何进行自我介绍

  • 基本信息,姓名、年龄、学校、学历。
  • 工作经验,最近一段时间开发的最熟悉、最有价值、技术栈最丰富的项目,在项目中承担的主要职责以及解决项目的主要问题。
  • 企业背调,说下对这家面试公司的了解,行业解决方案。
  • 面试官,上面就是我的个人简介,您看您有什么想了解的。

如何陈述自己的项目

  • 用一个最近且最熟悉的项目进行陈述。
  • 描述项目的核心价值和包含的功能以及参与的核心模块和使用到的技术栈。
  • 描述项目开发过程中存在的技术问题以及解决思路和方案。
  • 面试官,上面就是我的项目经验的简单介绍,您看您有什么想问的。

如何回答自己会的问题

  • 按照总分的思想,分步骤来说明实现的机制,结合实际项目经验谈怎么解决。
  • 面试官,我已经回答完了,刚刚是我对于这个知识的理解,您看一下哪里有问题,可以帮我指点下。

如何回答自己不会的问题

  • 有一点了解或者接触过类似的,按照知道的表述出来。
  • 一点都不会,可以问面试官,这个技术在项目中有什么作用。

如何回答自己的缺点

  • 把优点说成缺点,如个人执着、代码洁癖等等。

如何回答自己的职业规划

  • 我从不做三年以上的职业规划,但我的终极目标是系统架构师。
  • 如果我有幸入职贵公司的话,我会从本职工作做起,争取让自己先做到高级开发工程师。
  • 我相信在贵公司的平台下,我能获得更好的发展,取得更高的成就,给公司创造更大的价值。
  • 如果我有幸入职的话,贵公司对我的安排是怎样的。

如何回答离职原因

  • 千万不要说上家公司的不好,这是禁忌。
  • 公司倒闭、不发工资、仲裁。
  • 之前的项目一直在改动,没有新项目。
  • 个人发展问题。

如何回答你有什么想问的

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

推荐阅读更多精彩内容