杨明翰的中高级Java后端面试题攻略v0.3(持续更新)

技术文章 专栏收录该内容
8 篇文章 0 订阅

文章目录


前言

每个人的时间与精力都是有限的,有限的时间做有限的事情,
不要什么都学,做减法,要有自己的强项,
知识是学不完的,要打造自己的优势与强项,再扩展一些辅助知识,
打造金字塔结构的知识体系。

很多面试官问java基础都只会问:集合、多线程、concurrent并发包、jvm、nio。
那我们就把有限的时间与精力放到这些上面。

自己要掌握结构化学习,自己的脑子里把知识点分块,分类,这样记忆。

不要死记硬背面试题,要理解,看完的树与文章要复盘与总结,过一段时间再复习一遍。

对于知识的掌握,有四个维度:
了解——对该领域有初步的认知,具有学习经验或实习经验;
掌握——具备基本的知识结构,可以运用本专业知识完成相关工作;
熟练——具备系统的知识和应用能力,可以较快较好的完成相关工作;
精通——对该领域有深刻的研究和认识,对本专业的各种问题都有很好的解决能力。

在中高级Java工程师面试中,
很少有面试官会问你类似于重载与重写的区别等之类的基础问题,
因此萌新玩家并不适合此系列文章。


Java基础

Java综合

请按顺序并按分类写出java的8种基本数据类型?

(这个就像英语中26个英文字母一样,分类记忆法,记忆的更牢固。)

  • 整型
    byte(8位)、short(16位)、int(32位)、long(64位)
  • 浮点型
    float(32位),double(64位)
  • 字符型
    char(16位)
  • 布尔型
    boolean(1位,但是java中没有1位的单位,因此使用8位表示)

Integer与int的区别?

int是基本数据类型,直接存数值,而Integer是引用数据类型,
用一个引用指向一个对象。
Integer是int的包装类,是int的扩展,定义了很多的转换&工具方法。
int的初始值是0,Integer的初始值是null。

Object类中的方法的有什么,其作用?

  • clone()
    创建并返回此对象的一个副本。

  • equals(Object obj)
    指示某个其他对象是否与此对象“相等”。

  • finalize()
    在GC执行时会调用被回收对象的此方法,
    可以覆盖此方法提供GC时的其他资源回收,例如关闭xxx等。
    需要慎用此方法,因为会有java.lang.ref.Finalizer内存泄露的风险。

  • getClass()
    返回一个对象的运行时类。

  • hashCode()
    返回该对象的哈希码值,相当于对象在内存中的地址。

  • wait()
    导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。

  • notify()
    唤醒在此对象监视器上等待的单个线程。

  • notifyAll()
    唤醒在此对象监视器上等待的所有线程。

  • toString()
    返回该对象的字符串表示,在与字符串连接使用时默认调用当前对象的toString()方法。

equals()与==的区别?

是比较两个对象的内存地址是否一致,基本类型则是比较值,
而equals()是Object类的方法,默认是与
一致,但像String这种类,
会重写equals()来自定义判断逻辑,程序员也可以重写euqals()。

什么时候需要重写equals()与hashcode()?
(要回答这个问题必须要弄懂HashMap的底层原理,哈希表如何存储如何查找。)
hashCode()主要用于在哈希表存储结构中快速查找对象的存储位置(快速寻址)。
(主要是Hash开头的类,如HashMap、HashSet、Hashtable等等)
hashCode()是Object类中的方法,hashCode()默认会返回对象在内存中的地址,用整数表示。
HashSet与HashMap都不允许存放重复数据,那么如何来界定重复与不重复呢?
拿HashSet举例子,最笨的方法遍历集合中所有元素,再使用if()+equals(),但这样比较low。
于是有人发明一种hash算法,直接通过对象的hashcode来计算集合中存放元素的位置,
这样就不用去遍历if()+equals()了。
(别忘了jdk1.8之前的HashMap底层实现方式是数组+链表,那个位置就是数组里的元素位置。)
但hashcode是有可能重复的,因为整数是有限制的,如果遇到hashcode重复,
那么再用equals进行判断,equals的执行概率很低(因为equals的执行效率也低所以没问题)。
那为什么我们要重写equals()与hashCode()呢?
Object原生的equals()是比较内存地址,而hashCode是根据内存地址计算出来的整数。
如果现在集合中放到都是Person这种自定义对象,
那即使2个各种属性都相同的Person对象在HashSet里也会认为是不同的对象,
因此我们需要重写equals()与hashCode()。
所以如果是使用HashSet(底层也是HashMap)或HashMap的key元素对象,必须重写这2个方法。
注意(这里也是面试考点):
不要去修改集合中已有对象的属性,前提是这个属性参加了hashcode运算,
这样会导致老对象“失踪”,因为新对象与老对象的hashcode发生改变,
而如果想去删除集合中的老对象也会失败,因为寻址都是依靠hashcode,从而导致内存泄漏。
(当然这种hash思想也可以用于数据库的分库分表以及网关的路由上)

Java类的初始化顺序?

1.父类静态变量&父类静态初始化块
2.子类静态变量&子类静态初始化块
3.父类实例变量&父类初始化块
4.父类构造
5.子类实例变量&子类初始化块
6.子类构造
注意:
静态变量与静态初始化块的执行顺序与定义顺序相同(谁写在上面谁先走)。
实例变量与初始化块的执行顺序与定义顺序相同(谁写在上面谁先走)。

Java的参数传递是值传递还是引用传递?

Java的参数传递方式只有一种,那就是值传递。
值传递:将实参的副本复制传入方法内作为形参,方法里操作的是形参,而实参不会收到影响。
引用数据类型也是值传递,至于对象的属性值发生变化,是因为值传递中的值是对象的引用地址,
方法内依然进行了副本的复制工作,只不过复制的不是对象,而是一个引用变量而已,
使得实参与形参指向同一个引用地址。
在方法内把对象赋值成null,待方法执行完毕再调用实参,结果并不是null。

Java为什么要把String设计成final类型?

String在内存中存储在什么地方?
存在字符串常量池中,有什么好处吗?读取会更快?
String是线程安全的吗?不安全

常见版本有什么新特性?

JDK5
  1. 支持泛型
    未完待续
JDK7
  1. 所有IO类全部实现AutoCloseable接口,可通过自动关闭资源的try语句来关闭io流;

  2. switch允许String作为参数;

  3. forkJoin
    未完待续

JDK8
  1. lambda表达式
    使用Lambda表达式可以使代码变的更加紧凑,Lambda允许把函数作为一个方法的参数

  2. stream流处理
    把真正的函数式编程风格引入到Java中

  3. 函数式编程

  4. 方法引用
    方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。

  5. 函数式接口
    就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。
    函数式接口可以被隐式转换为 lambda 表达式

Date Time API − 加强对日期与时间的处理。
未完待续

反射的底层原理?

如何进行反射,如何提高反射的性能
未完待续

反射讲一讲,主要是概念,都在哪需要反射机制,反射的性能,如何优化

答:反射机制的定义:

是在运行状态中,对于任意的一个类,都能够知道这个类的所有属性和方法,对任意一个对象都能够通过反射机制调用一个类的任意方法,
这种动态获取类信息及动态调用类对象方法的功能称为java的反射机制。

反射的作用:
1、动态地创建类的实例,将类绑定到现有的对象中,或从现有的对象中获取类型。
2、应用程序需要在运行时从某个特定的程序集中载入一个特定的类

如何实现动态代理?以及底层原理?为什么需要实现接口?

如何手写出一个jdk的动态代理(Proxy,InvocationHandler)?
未完待续

Java集合

集合是Java基础的重点,几乎所有面试官都会问关于集合的问题,
建议大家花一些时间看看常用集合类的源码,例如著名的HashMap与ConcurrentHashMap。
以及衍生出线程安全和并发的概念等等。

下面2张图需要徒手画出来,很重要,以及每个接口、实现类的特点,
经常会有面试官来考这个来检查你的基本功是否扎实。

Map接口与Collection接口是两个接口,千万不要弄混,Map接口的应用非常广泛,
主要用于存储KV结构数据。
HashMap是面试的重中之重,10个面试官几乎有8个会问这个问题,由此可见HashMap是多么的重要

Collection的继承体系以及相关类与接口的特点?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qc52xJjb-1588123493298)(evernotecid://85A94EB2-9BD2-45DE-A16D-D9C9CD7ECF0B/appyinxiangcom/12192613/ENResource/p359)]

Set接口

~Set接口保存无序的、不可重复的元素。
~HashSet的元素可为null,线程不安全。å
~LinkedHashSet的元素有序(插入顺序)。
~TreeSet的元素有序(比较顺序)。

Queue接口

~Deque是双端队列。
~PriorityQueue可按大小排序。(插入还是比较?)
~ArrayDeque是队列,先进先出。

List接口

~List接口保存有序的、可重复的元素。
~Vector与Stack线程安全,但性能差。
~ArrayList、LinkedList线程不安全。
~LinkedList是双向链表,可做队列&栈。

Map的继承体系以及相关类与接口的特点?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-shTgC4zO-1588123493300)(evernotecid://85A94EB2-9BD2-45DE-A16D-D9C9CD7ECF0B/appyinxiangcom/12192613/ENResource/p358)]

Map接口
  1. HashMap,元素可为null,无序,线程不安全。
  2. Hashtable,元素不为null,无序,线程安全。
  3. LinkedHashMap,有序,链表保存内部顺序。(插入顺序,要试一下)
  4. IdentityHashMap用==而非equals比较键。
  5. TreeMap,有序,按大小排序。(比较顺序,要试一下)
  6. WeakHashMap:实现弱引用的HashMap。

HashMap

HashMap的特点与原理?(连环炮)

HashMap可以接受null的键和值,HashMap是非线程安全的。

HashMap原理

HashMap用于存储键值对的集合,每个键值对叫做entry,这些entry分散存储在一个数组里。
HashMap主要使用key的hashcode做位运算得到存储的位置index(数组的下标&索引)。

put方法原理

hashMap.put(“apple”,0);插入一个key为"apple"的元素,
需要使用哈希函数来确定entry的插入位置index。
index = hash(“apple”),
假设结果等于2,那么就把这个entry放在下标为2的数组下标里。
由于HashMap的数组长度是有限的,当插入的Entry越来越多时,
哈希函数会出现index冲突&哈希碰撞的情况。

那冲突的情况就需要使用链表来解决冲突。

HashMap数组的每一个元素不止是一个entry对象,也是一个链表的头节点。
每个entry对象通过next指针来指向它的下一个entry节点。
当新entry的index冲突时,只要插入到对应的链表中即可,此时插入的是链表的头部。

get方法原理

hashMap.get(“apple”);查找一个key为"apple"的元素,
还是需要使用哈希函数来确定entry的位置index。
index = hash(“apple”),
由于刚才所说的Hash冲突,同一个位置有可能匹配到多个Entry,
这时候就需要顺着对应链表的头节点,一个一个向下来查找。
但这样的读取效率比较低,在JDK8之后,对HashMap进行优化来解决这个读取效率低的问题。

在JDK8中,HashMap的结构有怎样的优化?

JDK8中HashMap使用数组+链表+红黑树实现,其中红黑树是JDK8中新加入的。
数组里存储的元素是一个Node对象,Node也是JDK8中新加入的,之前是Entry对象。

Node对象主要包含hash(定位数组索引位置)、key、value以及next指针(指向Node)。
JDK8当链表长度超过8时候引入红黑树,增删改查的特点提高HashMap的性能。

HashMap的默认长度是多少?为什么要这么规定?

HashMap的默认长度是16,每次自动扩展或手动扩展必须是2的幂。
因为计算index的hash函数,要实现一个尽量均匀分布的hash函数。

如果长度不是16或者是2的幂,会在位与运算中出现更多的重复index,
会造成不符合Hash算法均匀分布的原则。
16或者是2的幂会使得length-1后二进制的所有位全都是1,
这种情况下,index的结果等同于HashCode后几位的值。

只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。

HashMap中的load factor的作用?

负载因子,用于计算HashMap的容量阈值threshold(阈值=容量x负载因子)。
默认值为0.75,这是时间与空间成本的一种折中。
增大负载因子减少Entry数组占用内存,但增加查询数据的时间。
减少负载因子增加Entry数组占用内存,但减少查询数据的时间。

高并发下,HashMap会出现什么问题?

HashMap会出现死循环,
造成死锁的原因是因为rehash(),在hashmap容量不够时,
会调用resize()进行扩容,
创建一个新数组,调用rehash()重新计算各个元素的hashcode与index。
在高并发下,会有概率造成环形链表,当调用get()时查找一个不存在的key,
而这个key正好处于发生环形链表的index时,就会出现死循环。

HashMap是线程不安全的,为什么?有什么办法可以做到线程安全?

未完待续

1.Collections.synchronize();
2.ConcurrentHashMap
ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap可以代替HashTable,
但是HashTable提供更强的线程安全性。

ConcurrentHashMap的原理?

ConcurrentHashMap采用分段锁的机制,实现并发的更新操作,
底层采用数组+链表+红黑树的存储结构,
其包含两个核心静态内部类Segment和HashEntry,
默认创建包含16个Segment对象的数组。

首先将数据分成一段一段的存储(Segment继承ReentrantLock),
每个 Segment 对象守护每个散列映射表的若干个桶
(每个桶是由若干个 HashEntry 对象链接起来的链表),
当一个线程占用锁访问其中一个数据时,
其它段的数据也能被其它的线程访问修改,而Hashtable锁的是整个hash表,
效率较低JDK1.8的实现已经抛弃了Segment分段锁机制,
利用CAS+Synchronized来保证并发更新的安全,
底层依然采用数组+链表+红黑树的存储结构。

ConcurrentHashMap如何做到线程安全?

HashMap与Hashtable的区别?

1.HashMap允许将null作为一个entry的key或者value,而Hashtable不允许;
2.Hashtable线程安全(使用synchronize修饰方法,效率低),HashMap线程不安全;
3.HashTable使用Enumeration进行遍历,HashMap使用Iterator进行遍历;
4.HashTable直接使用对象的hashCode。而HashMap重新计算hash值;
5.HashTable中hash数组默认大小是11,增加的方式是 old x 2+1。
HashMap中hash数组的默认大小是16,而且一定是2的指数;

HashTable的底层原理?如何保证线程安全?

LinkedHashMap的底层原理?如何实现有序?

LinkedHashMap也是基于HashMap实现的,不同的是它定义了一个Entry header,
这个header不是放在Table里,它是额外独立出来的。

LinkedHashMap通过继承hashMap中的Entry,
并添加两个属性Entry before,after,和header结合起来组成一个双向链表,
来实现按插入顺序或访问顺序排序。

LinkedHashMap定义了排序模式accessOrder,该属性为boolean型变量,
对于访问顺序,为true;
对于插入顺序,则为false。

一般情况下,不必指定排序模式,其迭代顺序即为默认为插入顺序,
LinkedHashMap保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,
先得到的记录肯定是先插入的。

TreeMap的底层原理?如何实现有序?

红黑树,未完待续

如何高效遍历一个HashMap?
for (Map.Entry<String, String> entry : map.entrySet()) {
  System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}

List

Arraylist与Linkedlist的区别?(58到家面试题)

  1. ArrayList是基于动态数组的数据结构,LinkedList是基于链表的数据结构。
  2. 对于随机访问get和set,ArrayList优于LinkedList,
    因为LinkedList要移动指针。
  3. 对于指定位置的新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。

但是LinkedList的随机增删整体上不一定有Arraylist快,
首先需要看整个集合的长度,
如果集合长度很长,那么在执行随机增删时,Linkedlist是需要寻址的,
这个寻址的过程就很慢,而ArrayList的寻址过程很快。

LinkedList底层原理?

ArrayList的底层原理?

ArrayList底层使用数组来保存元素,动态数组,插入中间位置要集体搬家。
未完待续。

ArrayList如何去重?

把list里放到set中,再从set中拿出来放到一个新的list中,
如果是引用类型去重别忘了重写euqals与hashCode。
arraylist = new ArrayList(new HashSet(arraylist));

Set

HashSet的底层原理?

HashSet底层封装了一个HashMap对象来保存数据,
使用HashMap的key来保存HashSet的所有元素。
HashSet定义了一个静态的无用对象作为HashMap的value。
HashSet为散列集,其底层采用的是HashMap进行实现的,
但是没有key-value(即它不需要Key和Value两个值),
只有HashMap的key-set的视图,

HashSet不容许重复的对象
调用HashSet的add方法时,实际上是向HashMap中增加了一行(key-value对),
该行的key就是向HashSet增加的哪个对象,该行的value就是一个Object类型的常量

多线程&并发

多线程也是面试的重灾区,多线程玩到后面可以玩的很深,需要借助一些经典的书籍来巩固自己。
与NIO一样,多线程的知识点也切记不要停留在概念层面,需要去coding,尤其是线程池。

线程安全、线程池是重灾区的重灾区。

多线程综合

Java多线程有几种实现方式?

继承Thread
public class ThreadDemo1 {
	public static void main(String[] args) {
		// 创建三个线程对象
		ThreadDemo t1 = new ThreadDemo();
		ThreadDemo t2 = new ThreadDemo();
		ThreadDemo t3 = new ThreadDemo();
		// 启动三个线程对象
		t1.start();
		t2.start();
		t3.start();
	}
}

class ThreadDemo extends Thread {
	/**
	 * 线程要执行的代码
	 */
	public void run() {
		for (int i = 0; i < 10; i++) {
			// this.getName()可以获取当前线程名称
			System.out.println(this.getName() + " ==>" + i);
		}
	}
}
实现Runnable
public class RunnableDemo1 {
	public static void main(String[] args) {
		RunnableDemo r1 = new RunnableDemo();
		Thread t1 = new Thread(r1);
		Thread t2 = new Thread(r1);

		t1.start();
		t2.start();
	}
}

class RunnableDemo implements Runnable {
	 int i = 0;
	/**
	 * 线程要执行的代码
	 */
	public void run() {
		for (; i < 10; i++) {
			// Thread.currentThread().getName()可以获取当前线程名称
			System.out.println(Thread.currentThread().getName() + "==>" + i);
		}
	}
}
实现Callable
public class CallableDemo1 {
	public static void main(String[] args) {
		//注意Callable需要泛型支持
		FutureTask<Boolean> ft1 = new FutureTask<>(new CallableDemo(1));
		FutureTask<Boolean> ft2 = new FutureTask<>(new CallableDemo(0));
		FutureTask<Boolean> ft3 = new FutureTask<>(new CallableDemo(2));
		Thread t1 = new Thread(ft1);
		Thread t2 = new Thread(ft2);
		Thread t3 = new Thread(ft3);

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

		// 获取线程返回值
		try {
			//get方法获取返回值时候会导致主线程阻塞,直到call()结束并返回为止
			System.out.println(ft1.get());
			System.out.println(ft2.get());
			System.out.println(ft3.get());
		} catch (Exception e) {
			//可以catch住线程体里的异常
			e.printStackTrace();
		}
		
		t1.start();
	}
}

class CallableDemo implements Callable<Boolean> {
	int flag;
	int i = 0;

	public CallableDemo(int flag) {
		this.flag = flag;
	}

	/**
	 * 线程要执行的代码
	 */
	public Boolean call() throws Exception {
		for (; i < 10; i++) {
			// Thread.currentThread().getName()可以获取当前线程名称
			System.out.println(Thread.currentThread().getName() + "==>" + i);
		}

		if (flag == 1) {
			return true;
		} else if(flag == 0){
			return false;
		}else {
			throw new Exception("哇咔咔咔");
		}
	}
}

Java线程的状态以及生命周期?

(java里的线程状态是6种,而不是5种,并不是操作系统中线程的状态,这个图要能徒手画出。)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fhq9nBzh-1588123493303)(evernotecid://85A94EB2-9BD2-45DE-A16D-D9C9CD7ECF0B/appyinxiangcom/12192613/ENResource/p360)]

sleep()和wait()的区别?

  1. sleep没有释放监视器的锁,wait方法释放了监视器的锁。
  2. sleep是Thread类的方法,wait是Object类的方法。
  3. sleep暂停线程指定时间后自动恢复,wait暂停后通常通过notify或notifyAll唤醒。
  4. sleep可以在任何地方使用,而wait需要在synchronized块中使用。
  5. sleep需要手动捕获异常,wait不需要手动捕获异常。

如何捕获线程中的异常?

无法通过正常方式捕获多线程中抛出的异常,因为异常从线程中逃逸,异常会直接抛到控制台。

使用Callable方式创建线程,FutureTask作为返回值
public class TempClass {
    public static void main(String[] argus) throws Exception{
        FutureTask<Integer> a = new FutureTask(new Callable() {
            @Override
            public Object call() throws Exception {
                System.out.println(1/0);
                return 999;
            }
        });

        try {
            Thread t1 = new Thread(a);
            t1.start();

            System.out.println(a.get());
        } catch(Exception ex) {
            System.out.println("可以捕获到异常");
        }
    }
}
使用Thread的setUncaughtExceptionHandler
public class TempClass {
    public static void main(String[] argus){
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(1/0);
            }
        });
        
        // 设置自定义多线程未捕获异常处理器
        // 如果是所有线程都要用,那就Thread.setDefaultUncaughtExceptionHandler
        t1.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler(){
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                System.out.println("我来了..."+t.getName());
                e.printStackTrace();
            }
        });
        t1.start();
    }
}

线程开的多就一定快吗?

不一定,多线程会消耗CPU和内存,
CPU切换时间片和上下文切换(保存当前线程状态切换下一个线程,以便下次再加载回来)
会影响执行速度。

在并发量较少的情况下,使用多线程会更慢。

Java内存模型是怎么设计的?为什么要这么设计?

(需要徒手画图的能力)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u8I1Y2lV-1588123493305)(evernotecid://85A94EB2-9BD2-45DE-A16D-D9C9CD7ECF0B/appyinxiangcom/12192613/ENResource/p361)]

Java内存模型规定了所有公有变量都存储在主内存中
(此处的主内存跟物理硬件的主内存只是名字相同,此处仅仅是指虚拟机内存的一部分),

而每条线程还会有自己的工作内存(可以类比CPU的寄存器和高速缓存),
线程的工作内存中会复制一份主内存中的变量,
线程对变量的所有操作都必须在工作内存中进行,
而不能直接读写主内存中的变量。

不同线程之间也无法直接访问对方工作内存中的变量,
线程之间变量值的传递都需要通过主内存完成。

什么是Happens-before原则?

先行发生原则通过一些规则来判断数据是否存在竞争、冲突、线程是否安全。

什么是先行发生呢?
在JMM中定义两项操作的执行顺序关系,如果操作A先于操作B发生,在操作B之前可以观察到操作A对共享变量的改动。

默认先行发生规则:

  • 程序次序规则(Program Order Rule):在一个线程内,按照程序的控制流顺序,谁写在前面谁先发生。

  • 管理锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面(时间层面上的后面)对同一个锁的lock操作。

  • volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面(时间层面上的后面)对这个变量的读操作。

  • 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。

  • 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

  • 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生与被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。

  • 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

  • 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那么就可以得出操作A先行发生于操作C的结论。

多线程通信有几种方式?

  1. wait、notify、notifyAll
  2. Condition
  3. BlockedQueue

wait,notify的原理?

未完待续,改变线程状态。

多线程的使用场景?

  1. 异步处理&非阻塞
    可以把占据长时间的程序中的任务放到新线程去处理,缩短响应时间;
    在I/O阻塞时,程序可以用另一个线程去做别的事情而并非一直傻傻的在等待I/O返回;

  2. 定时向大量(例如100万以上)的用户发送邮件&消息&信息;

  3. 统计分析的业务场景,让每个线程去统计一个部门的某类信息;

  4. 后台进程,例如GC线程;

  5. 多线程操作文件,提高程序执行时间;

下面这段引用自网络:
假设有一个请求需要执行3个很缓慢的io操作(比如数据库查询或文件查询),
那么正常的数据可能是:

a.读取文件1(10ms)
b.处理1的数据(1ms)
c.读取文件2(10ms)
d.处理2的数据(1ms)
e.读取文件3(10ms)
f.处理3的数据(1ms)
g.整合1,2,3的数据结果(1ms)
单线程总共需要34ms,但如果你把ab,cd,ef分别分给3个线程去做,就只需要12ms了。

再假设
a.读取文件1(1ms)
b.处理1的数据(1ms)
c.读取文件2(1ms)
d.处理2的数据(1ms)
e.读取文件3(28ms)
f.处理3的数据(1ms)
g.整合1,2,3的数据结果(1ms)

单线程总共需要34ms,如果还是按照上面的划分方案,类似于木桶原理,
速度取决于最慢的那个线程。在这个例子里,第三个线程执行了29ms,
那么最后这个请求的耗时是30ms,比起不用单线程,就节省了4ms,
但有可能线程调度切换也要花个1-2ms,因此这个方案显示的优势就不明显了,
还带来了程序复杂性的提升,不值得。

所以我们要优化文件3的读取速度,可以采用缓存,减少一些重复读取,
假设所有用户都请求这个请求,相当于所有的用户都需要读取文件3,那你想想,
100个人进行了这个请求,相当于你花在读取这个文件上的时间久是28*100 = 2800ms,
如果你把这个文件缓存起来,那只要第一个用户的请求读取了,第二个用户不需要读取了,
从内存读取是很快的,可能1ms都不到。

在使用多线程的过程中有没有遇到过什么问题?

未完待续

线程安全

什么是线程安全,线程不安全?

线程不安全是当多个线程访问一个共享变量时,该变量会因为多线程访问产生意想不到的问题,
为了避免多线程访问的不可预知的问题,程序中多线程能访问到的变量要加锁,即加synchronized,就变成了线程安全。

Java中有几种方式来保证线程安全?

1.互斥&阻塞同步(悲观锁)
1.1 Synchronized锁(内部锁、重量级锁)
1.2 ReentrantLock锁

2.非阻塞同步(乐观锁)
2.1 原子类
2.2 voliate关键字

3.无同步
3.1 ThreadLocal
3.2 无共享数据

线程阻塞与同步的关系?

同步一定阻塞吗?
阻塞一定同步吗?
未完待续

Synchronized锁的底层原理?

Synchronized用的锁存储在Java对象头内,对象头中含有一个Mark Word字段,默认存储对象的Hashcode、分代年龄、锁标记位。

synchronized关键字经过编译之后, 会在同步块的开始处插入monitorenter指令,在同步块结束处与异常处插入monitorexit指令,
这两个字节码指令都需要指定一个要锁定&解锁的对象。

这个要锁定&解锁的对象可以理解成锁,
那么这个要锁定&解锁的对象是谁呢?
Java中的任意一个对象都都可以当做Synchronized的锁。

  1. 对于普通&实例Synchronized方法,锁是this对象引用&调用该方法的对象引用。
  2. 对于静态Synchronized方法,锁是当前类对象(Class对象),该类的所有对象引用共享一把锁。
  3. 对于Synchronized块,锁是是传入参数的对象引用。
执行monitorenter指令
  1. 首先尝试获取对象的锁,如果这个对象没有被其他线程锁定或当前线程已经拥有了这个对象的锁,把锁的计数器进入数加1,当前线程拥有这个对象的锁。

  2. 如果这个对象已经被其他线程锁定,获取对象的锁失败,则当前线程进入阻塞状态,直到其他线程释放锁为止。

执行monitorexit指令

把锁的计数器进入数减1,当减到0时,锁被释放,其他线程有机会获取到锁。

ReentrantLock的底层原理?

未完待续

Synchronized锁和Lock锁的区别?

  1. Lock更灵活更强大,lock锁可跨类与方法,并自由定义多把锁的加锁解锁顺序并提供多种加锁方案,
    lock阻塞式, trylock无阻塞式, lockInterruptily可打断式(让等待锁的线程响应中断),
    还有trylock的带超时时间版本。

  2. Synchronized是悲观锁(隐式),lock是乐观锁,性能更高(显式)。

  3. Lock需要显式的lock与unlock,而Synchronized不用。

synchronized修饰static方法与非static方法的区别?

这里涉及到一个概念,类锁与对象锁。
类的实例对象可以有多个,但是每个类只有一个class对象,
所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。

当synchronized修饰一个static方法时,多线程下,获取的是类锁。
(即Class本身,注意:不是实例),此类所有的实例化对象在调用此方法,共用同一把锁。

当synchronized修饰一个非static方法时,多线程下,获取的是对象锁。
(即类的实例对象,方法锁也是对象锁)

什么是Java多线程中的悲观锁与乐观锁?

悲观锁:比较悲观,总认为不加锁&互斥同步那就肯定会出问题,无论共享数据是否真的会出现竞争都必须要加锁,称为阻塞&互斥同步,线程阻塞&唤醒会造成性能问题。

乐观锁:比较乐观,总认为不加锁不会出问题,先进行操作,如果没有其他线程争用共享数据,那操作就成功了,如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(例如:不断重试,直到成功为止),乐观锁的很多实现都不需要把线程挂起,因此也成为非阻塞同步。

通过CAS来保证【操作】与【冲突检测】这两个步骤具备原子性。

什么是CAS?

CAS,全称为Compare and Swap,即比较并替换,是一个处理器指令,保证语义上需要多次操作的行为只通过一条处理器指令便可完成。
CAS指令需要3个操作数:内存位置V(Java中变量的内存地址)、旧值A、新值B。
CAS指令执行时,当且仅当旧值A和内存位置V相同时,才会将用B更新V的值,否则什么都不做,这是一个原子操作。

使用原子类会间接使用到CAS的相关代码。

CAS的ABA问题是什么?

一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然为A值,如果在这段期间它的值曾经被改成了B,
后来又被改成了A,那CAS操作就会误认为它从来没有改变过。

为了解决ABA问题,提供了一个带标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性。

Atomic类的作用及其原理?

Atomic类其内部实现是一个更为高效的方式CAS (compare and swap) + volatile和native方法,
从而避免了synchronized的高开销,执行效率大为提升。

当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,
否则旧值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。

public class AtomicDemo1 {
    static AtomicInteger a = new AtomicInteger(0);
//    static int a = 0;
    static final int THREAD_COUNT = 20;

    static void increase(){
        a.incrementAndGet();
//        a++;
    }

    public static void main(String[] args) throws Exception{
        System.out.println("aaaaa");
        Thread[] threads = new Thread[THREAD_COUNT];
        for (int i = 0 ; i < THREAD_COUNT; i++){
          threads[i] = new Thread(new Runnable() {
              @Override
              public void run() {
                  for(int i = 0 ; i < 100000; i++){
                      increase();
                  }
              }
          });
          threads[i].start();
        }

        Thread.sleep(10 * 1000);
        System.out.println(a);
    }
}

volatile的作用以及原理?

(在讲述volatile关键字之前,我们必须要掌握一些基础概念:
Java内存模型,知道主内存与工作内存的关系,
以及并发编程中的三个特性,原子性、可见性、有序性,CPU与虚拟机的指令重排等知识点。)

我们可以使用volatile关键字来修饰一个共享变量,被修饰的变量具有两种特性:

  1. 保证被修饰的变量对所有线程的可见性,当前线程修改变量的值后,其他线程立即可见,
    但普通变量则不能立即可见,因为普通线程需要主内存与工作内存的交互才能完成传递。

使用工作内存变量前必须先从主内存中刷新,用于保证自己可以看到其他线程的修改。
工作内存变量改变后必须立刻同步回主内存,用于保证其他线程可以看到自己的修改。

  1. 禁止指令重排,代码的顺序与执行的顺序相同,普通变量则会指令重排,导致执行顺序不可控。
    volatile变量在并发下进行运算时,并不是安全的,因为运算这个过程并不是原子性操作。
    例如:volatile int a,然后多线程i++,这个i++并不是原子性操作,因此不安全。
volatile底层原理

使用volatile关键字修饰后,在字节码指令层面会添加一个lock操作,
lock操作相当于一个内存屏障(memory barrier&fence),
执行指令重排时不能把【处于下面的指令】重排到内存屏障上面的位置。

lock操作使得当前CPU的高速缓存写入主存,同时使其他CPU的高速缓存失效,
因此可以让volatile变量的修改对其他CPU立即可见。

volatile操作读操作与普通变量区别不大,但是写操作会慢一些,
因为需要插入一些内存屏障指令, 来保证没有指令重排。
在线程安全的情况下加volatile会牺牲性能。

synchronized与volatile的区别?

  1. volatile仅在变量级别,Synchronized可以在变量、方法、类;
  2. volatile仅能实现变量的修改可见性,并不能保证原子性,Synchronized则可以保证可见性和原子性;
  3. volatile不会造成线程的阻塞(没加锁),Synchronized会造成线程的阻塞;
  4. volatile标记的变量不会被编译器优化,Synchronized标记的变量可以被编译器优化

ThreadLocal的作用以及其原理?

ThreadLocal代表一个线程局部变量,是JAVA为线程安全提供的工具类,实现线程本地存储。

通过把数据放在ThreadLocal对象中就可以让每个线程创建一个ThreadLocal对象的副本,
每个线程都可以独立的改变自己变量副本中的值而不会和其他线程的副本起冲突,
好像每一个线程都完全拥有该变量一样,从而避免并发访问的线程安全问题。

ThreadLocal与其他的同步机制类似,都是为了解决多线程对同一变量的访问冲突,
ThreadLocal不能代替同步机制,维度不一样。

同步机制是通过加锁的机制为了同步多个线程对相同资源的并发访问,在竞争状态下获得共享数据,
而ThreadLocal是为了隔离多个线程的数据共享,从根本上避免多个线程对共享资源的竞争。

ThreadLocal不能代替同步机制,因为需要同步机制共享数据达到线程通信的目的。

此外ThreadLocal还有一个小功能,就是可以在一次线程调用中,TheadLocal可以携带共享资源&变量跨越多个类与方法,避免复杂参数传递。

public class ThreadLocalDemo1 {
	public static void main(String[] args) {
		Person p = new Person();
		//启动两个线程,两个线程共享person对象
		new ThreadDemo1(p).start();
		new ThreadDemo2(p).start();
	}
}

class ThreadDemo1 extends Thread {
	Person p;

	public ThreadDemo1(Person p) {
		this.p = p;
	}

	public void run() {
		System.out.println(this.getName() + " start " + p.getName());
		// 将线程局部变量赋值
		p.setName("张三");
		System.out.println(this.getName() + " end " + p.getName());
	}
}

class ThreadDemo2 extends Thread {
	Person p;

	public ThreadDemo2(Person p) {
		this.p = p;
	}

	public void run() {
		System.out.println(this.getName() + " start " + p.getName());
		// 将线程局部变量赋值
		p.setName("李四");
		System.out.println(this.getName() + " end " + p.getName());
	}
}

class Person {
	// 定义线程局部变量,每个线程都会保留该变量的一个副本,多个线程之间并不互相印象。
	private ThreadLocal<String> name = new ThreadLocal<>();

	public String getName() {
		return this.name.get();
	}

	public void setName(String name) {
		this.name.set(name);
	}
}
ThreadLocal底层原理

每个线程的Thread对象中都有一个ThreadLocalMap对象,该对象是一个轻量级的Map对象,
与Map的区别是桶里放的是entry而不是entry的链表。

这个对象存储以ThreadLocal.threadLocalHashCode为key,
以本地线程变量为value的KV键值对,每一个ThreadLocal对象都包含了一个独一无二的threadLocalHashCode值,
使用这个值就可以在KV键值对找到对应的本地线程变量。

主要方法是get()和set(T a), set之后在map里维护一个threadLocal -> a,get时将a返回。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WLU03iDc-1588123493306)(evernotecid://85A94EB2-9BD2-45DE-A16D-D9C9CD7ECF0B/appyinxiangcom/12192613/ENResource/p367)]

什么是多线程死锁?

当两个线程相互等待对方释放锁时就会发生死锁,一旦发生死锁,程序不会发生异常,
也没有提示,所有线程处于阻塞状态,死锁在多锁状态下是很容易触发的。

public class DeadLockDemo1 {
	public static void main(String[] args) {
		A a = new A();
		B b = new B();
		
		/**
		 * 线程1调用获得b对象的监视器锁,线程被暂停了,但还没有释放。
		 * 线程2调用并获得a对象的监视器锁,线程被暂停了,但还没有释放。
		 * 线程1恢复并调用a对象的监视器锁,但a对象的锁被线程2持有还没有释放,因此被阻塞。
		 * 线程2恢复并调用b对象的监视器锁,但b对象的锁被先吃1持有还没有释放,并且线程1还在等待线程2释放a对象的锁,
		 * 最后导致两个线程互相等待对方释放锁,因此出现死锁。
		 */
		
		//线程1
		new Thread(new Runnable() {
			public void run() {
				//
				b.one(a);
			}
		}).start();
		
		//线程2
		new Thread(new Runnable() {
			public void run() {
				//
				a.one(b);
			}
		}).start();
	}
}

class A {
	public synchronized void one(B b) {
		System.out.println(Thread.currentThread().getName()+" 进入A one");
		
		try {
			Thread.sleep(1*1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+" 调用B two");
		b.two();
	}

	public synchronized void two() {
		System.out.println("进入A two");
	}
}

class B {
	public synchronized void one(A a) {
		System.out.println(Thread.currentThread().getName()+" 进入B one");
		try {
			Thread.sleep(1*1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+" 调用A two");
		a.two();
	}
	
	public synchronized void two() {
		System.out.println("B two");
	}
}

concurrent包

CyclicBarrier的作用与实现原理?

实现所有的线程一起等待某个事件的发生,当某个事件发生时,所有线程一起开始往下执行

CopyOnWriteArrayList的作用与原理?

CopyOnWriteArrayList,采用复制底层数组的方式来实现写操作。

当线程对CopyOnWriteArrayList执行读操作时,线程将会直接读取集合本身,不用加锁与阻塞。

当线程对CopyOnWriteArrayLis执行写操作时,
该集合会在底层复制一份新的数组,之后对新的数组进行写入操作,因为写入操作都是对数组的副本执行操作,因此它是线程安全的。

由于CopyOnWriteArrayList写入时要频繁的复制数组,性能比较差,
但由于读写不操作同一个数组,
而且读操作也不需要加锁,因此读操作比较快且安全。
CopyOnWriteArrayList适合用在读多写少的场景,例如缓存。

(
以Concurrent开头与CopyOn开头的类都是线程安全的。

以Concurrent开头的集合类代表支持并发访问的集合,
他们可以支持多个线程并发写入访问,
这些写入线程的所有操作都是线程安全的,但读取操作不必锁定,
采用更复杂的算法保证永远不会锁住整个集合,因此在并发写入时拥有较好的性能。
)

LockSupport的作用?

未完待续

线程池

什么是线程池?(连环炮)

新线程的创建成本较高,使用线程池可以提高性能,尤其是需要创建大量生命周期很短的线程时。
此外线程池能够有效的控制并发线程数量(最大线程数)。

减少在创建和销毁线程上所花的时间以及系统资源的开销,复用线程。
能够对线程进行简单的管理并提供定时执行、间隔执行等功能。
能够有效的控制并发线程数量(最大线程数)

如何创建线程池?

从JDK1.5开始支持线程池,主要使用Executors工厂类来创建线程池,有如下方法可以创建:

创建线程池方法描述
newCachedThreadPool()具有缓存功能的线程池,线程被缓存在线程池中,返回ExecutorService对象。
newFixedThreadPool(int)可重用的、具有固定线程数的线程池。返回ExecutorService对象。
newSingleThreadExecutor()只有单线程的线程池。返回ExecutorService对象。
newScheduledThreadPool(int)指定线程数的线程池,可以指定延迟时间执行线程,即使线程是空闲的也会保存在线程池中。返回ScheduledExecutorService对象。
newSingleThreadScheduledExecutor()只有单线程的线程池,可以指定延迟时间执行线程。返回ScheduledExecutorService对象。

//newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

  • newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

//newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

//newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。

//newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

Java 5+中的Executor接口定义一个执行线程的工具。它的子类型即线程池接口是ExecutorService。

  • newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

  • newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

  • newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

  • newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。

public class ThreadPoolDemo1 {
	public static void main(String[] args) {
		//创建线程池
		//不用以前的start()来启动线程,而是直接提交给线程池即可运行线程。

		ExecutorService pool = Executors.newFixedThreadPool(3);
		Runnable t = new Runnable() {
			public void run() {
				System.out.println(Thread.currentThread().getName());
			}
		};
		
		for(int i = 0 ; i < 9 ;i++) {
			pool.submit(t);
		}
		//关闭线程池
		pool.shutdown();
		/**
		 * 调用shutdown()后线程池不再接受新任务,但会将以前所有已提交的任务执行完成,
		 * 待所有任务执行完毕后,线程池内的所有线程会全部死亡。
		 */
	}
}

创建线程池时,都需要传递什么参数?作用是什么?

未完待续
在构造时需要提供池大小等参数。

ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler
)

线程池的线程池的组成部分有哪些以及底层原理?

任务队列(TaskQueue):用于缓存已经提交单还没有处理的任务,提供一种缓冲机制。

QueueSize:为了防止内存溢出,需要有limit对任务队列数量有所限制。

线程数量管理:管理和控制线程的数量,通过3个参数来实现:

  1. 创建线程池时的初始化线程数量init。
  2. 线程池自动扩充时最大线程数量max。
  3. 线程池空闲时需要释放线程,同时需要维护一定数量的活跃&核心线程数core。
    init <= core <= max。

任务拒绝策略:线程数量已达上限且任务队列已满,需要有相应的拒绝策略来通知任务提交者。

线程工厂:用于个性化制定线程,设置守护线程、线程名称等等。

KeepedAlive:决定线程各个重要参数自动维护的时间间隔。

----====
线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;

工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,
可以循环的执行任务;

任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,
它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;

分析线程池的实现原理和线程的调度过程?

在使用线程池时,因为使用了无界队列,在远程服务异常情况下导致内层飙升,怎么去解决?你要是连线程池都不清楚,你怎么去玩?

java线程池里面的arrayblockingqueue,linkedblockingqueue的用途和区别。

线程池满了怎么办?拒绝策略是什么?

一个线程任务(一个Runnable类型的对象)通过execute(Runnable)方法被添加到线程池,任务的执行方法就是Runnable对象的run()方法,
当被添加到线程池后,需要注意:

  1. 如果此时线程池中的数量小于corePoolSIze,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  2. 如果此时线程池中的数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
  3. 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
  4. 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过handler所指定的策略来处理此任务。

锁机制

JDK1.6为了减少获得/释放锁所带来的的性能开销,从而引入了偏向锁和轻量级锁。

线程中锁的种类与状态?

锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,
他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,
这种策略是为了提高获得锁和释放锁的效率。

自旋锁

采用让当前线程不停地的在循环体内执行实现的,
当循环的条件被其他线程改变时才能进入临界区(TicketLock ,CLHlock 和、MCSlock)。
用循环不断的轮询锁的状态,锁可用的时候就退出。这就是自旋锁,众所周知,
这样里面基本不做什么事情的循环是非常耗CPU的,如果等待锁的时间很长,
用这种方式是不合适的。

阻塞锁

可以说是让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,
才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。(synchronized 关键字(其中的重量锁),ReentrantLock)

可重入锁(ReentrantLock)

也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,
内层递归函数仍然有获取该锁的代码,但不受影响

读写锁

在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,
读是无阻塞的,在一定程度上提高了程序的执行效率(ReentrantReadWriteLock)

锁优化有哪些?

自旋锁
锁消除
锁粗化
轻量级锁
偏向锁

JVM

超过3年的Java程序员,在面试时如果还回答不出一些JVM的基本问题,那真的是很尴尬,
而对JVM的掌握程度貌似成为了java程序员的一种能力衡量,一种准入门槛。

需要深入理解的是JVM的存储部分(存储?在硬盘?在数据库?nono,只有在内存)
我们写的所有类,常量,方法等都在内存中,这决定着我们程序运行的是否健壮,是否高效。

JVM综合

JVM的组成部分及其含义?分别存储哪些数据?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gTyHtjc2-1588123493307)(evernotecid://85A94EB2-9BD2-45DE-A16D-D9C9CD7ECF0B/appyinxiangcom/12192613/ENResource/p362)]

把JVM分为四大部分:ClassLoader、ExecutionEngine、NativeInterface、RuntimeDataArea。
整个JVM 框架由加载器加载文件,加载到内存区后,然后执行器在内存中处理数据,需要与异构系统交互是可以通过本地接口进行,瞧,一个完整的系统诞生了!

ClassLoader类加载器

负责加载类文件(.class文件)到内中存中,
ClassLoader对于class文件有格式要求。
ClassLoader只负责加载,只要符合文件结构就加载,
后面由由ExecutionEngine负责能不能运行。

ExecutionEngine执行引擎

负责解释命令,提交操作系统执行。

NativeInterface本地接口(很少使用)

负责融合不同的编程语言为Java所用,初衷是融合&调用C/C++ 程序,
在内存中专门开辟了一块区域处理标记为native的代码,
它的具体做法是Native Method Stack中登记native方法,
在Execution Engine 执行时加载native libraies。

RuntimeDataArea运行数据区(JVM的内存管理)

我们所有写的程序都被加载到这里,之后才开始运行。

Stark栈&栈内存(线程私有)

栈是Java程序的运行区,在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放。(对于栈来说不存在垃圾回收问题,只要线程一结束,该栈就没了。)
栈的大小是有一定的限制,比如方法无限调用自己,可能出现StackOverFlow问题。

Heap堆&堆内存(所有线程共享)

一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。
类加载器读取了类文件后,需要把类(类对象java.lang.Class)、方法、常量放到堆内存中,
以方便执行器执行,这里可能出现的异常java.lang.OutOfMemoryError: Java heap space
堆内存分成三部分:新生区、养老区、永久区。

2.1 YoungGenerationSpace新生区
新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集。
新生区又分为两部分:伊甸区与幸存者区。
2.1.1EdenSpace伊甸区
所有的对象都是在伊甸区被new出来的。
2.1.2SurvivorSpace幸存者区
幸存区有两个: 0 区(Survivor 0 space )和1 区(Survivor 1 space )。
当伊甸园的空间用完时,程序又需要创建对象,GC将对伊甸园区进行垃圾回收,
将伊甸园区中的不再被其他对象所引用的对象进行销毁。
然后将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,
然后移动到1区。那如果1区也满了呢?再移动到养老区。

2.2 TenureGenerationSpace养老区
养老区用于保存从新生区筛选出来的JAVA对象,一般池对象都在这个区域活跃。
对象最活跃的区域是养老区,因为经过各种垃圾回收之后对象都跑到这里来了。

MethodArea方法区(所有线程共享)

该区域保存所有属性和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。
被装载的class的信息存储在Method area的内存中。
当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件内容并把它传输到虚拟机中。紧接着虚拟机提取其中的类型信息,并将这些信息存储到方法区。
方法区存储的内容:
类变量(静态变量);
这里可能出现的异常java.lang.OutOfMemoryError: PermGen full

PCRegister PC寄存器(又称程序计数器,PC=Program counter,线程私有)

每个线程都有自己的PC寄存器,线程启动时创建,就是一个指针,指向方法区中的方法字节码,
由执行引擎读取下一条指令。
PC寄存器的内容总是指向下一条将被执行指令的地址,这里的地址可以是一个本地指针,
也可以是在方法区中相对应于该方法起始指令的偏移量。

NativeMethodStack本地方法栈(线程私有)

对于一个运行中的Java程序而言,它还能会用到一些跟本地方法相关的数据区。
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受JVM限制的世界。
本地方法可以通过本地方法接口来访问JVM的运行时数据区,不止如此,它还可以做任何它想做的事情。比如,可以调用寄存器,或在操作系统中分配内存等。总之,本地方法具有和JVM相同的能力和权限。 (这里出现JVM无法控制的内存溢出问题native heap OutOfMemory )

JVM中一个对象从创建到销毁的过程?

未完待续。

JVM常用参数都有哪些?

-Xms,堆的初始内存,默认为物理内存的1/64;
-Xmx,堆的最大内存,默认为物理内存的1/4;
默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;
默认空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。
因此服务器一般设置-Xms、-Xmx相等以避免在每次GC后调整堆的大小。

-XX:PermSize,设置非堆内存初始值,默认是物理内存的1/64;
-XX:MaxPermSize,设置最大非堆内存的大小,默认是物理内存的1/4。

-Xmn — 堆中年轻代的大小
-XX:-DisableExplicitGC — 让System.gc()不产生任何作用
-XX:+PrintGCDetails — 打印GC的细节
-XX:+PrintGCDateStamps — 打印GC操作的时间戳
-XX:NewSize / XX:MaxNewSize — 设置新生代大小/新生代最大大小
-XX:NewRatio — 可以设置老生代和新生代的比例
-XX:PrintTenuringDistribution — 设置每次新生代GC后输出幸存者乐园中对象年龄的分布
-XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:设置老年代阀值的初始值和最大值
-XX:TargetSurvivorRatio:设置幸存区的目标使用率

new完对象之后,该对象进入了JVM的堆里哪个分区?(网易面试题)

-伊甸区

什么时候会发生JVM堆溢出?内存溢出了怎么办?(阿里面试题)

未完待续

如何进行JVM参数优化?

未完待续

如何排查&解决线上OOM、 CPU高问题?

首先我们需要有报警机制,在CPU使用率超过一个阈值后报警让开发&运维工程师感知到,
之后我们需要通过SSH工具进入到相应的Linux系统中。

  1. 查看占CPU最高的进程
    输入top -c,之后按P或M排序,得到PID进程ID,例如:12345。

  2. 查看占CPU最高的线程
    输入top -Hp 12345,之后按P或M排序,得到PID线程ID,例如:67890。

  3. 将线程ID转成16进制
    输入printf “%x\n” 67890,得到16进制,例如:0x2a34。

  4. 通过16进制线程ID查找堆栈中的线程
    输入jstack 12345 | grep ‘0x2a34’ -C5 --color,
    显示线程对应的线程名称,以及看到了该线程正在执行代码的堆栈,根据堆栈里的信息,找到对应的代码。

4.1 如果没有看到”VM Thread“
代表代码中有比较耗时的计算,那么我们得到的就是一个线程的具体堆栈信息。

4.2 如果看到”VM Thread“
代表是垃圾回收线程,垃圾回收过于频繁,导致GC停顿时间较长。
输入jstat -gcutil 9 1000 10(jstat -gcutil <times),看到Full GC数量,如果数量很多,则确认是因为内存泄露导致OOM。

通过报警机制或者运维同事给出OOM异常,之前需要配置JVM参数-XX:+HeapDumpOnOutOfMemoryError,
使用工具对hprof或dump文件进行分析,找到占用内存最大的对象(如果内存最大对象是数组,则通过查看GC Roots查看谁持有了数组的引用)。

也可能是依赖包中有System.gc()导致。

导致OOM的原因可能有:

  • 有可能是内存分配确实过小,而正常业务使用了大量内存;
  • 某一个对象被频繁申请,却没有释放,内存不断泄漏,导致内存耗尽;
  • 某一个资源被频繁申请,系统资源耗尽,例如:不断创建线程,不断发起网络连接;

4.2.1 确认是不是内存本身就分配过小
输入jmap -heap 10765
可以查看新生代,老生代堆内存的分配大小以及使用情况,看是否本身分配过小。

4.2.2 找到最耗内存的对象
输入jmap -histo:live 10765 | more
对于实例数较多,占用内存大小较多的实例/类,相关的代码就要针对性review了

如果发现某类对象占用内存很大(例如几个G),很可能是类对象创建太多,且一直未释放。例如:

  • 申请完资源后,未调用close()或dispose()释放资源

  • 消费者消费速度慢(或停止消费了),而生产者不断往队列中投递任务,导致队列中任务累积过多

会引发full GC,生产环境慎用。
线上执行该命令会强制执行一次fgc。另外还可以dump内存进行分析。

4.2.3 确认是否是资源耗尽
输入netstat,查看进程创建的线程数,以及网络连接数,如果资源耗尽,也可能出现OOM。

这里介绍另一种方法,通过

  • /proc/${PID}/fd
  • /proc/${PID}/task
    可以分别查看句柄详情和线程数。

例如,某一台线上服务器的sshd进程PID是9339,查看

  • ll /proc/9339/fd
  • ll /proc/9339/task
    如上图,sshd共占用了四个句柄
  • 0 -> 标准输入
  • 1 -> 标准输出
  • 2 -> 标准错误输出
  • 3 -> socket(容易想到是监听端口)

sshd只有一个主线程PID为9339,并没有多线程。

所以,只要

  • ll /proc/${PID}/fd | wc -l
  • ll /proc/${PID}/task | wc -l (效果等同pstree -p | wc -l)
    就能知道进程打开的句柄数和线程数。
为什么线上系统化突然变慢?

如果变慢导致系统不可用,需要先导出jstack和内存信息,之后重启系统。

  1. 代码中某个位置读取数据量较大,导致系统内存耗尽,从而导致Full GC次数过多,系统缓慢;
  2. 代码中有比较耗CPU的操作,导致CPU过高,系统运行缓慢;
  3. 代码某个位置有阻塞性的操作,导致该功能调用整体比较耗时,但出现是比较随机的;
  4. 某个线程由于某种原因而进入WAITING状态,此时该功能整体不可用,但是无法复现;
  5. 由于锁使用不当,导致多个线程进入死锁状态,从而导致系统整体比较缓慢。
Full GC的原因?

代码中一次获取了大量的对象,导致内存溢出,此时可以通过工具查看内存中有哪些对象比较多;

内存占用不高,但是Full GC次数还是比较多,此时可能是显示的System.gc()调用导致GC次数过多,
这可以通过添加-XX:+DisableExplicitGC来禁用JVM对显示GC的响应。

GC

什么是GC?(连环炮)

垃圾回收,需要考虑三件事情:

1. 哪些内存需要GC?

大体上主要是堆和方法区的内存需要回收,这些地方的内存分配存在不确定性,
我们只有在程序运行的时候才知道哪些对象会被创建,内存的分配与回头都是动态的。

细节上需要有算法来判定哪些对象需要GC。

可达性分析算法

通过一堆GC Roots对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(GC Roots到这个对象不可达)时,证明此对象是不可用的。

可以作为GC Roots的对象有:栈帧中的本地变量表中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(native方法)引用的对象。

2. 什么时候GC?为什么?

未完待续

3. 如何GC?
GC算法有哪些?分别都有什么优劣势?都适用于什么场景?
  • 标记-清除算法
  • 复制算法
  • 标记-整理算法
  • 分代收集算法
垃圾收集器有哪些?分别都有什么优劣势?都适用于什么场景?
  • Serial
  • ParNew
  • Parallel Scavenge
  • Serial Old
  • Parallel Old
  • CMS
  • G1
GC如何优化?

未完待续

Java对象有几种引用类型,作用是什么?

强引用(strong reference)

这是java中最常见的饮用方式。
程序创建一个对象,并把这个对象赋给一个引用变量,
程序通过该引用变量来操作实际的对象。
当一个对象被一个或一个以上的引用变量所引用时,gc并不会回收,
当没有引用变量引用时,则会被gc回收。

弱引用(weak reference,由WeakReference类实现)

当被弱引用指向的对象除该弱引用之外不再有其他的引用指向这个对象时(被标记成垃圾时),
这时这个对象依然可以通过弱引用访问,一旦gc检查到这个弱引用对象,就会把他回收。

弱引用有个防止内存溢出的用途:
对象放在HashMap里面,当对象变成垃圾后,却没有从hashmap里去掉,
则gc不会回收,导致内存溢出风险。而把对象放在WeakHashMap里面,
WeakHashMap里的键值对象均为弱引用,故gc会回收。

软引用(soft reference,由SoftReference类实现)

当被软引用指向的对象除该软引用之外不再有其他的引用指向这个对象时(被标记成垃圾时),
这时这个对象依然可以通过软引用访问,一旦gc检查到这个软引用对象,
在内存不足时,gc会回收他,否则不会。

软引用最大的用途是用来做缓冲,保存一些垃圾对象,
延长其存活时间,只要内存不溢出还有富裕,
对象不被gc回收,内存吃紧时,gc在抛出内存溢出异常前才会回收这些垃圾对象。

虚引用&幻影引用(phantom reference,由PhantomReference类实现)

幻影引用不能延长垃圾对象的存活时间,
必须和ReferenceQueue类(引用队列)一起使用,
当gc要回收被幻影引用指向的对象时,gc会将其挂在ReferenceQueue队列中,
起到类似消息传递的作用,使对象内存在被回收前执行特定的pre-mortem操作。
虚引用主要用于跟踪垃圾对象回收的状态。

程序可以通过检查引用队列中是否已经包含了该虚引用,
从而了解虚引用所引用的对象是否被回收。

三个GC的区别(58面试题)?

不管什么GC,都会发生stop-the-world,区别是发⽣的时间长短。
⽽这个时间跟垃圾收集器又有关系,
Serial、PartNew、Parallel Scavenge收集器⽆论是串⾏还是并⾏,都会挂起⽤户线程,
⽽CMS和G1在并发标记时,是不会挂起⽤户线程的,但其它时候⼀样会挂起⽤户线程,
stop-the-world的时间相对来说就小很多了。

Minor GC(清理年轻代)

Minor GC指新生代GC,即发生在新生代(包括Eden区和Survivor区)的垃圾回收操作,
当新生代无法为新生对象分配内存空间的时候,会触发Minor GC。

因为新生代中大多数对象的生命周期都很短,所以发生Minor GC的频率很高,
虽然它会触发stop-the-world,但是它的回收速度很快。

Major GC(清理老年代)

Major GC清理老年代,用于回收老年代,出现Major GC通常会出现至少一次Minor GC。

Full GC 清理整个堆空间—包括年轻代、老年代、元空间

Full GC是针对整个新生代、老生代、元空间及堆外内存的全局范围的GC。

Full GC不等于Major GC,也不等于Minor GC+Major GC,
发生Full GC需要看使用了什么垃圾收集器组合,才能解释是什么样的垃圾回收。

触发Full GC的原因?
  • 当年轻代晋升到⽼年代的对象⼤⼩,并⽐⽬前⽼年代剩余的空间⼤⼩还要⼤时,会触发Full GC;
  • 当⽼年代的空间使⽤率超过某阈值时,会触发Full GC;
  • 当元空间不⾜时(JDK1.7永久代不足),也会触发Full GC;
  • 当调⽤System.gc()也会安排⼀次Full GC。

单例会被GC吗?

会,没有被其他对象引用的时候就会被GC。

被static修饰会被GC吗?

未完待续

如果不想被GC/想在GC中生存一次怎么办怎么办?

未完待续

ClassLoader

什么是ClassLoader?

类加载器的作用是加载类文件到内存,比如编写一个HelloWord.java 程序,
然后通过javac 编译成class 文件。

那怎么才能加载到内存中被执行呢?Class Loader 承担的就是这个责任,
那不可能随便建立一个.class 文件就能被加载的,Class Loader 加载的class 文件是有格式要求
(Class Loader 只管加载,只要符合文件结构就加载,至于说能不能运行,则不是它负责的,那是由Execution Engine 负责的。

类加载器由JVM提供,负责将类加载进内存,是所有JAVA程序运行的基础,
我们也可以自定义类加载器。

当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过加载、连接、初始化单个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载或类初始化。

JVM加载一个类的过程(阿里面试题)?

1.检测此Class是否载入过(即缓存区是否有此Class),如果有则直接进入第8步,否则接着执行第2步。
2.如果父类加载器不存在(要么parent是根类加载器,要么本身就是根类加载器),则跳到第4步执行;如果父类加载器存在,则接着执行第3步;
3.请求使用父类加载器去载入目标类,如果成功载入则跳到第8步,否则接着执行第5步;
4.请求使用根类加载器来载入目标类,如果成功载入则跳到第8步,否则跳到第7步;
5.当前类加载器尝试寻找Class文件(从与该ClassLoader相关的类路径中寻找),如果找到则执行第6步,如果找不到则跳到第7步;
6.从文件中载入Class,成功载入后跳到第8步;
7.抛出ClassNotFoundException异常;
8.返回对应的java.lang.Class对象;

加载

系统可能在第一次使用某个类时加载该类,也可能采用预加载机制来加载某个类。
类加载器将类的class文件读入内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类,系统都会为之创建一个java.lang.Class系统中所有类其实也是实例——它们都是java.lang.Class的实例。
使用不同的类加载器,可以从不同来源加载类,通常有以下几种来源:
从本地文件系统加载class文件,这是最常见的方式
从JAR包加载class文件
通过网络加载class文件
把一个Java源文件动态编译,并执行加载
将类的class文件读入内存,并为之创建一个java.lang.Class对象,(系统中的所有类也是实例,他们都是java.lang.Class的实例。
类的加载由类加载器完成,类加载器通常由jvm提供,这些类加载器也是签名所有程序运行的基础(也称为系统加载器)
coder也可以通过继承ClassLoader基类来创建自己的类加载器。
类加载器无需等到“首次使用“该类时才加载该类,java虚拟机规范允许系统预先加载某些类。

连接

当类被加载之后,系统为之生成一个对应的java.lang.Class对象,接着将会进入连接阶段。
连接阶段负责把类的二进制数据合并到JRE中。
类的连接有如下3个阶段:

  1. 验证:检查被加载的类是否有正确的内部结构并和其他类协调一致。
  2. 准备:类负责为类的静态变量分配内存,并设置默认初始值。
  3. 解析:将类的二进制数据中的符号引用替换成直接引用。

初始化

JVM负责对类进行初始化,主要对静态变量进行初始化,在java类中对静态变量指定初始值有2种方式:
1.声明静态filed时指定初始值。
2.使用静态初始化块为静态filed指定初始值。
jvm会按这些语句在程序中的排列顺序依次执行他们。
JVM初始化一个类包含以下几个步骤:
1.假如这个类还没有被加载和连接,则先加载并连接该类
2.假如该类的直接父类还没有被初始化,则先初始化其直接父类
3.假如类中有初始化语句,则系统依次执行这些初始化语句

Java的类加载器都有哪些?(连环炮)

每个类加载器都负责加载哪些类?
类加载之间的父子关系是怎样的

类加载器负责将.class文件加载到内存中,并为之生成对应的java.lang.Class对象。
类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class实例。
一旦一个类被载入JVM中,同一个类就不会被再次载入了。

在Java中,一个类用其全限定类名(包括包名和类名)作为标识;
但在JVM中,一个类用其全限定类名和其类加载器作为唯一标识这就意味着两个类加载器加载的同名类(Person、pg、kl)和(Person、pg、kl2)是不同的,它们所加载的类也是完全不同、互不兼容的。

当JVM启动时,会形成有三个类加载器组成的初始类加载器层次结构。
此外用户也可以继承ClassLoader来实现自定义类加载器。
他们的层级结构是:根类加载器<-扩展类加载器<-系统类加载器<-自定义类加载器。

Bootstrap ClassLoader 根类加载器

负责加载java的核心类,例如String、System、ArrayList、HashMap等等。
它并不是java.lang.ClassLoader的子类,而是由JVM自身实现的,不是java语言实现的。

Extension ClassLoader 扩展类加载器

负责加载JRE的扩展目录(%JAVA_HOME%/jre/lib/ext)中jar包的类,
通过这种方式,就可以为Java扩展核心类以外的新功能,只要把自己开发的类打包成JAR文件,然后放入JAVA_HOME/jre/lib/ext路径即可。

System&Application ClassLoader 系统&应用类加载器

它负责在JVM启动时加载来自java命令的-classpath选项、java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。
程序可以通过ClassLoader.getSystemClassLoader()来获取系统类加载器,如果没有特别的指定,
则用户自定义的类加载器都以系统类加载器作为父类。

类加载机制有哪些?

全盘负责

当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其它Class也将由该类加载器负责载入,除非显式使用了另一个类加载器来载入。

双亲委派&父类委托

先让parent(父)类加载器试图加载该Class,只有在父类加载器无法加载该类时,
才尝试从自己的类路径中加载该类。

为什么Java的类加载器要使用双亲委派模型?
缓存机制

缓存机制将会保证所有加载过的Class都会被缓存,当程序需要使用某个Class时,类加载器先从缓冲区搜索该CLass,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。(这就是为什么修改Class后,必须重新启动JVM,程序所做的修改才会生效的原因)

如何自定义自己的类加载器,自己的类加载器和Java自带的类加载器关系如何处理?

IO&NIO

要是以为io就只是用于一个文件的上传和下载那你可就大错特错了。。。
但凡涉及到网络通信,都离不开io的知识。。。
其中NIO是重中之重,想深入学习NIO的童鞋可以去玩netty底层代码。
切记NIO不能只停留在表面概念阶段,这样被面试官发现会大大的减分。
NIO要结合java的网络编程知识体系,把代码写出来,加深印象。

IO

IO包中运用了哪些设计模式?为什么?

装饰器模式,未完待续。

什么是序列化,反序列化,如何实现序列化?

序列化是允许将内存中的java对象转换成与平台无关的二进制流,通过硬盘或网络进行传输。
反序列化是将二进制流转换成内存中的java对象。
默认可通过ObjectOutputStream、ObjectInputStream实现序列化与反序列化。

Serializable与serialVersionID的作用?

必须让类实现java.io.Serializable接口表示该类的对象支持序列化机制,该接口没有需要实现的方法,
他仅仅告诉java底层该类的对象是可以进行序列化的,
并且序列化版本ID由serialVersionUID变量提供。

变量serialVersionUID是一个静态的long型的常量,用于在序列化和反序列化过程中,
起到一个辨别类的序列化版本作用,如果两个类名完全相同,
则通过serialVersionUID来判断该类是否符合要求,如果不行则抛异常。

当一个类升级后(对原有类进行修改并编译成class文件),只要serialVersionUID保持不变,
序列化机制会把他们当成同一个序列化版本。

如果不显式定义则jvm会自动计算创建serialVersionUID,
而修改后的类与修改前的类计算结果往往不同,而造成版本不兼容问题。
我们应该显式的定义个serialVersionUID,即使在某个对象被序列化之后,
它所对应的class文件被修改了,该对象也依然可以被正确的反序列化。

NIO

什么是NIO?为什么要使用NIO?

传统IO读取数据时,如果数据源没有数据则会导致线程阻塞,都是阻塞式的输入输出。
传统IO都是通过字节的移动来处理,一次只能处理一个字节,效率不高。

JDK1.4新增了一系列改进的IO处理功能,被称为NIO。
NIO采用内存映射文件的方式来处理输入输出,NIO将文件或文件的一段区域映射到内存中,
这样就可以像访问内存一样访问文件了。非阻塞, 性能大幅度提高。

Channel(通道)是传统输入输出的模拟,在NIO中所有数据的传输都需要通过通道。
Channel有个map()方法,可以直接将"一块数据"映射到内存中。
传统IO是面向流的处理,NIO是面向块的处理。

Buffer可以理解成一个容器,本质上是一个数组,数据通过Channel传输之前都需要放入到Buffer中,
Buffer像竹筒,去Channel里取水,也允许使用Channel直接将文件的模块数据映射成Buffer。

NIO需要使用Buffer、Channel、Selector结合使用才能方显威力(具体参考java非阻塞网络编程)
把Socket通过Channel注册到Selector,使用一个线程在Selector中轮询,
发现Channel有读写的事件,就可以分配给其他线程来处理(通常使用线程池)。

什么是BIO、NIO、AIO?

BIO NIO AIO
同步阻塞 同步非阻塞 异步非阻塞
jdk1.4以前 jdk1.4 jdk1.7(NIO.2)
网络连接时,一个请求一个线程,在数据没有开始传输以及数据没有传输完毕前,线程要进行阻塞,系统资源消耗大,吞吐量低。 网络连接时,多个请求对应一个或少量线程,在数据没有开始传输,线程不阻塞,而是轮询check哪个请求的数据来了才去服务哪个请求,需要结合selector以及线程池。 未完待续
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中 NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中 AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作

NIO的Reactor模型是什么(滴滴面试题)?

未完待续。

NIO的底层原理?Selector的内部实现poll与epoll解释?(滴滴面试题)

NIO的核心是IO线程池,未完待续。


MySQL

MySQL作为全球最流行的开源关系型数据库之一,早已经把Oracle淘汰的不要不要的。
MySQL为什么这么火?开源与免费是两个很大的点,
像58同城这么大的量也可以使MySQL做256个分库等等,
像BAT的专家们还会修改MySQL源码。。。
MySQL的面试重灾区在于索引、事务、锁机制、MySQL优化等等。

即使现在NOSQL、NEWSQL也开始逐渐流行,但绝对没有替代MySQL的意思,
最多最多也只是相辅相成。

MySQL综合

InnoDB引擎有什么特点?(阿里面试题)

为什么表中要建立一个与业务无关的自增主键?(京东面试题)

使用整型创建主键比字符类型效率更高,使用MySQL底层自增机制性能好,
为以后做分库分表做路由打基础。
按时间排序,但是没有给时间字段建索引,就可以通过id来进行排序

如何进行MySQL的优化?

1.开启mysql缓存
不使用CURDATE()、NOW()、RAND()等函数,这些函数会导致缓存不开启,
更新一条数据后会导致缓存失效。

2.explain关键字优化sql
使用explain关键字排查sql运行的信息,以及可以使用PROCEDURE ANALYSE()分析问题。

3.建立并使用索引
注意where条件的的顺序以及索引的顺序,注意索引的最左原则,
字段使用NOT NULL,尽量不要在索引列使用函数,
不要对含有大量重复值的列建立索引,有助于索引优化。

4.只查询需要的字段,不要使用select *
使用select *会导致性能下降,并会加重网络传输的负担。

5.列长度尽可能的小,最好固定长度
能用char就不要用varchar,能用date就不要用datetime,能用int(4)就不要用int(10),
越小的列会越快,减少硬盘io。

6.不使用存储过程,触发器,视图,自定义函数等功能
数据库是一项比较珍贵的资源,不要让他做太多事情。

7.读写分离与分库分表;
8.拒绝复杂sql。
9.使用数据库连接池;
10.使用innodb引擎;
11.mysql参数优化,例如最大连接数;
12.只查询一条记录时,使用limit 1;
13.如果只是查询数据则使用count(1)而非count(*);

14.垂直拆分表
定长字段与不定长字段放在2张表中,表中都是定长字段会加速,或冷热字段分离,
还有用户表中把最后登录时间放到从表中,对mysql查询缓存友好。

15.将热点数据放在如redis&memcached的分布式缓存中;

16.执行批量删除时加LIMIT
例如:DELETE FROM xxxx WHERE zzz <= ‘2009-11-01’ LIMIT 1000

执行批量增删可能会导致锁表。

MySQL如何恢复数据?

binlog,未完待续

binlog的实现原理?

未完待续

SQL

行转列,使用一条sql将例1改成例2.

例1:
year month amount
1991 1 1.1
1991 2 1.2
1991 3 1.3
1991 4 1.4
1992 1 2.1
1992 2 2.2
1992 3 2.3
1992 4 2.4

例2:
year m1 m2 m3 m4
1991 1.1 1.2 1.3 1.4
1992 2.1 2.2 2.3 2.4

SELECT YEAR,
MAX(CASE WHEN MONTH = 1 THEN amount ELSE 0 END ) AS m1,
MAX(CASE WHEN MONTH = 2 THEN amount ELSE 0 END ) AS m2,
MAX(CASE WHEN MONTH = 3 THEN amount ELSE 0 END ) AS m3,
MAX(CASE WHEN MONTH = 4 THEN amount ELSE 0 END ) AS m4
FROM aaa GROUP BY YEAR 
删除重复的数据

一个表有三个字段key_id(主键)、a、b,用一条SQL来删除表中a,b字段重复的数据,但要至少保留一条重复数据。

DELETE FROM table_name WHERE key_id IN (

SELECT key_id FROM (
SELECT aaa.key_id FROM table_name AS aaa 
INNER JOIN table_name AS bbb 
ON aaa.key_id >= bbb.key_id 
AND aaa.a = bbb.a 
AND aaa.b = bbb.b 
GROUP BY aaa.key_id HAVING COUNT(aaa.a) > 1
) AS xxx  

)

索引

为了提高查询效率,需要使用效率更高的查询算法,以及满足高效查询算法的数据结构,
这些数据结构以某种方式引用(指向)数据,这种数据结构就是索引。

索引的底层原理是什么?

(回答这个问题必须要深刻理解树这种数据结构,以及二叉树,红黑树,B树,B+树)

索引是存储引擎层面的概念,不同的存储引擎使用不同的索引规则。
InnoDB存储引擎是使用B+树作为索引的数据结构,B+树相对于B树的优势:

  1. B+树的中间节点没有卫星数据,所以同样大小的磁盘页可以容纳更多的节点元素,
    在数据量相同的情况下,B+树比B树更加矮胖,因此查询时的IO更少。

  2. B+树每一次查询都必须落在叶子节点上,保证了每一次查找都是稳定的,而B树具有不稳定性。
    (试想一下,如果一个数据库查询,有时候执行10ms,有时候执行100ms,不如每次都执行30ms)

  3. B+树的范围查询,可以使用有序链表遍历,更加高效,而B树则只能使用复杂的中序遍历。

索引文件是存储在磁盘上的,磁盘IO是很重要的性能指标。
当我们查询使用索引时,并不能把整个索引文件全部加载到内存中,
数据量大的情况下,索引文件可能会高达数GB,
我们只能逐一加载每一个磁盘页,每个磁盘页对应着索引树的节点。

IO的次数由树的高度决定,所以为了减少磁盘IO,我们需要把瘦高的树变得矮胖,以此来提升性能。

联合索引在B+树种如何存储?

未完待续

什么情况下应该建立索引?什么情况下不应该建立索引?

不应该建立索引:
表记录太少。
经常插入、删除、修改的表。
重复值多的表。
有null值

索引的优点和缺点?

未完待续

索引中的最左原则?

未完待续

如何进行索引优化?

  1. 复合索引与where条件后面的列排序应该保持一致,衍生出最左原则和where条件列顺序的概念。
  2. 索引不会包含有NULL值的列只要列中包含有NULL值都将不会被包含在索引中,
    复合索引中只要有一列含有NULL值,那么这一列对于此复合索引就是无效的。
    所以我们在数据库设计时不要让字段的默认值为NULL。
  3. MySQL一次查询只用一个索引,如果where中使用了索引,那么order by中就不会使用索引。
    因此数据库默认排序可以符合要求的情况下不要使用排序操作;
    尽量不要包含多个列的排序,如果需要最好给这些列创建复合索引。
  4. 一般情况下不鼓励使用like操作,如果非使用不可,如何使用也是一个问题。
    like “%aaa%” 不会使用索引而like “aaa%”可以使用索引。
  5. 不要在where条件列上使用函数,会导致索引失效。
  6. 不使用NOT IN和<>,会导致索引失效。NOT IN可以NOT EXISTS代替,
    id<>3可使用id>3 or id<3来代替。

WHERE条件的顺序有什么需要注意的?
where条件的顺序是从左往右执行的,需要将过滤条件越严格的(排除数据越多的)的字段放在第一个。
应该尽可能地第一次就过滤掉大部分无用的数据,只返回最小范围的数据,可以提高查询效率。

事务

事务的ACID是什么?

原子性(Atomicity)

事务是一个最小执行单位,具有逻辑上不可再分割的特性,
要么全部成功,要么全部失败回滚。

一致性(Consistency)

无论事务执行成功或失败,数据都要保证一致性的状态,保证数据的完整性。

例如:要回滚的事务对数据库的修改已被写入数据库,此时,数据库就处于不一致的状态。
例如:A给B转账,从A中扣除的金额必须与B中存入的金额一致。

隔离性(Isolation)

数据库系统提供一定的隔离机制,用来保障各个事务的执行互不干扰,
并发执行的事务之间不能互相影响。

当然隔离性越高,并发性就会越差,反之亦然,
后面会衍生出事务隔离级别和锁机制等概念,用于并发事务的隔离。

持久性(Durability)

事务一旦提交,对数据库所做的任何改变,都要落盘,保存进物理数据库中。

事务几种缺陷分别是什么?

为了避免下述问题,需要在某个事务的进行过程中锁定正在更新或者查询的数据,
直到目前的事务完成,然而如果是完全锁定,则另一个事务来查询同一份数据就必须等待,
直到前一个事务完成并解除锁定位置,这会造成性能问题。

在现实场景中,根据需求的不同,并不用完全锁定,可以设置不同的隔离级别来满需求。

脏读(dirty reads)

事务A读到了事务B未提交的数据,事务B回滚了,事务A形成脏读。

不可重复读(non-repeatable reads)

事务A多次读取相同条件的数据但返回结果却不一样,
事务A后续读取到了事务B已提交的更新&删除数据。

相反,可重复读则为多次读到的数据是一样的,
也就是多次读取不能读取到其他事务已经提交的数据。

幻读(phantom reads)

事务A根据条件检索得到0条数据,然后事务B新增了一条数据并提交,
导致这条数据也符合事务A当时的搜索条件。

此时事务A再次搜索发现还是0条(因为可重复读),
但是如果事务A也插入一条同样的数据就会报错,就产生了幻读,相当于之前读出的0条是假象。

在一个应用程序中,可能有多个事务同时在进行,
这些事务应当彼此之间互不知道另一个事务的存在,
由于事务彼此之间的独立,若读取的是同一个数据的话,就容易发生问题。

第一类丢失更新(lost update)

在没有事务隔离的情况下,两个事务都同时更新一行数据,
但是第二个事务却中途失败退出,导致对数据的两个修改都失效了。

例如:
张三的工资为5000,
事务A取5000,
事务B取5000,存100,并提交数据库,工资变为5100,

随后事务A发生异常,回滚了,恢复张三的工资为5000,这样就导致事务B的更新丢失了。

第二类丢失更新(second lost update)

不可重复读的特例,有两个并发事务同时读取同一行数据,
然后其中一个对它进行修改提交,而另一个也进行了修改提交。这就会造成第一次写操作失效。

例如:
事务A读到张三的存款为5000,操作没有完成,事务还没提交。
与此同时,
事务B,存1000,张三的存款改为6000,并提交了事务。
随后,
在事务A中,存储500,把张三的存款改为5500,并提交了事务,
这样事务A的更新覆盖了事务B的更新。
(对第一次更新加锁,可以避免此问题)

redo日志的作用是什么?

如果每次事务提交都刷到磁盘上,会导致性能很低(磁盘随机写效率低),
所以先将修改操作写到redo日志里(随机写变顺序写),
再定期刷到磁盘上,提升性能。

如果某天数据库宕机没有及时刷盘,在数据库重启后,
会重做redo日志里的内容,保证刷盘。

undo日志&事务回滚的原理是什么?

事务未提交时,会将事务修改前的旧版本存放到undo日志里,
当事务回滚时,可以利用undo日志实施回滚,undo日志存储在回滚段中。
回滚段里的数据,其实是历史数据的快照(snapshot)。

事务隔离级别分别是什么?

-dirty readsnon-repeatable readsphantom reads
SERIALIZABLEnonono
REPEATABLEREADnono
READ COMMITTEDnoyesyes
READ UNCOMMITTEDyesyesyes

可能会问在之前公司是如何使用这些隔离级别的,要提前做好准备。

MySQL锁机制

解释乐观锁与悲观锁?

悲观锁(Pessimistic Lock)

悲观锁并发模式假定系统中存在足够多的数据修改操作,以致于任何确定的读操作都可能会受到由个别的用户所制造的数据修改的影响。
也就是说悲观锁假定冲突总会发生,通过独占正在被读取的数据来避免冲突。但是独占数据会导致其他进程无法修改该数据,
进而产生阻塞,读数据和写数据会相互阻塞。

顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

读取的时候为后面的更新加锁,之后再来的读操作都会等待。这种是数据库锁

乐观锁(Optimistic Lock)

乐观锁假定系统的数据修改只会产生非常少的冲突,也就是说任何进程都不大可能修改别的进程正在访问的数据。

乐观并发模式下,读数据和写数据之间不会发生冲突,只有写数据与写数据之间会发生冲突。

即读数据不会产生阻塞,只有写数据才会产生阻塞。

顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,

但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。

表里新增一个version字段,第一次读的时候,
获取到这个字段。

处理完业务逻辑开始更新的时候,
需要再次查看该字段的值是否和第一次的一样。
如果一样更新,反之拒绝。

当我们要从数据库中读取数据的时候,同时把这个version字段也读出来,如果要对读出来的数据进行更新后写回数据库,则需要将version加1,同时将新的数据与新的version更新到数据表中,且必须在更新的时候同时检查目前数据库里version值是不是之前的那个version,如果是,则正常更新。如果不是,则更新失败,说明在这个过程中有其它的进程去更新过数据了。

之所以叫乐观,因为这个模式没有从数据库加锁。

(思考:更新前,再次查询verison是否与第一次一致,
倘若此时查询为一致,突然,其他线程将version更新,然后当前线程再更新,会有更新丢失的问题。)

乐观锁与悲观锁的使用场景

乐观锁适用于读多写少的场景,写冲突较少,读不加锁,节省锁开销,提升吞吐量。

如果是写冲突较多,乐观锁会不断的retry,这样反倒降低了性能,此时应该用悲观锁。

解释共享锁与排它锁?

未完待续

解释行级锁和表级锁?

未完待续

解释意向锁?

未完待续

什么是MVCC?什么是快照读&一致性非阻塞读&CNR?

InnoDB通过数据行多版本控制(multi versioning)的方式来读行数据。
如果读的数据行正在执行delete、update操作,这时读操作不会因此等待行上的锁释放,
而是读数据行的一个快照版本数据。innoDB默认的select语句就是CNR。(例如:select xxx from t_uuu;)
那么相反的,一致性阻塞&锁定读就是之前讲的共享锁与排他锁加在select中的样子。

快照版本数据是指该行之前版本的数据,通过undo段来实现,undo用来在事务中回滚数据,
因此快照本身是没有额外的开销,读快照数据不需要上锁,因为没有必要对历史的数据进行修改。

CNR大幅度提高了读的并发性,在InnoDB存储引擎默认设置下,默认使用CRN。
快照数据是当前数据之前的历史版本,可能有多个版本。这种技术为行多版本技术。
由此带来的并发控制,称之为多版本并发控制(Multi Version Concurrency Control,MVCC)。

通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一致性读取。从用户的角度来看,好象是数据库可以提供同一数据的多个版本,因此,这种技术叫做数据多版本并发控制(MultiVersion Concurrency Control,简称MVCC或MCC),也经常称为多版本数据库。

什么时候会产生死锁?

事务A,事务B,上s锁,未完待续。

死锁之后会发生什么?

未完待续

减少死锁的解决方案是什么?

1.减少事务中的长度,事务里干的事情越多,死锁的几率越大。
2.使用低隔离级别。
3.按同一顺序访问对象。


Spring

Spring框架是Java开发的龙头老大,如果有机会必须要看懂Spring的核心源码,
以及至少能手写一个简陋的Spring框架,Spring的面试重灾区在于IOC、AOP、Spring事务的实现原理。

Spring综合

看过哪些Spring的源码(阿里面试题)?

未完待续

BeanFactory与ApplicationContext的区别与联系?

(此处如果阅读过Spring源码更能深刻理解两者的区别)
Application是BeanFactory的扩展,对BeanFactory+XmlBeanDefinitionReader的一个封装。

ApplicationContext提供了一种解决文档信息的方法,一种加载文件资源的方式(如图片),他们可以向监听他们的beans发送消息。
另外,容器或者容器中beans的操作,这些必须以bean工厂的编程方式处理的操作可以在应用上下文中以声明的方式处理。
应用上下文实现了MessageSource,该接口用于获取本地消息,实际的实现是可选的。

未整理

未完待续

Sring的bean用到了BeanFactory的哪些方法?

未完待续

IOC的含义与原理?

依赖注入、控制反转,以前的new写法调用会造成强依赖紧耦合,
可以把依赖关系交给Spring容器去管理(通过xml或注解),
Spring容器在初始化时就会把依赖关系创建好,
并通过注解注入到相应的对象中。

1.首先在web.xml中配置好Spring监听器,以及需要扫描的类?
2.web容器启动后会去初始化spring容器
3.在初始化spring容器的过程中,需要去解析xml再反射或用反射读取注解来创建需要让Spring容器来管理的实例(Singleton或Prototype),
之后把这些实例缓存在一个Map结构的集合中,
key=bean的名字,value=bean的实例,扫描需要注入的类,发现有注解病需要注入的地方,
就把map里的value放进去。
在需要使用实例时,直接从这个map里拿就可以。

通过BeanFactory去创建bean的实例,里面通过反射。

IoC:反转控制。

反转控制就是指将控制权由类内部抽离到容器,由容器类的实例化及动作进行配置管理。

Dependency-injection:依赖注入

对象的依赖关系由负责协调系统中各个对象的第三方组件在创建对象时设定。对象不自行创建或管理它们的依赖关系,依赖关系被自动注入到需要它们的对象中。通过参数和配置能够体会出“注入”这个词在这里有多形象。依赖注入的最大好处就是松耦合。不需要再类内部去和特定的类进行绑定,而是将一些依赖关系以参数的形式注入到类内部。

spring aop的底层实验原理。 aop与cglib,与asm的关系。
spriong ioc的生命周期,(init-method,intilizingbean接口方法afterPropertiesSet的先后顺序)等。

AOP的含义与原理?

面向切面编程,把一些通用的功能横切出来,已达到解耦的目的,属于一种更高级的抽象。

需要形成一个AOP类,
需要配置横切点(注入到什么类),
需要配置通知(在方法的位置注入?上中下?)

使用JDK动态代理(面向接口编程)或CGLIB(非面向接口编程)技术,生成动态代理类。

springAOP可以使用哪些代理,有什么区别

在软件开发中,分布于应用中多处的功能被称为横切关注点。这些横切关注点往往和业务逻辑是相分离的,将这些横切关注点与业务逻辑相分离正式AOP要解决的。AOP编程能够让遍布在应用各处的功能分离出来形成可重用的组件。是高内聚低耦合的又一个体现,将通用实现模块与核心业务模块相分离。

为什么CGlib方式可以对接口实现代理?RMI与代理模式

未完待续

Spring怎么做到依赖注入?

注解+反射。

一些被Spring创建出来的bean实例没有被使用,会被GC回收吗?

如果被回收掉的话,那是不是就相当于浪费资源了?因为先创建再回收。
在调用bean对象的时候才会去实例化???面试官是不是在唬我?
面试官说spring并不会设计的这么蠢,这块需要去看源码。
面试官说,初始化时,只是把一堆bean放到注册列表里面,并不是真正的实例化。

Spring的常用注解分别是什么?含义是什么?

@Component:标注一个普通的spring bean类
@Controller:标注一个控制器组件类
@Service:标注一个业务逻辑组件类
@Repository:标注一个DAO组件
@Scope:分配bean的作用域,默认为singleton
@Resource:注入容器中的依赖
@PostConstruct和@PreDestroy定制生命周期行为,初始化bean和销毁bean时使用
@Autowired注解来指定自动装配,spring将会自动搜索容器中类型为xxx的bean实例,并将该bean实例注入。默认采用的是byType的自动装配策略。

IOC容器如何管理bean的生命周期?

spring容器可以管理singleton作用域bean的生命周期,spring可以精准的知道该bean何时被创建,何时被初始化完成,容器何时准备销毁该bean实例。spring容器可以管理注入依赖关系结束之后和销毁之前的行为。

对于singleton作用域的bean,每次客户端代码请求时都返回一个共享实例,客户端代码不能控制bean的销毁,spring容器可以跟踪bean实例的产生,销毁。容器可以在创建bean之后,进行某些通用资源申请,还可以在销毁bean实例之前,先回收某些资源,比如数据库连接。

spring容器对于prototype作用域的bean,spring容器只负责创建。
当容器创建了bean实例之后,bean实例完全交给客户端代码管理,容器不再跟踪其生命周期。
每次客户端请求prototype作用域的bean时,spring都会产生一个新的实例,spring容器无从知道她曾经创建了多少个prototype作用域bean,也无从知道这些prototype作用域bean什么时候才会被销毁。
管理bean的生命周期行为主要有如下两个时机:
1.注入依赖关系之后:全部依赖关系设置结束后自动执行。
1.1,使用init-method属性
public class Chinese{
public void initAlex(){
System.out.println(“正在执行初始化方法…”);
}
}

1.2,实现InitializingBean接口(不推荐,入侵式设计,污染代码)
public class Chinese implements InitializingBean{
public void afterPropertiesSet() throws Exception{
System.out.println(“正在执行初始化方法…”);
}
}

2.即将销毁bean之前:在bean销毁之前被自动执行
2.1,使用destroy-method属性
public class Chinese {
public void closeAlex() throws Exception{
System.out.println(“正在执行销毁方法…”);
}
}

2.2,使用DisposableBean接口(不推荐,入侵式设计,污染代码)
public class Chinese implements DisposableBean {
public void destroy() throws Exception{
System.out.println(“正在执行销毁方法…”);
}
}

此外,如果容器中很多bean需要指定特定的生命周期行为,则可以利用



Spring4.x+的跨域服务端如何实现?

未完待续

Spring事务

Spring事务的原理?

Spring事务管理是基于AOP,底层是动态代理或CGLIB。

1.通过配置文件或@Transactional注解来标注需要受Spring事务管控的类或方法。

2.Spring启动时会去解析生成相关的bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,
并根据@Transaction的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。

3.真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。
java.lang.reflect.Proxy.newProxyInstance方法根据传入的接口类型 (obj.getClass.getInterfaces())动态构造一个代理类实例返回,这也说明了为什么动态代理实现要求其所代理的对象一定要实现一个接口。

这个代理类实例在内存中是动态构造的,它实现了传入的接口列表中所包含的所有接口。

InvocationHandler.invoke方法将在被代理类的方法被调用之前触发。通过这个方法,我们可以在被代理类方法调用的前后进行一些处 理,如代码中所示,InvocationHandler.invoke方法的参数中传递了当前被调用的方法(Method),以及被调用方法的参数。

同 时,可以通过method.invoke方法调用被代理类的原始方法实现。这样就可以在被代理类的方法调用前后写入任何想要进行的操作。

Spring的事务管理机制实现的原理,就是通过这样一个动态代理对所有需要事务管理的Bean进行加载,并根据配置在invoke方法中对当前调用的 方法名进行判定,并在method.invoke方法前后为其加上合适的事务管理代码,这样就实现了Spring式的事务管理。Spring中的AOP实 现更为复杂和灵活,不过基本原理是一致的。

Spring中的事务管理机制,一般是使用TransactionMananger进行管理,可以通过Spring的注入来完成此功能。
spring提供了几个关于事务处理的类:
TransactionDefinition //事务属性定义
TranscationStatus //代表了当前的事务,可以提交,回滚。
PlatformTransactionManager这个是spring提供的用于管理事务的基础接口,其下有一个实现的抽象类AbstractPlatformTransactionManager,我们使用的事务管理类例如DataSourceTransactionManager等都是这个类的子类。

Spring的事务是怎么管理到MySQL的事务(网易面试题)?

Spring本身没有事务,是依赖于各数据库提供的JDBC驱动包实现的,
他的事务管理只是代理了你获取DBConnection连接关闭以及事务。

b方法(没有事务)调用a方法(有事务),a方法最后是没有事务,为什么?(网易面试题)

由于在同一个service类中的a方法调用b方法是会导致b跟a走的,因此下面的所有结论都是在a,b方法不在同一个service类。

@Transactional的事务开启是基于接口/类的代理被创建。(网易面试题)
颗粒度在类,而不是方法,所以在同一个类中一个没有用事务的方法调用另一个方法有事务的方法,
第二个方法的事务是不会起作用的。
或者说第二个方法的事务传播永远是跟第一个方法走。

Spring事务的传播级别有哪些?

(以及你都用过哪些?分别是什么场景?)

REQUIRED:指定当前方法必需在事务环境中运行,如果当前有事务环境就加入当前正在执行的事务环境,如果当前没有事务,就新建一个事务。这是默认值。

SUPPORTS:指定当前方法加入当前事务环境,如果当前没有事务,就以非事务方式执行。

MANDATORY:指定当前方法必须加入当前事务环境,如果当前没有事务,就抛出异常。

REQUIRES_NEW:指定当前方法总是会为自己发起一个新的事务,如果发现当前方法已运行在一个事务中,则原有事务被挂起,我自己创建一个属于自己的事务,直我自己这个方法commit结束,原先的事务才会恢复执行。

NOT_SUPPORTED:指定当前方法以非事务方式执行操作,如果当前存在事务,就把当前事务挂起,等我以非事务的状态运行完,再继续原来的事务。

NEVER:指定当前方法绝对不能在事务范围内执行,如果方法在某个事务范围内执行,容器就抛异常,只有没关联到事务,才正常执行。

NESTED:指定当前方法执行时,如果已经有一个事务存在,则运行在这个嵌套的事务中.如果当前环境没有运行的事务,就新建一个事务,并与父事务相互独立,这个事务拥有多个可以回滚的保证点。就是指我自己内部事务回滚不会对外部事务造成影响,只对DataSourceTransactionManager事务管理器起效。

Spring事务的隔离级别有哪些?

(以及你都用过哪些?分别是什么场景?)
DEFAULT:采用数据库默认隔离级别

SERIALIZABLE:最严格的级别,事务串行执行,资源消耗最大;

REPEATABLE_READ:保证了一个事务不会修改已经由另一个事务读取但未提交(回滚)的数据。避免了“脏读取”和“不可重复读取”的情况,但是带来了更多的性能损失。

READ_COMMITTED:大多数主流数据库的默认事务等级,保证了一个事务不会读到另一个并行事务已修改但未提交的数据,避免了“脏读取”。该级别适用于大多数系统。

READ_UNCOMMITTED:保证了读取过程中不会读取到非法数据。隔离级别在于处理多事务的并发问题。

SpringMVC

SpringMVC是目前比较流行的mvc框架,尤其是其参数绑定与视图解析器功能。

SpringMVC的访问流程以及工作原理?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q8Isipvu-1588123493309)(evernotecid://85A94EB2-9BD2-45DE-A16D-D9C9CD7ECF0B/appyinxiangcom/12192613/ENResource/p363)]
1.在web.xml中配置DispatcherServlet,以及init-param去加载springmvc的配置文件。
2.在mvc.xml配置文件中,需要启用注解和扫描注解(只扫描controller)。
3.扫描到注解后,将url与类&方法做映射。
HandlerMapping,是SpringMVC中用来处理Request请求URL到具体Controller的,其自身也分成很多种类;
HandlerAdapter,是SpringMVC中用来处理具体请求映射到具体方法的,其自身也分很多种类;
@RequestMapping这个注解的主要目的就是对具体的Controller和方法进行注册,将方法成为handler方法。
以方便HandlerMapping用来处理请求的映射。(但是@RequestMapping需要结合<mvc:annotation-driven />使用才能生效。)

  1. 所有请求都被DispatcherServlet拦截,它会委托应用系统的其他模块负责对请求进行真正的处理工作。
  2. DispatcherServlet查询一个或多个HandlerMapping,找到处理请求的Controller.
  3. DispatcherServlet 请求提交到目标Controller
  4. Controller进行业务逻辑处理后,会返回一个ModelAndView
  5. Dispathcher查询一个或多个ViewResolver视图解析器,找到ModelAndView对象指定的视图对象
  6. 视图对象负责渲染返回给客户端。

1.spring mvc将所有的请求都提交给DispatcherServlet,它会委托应用系统的其他模块(Controller、Handler Mapping、View Resolver等)负责对请求进行真正的处理工作。

2.DispatcherServlet查询一个或多个HandlerMapping,找到处理请求的Controller.

3.DispatcherServlet 请求提交到目标Controller

4.Controller进行业务逻辑处理后,会返回一个ModelAndView
handler方法处理完请求后,它把控制权委托给视图名与handler方法返回值相同的视图。为了提供一个灵活的方法,一个handler方法的返回值并不代表一个视图的实现而是一个逻辑视图,
即没有任何文件扩展名。你可以将这些逻辑视图映射到正确的实现,并将这些实现写入到上下文文件,
这样你就可以轻松的更改视图层代码甚至不用修改请求handler类的代码。
为一个逻辑名称匹配正确的文件是视图解析器的责任。一旦控制器类已将一个视图名称解析到一个视图实现。它会根据视图实现的设计来渲染对应对象。

5.Dispathcher查询一个或多个ViewResolver视图解析器,找到ModelAndView对象指定的视图对象

6.视图对象负责渲染返回给客户端。

Spring容器怎么访问SpringMVC容器里的bean(网易面试题)?

IOC容器与SpringMVC容器的区别与联系?

未完待续

SpringMVC如何做到返回一个json对象?

1.添加jackson的jar包;
2.使用@ResponseBody
该注解用于将Controller方法返回的对象,通过适当的HttpMessageConverter转换为指定格式后,写入到Response对象的body数据区。
3.配置<mvc:annotation-driven />
<mvc:annotation-driven />会自动注册DefaultAnnotationHandlerMapping、AnnotationMethodHandlerAdapter两个bean,
这两个bean是spring MVC为@Controllers分发请求所必须的。
并提供了数据绑定支持,@NumberFormatannotation支持,@DateTimeFormat支持,@Valid支持,读写XML的支持(JAXB),读写JSON的支持(Jackson)。

如何实现SpringMVC的自定义参数解析器(网易面试题)?

SpringMVC中如何定义一个Rest风格的URL?

未完待续

SpringMVC的常用注解有哪些?

@ResponseBody

如果@ResponseBody+返回值是String,则返回给浏览器一个字符串
Content-Type:text/html;charset=UTF-8
如果@ResponseBody+返回值是对象,则返回给浏览器一个json对象(好像要在浏览器中配置)
Content-Type:application/json;charset=UTF-8

@PathVariable

绑定请求参数值到方法的参数上

@Controller
@RequestMapping("/owners/{ownerId}")
public class RelativePathUriTemplateController {
  @RequestMapping("/pets/{petId}")
  public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {     
    // implementation omitted
  }
}
@RequestMapping

关联url,映射路径
可以写在类级or方法级上,限定访问http的method。

@RequestMapping(value = "/xxxxx", headers = { "Connection=keep-alive" },
      params = { "param1=1", "param2!=2", "param3" }, method = RequestMethod.GET,
      consumes = "text/plain", produces = "application/json")
@RequestBody

后端如果是用@RequestBody来接收参数,那必须http请求的
requrst headers里要有:
ContentType:application/json;charset=UTF-8

@RequestParam

直接在形参列表中写判断非空

@RequestMapping("/greeting")
public String greeting(@RequestParam(value="name", required=false, defaultValue="World") String name, Model model) {
        model.addAttribute("name", name);
        return "greeting";
}
@RequestHeader

查看 http请求头的相关信息

@RequestMapping("/displayHeaderInfo.do")
public void displayHeaderInfo(@RequestHeader("Accept-Encoding") String encoding, @RequestHeader("Keep-Alive") long keepAlive)  {

}
@CookieValue

查看cookie

@RequestMapping("/displayHeaderInfo.do")
public void displayHeaderInfo(@CookieValue("JSESSIONID") String cookie) {

}

@Service
@Controller
@AutoWired
@Component

SpringBoot

SpringBoot的底层原理?

未完待续


分布式

分布式综合

什么是CAP定理?

C(Consistency):一致性;
A(Availability):可用性;
P(Partition tolerance):分区容错性;
三种保证无法同时满足,最多只能同时满足两个,CP或AP(CA属于单机系统)。
在分布式系统中,会出现节点与节点之间无法通信的情况,
此时只能选择一致性或可用性,没法同时选择。

~CAP原则(一致性,可用性,分区容错性,最多满足其两个,不能3个都满足)

C一致性:所有数据的变动都应该是同步的,一个集群中有多台服务器,某一台服务器某一时刻成功更新了某条数据,那么下一时刻,所有机器在查询的时候都将查到最新的数据,保证了数据的强一致性。

A可用性:高性能,高可用,随着数据的增长,应用始终能保证高性能的查询与相应的操作,
或任意节点的应用不可用也不会对整个分布式系统提供服务造成影响。

P分区容错性:系统应该能持续提供服务,即使系统内部有消息丢失。
例如对数据的分区管理,将一份数据同步成几份数据,那么几份数据都将用于查询,假如在数据同步时,有消息丢失,如果有分区容错性,那这一次的操作仍将成功,不会影响用户。

~复杂的事情简单化
随着复杂的上升,企业应用会有很多瓶颈,架构就去解决这些问题。

HA高并发架构的解决方案?

数据库缓存、分布式缓存(Memcached、Redis等)
消息队列(RabbitMQ、RocketMQ、Kafka等)
CDN加速、
服务器的集群化以及负载均衡、
分布式框架(Dubbo、SpringCloud等)
分布式文件服务器
分布式数据库
Docker+K8S容器动态扩容
使用缓存来减少数据库IO,
使用无锁结构来减少响应。
使用异步来增加单台服务器吞吐。

如何实现高可用?(连环炮)

解决高可用的普遍思路是冗余,冗余多个站点&服务&数据,用复制的方式来保证可用性。

熔断,降级,限流。

什么是降级?

在大并发、有限资源的场景下,降级是为了给核心程序腾出更多的使用资源,
以最大程度保障核心程序的稳定性、可用性,这需要减少非核心非刚需程序所使用资源。

常见的降级方案表现形式:

1.牺牲用户体验
为了减少对冷数据的读取,可以禁用分页功能。
为了放缓流量速度,可以添加验证码机制,减少大范围查询,增加查询选项,减少模糊查询。
动态数据静态化,或者直接扔出来一个静态降级页面告知用户。

2.牺牲功能完整性,让系统裸奔一会,也可以节约资源
例如:临时关闭风控,取消部分判断与校验,关闭或减少日志记录等等。

3.牺牲时数据时效性,降低数据一致性。
例如:原来的显示库存个数,可以变成“有货”。
原来的数据刷新&同步时间是3秒,可以变成30秒。
将一些不重要的异步操作放缓或暂停,例如送积分,送礼券等。

如何降级?

1.定级定序,排兵布阵
确定每个功能的重要程度,例如级别1是最高,级别5是最低,那么级别5是最先可以被降级的,
降级级别5后资源还不够的话,再一次降级级别4,降级级别3,以此类推。
但被标记成5的功能有100个,降级时是一下子把这100个功能全部降级吗?
这样的粒度未免有些太大了,因此我们需要在每个级别上再定义一个序号,
先降级级别5序号9的,再降级级别5序号8,再降级级别5序号7,以此类推。

在业务无关的前提下, 支撑上游业务越多的下游服务理应该被越后降级。
在业务有关的前提下,业务越重要的服务应该被越后降级。
一个服务所依赖的下游服务的级别不能低于当前服务的级别,
一旦下游服务被降级,那么上级服务也将变得不可用。

2.降级实现
提前制定降级的触发机制,可以使用一些系统的指标,
例如:接口的超时率,错误率,系统资源消耗率&使用率等等。

首先我们要定义级别和序号的全局变量(可以放在分布式缓存中),为程序制定自己的级别与序号。
当程序发现满足了上述条件后,使用程序将全局变量修改至相应的等级与序号,
部分代码就进入了降级模式,之后通过AOP+注解来进行降级判断和相关代码的处理。
总之,降级策略越简单越好。

例如:
if(myLevel > runLevel && myIndex > runIndex){
//降级处理
} else {
//正常处理
}

每个服务的级别和序号并不是一成不变的,而是需要随时动态的进行调整,
降级工作是一个不断打磨和优化的过程。

如何实现分布式事务?

2阶段,3阶段提交已经被弃用,现在都是柔性事务,最终一致性,可以用RocketMQ事务消息
可以考虑对账一致。

消费者需要保持幂等,可以通过redis。看redis里这个事情做过没有,做过了就没做。

柔事务,强一致性,弱一致性。

未完待续

什么是分布式锁以及如何实现?

分布式锁一般用来解决分布式场景下的数据一致性问题。

当某个资源在多系统之间,具有共享性的时候,为了保证大家访问这个资源数据是一致的,那么就必须要求在同一时刻只能被一个客户端处理,不能并发的执行,否者就会出现同一时刻有人写有人读,大家访问到的数据就不一致了。

在线程不安全的场景下,我们一般会在本机加synchronized锁来保证线程安全,
但由于我们的程序有可能是同时运行在一台机器&节点上的多个JVM进程里,那我们可以使用系统的文件读写锁来解决此问题。
如果是同时运行在多个机器&节点上,那我们就需要使用分布式锁。

常见实现分布式锁的方式有:数据库的乐观锁&悲观锁、Redis、Zookeeper、

解决思路:
每次执行代码时,先查询Redis里面是否已经有锁的key,如果没有就写入,然后就开始执行后面的业务代码,如果有就等待。

如果2个线程同时查询Redis,发现没有锁,会同时写入成功并获得锁,还是会出现问题。

我们可以使用setnx(SET if Not eXists),支持原子性写入。

分布式锁要设置超时时间,如果某个机器&节点获得锁之后hang住或死机了,必须要能释放锁,否则会造成死锁。

但如果负载较高的情况下,线程A还没有执行完就超时了,
被线程B抢到锁再执行,会有问题。

抢占到锁之后,开启一个守护线程,定时去redis哪里询问,是不是还是由我抢占着当前的锁,还有多久就要过期,如果发现要过期了,就赶紧续期。

Redisson

如何实现幂等性?

可以通过redis,
看redis里这个事情做过没有,做过了就别再做了。

微服务&Spring Cloud Alibaba

Spring Cloud其实是一套规范,而不是一个可以拿来即用的框架,
Spring Cloud Alibaba是Spring Cloud的准第二代实现标准。

Spring Cloud规范的实现:
Spring Cloud Netflix(以前大部分人用)
Spring Cloud Consul
Spring Cloud Alibaba

注册中心、配置中心Nacos服务注册、发现、配置管理,单独启动,代替Eurekba、Consul、Zookeeper
熔断、限流、升降级Sentinel
RPC服务调用Dubbo
分布式事务Seata
分布式消息RocketMQ

Nacos

Sentinel

Dubbo

RPC的底层原理?

未完待续

RocketMQ

为什么要使用消息队列?

1.提供分布式事务的支持,确保数据全局一致性。
当某一业务的数据存储成功后,生产消息到消息队列,其他业务作为消息的消费者,
分别进行业务操作。

2.数据复制&多路转发
随着一些专业系统(例如:ES、storm&spark&hadoop、hbase、OpenTSDB、mysql)的运行,
通常需要同一份数据被多个专业系统所使用。
例如通过mysql参数原始数据后,通过binlog等机制可以将数据生产到消息队列中,
然后让其他专业系统进行消息的消费。
我们可以使用消息队列当做是数据中转站,将同一份数据导出到不同的专业系统中。
一份数据可以被同时消费多次。

3.日志同步&分析
各个业务平台每天会产生大量的日志数据,将这些日志数据发送到消息队列中,
消息队列相当于是一个日志收集中心,
应用系统与分析系统应该是分开的、解耦的,使用消息队列进行关联解耦。
之后可以通过Hadoop&Strom&Spark&HBase等技术,
对日志进行在线&离线分析、存储、监控、实时处理等操作。

4.延迟队列
分布式环境下的定时器,延迟投递,削峰平谷。

5.广播通知
集群内广播通知,用于通知一些事件。

如何处理消息丢失问题?

Seata

Feign

分布式缓存

缓存综合

如何保证数据库与缓存的一致性问题?

未完待续

Redis

Redis与Memcached的区别?

未完待续

Redis有几种常用的数据类型?

1.string,字符串类型
2.hash,散列类型
3.list,列表类型
4.set,集合类型
5.zset,有序集合类型

Redis有哪些应用场景?

1.分布式锁(string)
setnx key value,当key不存在时,将 key 的值设为 value ,返回1。
若给定的 key 已经存在,则setnx不做任何动作,返回0。
当setnx返回1时,表示获取锁,做完操作以后del key,表示释放锁,如果setnx返回0表示获取锁失败,整体思路大概就是这样,细节还是比较多的,有时间单开一篇来讲解。

2.计数器(string)
3.分布式全局唯一id(string)
4.消息列表(list)
5.用户消息列表(list)
6.抽奖活动(set)
7.点赞、签到(set)
8.关注的人,可能认识的人(set)
9.电商商品筛选(set)
10.排行榜(zset)

Redis的过期&失效策略是什么?

未完待续

Redis的内存淘汰策略是什么?

未完待续

Redis的持久化策略是什么?

未完待续

如何解决缓存雪崩?

未完待续

如何解决缓存穿透?

未完待续

分布式数据库

如何设计一个数据库中间件?

如何分库分表?

主要分为两大类,垂直拆分(竖着拆)、水平拆分(横着拆)。

垂直切分(竖着拆)
垂直分库

将不同业务模块的表(单个或多个表)放到不同的数据库中,
每个数据库又处于不同的数据库实例&节点中。

【为什么这样设计?】
数据分散到不同的数据库实例上这样有利于单机硬件性能的提升。

垂直分表

假设一张表中有许多列,可以新建一张表作为扩展表,
将使用频率不高或特别长(存储数据特多)的列挪到新表中,
类似于冷热列分离。

【为什么这样设计?】
MySQL底层存储使用“页”来存储数据,每个页是有容量限制的,一些大型列会占用很多的页,跨页查询会造成额外的性能开销。

读取数据时,数据以行为单位被读到内存中,由于内存容量有限,对于大型列来说会增加磁盘IO。

水平切分(横着拆)
水平分表(只分表不分库)

将一张表中的数据按照一定的规则&算法划分到n张表中。

【为什么这样设计?】
单表的数据量就被有效的降低了,对索引友好提升性能,不用考虑分布式事务。

1.按范围分片

按范围分片,可以有两种方式:
按时间(把2018年的数据放在一张表);
按id(把id为1-100000的数据放在一张表);

优点:

  • 路由规则简单。
  • 扩容时,不需要数据进行迁移。
  • 不需要进行跨库跨表查询,效率较高。

缺点:

  • 对数据有限制:主键必须是自增整型或必须有日期字段。
  • 会出现热点数据频繁查询,热表很可能快速成为系统性能瓶颈。

2.按某列取模&哈希分片

按分片列做取模或哈希运算后,得知数据要放到哪。

优点:

  • 路由规则简单。
  • 数据相对均匀,没有热点数据问题。

缺点:

  • 扩容时需要数据迁移。
  • 使用非分片列查询,需要进行全局轮训查询,性能会很低,因此需要把查询最频繁的列定位分片列。

2.1 映射关系法
创建一个映射关系表&索引表(只有2列,KV结构,可放缓存中),把非分片列与分片列做映射。

当使用非分片列查询时,先去映射表查询到对应的分片列的值,再用分片列的值进行路由。

缺点:

  • 多一次数据库&缓存的查询。
  • 非分片字段必须是唯一的,不能有重复值。
  • 非分片列的值变化后需要同步到映射表&缓存中。

2.2 基因法
自定义一个函数F,MD5(非分片列的值) = 128bit ,再截取最后3位,这3位随机码是基因,用于路由。

再将基因放到分片列的后3位中(假设分片列值是64位整数,61和以前一样,后3放基因)。
再将有基因的分片列插入数据库中,落到不同的库表中。

用非分片列查询时,先用函数F得出3位基因,然后通过基因路由到对于的库表中。

缺点:

  • 路由规则复杂
  • 非分片列的值是不能被修改

3.主流水平切分场景
3.1 单key业务-用户表
user(uid,uname,pwd,age,create_time)

1%的使用场景-登录或按其他列查询:
WHERE uname = ? AND pwd = ?
WHERE age = ?

99%的使用场景-按主键查询详情:
WHERE uid = ?

结论:
使用uid作为分片列,非分片列查询则使用映射关系法或基因法。

3.2 一对多业务-评论表
comment(cid,uid,title,content,create_time)

结论:
使用一对多里的一方的主键做分片字段,即uid,
之后使用映射关系法来映射uid与cid的关系或基因法来通过uid来生成cid。
cid = 全局唯一id(前61位)+ uid的分库基因(后3位)

3.3 多对多业务-好友表
friend(uid,friend_uid,remark,create_time)

50%的使用场景-查询我的好友:
WHERE uid = ?

50%的使用场景-查询加我好友用户:
WHERE friend_uid = ?

结论:
使用数据冗余方案,多份数据使用多种分库手段。

3.4 多key业务-订单表
order(oid,buyer_id,seller_id,order_info)

80%的使用场景-按主键查询详情
WHERE oid = ?

19%的使用场景-查询用户的订单
WHERE buyer_id = ?

1%的使用场景-查询商户的订单
WHERE seller_id = ?

方案A:使用2+3
方案B:1%的请求采用多库查询

先分库再分表

分布式解决方案

如何实现防重复提交?
前端防重复

方法A:当用户第一次提交后,将提交按钮设置为“disable”状态,待后端返回后,按钮恢复。

方法B:使用js设置一个局部变量,当用户点击按钮后,局部变量设为false,用户再点击按钮不发请求,待后端返回后恢复成true。

方法C:
第一次提交后,页面显示一个loading框,把页面锁住,待服务器返回后再把loading框解锁。

方法D:
在前端有个计数器,当两次提交的时间小于x秒则提示用户点击的太快了。

后端防重复

方法A:在页面加载时或初始化接口时拿到一个我们自己服务端生成的token放到session&redis中,
提交时带上这个token参数,在拦截器中做检验,判断参数token与session&redis中的token是否一致,如果一致则正常往下走,否则返回。(把token放到key中,而不是value中,因为会有多个token同时存在的情况。如果出现错误情况要有机制让前端继续能提交。)

方法B:则把访问路径与参数作为key存入redis缓存中,并设置超时时间为x秒,重复提交时,发现缓存中如果存在此key则不往下走。
或者把一个flag状态放入redis缓存中,进方法和出方法都去更新这个flag状态,相当于是一个分布式锁。

微服务

微服务的范围如何设定才合理?

未完待续


网络协议

HTTP协议

HTTP的实现原理

未完待续

长连接与短连接的区别?

在HTTP/1.0中,默认使用的是短连接。也就是说,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。如果客户端浏览器访问的某个HTML或其他类型的 Web页中包含有其他的Web资源,如JavaScript文件、图像文件、CSS文件等;当浏览器每遇到这样一个Web资源,就会建立一个HTTP会话。
但从 HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头有加入这行代码:Connection:keep-alive
在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的 TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接要客户端和服务端都支持长连接。
HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。
长连接短连接操作过程
短连接的操作步骤是:
建立连接——数据传输——关闭连接…建立连接——数据传输——关闭连接
长连接的操作步骤是:
建立连接——数据传输…(保持连接)…数据传输——关闭连接

长连接和短连接的优点和缺点?

由上可以看出,长连接可以省去较多的TCP建立和关闭的操作,减少浪费,节约时间。对于频繁请求资源的客户来说,较适用长连接。不过这里存在一个问题,存活功能的探测周期太长,还有就是它只是探测TCP连接的存活,属于比较斯文的做法,遇到恶意的连接时,保活功能就不够使了。在长连接的应用场景下,client端一般不会主动关闭它们之间的连接,Client与server之间的连接如果一直不关闭的话,会存在一个问题,随着客户端连接越来越多,server早晚有扛不住的时候,这时候server端需要采取一些策略,如关闭一些长时间没有读写事件发生的连接,这样可 以避免一些恶意连接导致server端服务受损;如果条件再允许就可以以客户端机器为颗粒度,限制每个客户端的最大长连接数,这样可以完全避免某个蛋疼的客户端连累后端服务。
短连接对于服务器来说管理较为简单,存在的连接都是有用的连接,不需要额外的控制手段。但如果客户请求频繁,将在TCP的建立和关闭操作上浪费时间和带宽。
长连接和短连接的产生在于client和server采取的关闭策略,具体的应用场景采用具体的策略,没有十全十美的选择,只有合适的选择。

什么时候用长连接,短连接?

长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况,。每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。
而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连好。

对于web网站的http服务,如果并发量很大,但用户的操作不多,则使用短连接,如果并发量不大,但操作数多,则使用长链

HTTP1.0、HTTP1.1、HTTP2.0的区别?

未完待续

HTTPS如何实现安全?

未完待续

HTTP报文包含内容?

1.request line
1.1请求的类型(GET或POST)
1.2要访问的资源(如\res\img\a.jif)
1.3Http版本(http/1.1)
2.header line
服务器要使用的附加信息
3.blank line
这是Http的规定,必须空一行
4.request body
请求的内容数据

???
http header 字段包括通用头,请求头,响应头,实体头四部分,
每个header字段由一个字段名、冒号、和字段值3部分组成。

通用header字段包含请求和响应消息都支持的header字段:Cache-Control、Connection、Date、Pragma、Transfer-Encoding、Upgrade、Via

http请求&响应的组成部分
请求头,消息报头,正文
响应行,消息报头,消息征文

TCP协议

TCP的稳定传输是如何保证的?(滴滴面试题)
TCP的三次握手、四次握手?(滴滴面试题)
三次握手

第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户端的SYN,同时自己也发送一个SYN包,
即SYN(syn=k) +ACK(ack=j+1)包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,
客户端和服务器进入ESTABLISHED状态,完成三次握手。

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据
断开连接时服务器和客户端均可以主动发起断开TCP连接的请求,断开过程需要经过四次握手。

四次握手:

第一次握手:客户端通知TCP数据已经发送完毕,向服务器发送一个带有FIN附加标记的报文段。
第二次握手:服务器收到这个FIN报文段之后,并不立即用FIN报文段回复客户端,
而是先向客户端发送一个确认序号ACK,同时通知自己相应的应用程序:
对方要求关闭连接(先发送ACK的目的是为了防止在这段时间内,对方重传FIN报文段)。
第三次握手:服务器向客户端发送一个FIN报文段,表示服务器要彻底的关闭连接。
第四次握手:客户端收到这个FIN报文段后,向服务器发送一个ACK表示连接彻底释放。

为什么要三次握手,两次不可以吗?

A : 试想一下,A第一次发送请求连接,但是在网络某节点滞留了,A超时重传,然后这一次一切正常,A跟B就愉快地进行数据传输了. 等到连接释放了以后,那个迷失了的连接请求突然到了B那,如果是两次握手的话,B发送确认,它们就算是建立起了连接了. 事实上A并不会理会这个确认,因为我压根没有要传数据啊. 但是B却傻傻地以为有数据要来,苦苦等待. 结果就是造成资源的浪费.

三次握手其实就是为了检测双方的发送和接收能力是否正常。

为什么要四次挥手,而不是两次,三次?

A :
首先,由于TCP的全双工通信,双方都能作为数据发送方. A想要关闭连接,必须要等数据都发送完毕,才发送FIN给B. (此时A处于半关闭状态)
然后,B发送确认ACK,并且B此时如果要发送数据,就发送(例如做一些释放前的处理)
再者,B发送完数据之后,发送FIN给A. (此时B处于半关闭状态)
然后,A发送ACK,进入TIME-WAIT状态
最后,经过2MSL时间后没有收到B传来的报文,则确定B收到了ACK了. (此时A,B才算是处于完全关闭状态)
PS : 仔细分析以上步骤就知道为什么不能少于四次挥手了.
Q : 为什么要等待2MSL(Maximum Segment Lifetime)时间,才从TIME_WAIT到CLOSED?
A : 在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。
更加接地气的解释 :
第一次挥手 : A告诉B,我没数据发了,准备关闭连接了,你要发送数据吗
第二次挥手 : B发送最后的数据
第三次挥手 : B告诉A,我也要关闭连接了
第四次挥手 : A告诉B你可以关闭了,我这边也关闭了


设计模式

在工作中遇到过哪些设计模式,是如何应用的。

单例模式有多少种?(58到家面试题)

7种,未完待续

动态代理模式?


数据结构与算法

快速排序法的原理?(阿里面试题)

(非常非常非常重要,一定要掌握。)
写出快排,并说出快排的时间复杂度,还有最差情况是什么情况下

说一下快速排序的原理?
未完待续

什么是一致性哈希算法?

非常非常非常重要
一致性hash算法

常用的hash算法有哪些?什么是一致性哈希?

未完待续

链表和数组的区别?(阿里面试题)

未完待续

递归算阶层

实现一个递归算阶层方法? 输入5,得出54321=120

public static int eat(int param) {
  if(param ==1||param ==0) {
    return 1;
  }else {
    return param *eat(param -1);
  }
}

注意:当一个方法不断的调用它本身时,必须在某个时刻不再调用它本身,能走出这个递归循环,
否则就变成了无穷递归(死循环)。

已知f(0)=1,f(1)=4,f(n+2) = 2*f(n+1)+f(n),求f(10)?

public static int eat(int param) {
  if(param ==0) {
    return 1;
  }else if(param ==1) {
    return 4;
  }else {
    return 2 *eat(param-1)+eat(param-2);
  }
}

System.out.println(eat(10));

解题思路:
把大公式想办法弄成f(0)、f(1)的形式。
大公式可以变成:
f(n+2-2) = 2f(n+1-2)+f(n-2);
f(n) = 2
f(n-1) + f(n-2);
f(10) = 2f(9) + f(8);

f(2) = 2
f(1) + f(0);
f(2) = 2*4+1;

B+树和二叉树查找时间复杂度?

什么是红黑树?相关原理?

未完待续

红黑树与B树与B+树的区别?

未完待续


智力&逻辑题

哪天说谎?

某地有两个奇怪的村庄,张庄的人在星期一、三、五说谎,
李庄的人在星期二、四、六说谎。

在其他日子他们说实话。一天,一个外地人来到这里,见到两个人,分别向他们提出关于日期的题。

两个人都说:"前天是我说谎的日子。"如果被问的两个人分别来自张庄和李村,那么这一天是星期几?
-星期一

多少条病狗?(IBM面试题)

村子里有50个人,每人有一条狗,在这50条狗中有病狗(这种病不传染),于是人们要找出病狗。
每个人可以观察其他49条狗,以判断他们是否生病,(如果有病一定能看出来),只有自己的狗不能看,
观察后得到的结果不得交流,也不能通知病狗的主人。主人一旦推算出自己家的狗是病狗就是枪毙自己的狗
(发现后必须在一天内枪毙),而且每个人只有权利枪毙自己的狗,没有权利打死其他人的狗。
第一天大家全看完了,但枪没有响,到了第三天传来一阵枪声,问村里共有几条病狗,如何推算出来的?

这个推理很简单的:
假设有一条病狗,第一天就会被发现,并枪杀;
假设有两条病狗,第二天就会被发现,并枪杀;
假设有三条病狗,第三天就会被发现,并枪杀;
假设有四条病狗,第四天就会被发现,并枪杀;
。。。。
你肯定会问为什么?
因为如果有一条病狗,所有人(除了病狗主人外)都会只发现一条病狗,而病狗主人会发现没有病狗,他从而根据有病狗的事实,推断出自己家的狗是病狗,所以如果只有一条病狗,第一天就会被发现,并枪杀;
如果有两条病狗,所有人(除了两条病狗主人外)都会发现有两条病狗,但是病狗主人会发现只有一条病狗,因为他自己家的狗他没办法看到。但是在第一天,他没办法推断自己家的狗是否病狗,所以这两只病狗能活过第一天。但到了第二天,他发现他的伙伴,同病相邻的病狗主人也没有枪杀它的狗。这时候,很明显,她知道了这个事实,不止一条病狗,从而推断出自己家的狗是病狗。这天,两条可怜的小狗将会死于非命。
后面的不再赘述了,大家依此类推吧。

美国有多少量汽车?

-美国一共有多少人口?这些人中又有多少会开车?
而会开车的人中又有多少有经济实力可以买车?
有多少人会买车?

分蛋糕

一盒蛋糕切成6份,分给6个人,但蛋糕盒中还必须留一份,怎么办?
-先切成6份,从中拿出5份分给5个人,再把最后一份连蛋糕盒也给第六个人。

3盏灯和3个开关

有两个封闭的房间,一间房有三个灯泡,另一间房有控制这3个灯泡的开关,现在要你分别进入这两间房各一次,
然后判断出这三个灯泡分别是由哪三个开关控制。
-先进入开关房间,先打开A开关,过段时间,关A,开B,去另一个房间,亮着灯的是B控制,不凉的灯中热的是A控制,亮的是C控制。

戴帽子

有5个帽子,3个黑色,2个白色,分别给三个人分别带上了3个黑帽子,
但这3个人看不到自己的帽子,只能看到其他2个人的帽子,让大家猜自己的帽子的颜色,
过一会,其中一个人说出来自己是黑帽子,为什么?
-A看到其他2个人的帽子都是黑色时,他先假设自己是白色帽子,那么其他2个人看到的就是一黑一白了。
因此B就会思考,已经出现了一个白帽子,那如果B的帽子也是白色的,那么C就知道自己肯定是黑帽子了,
但是C又没有说出来,就证明B带的是黑帽子,但是B自己也没有说出来,就证明A的帽子不是白色。

海盗分金

5个海盗得到了100个金币,他们依次提出分派方案,如果有一半或一半以上的人同意就通过,大家都想利益最大化。
否则就把这个海盗扔到海里喂鲨鱼,再让后面的海盗继续提出分配方案,那么第一个海盗如何能活且利益最大化?
-98:0:1:0:1

找出轻球

12个外表一样的球,其中只有一个重量轻,给你一个没有刻度的天枰,只能用3次,把轻球找到。
-分3堆,4:4:4,把上步确定好的4个球进行2:2过秤,最后把最后2个球过秤。

烧香

有两根香,每根烧完的时间是1小时,你能用什么方法来确定一段15分钟的时间?
-点燃第一根香的2端,同时点燃第二根香的一头。
等到第一根香烧尽了以后,再把第二根香的另一头点燃。
从第一根香烧尽开始算起,直到第二根香烧尽,就是15分钟。


HR题

一般来说能进到HR面这个环节,就离胜利不远了,但千万不要掉以轻心。

  • 2
    点赞
  • 0
    评论
  • 8
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值