复习专栏之---面试总结

Fighting

1. 计算机基础

1. 简述协程的优缺点

协程是一种轻量级线程

优点:
跨平台,跨体系架构
无需线程上下文切换的开销
无需原子操作锁定及同步的开销
方便切换控制流,简化编程模型
高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发
处理。

缺点:
无法利用多核资源,协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需
要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,
除非是cpu密集型应用。
进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序,这一点和事件驱动一样,可以
使用异步IO操作来解决

2. 如何判断一个hash函数好不好?

hash的作用就是把数据均匀分布,离散性就是它的目的,但任何事情,永远不可避免的就是时间成本,即使一个hash函数很好,但需要一年才能结束,我想绝大部分人都无法接受。

因此好不好看两点:
1. 离散程度,hash算法一定要把数据哈希均匀,不能全部挤在一坨
2. 时间成本,也可以理解为计算性能

hashCode的设计初衷是提高哈希容器的性能,java离不开hashCode,equals或者集合等都离不开hashcode。

参考:真正搞懂hashCode和hash算法

3. http中post和get的区别

请求回退:GET在浏览器回退时是无害的,而POST会再次提交请求。
连接收藏:GET产生的URL地址可以被Bookmark,而POST不可以。
缓存机制:GET请求会被浏览器主动cache,而POST不会,除非手动设置。
编码方式:GET请求只能进行url编码,而POST支持多种编码方式。
请求参数:GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保
留。
参数长度:GET请求在URL中传送的参数是有长度限制的,而POST么有。
参数类型:GET只接受ASCII字符,而POST没有限制。
安全程度:GET比POST更危险,其参数直接暴露在URL上,所以不能用来传递敏感信息。
参数位置:GET参数通过URL传递,POST放在Request body中。

然而实际上,它们的本质都是TCP连接,并无区别。上面的答案纯粹是为了应付面试官。真正导致产生区别的原因是 HTTP 的规定以及浏览器/服务器的限制,这才导致它们在应用过程中可能会有所不同。

简单说一下TCP,感觉会问到,首先这是一种协议,全称是传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,其细节就不说了,名词和内容其实没有太大意义,就说一下著名的建立一个连接需要三次握手,而终止一个连接要经过四次握手。

通俗点说:
建立连接的目的就是测试c/s两端的收发能力都正常,同时让s和c都知道彼此收发都可用
第一次:c-s,s知道c的发可用,自己收可用,c一无所知。
第二次:s-c,c知道自己的收发可用,s的收发可用,s没有新消息获得
第三次:c-s,s知道了c的收可用,自己的发可用
至此,彼此都知道,自己与对方的收发都是正常的。

断开连接的目的就是拿完最后一条消息,然后跑路
第一次:c-s,此条信息发完了,我要跑路了,你赶紧发完给我的消息
第二次:s-c,知道了,我尽快发完消息
第三次:s-c,嘟嘟嘟,消息发完了,你走吧
第四次:c-s,收到,撤了。
至此,消息收发完毕,彼此都知道散伙了。

4. 解决hash冲突的方式

先科普一下什么是hash函数,其实一直都没仔细看其定义

哈希法又称散列法、杂凑法以及关键字地址计算法等,相应的表称为哈希表。这种方法的
基本思想是:首先在元素的关键字k和元素的存储位置p之间建立一个对应关系f,使得
p=f(k),f称为哈希函数。创建哈希表时,把关键字为k的元素直接存入地址为f(k)的单元;
以后当查找关键字为k的元素时,再利用哈希函数计算出该元素的存储位置p=f(k),从而达
到按关键字直接存取元素的目的。
但是,当关键字集合很大时,关键字值不同的元素可能会映象到哈希表的同一地址上,即
k1≠k2 ,但 H(k1)=H(k2),这种现象称为冲突,此时称k1和k2为同义词。实际中,
冲突是不可避免的,只能通过改进哈希函数的性能来减少冲突。

综上所述,哈希法主要包括以下两方面的内容:
1)如何构造哈希函数
2)如何处理冲突。
构造哈希函数的原则是:①函数本身便于计算;②计算出来的地址分布均匀,即对任一关
键字k,f(k) 对应不同地址的概率相等,目的是尽可能减少冲突。常用方法,平方取中、数	
字分析、分段叠加、除留取余、伪随机数等等

回归主题,如何解决hash冲突,一般有四种方法
1)拉链法,我们都见过,linkedhashmap,这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
2)再hash法, 这种方法是同时构造多个不同的哈希函数, Hi=RH1(key)  i=1,2,…,k;当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。
3)开放地址法:这种方法也称再散列法,其基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。这种方法的特点是:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。这种方法有一个通用的再散列函数形式:  Hi=(H(key)+di)% m   i=1,2,…,n;其中H(key)为哈希函数,m 为表长,di称为增量序列。增量序列的取值方式不同,相应的再散列方式也不同。主要有三种,三种再散列的方式也很类似,主要是d的计算方法不同, a.线性探测再散列,d的值为1,顺序查看全表,b.二次探测再散列,d的值为n,左右跳跃查看全表,c.伪随机探测再散列,d的值近似于随机数,查看全表。
4)建立公共溢出区,这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

5. 悲观锁乐观锁原理及应用场景

顾名思义即可

悲观锁:
悲观主义,每次拿数据时都认为数据会被别人更改,因此每次操作之前都会加锁,如读写
锁、行锁、表锁等,synchronized的原理也是悲观锁;
适用于多写的操作。

乐观锁:
乐观主义,每次拿数据时都认为数据不会被别人更改,因此每次操作时都不会加锁,但是
操作完数据进行更新时会判断在此期间数据有没有被人更改过,如果有的话,则会返回冲
突信息,让用户决定如何操作
适用于多读的操作。

6. RPC协议与HTTP协议的区别?

RPC:远程过程调用,一般用于一台计算机调用另一个计算机上的服务,rpc能让我们像调
用本地方法一样调用远程方法;
HTTP:超文本传输协议,一般用于浏览器和服务器之间的通讯;

RPC效率更高,但是实现起来较复杂;HTTP一般使用json传输数据,RPC一般采用二进
制;

7. 什么websocket?它有什么特点?

一种协议:
用来和服务端保持长连接,使服务端能主动推送消息给客户端,不同于http的轮询和长连
接

对比http:
websocket是一种类似http的协议,可以理解成http协议的加强版;http每次请求都需要建
立tcp连接,而且一个request只能对应一个response,每次请求必须由客户端发起,由服
务端响应;而websocket能使客户端在和服务端建立好tcp连接后,与服务端保持会话连
接,服务端可以自由向客户端推送消息;

注意:如果服务端同时维护了很多websocket连接,会对服务端造成很大压力,需要我们
对websocket做一些优化(有个互联网独角兽公司面试官这么问我的)

我目前能想到的优化就是:其实不外乎开源节流
1)合并推送:将一些能合并的消息整合到一次推送,以此减少websocket连接
2)横向扩展:增加服务器 0.0

参考:
websocket 实现长连接原理
彻底理解 WebSocket 原理

8. OOM

OOM就是我们常说的Out of Memory内存溢出,它是指需要的内存空间大于系统分配的内存空间。

  1. 为什么操作系统需要内存管理和虚拟内存?
    第一,由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的。进程也没有办法访问其他进程的页表,所以这些页表是私有的。这就解决了多进程之间地址冲突的问题。
    第二,页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性。

  2. 内存分配过程?
    应用程序通过 malloc 函数申请内存的时候,实际上申请的是虚拟内存,此时并不会分配物理内存。
    当应用程序读写了这块虚拟内存,CPU 就会去访问这个虚拟内存, 这时会发现这个虚拟内存没有映射到物理内存, CPU 就会产生缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的 Page Fault Handler (缺页中断函数)处理。
    缺页中断处理函数会看是否有空闲的物理内存,如果有,就直接分配物理内存,并建立虚拟内存与物理内存之间的映射关系。
    如果没有空闲的物理内存,那么内核就会开始进行回收内存的工作,回收的方式主要是两种:直接内存回收和后台内存回收。
    后台内存回收(kswapd):在物理内存紧张的时候,会唤醒 kswapd 内核线程来回收内存,这个回收内存的过程异步的,不会阻塞进程的执行。
    直接内存回收(direct reclaim):如果后台异步回收跟不上进程内存申请的速度,就会开始直接回收,这个回收内存的过程是同步的,会阻塞进程的执行。
    如果直接内存回收后,空闲的物理内存仍然无法满足此次物理内存的申请,那么内核就会放最后的大招了 ——触发 OOM (Out of Memory)机制。

  3. 什么是OOM?
    OOM:Out of Memory 内存溢出,以前一直以为oom是一个现象名词,原来它是一种机制
    OOM (Out of Memory)机制—OOM Killer 机制:会根据算法选择一个占用物理内存较高的进程,然后将其杀死,以便释放内存资源,如果物理内存依然不足,OOM Killer 会继续杀死占用物理内存较高的进程,直到释放足够的内存位置。
    所以当一个任务占用内存很大时就容易被oom。

  4. 总结
    内核在给应用程序分配物理内存的时候,如果空闲物理内存不够,那么就会进行内存回收的工作,主要有两种方式:
    后台内存回收:在物理内存紧张的时候,会唤醒 kswapd 内核线程来回收内存,这个回收内存的过程异步的,不会阻塞进程的执行。
    直接内存回收:如果后台异步回收跟不上进程内存申请的速度,就会开始直接回收,这个回收内存的过程是同步的,会阻塞进程的执行。
    可被回收的内存类型有文件页和匿名页:
    文件页的回收:对于干净页是直接释放内存,这个操作不会影响性能,而对于脏页会先写回到磁盘再释放内存,这个操作会发生磁盘 I/O 的,这个操作是会影响系统性能的。
    匿名页的回收:如果开启了 Swap 机制,那么 Swap 机制会将不常访问的匿名页换出到磁盘中,下次访问时,再从磁盘换入到内存中,这个操作是会影响系统性能的。
    文件页和匿名页的回收都是基于 LRU 算法,也就是优先回收不常访问的内存。回收内存的操作基本都会发生磁盘 I/O 的,如果回收内存的操作很频繁,意味着磁盘 I/O 次数会很多,这个过程势必会影响系统的性能。
    针对回收内存导致的性能影响,常见的解决方式。
    设置 /proc/sys/vm/swappiness,调整文件页和匿名页的回收倾向,尽量倾向于回收文件页;
    设置 /proc/sys/vm/min_free_kbytes,调整 kswapd 内核线程异步回收内存的时机;
    设置 /proc/sys/vm/zone_reclaim_mode,调整 NUMA 架构下内存回收策略,建议设置为 0,这样在回收本地内存之前,会在其他 Node 寻找空闲内存,从而避免在系统还有很多空闲内存的情况下,因本地 Node 的本地内存不足,发生频繁直接内存回收导致性能下降的问题;
    在经历完直接内存回收后,空闲的物理内存大小依然不够,那么就会触发 OOM 机制,OOM killer 就会根据每个进程的内存占用情况和 oom_score_adj 的值进行打分,得分最高的进程就会被首先杀掉。
    我们可以通过调整进程的 /proc/[pid]/oom_score_adj 值,来降低被 OOM killer 杀掉的概率。

2. 数据结构

1. 简述一下二叉树,有什么特点,以及它的三种遍历方式

一个递归的树形数据结构,每个节点最多有两个子节点;二叉树一般都是二分查找树,每
个节点的值大于它左子节点的值,小于它右子节点的值

在这里插入图片描述

递归遍历:

前序遍历:先访问根节点,再访问左子节点,最后访问右子节点
上图中前序遍历结果:30、20、5、28、50、38、58

中序遍历:先访问左子节点,再访问根节点,最后访问右子节点
上图中中序遍历结果:5、20、28、30、38、50、58

后序遍历:先访问左子节点,再访问右子节点,最后访问根节点
上图中后序遍历结果:5、28、20、38、58、50、30

非递归遍历:

常用的是利用栈的先进后出特性,不断地将节点入栈,然后再出栈

非递归前序遍历和非递归中序遍历两种方式很好理解,控制遍历时机即可,而非递归后序
遍历较为复杂,需要额外维护一个最后访问节点

参考:八旬老人彻夜难眠,竟是为了学会二叉树

3. 算法题

1. 你能想到几种方法实现两数交换?

1)引入第三变量(需要建新对象,但是可以用于任何对象的交换)
2)利用加减运算(可能超出范围,或者出现精度转换)
3)利用乘除运算(可能超出范围,或者出现精度转换)
4)利用位运算(若是整数则全场最佳,但只有整数可以位运算)
public class Demo005 {
	public static int a = 1;
    public static int b = 3;
    public static void main(String[] args) {
		change();
        change1();
        change2();
        change3();
    }
    //引入第三变量
    public static void  change(){
        int c;
        c = a;
        a = b;
        b = c;
        System.out.println("a:"+ a +"b:"+b );
    }

	//使用加减运算
    public static void change1(){
        a = a+b;
        b = a-b;
        a = a-b;
        System.out.println("a:"+ a +"b:"+b );
    }

	//使用乘除运算
    public static void change2(){
        a = a*b;
        b = a/b;
        a = a/b;
        System.out.println("a:"+ a +"b:"+b );

    }

	//使用位运算
    public static void change3(){
        a = a^b;
        b = a^b;
        a = a^b;
        System.out.println("a:"+ a +"b:"+b );
    }
}	

4. Java

1. String类能不能被继承

源码是学习的最佳场所

package java.lang;
/**
 * The {@code String} class represents character strings. All
 * string literals in Java programs, such as {@code "abc"}, are
 * implemented as instances of this class.
 * <p>
 * Strings are constant; their values cannot be changed after they
 * are created. String buffers support mutable strings.
 * Because String objects are immutable they can be shared. For example:
 * <blockquote><pre>
 *     String str = "abc";
 * </pre></blockquote><p>
 * is equivalent to:
 * <blockquote><pre>
 *     char data[] = {'a', 'b', 'c'};
 *     String str = new String(data);
 * </pre></blockquote><p>
 * Here are some more examples of how strings can be used:
 * <blockquote><pre>
 *     System.out.println("abc");
 *     String cde = "cde";
 *     System.out.println("abc" + cde);
 *     String c = "abc".substring(2,3);
 *     String d = cde.substring(1, 2);
 * </pre></blockquote>
 */ <p>
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;
	......
}

通过上面源码中的介绍就可以很清楚,String类不能被继承,final修饰不能继承重写修改,且String是不可变类,因为不可变因此可以更好的共享,若易变请选择Stringbuffer类,String重载了多种构造器,可以空初始化,也可以传入字符串,还可以传入字符数组,还可以传入Stringbuffer,并且传入Stringbuffer时会加锁。

刚刚看源码,发现equals方法除了String类重写了,Stringbuffer和Stringbuild甚至HashMap等都是没有重写的。

2. 简述Java的反射机制和使用场景

反射是Java的一种机制,可以让我们在运行时获取类的信息

通过反射我们可以获取到类的所有信息,比如它的属性、构造器、方法、注解等

适用于需要动态创建对象的场景

参考:反射通俗易懂

3. String str = new String(“abc“)到底new了几个对象?

以前一直以为是两个,其实非也,可能是一个也可能是两个

  • 两个:如果常量池里面没有“abc”这个字符串,那虚拟机就会在堆内存中new出一个
    String对象,还会在常量池中new一个abc字符串对象;
  • 一个:如果常量池中已经有"abc"这个字符串,也就是说你在前面已经new过一个值
    为“abc”的字符串,那虚拟机就只会在堆内存中new一个String对象,并将常量池中“abc”
    的地址指向你刚刚new的String对象。

4. 接口和抽象类的异同

1、 都能包含抽象的方法,这些方法没有实现体,作用是描述一个类具有的功能,jdk1.8允
许接口有一个默认的实现方法,default方法。
2、 抽象类是对事物本质的抽象,接口是对事物行为的抽象。
3、接口中的变量必须给予初始值,接口则不需要
4、单继承、多接口
5、抽象类中可以有非抽象方法,从而避免多个子类重复实现,可以提高代码的复用性,简
洁性,但是接口中只能有抽象方法。
6、接口和抽象类其实都可以被继承,只是接口只能被接口继承。

针对第二点和第四点举个例帮助理解:

对于抽象类:比方说有公鸡、母鸡、公狗、母狗,我们可以抽象出两个更高级的类,鸡类
和狗类,因为你不能又是鸡又是狗,所以你只能继承其中一个,这就是为什么抽象类只
能单继承;

对于接口:众所周知,鸡都会唱、跳、rap,于是,我们可以把这些鸡的基本操作抽象成接
口A,而有的鸡通过练习两年半可能学会打篮球,那么对于这种鸡的高端操作我们可以再抽
象出一个接口B,重点来了,对于常规鸡,这种鸡只会唱跳rap,所以只需要实现接口A;
那对于一些高端鸡,这种鸡既会唱跳rap又会打篮球,我们就同时实现接口A和接口B,这
就是为什么接口可以多实现。

5. sleep和wait的区别

1、sleep是Thread类中的方法,而wait是Object类中的方法,两者都是native方法,也就是
非java语言实现的方法。
2、sleep会把获取到的锁保留,而wait会把获取到的锁放开。因为两者的使用场景有异,
前者主要是线程等待,后者主要是因为资源不够,从而决定了是否放开锁。
3、wait一般用notify或者notifyall方法唤醒,sleep一般是自定义时间唤醒,前者只能
用在同步控制方法或者同步控制块中,后者可以在任何地方使用。
4、sleep必须捕获异常,而wait不需要捕获异常。

6. Java如何进行高效的数组拷贝?

说到高效就不得不提起面向过程和面向对象,面向过程性能优于面向对象。不只是解题步
骤,重点在于维护对象是性能开支。

数组拷贝时使用Arrays.copyOf或 System.arraycopy是自己new数组, 然后for循环复制
效率的两倍左右

为什么快,因为它们是native方法;native一般是c,正是面向过程。

7. 变量和方法的区别

变量可以理解为属性,方法可以理解为行为

变量:类变量(静态变量)、实例变量
方法:类方法(静态方法)、实例方法、构造方法

简单说,静态的都是归类所有,是所有实例对象共享的信息。调用的时候,可以用类直接
调用,也可以用实例对象调用,归根结底他们属于类,存在方法区。其生命周期取决于类
的生命周期;类方法没有this,因为没有实例,链式调用时很好理解this。
实例的都是归实例对象所有,访问的时候只能通过实例对象来访问,实例变量存在堆中毫
无疑问,但是实例方法存在哪里呢,查着感觉像是方法区,并且维持一份,这个问题可以
和面试官互动一下。实例变量的生命周期取决于实例对象的生命周期。

8. Java编译后的.class文件包含了哪些内容?

这个问题,其实很复杂,如果真的问到,可以说这个还没有机会深入研究过,只是以前查
过简单了解过一些

编译后的.class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序
紧凑地排列在.class文件之中,中间没有添加任何分隔符;
根据Java虚拟机规范的规定,.class文件格式采用一种类似于C语言的伪结构来存储数据,
包含无符号数和表:
无符号数:以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符
号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串
值;
表:由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性的
以”_info“结尾。
Class文件的结构没有分隔符,无论你是数量还是顺序,都是严格规定的,哪个字节代表什
么含义、长度多少、先后顺序如何,都不允许改变。

详解:class文件剖析

9. hashCode和equals方法的联系

hashcode是存在冲突的可能的,因此:

hashcode相等的,equals不一定相等;
equals相等的,hashcode一定相等。

10. 什么是重写和重载

1、重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也
一样的方法,就称为重写(Override),常见于继承。
2、重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和
次序不同,则称为方法的重载(Overload),常见于一个类的构造方法。
3、重载是一个类的多态性表现,而重写是子类与父类的一种多态性表现。

11. Java有几种基本数据类型?分别占用多少字节?

类型 | 字节

  • | -
    byte |1
    short|2
    int|4
    long | 8
    char| 2(c语言中是1个字节,可以存储一个汉字)
    float| 4
    double| 8
    boolean|1或者4(详解在下面)

    1、1个bit(1/8个字节):boolean类型的值只有true和false两种逻辑值,在编译后会使用1和0来表示,这两个数在内存中按位算,仅需1位(bit)即可存储,位是计算机最小的存储单位。在传智播客java基础班中也有有此理由(复习时所参考的视频)。
    2、1个字节:虽然编译后1和0只需占用1位空间,但计算机处理数据的最小单位是1个字节,1个字节等于8位,实际存储的空间是:用1个字节的最低位存储,其他7位用0填补,如果值是true的话则存储的二进制为:0000 0001,如果是false的话则存储的二进制为:0000 0000。
    3、4个字节:在《Java虚拟机规范》一书中的描述:“虽然定义了boolean这种数据类型,但是只对它提供了非常有限的支持。在Java虚拟机中没有任何供boolean值专用的字节码指令,Java语言表达式所操作的boolean值,在编译之后都使用Java虚拟机中的int数据类型来代替,而boolean数组将会被编码成Java虚拟机的byte数组,每个元素boolean元素占8位”。也就是说JVM规范指出boolean当做int处理,也就是4字节,boolean数组当做byte数组处理,这样我们可以得出boolean类型占了单独使用是4个字节,在数组中是确定的1个字节。

    一般来说,大家认同第三条,一个小问题:那虚拟机为什么要用int来代替boolean呢?为什么不用byte或short,这样不是更节省内存空间吗。经过查阅资料发现,使用int的原因是,对于当下32位的处理器(CPU)来说,一次处理数据是32位(这里不是指的是32/64位系统,而是指CPU硬件层面),32 位 CPU 使用4 个字节是最为节省的,哪怕你是 1 个 bit 他也是占用 4 个字节。因为 CPU 寻址系统只能 32 位 32 位地寻址,具有高效存取的特点。

    结论:java规范中,没有明确指出boolean的大小。在《Java虚拟机规范》给出了4个字节,和boolean数组1个字节的定义,具体还要看虚拟机实现是否按照规范来,It depends on Java virtual machine;也就说最终是取决于系统和JVM。
    因此:不是占1个字节就是占4个字节。

12. Java异常有哪些类型?

查找接口的实现类:

IDEA 风格 ctrl + alt +B

查看类或接口的继承关系:

ctrl + h

查看结构图:

右键-Diagrams-show diagrams

Java所有的异常都继承至Throwable,分为Error和Exception两大类,其中:
Error是系统级错误,在代码层无法处理,常见的有StackOverflowError、OutOfMemoryError等;
Exception是异常,通常可以在代码层处理,常见的有NullpointerException、ClassCaseException、IndexOutOfBoundsException等

在这里插入图片描述

13. 简述jvm内存模型

从区域类型划分,有5种,本地方法栈、虚拟机栈、堆、方法区、程序计数器;
从内容划分,有2种,线程隔离区和线程共享区,意味着是否一个线程生成一份。

本地方法栈/虚拟机栈:都是栈类型,唯一区别就是运行的方法不同,本地方法栈运行的是
native方法,虚拟机栈运行的是java方法。线程私有,每个线程会单独生成一个栈区域,每
个方法在运行之前都会生成一个栈帧,方法的调用就是栈帧入栈到出栈的过程,入栈表示
方法开始被调用,出栈表示方法执行完毕,栈帧用于保存方法内部局部变量、操作数、
方法返回值、动态链接;我们平时说的栈其实一般就是指局部变量区:用于存放方法参
数、方法内定义的局部变量,还有已知的八大基本数据类型、对象引用、返回值地址;

堆:线程共享,在虚拟机启动的时候创建,用于存放对象实例,堆是GC管理的主要区域;

方法区:线程共享,其实方法区也是堆的物理组成部分,用于存放常量、类变量 、 类信息
(构造方法/接口定义) 、运行时常量池,字符串常量池、数字常量池;(jdk1.8之前,方法
区的实现是永久代,从1.8开始,用元空间代替了永久代,注意一点,方法区还是那个方法
区)

程序计数器:线程私有,各线程之间独立储存,互不影响,若当前执行的是Java方法,则
记录的就是当前执行指令的地址,若是native方法,则为空;

最后,有关运行时常量池和字符串常量池:jdk1.6的时候,两者都是属于方法区,1.7开
始,字符串常量池被移到了堆内存;运行时常量池用于存放编译期生成的各种常量
(“abc”,123等)和符号引用;而字符串常量池是为了提高jvm效率单独用来存放字符串的,
因为字符串不同于其他数据类型,它可以很长很长;

14. 简述GC机制,新生代和老年代的区别?

建议还是好好好些时间理清楚JVM,不只是GC,还包括,
1)JVM的发展历程:至今有3种,从编译器和解释器的运作进步,到对象的内存读取机制、最后的热点探测技术
2)Jvm的区域划分:5大区域,1.8以后,永久代现在已经被删除了,取代的是元空间,元空间使用的是本地内存,而不再是JVM空间
3)对象的可达性分析:2种方法,计数法和可达性分析法
4)对象引用类型:4种引用类型,强软弱虚,各自特点
5)GC算法:大体有2种,标记清除和复制,细分有4种,是否整理,如何复制,新生代算法的主流,eden区域、from survivor、to survivor、8:1:1
6)GC方式:3种方式,新生代minor GC,老年代或永久代 major GC ,整体 Full GC
7)怎样GC:2点,3种方式进入老年代(年龄、大对象、survivor中同年龄所占空间超一半)、空间担保策略(就是计算本次GC是否安全,主要是minor GC,事先计算老年代连续内存空间是否大于新生代所有对象,不大于然后查看配置是否允许担保失败,允许则冒险一试,不允许则改为full GC)
8)GC器:5种,主流为G1收集器,目的就是快,既好又快的完成垃圾收集,并行和并发。

回答问题:
垃圾回收是java管理内存的一种机制,作用是清理无用对象避免内存泄漏,因此gc主要发
生在java堆上,一共有三种GC机制,minor、majro、full、分别对应不同的区域,minor应
该是使用最频繁的方式,它对应的是新生代,大部分的对象的生老病死都发生在新生代,
三种方式触发的条件分别是,
minor:当eden区域没有足够的空间创建新对象,此时还会触发担保机制,看老年代的空
间是否大于新生代所有对象所需要的空间,或者历次晋升所需的空间。最终触发minor或者	
full
major:当老年代中没有足够的内存空间来存放对象时。
再来说说新生代和老年代的区别,分代是为了提高gc效率,其实不分代也可以完成gc,只
不过gc机制会对堆的所有区域进行扫描,浪费资源,新生代还可以细分为三个虚拟的区,
Eden区、FromSurvivor区、ToSurvivor区,一开始对象都在Eden区,Eden区的对象经过
一次新生代gc(复制算法)后若还能存活,就会移动到survivor区(ToSurvivor区),在此
次新生代gc时,在survivor发生的改变就是,From区中的对象会根据年龄来决定去留,达
到阈值,会移动到老年代,没达到就移动到To区,经过此次新生代gc,Eden和From区都
已被清空,From区和To区会互换;

详解:新生代、老年代、永久代

15. 如何优化JVM频繁Minor GC?

这个问题有两个考虑的方面,任务问题无非就是开源节流、
1、节流:优化代码结构,避免创造一些无用的对象,减少JVM堆内存的使用
2、开源:在系统允许的情况下,适当的扩大JVM堆内存的空间大小

16. 简述类加载机制,什么是双亲委派?

1、加载:将源代码编译换成,class文件,传递给类加载器
2、验证:类加载器根据本地类库进行验证.class文件是否符合java虚拟机的规范
3、准备:给类变量也就是静态变量分配内存,并赋予默认初始值,也就是null或者0
4、解析:将常量池中的符号引用转换成直接引用(如string str = new string(“123”),str就是符号引用,123是直接引用)
5、初始化:对类中的静态变量赋予指定的初始值,执行静态块。

双亲委派:当一个类加载器收到类加载的请求时,它不会立刻就去加载这个类,而是把这
个请求委派给父类加载器,这样所有的类加载请求在最终都会传送到顶级的启动类加载
器。只有当父类加载器反馈自己无法加载这个类的时候,也就说父类加载在搜索范围内找
不到这个类,此时类加载器才会尝试自己加载这个类。

17. synchronized底层实现原理?它与lock相比有什么优缺点?

首先,synchronized是没有源码的,在idea里面点不进去,只能写一个synchronized然
后分析它的字节码文件字节码文件(怎么看?? idea => view => Show ByteCode),
也就是.class文件,可以看到标志性的关键字,monitorenter和moniterexit,并且是
一个monitorenter和两个monitorexit

因此,它的原理就是每一个对象都有一个监视器,ObjectMonitor,这个监视器有若干个
属性,包括对象当前所属的线程,正在等待的线程数,计数器等,其中计数器就是用来
实现synchronized,当该对象被线程操作的时候,计数器加一,当线程操作结束之后,
计数器减一,当计数器不为0的时候,其他线程访问对象就会被禁止。当对象被操作时
进入monitorenter,计数器加一,操作结束后,执行monitorexit,计数器减一,此时
其他线程才可操作,两个monitorexit的目的是为了计数器一定能够归零,也就是锁一定会
释放,第一个exit是正常释放,第二个exit是程序非正常退出之后,JVM释放。

与lock对比:
1)lock需要手动释放锁,synchronized会自动释放锁
2)lock可以设置公平锁和非公平锁,synchronized只能使用默认的非公平锁
3)lock提供的condition可以指定唤醒哪些线程,而synchronized只能随机唤醒一个线
程或者全部唤醒。

详解:synchronized底层实现原理

18. jvm指令重排原因?怎么避免?

原因:计算机系统的内存操作速度远小于cpu的运算速度,此时,就会造成cpu的空置,
为了提高cpu的利用效率,JVM虚拟机就会按照自己的一些规则去跳过执行比较慢的代码
转而去执行速度比较快的代码,提高JVM的整体性能,即代码执行顺序会变化,当然这个
变化在JVM看来不会影响代码的最终结果,单线程时确实是,但多线程就会出问题。

避免:因此多线程是需要避免这个问题,目的就是不允许指令重排,给会被执行顺序影响
输出结果的代码加上volatile关键字即可。

19. volatile关键字解决了什么问题?实现原理是什么?

volatile关键字主要有三个特点:
1)线程可见、
2)非原子性操作、
3)禁止指令重排。

所有它的使用场景也就是存在以上问题的场景:
1)保证了变量的可见性:它会强制将对自己的高速缓存中的修改操作立即写入主存,使得
其它线程能立马发现;
2)禁止了指令重排:禁止虚拟机按照自己的规则将它自认为可以改变顺序的代码不按照我
们写的顺序执行,保证了代码执行的严格顺序。

举个例子:
比如i=i+1,单线程操作没问题,如果使用多线程,比如两个线程,执行这段代码后(i初
始值为0),i应该等于2,但是如果不用volatile修饰变量i,结果会等于1,初始时,两个
线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i
的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值
为1,然后线程2把i的值写入内存。

原理:
基于内存屏障,关于内存屏障,搞java开发的同学在开发中不可能接触到,所以不用关心
太多,知道内存屏障有什么作用,面试官问到你能唬住他就行了,因为面试官自己也不
懂。

20. ThreadLocal实现原理?

我喜欢将它叫做本地线程,可以见字知意,该类的目的就是线程隔离,它的实现原理是底
层维护了一个map,key就是当前线程的名称,velue就是需要存入的值,这样不同的线程
就可以把自己想要的值私有化,进而可以做到线程隔离,保证了各个线程的数据互不扰。
该类的主要方法就是get、set、remove。

ThreadLocal和Synchonized对比:

其实两者是相反,前者是做到了各线程中的数据隔离,后者的目的是为了各线程中的数据
共享。不过ThreadLocal为解决并发编程提供了新的思路。
synchronized是利用锁的机制,使变量或代码块在某一时该只能被桶一个线程访问。而
ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是
同一个对象,这样就隔离了多个线程对数据的数据共享。

ThreadLocal使用不当引起的内存泄漏

原因:
ThreadLocal没有外部强引用,在发生垃圾回收的时候,ThreadLocal会被当成垃圾给干
掉,而ThreadLocal对象又是Map中的key,map的key没了,那对应的entry永远不会被访
问到,就无法被回收,进而造成内存泄漏

解决:
1、主动清除数据:每次用完ThreadLocal都调用它的remove()方法清除数据
2、静态变量:将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强
引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而被
清除

21. ReentrantLock是什么?有什么用?怎么用?和synchronized的区别?

Lock是一个接口:
public interface Lock {}。

ReentrantLock是Lock的一个实现类,:
public class ReentrantLock implements Lock, java.io.Serializable {}

作用:
就是用来给资源加锁,避免高并发造成的数据异常问题;跟synchronized作用相似。

使用:
直接实例化即可,实例对象调用lock方法获得锁,unlock释放锁。
    public void lockTest() {
    //创建lock实例,可传参数true或者false,表示是否是公平锁,默认是非公平锁
    ReentrantLock lock = new ReentrantLock();
    //在需要保证同步的代码前lock
    lock.lock();
    int i = 0;
    i++;
    //代码后unlock
    lock.unlock();
}

区别:
它和synchronized的区别就是lock和synchronized的区别,还是之前提过的3点。

22. 线程池实现原理?

任何池的实现都是池化技术,线程池、连接池、内存池、对象池等。	
池化技术简单点来说,就是提前保存大量的资源,以备不时之需。在机器资源有限的情况
下,使用池化技术可以大大的提高资源的利用率,提升性能等,是一种常用的CPU利用的
优化手段。

如果每次都是如此的创建线程->执行任务->销毁线程,会造成很大的性能开销。那能否一
个线程创建后,执行完一个任务后,又去执行另一个任务,而不是销毁。这就是线程池。
这也就是池化技术的思想,通过预先创建好多个线程,放在池中,这样可以在需要使用线
程的时候直接获取,避免多次重复创建、销毁带来的开销。

jdk提供给外部的接口也很简单。直接调用ThreadPoolExecutor构造一个就可以。

看一遍源码其实就可以看一个大概,直接调用一个api是很简单的,重要的就是参数和执行
流程。
1、corePoolSize(必填):核心线程数。 2、maximumPoolSize(必填):最大线程数。
3、keepAliveTime(必填):线程空闲时长。如果超过该时长,非核心线程就会被回收。
4、unit(必填):指定keepAliveTime的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
5、workQueue(必填):任务队列。通过线程池的execute()方法提交的Runnable对象将存储在该队列中。
6、threadFactory(可选):线程工厂。一般就用默认的。
7、handler(可选):拒绝策略。当线程数达到最大线程数时就要执行饱和策略。

总结:
所谓线程池本质是一个hashSet。多余的任务会放在阻塞队列中。

只有当阻塞队列满了后,才会触发非核心线程的创建。所以非核心线程只是临时过来打杂
的。直到空闲了,然后自己关闭了。

线程池提供了两个钩子(beforeExecute,afterExecute)给我们,我们继承线程池,在执行
任务前后做一些事情。

线程池原理关键技术:锁(lock,cas)、阻塞队列、hashSet(资源池)

参考1:线程池详解
参考2:线程池剖析

23. java是如何实现线程安全的?哪些数据结构是线程安全的?

1、锁机制:
用synchronize、lock给共享资源加锁;
2、使用java提供的安全类:
java.util.concurrent包下的类自身就是线程安全的,在保证安全的同时还能保证性能;
3、 线程安全的数据结构:
常见的有Atomicinteger、ConcurrentHashMap,很多,java.util.concurrent包下都是。

24. java线程间通信方式

1、基于synchronized+wait() 和 notify()
2、基于reentrantLock的Condition
3、基于volatile
4、基于CountDownLatch

详解:java线程间通讯的几种方式

25. 什么是公平锁?什么是非公平锁?如何实现

公平锁:排队
当前获得锁的线程释放锁后,其它所有等待中的线程会按照来的顺序执行,不会造成锁竞
争

非公平锁:打架
当前获得锁的线程释放锁后,其它所有等待中的线程会全部参与锁竞争;会造成锁竞争。

拿ReentrantLock来说,众所周知,没拿到锁对象的线程会被放入同步队列,当持有锁的线
程释放锁后,公平锁:队列中先来的线程先获取锁,不会造成锁竞争;非公平锁:队列中
所有线程一起竞争锁资源;

如何实现:
当前持锁线程把锁释放掉后,如果是公平锁,它只会唤醒队列头部第一个等待线程,如果
是非公平锁,就会唤醒队列中所有等待线程

26. 简单阐述Java中的io、nio、bio

io即input/output,就是指读写操作
io和nio的对比的话,io其实可以理解为bio,前者是blockio,后者是Noblockio

1、io是面向的流,nio是面向的通道和缓冲冲
2、io没有选择器的概念,所以网络编程时是阻塞的,nio有选择器,是非阻塞的。
3、io的性能不如nio,nio是标准io的替代。

NIO的两大核心---通道、缓冲区
通道用于连接数据源和应用程序;缓冲区用于用户存储数据
缓冲区:用于存储数据的,底层即是一个数组。不同类型的数组用以存储不用的数据类型
通道:用于连接数据源和缓冲区,只是用于连接,无法存放数据,并且必须和缓冲区一起
使用
选择器:用于网络编程的时候,标准的网络编程是阻塞式的,NIO的网络编程中存在一个
选择器,用于监控客户端的通道的状态的,将符合服务端的需求的通道过滤出来,给服务
端

同步异步,阻塞非阻塞,前者就是说能不能同时干其他的事情,后者就说说需不需要一直
等待响应。

27. 什么是内存泄漏,怎么确定内存泄漏?

内存泄露是指JVM内存没有及时释放,说白了就是创建的对象没有被及时回收,一般都是
编码不严谨导致的,new了很多值为null的对象,然后又不调用。(前提是你没有把堆的内
存故意的改小)

怎么确定:
linux有个工具叫valgrind,使用方法可以直接百度。

28. int和Integer有什么区别?

int是基本数据类型,默认值为0,integer是其包装类型,默认值为null

-  原始类型: boolean,char,byte,short,int,long,float,double
-  包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double

java实体类尽量都用integer,假设一个学生类,有个考试成绩属性,如果学生缺考,该属性不应该有值,应该为null,你用int的话默认值为0,考试成绩为0和缺考是两码事

29. String和StringBuilder、StringBuffer的区别?

Java平台提供了两种类型的字符串:String和StringBuffer/StringBuilder,它们可
以储存和操作字符串。

其中String是只读字符串,也就意味着String引用的字符串内容是不能被改变的。而
StringBuffer/StringBuilder类表示的字符串对象可以直接进行修改。

StringBuilder是Java 5中引入的,它和StringBuffer的方法完全相同,区别在于它是
在单线程环境下使用的,因为它的所有方面都没有被synchronized修饰,因此它的效率也
比StringBuffer要高。

30. 常见的运行时异常

ArithmeticException(算术异常)
ClassCastException (类转换异常)
IllegalArgumentException (非法参数异常)
IndexOutOfBoundsException (下标越界异常)
NullPointerException (空指针异常)
SecurityException (安全异常)

31. 线程池怎么设置线程数

首先,线程池绝对不是越多越好,否则无限创建线程池的结果只能是栈泄露或者内存泄
露,维护大量的线程以及线程切换也会耗费大量的cpu性能。

设置线程数必须根据任务性质来决定的

插一句:cpu是大脑,负责任务的调度和运算,相当于人的大脑,而内存和硬盘以及任何设
备都只是一个设备,他们的目的就是挺cpu的调遣,因此多线程主要是为了充分发挥cpu的
性能,而不是内存,搞清楚,性能瓶颈包括cpu、内存、IO、网络等等,但是多线程关心
的就是利用cpu。每一个线程都有一个栈,默认1024,可以改小一些。JVM5大区域分为线
程隔离和线程共享

cpu密集型:任务的主要需要做的就是运算,IO利用率不高,此时大脑高速运转,哪还有
时间调配其他设备呢,此时最好的办法就是一个核心一个线程,此时就是核心+1,多一个
为了轮询,看有没有提前完成的,不至于cpu空闲。
IO密集型:cpu安排完任务之后,一直在io大脑都没有事情做,只能开启新的线程了,把事
情提前安排好嘛

最后:计算密集型程序适合C语言多线程,I/O密集型适合脚本语言开发的多线程。

在这里插入图片描述

参考:
jvm角度理解多线程
多线程
线程池应用之如何合理配置线程数
线程池究竟怎样设置

32. 什么是死锁,怎么避免,怎么解决?

背景:
多线程以及多进程改善了系统资源的利用率并提高了系统的处理能力。同时也引发了一个
新的问题-----死锁。

注意:
单线程时不可能会出现死锁。

定义:
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待
状态,无法前往执行。当线程进入对象的synchronized代码块时,便占有了资源,直到它
退出该代码块或者调用wait方法,才释放资源,在此期间,其他线程将不能进入该代码
块。当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动
释放所占有的资源,将产生死锁。

简述:
两个线程互相请求对方所正在使用的资源;或者说当两个线程相互等待对方释放资源。

现象:
程序一直在运行,很长时间既不输出期望内容也不报错,而且在多线程中使用了多个锁。
基本即可判定为死锁。

事发地:
1)资源争抢
2)递归死锁

产生死锁的四大必要条件:
1)互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直
 到被该进程释放 
2)请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不
放。 
3)不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用 
4)循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),
造成永久阻塞。

如何避免?:
其实只要理解了出现的必要条件,随便打破一个即可避免死锁。
● 打破互斥条件:改造独占性资源为虚拟资源,大部分资源已无法改造。
● 打破不可抢占条件:当一进程占有一独占性资源后又申请一独占性资源而无法满足,则
退出原占有的资源。
● 打破占有且申请条件:采用资源预先分配策略,即进程运行前申请全部资源,满足则运
行,不然就等待,这样就不会占有且申请。
● 打破循环等待条件:实现资源有序分配策略,对所有设备实现分类编号,所有进程只能
采用按序号递增的形式申请资源。

常用手段:
1)加锁顺序:所有线程申请资源时按同一个顺序申请	 
2)加锁时限:对锁设置超时放弃获取机制,java中如果是synchronized,则需要自定义
锁
3)死锁检测:每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等
等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。
//这是一个简单的死锁原理,只是原理实例,真实情况一定比这个复杂的多,毕竟真实情况不会有这么无脑

//同时,这是用死锁检测的方式检测死锁是否发生的一个实例,利用了set的add、remove返回值

public class Demo005 {
    public static final String element1 = "element1";
    public static final String element2 = "element2";
    public static volatile Set<String> set = new HashSet<String>();
    public static void main(String[] args) {
            dead_lock();
    }
     public static void dead_lock(){
        Thread a = new Thread(){
            @Override
            public void run() {
                System.out.println(set.add(element1));
                synchronized (element1) {
                    System.out.println("线程a成功占有元素1");
                    try {
                        Thread.sleep(200);
                    } catch (Exception e) {
                        System.out.println(e);
                    }

                    System.out.println(set.add(element2));
                    synchronized (element2) {
                        System.out.println("线程a成功占有元素2");
                    }

                }
                System.out.println(set.remove(element1));
                System.out.println(set.remove(element2));
            }
        };

        Thread b = new Thread(){
            @Override
            public void run() {
                System.out.println(set.add(element2));
                synchronized (element2) {
                    System.out.println("线程b成功占有元素2");
                    try {
                        Thread.sleep(2200);
                    } catch (Exception e) {
                        System.out.println(e);
                    }

                    System.out.println(set.add(element1));
                    synchronized (element1) {
                        System.out.println("线程b成功占有元素1");
                    }
                }
                System.out.println(set.remove(element2));
                System.out.println(set.remove(element1));
            }
        };

        a.start();
        b.start();
    }
}
//所谓递归函数就是自调用函数,在函数体内直接或间接的调用自己,即函数的嵌套是函数
//本身。 

//递归方式有两种:直接递归和间接递归,直接递归就是在函数中出现调用函数本身。间接
//递归,指函数中调用了其他函数,而该其他函数又调用了本函数。 

//以下代码的意思都是这个意思即调用recursive()和businessLogic()并非一个线程
//(如果是在一个线程中就不存在死锁问题,例如下面的recursive变成private就不存在
//问题。)

[java] view plain copy

public class Test {  
    public void recursive(){  
        this.businessLogic();  
    }  
    public synchronized void businessLogic(){  
        System.out.println("处理业务逻辑");  
    System.out.println("保存到<a href="http://lib.csdn.net/base/mysql" class='replace_word' title="MySQL知识库" target='_blank' style='color:#df3434; font-weight:bold;'>数据库</a>");  
        this.recursive();  
    }  
}  
//以上这段代码就是个能形成死锁的代码,事实上这个“synchronized”放
//在“businessLogic()”和“recursive()”都会形成死锁,并且是多线程的情况下就会锁住!

//解决这个问题的方法就是避免在递归链上加锁,请看以下的例子
public class Test {  
    public void recursive(){  
        this.businessLogic();  
    }  
    public  void businessLogic(){  
        System.out.println("处理业务逻辑");  
        this.saveToDB();  
        this.recursive();  
    }  
    public synchronized void saveToDB(){  
        System.out.println("保存到数据库");  
    }  
}  



//记住:递归加锁需要慎重,一般来说,递归不要加锁,即使加锁也要保证最小粒度。以减小死锁的概率。

参考:死锁详解

33. 什么是锁竞争?

###非公平锁便会产生锁竞争,同时这也是并发编程中常见的事情###
一个资源释放之后,多个锁同时争抢该资源,会导致cpu压力很大,执行效率变慢。

其实该问题,更好的思路应该是锁优化,因为锁竞争很难避免,只要是非公平锁。

参考:
高并发下减少锁竞争
锁优化(5种方法)
锁不慢;锁竞争慢

34. 简述java内存模型

首先:
java内存模型和jvm内存模型一定要区分开,这是两种截然不同的内存模型。

背景:
cpu的运算速度远超内存,因此程序的整体效率往往被内存的运行速度所拖累

应对措施:
为了不被内存影响性能,cpu厂商给cpu加了一级二级甚至三级高速缓存,cpu读取数据时
先从一级缓存中找,没有的话找二级,再没有就找三级或者主存;java内存模型规定了变
量都是存储在主存中,程序运行时操作的是高速缓存中的数据,操作完之后再同步到主存

后遗症:
在cpu和主存之间增加了一个高速缓存固然提升了程序运行效率,单线程很happy,但是在
多线程并发操作同一个变量时,就可能造成数据不一致的问题。

再度规范:
所以就有了JMM(java内存模型,也可叫JMM规范),它保证了并发编程中数据的正确性
(正确性可细分为原子性、可见性、有序性),底层具体实现方式比较复杂,好在JMM为
我们日常编程提供了一些用于保证数据正确性的关键字,如synchronized(原子性、有序
性)、volatile(可见性、有序性);

35. 什么是泛型擦除

首先:
泛型只会存在于编码过程中,也就是程序员编写源码时会有泛型的概念,编译时是没有泛
型的概念的,也就是说源码编译时会出现泛型擦除。

作用:
1)编码期可以让我们更快地发现错误,比如你把User放进了ArrayList< Dog >中,编译器
立马会报错;并且大大增加了编码的灵活性,可以先不定义类。
2)编译期避免类型检查,从而避免在运行时抛出 classCastException。
ArrayList<User> users = new ArrayList<>();
ArrayList<Dog> dogs = new ArrayList<>();
System.out.println(users.getClass() == dogs.getClass());


//输出
//true

如上,运行结果表面两个list是相等的,因为经过编译后,泛型被擦除,两个list当然也就相等;

36. 什么是CAS操作?什么ABA问题?如何解决?

CAS全称compare and swap(比较并交换),作用是保证原子性

CAS操作包含三个操作数 —— 内存位置、预期原值、新值。 如果内存位置的值和预期原
值相等,就把该值更新为新值,如果不相等,则什么都不做;

ABA问题:CAS操作存在的一个并发问题,打个比方,有两个线程A、B同时操作变量x,A
读取到的预期原值是1,此时线程B先将x设置为2,再设置为1,等线程A再来操作的时候,
x变量的预期原值和当前值相等,但是x在整个过程中的值是发生过变化的,这在某些业务
场景下是不允许的;

解决:利用版本号,给变量x增加版本号,每次操作增加对本版好的判断和修改;

37. 什么是可重入锁?实现原理?

可重入锁:
不是一种锁,它是锁的一种性质,代表该锁是否可重入,可重入意思就是“任意线程在获取
到锁后能够再次获取该锁,不会被锁阻塞”,这个锁知道它属于谁,其它线程来了就会阻塞
等待;

实现原理:
锁的内部维护了线程计数器state和当前持有锁的线程对象,当线程A获取锁资源后,锁会
记录下A线程,并且state+1,此时如果有其它线程来获取锁,会被封装成node节点插到队
列尾部并且阻塞;而线程A再来获取锁资源时,会成功拿到锁,并且state+1;当线程退出
同步代码块时,state-1,如果计数器为0,则释放锁;

举例说明:
可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如
一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就
是可重入锁(因为这个原因可重入锁也叫做递归锁)。

Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现
类,包括synchronized关键字锁都是可重入的。如果你需要不可重入锁,只能自己去实现
了。网上不可重入锁的实现真的很多,就不在这里贴代码了。99%的业务场景用可重入锁
就可以了,剩下的1%是什么呢?我特喵也不晓得。

参考:通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其Java实现!

38. String有没有长度限制?

编译器2 ^ 16 - 1(65535)
运行期2 ^ 32 - 1(2147483647)

不管是编译器还是运行期,如果超出长度都会抛异常:常量字符串过长

39. 字符串压缩

字符串的压缩有两种压缩方式

1)只统计字符出现次数,比如aabcccccaaa,压缩成a5b1c5,利用hashMap键的唯一
性;
2)统计相邻字符串出现次数,比如aabcccccaaa,压缩成a2b1c5a3,需要维护一个当前
对比字符,一个字符出现次数,用第n个字符去和第n+1个字符对比;

参考:Java字符串压缩(两种压缩方式)

40. 为什么finally里的代码一定会执行?

编译器在编译的时候,会把finally里面的代码复制多份,分别放在try和catch内所有能够正
常执行以及异常执行逻辑的出口处,

最直观的就是我们可以在字节码文件里看到很多份finally内部代码;

在这里插入图片描述

41. 分别说下ConcurrentHashMap1.7和1.8的实现?

1.7
基于Segment数组和HashEntry,Segment继承自ReentrantLock,懂了吧,它自然就有
了锁的基本功能;每个Segment数组中都有多个HashEntry,我们的数据都存在HashEntry
里面,每次需要修改数据时,先对HashEntry所在的Segment加锁,其它Segment不受影
响,分段锁就是这么来的;

1.8
整体实现很像HashMap,在它基础上引入了synchronized,和大量的CAS操作,以及大量
的volatile关键字,所以1.8的ConcurrentHashMap中锁的粒度更小;

42. 什么是netty?

netty是一套在java NIO的基础上封装的便于用户开发网络应用程序的api. 应用场景很多,
诸如阿里的消息队列(RocketMQ),分布式rpc(Dubbo)通信层都使用到了netty(dubbo可以
用服务发现自由选择通信层)

参考:
通俗地讲,Netty 能做什么?

43. hashmap什么时候转成链表,红黑树等?

HashMap 是一个利用哈希表原理来存储元素的集合。遇到冲突时,HashMap 是采用的链
地址法来解决,在 JDK1.7 中,HashMap 是由 数组+链表构成的。但是在 JDK1.8 中,
HashMap 是由 数组+链表+红黑树构成,新增了红黑树作为底层数据结构,结构变得复杂
了,但是效率也变的更高效。

在这里插入图片描述

	//序列化和反序列化时,通过该字段进行版本一致性验证
    private static final long serialVersionUID = 362498820763181265L;
    //默认 HashMap 集合初始容量为16(必须是 2 的倍数)
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //集合的最大容量,如果通过带参构造指定的最大容量超过此数,默认还是使用此数
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认的填充因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //当桶(bucket)上的结点数大于这个值时会转成红黑树(JDK1.8新增)
    static final int TREEIFY_THRESHOLD = 8;
    //当桶(bucket)上的节点数小于这个值时会转成链表(JDK1.8新增)
    static final int UNTREEIFY_THRESHOLD = 6;
    /**(JDK1.8新增)
     * 当集合中的容量大于这个值时,表中的桶才能进行树形化 ,否则桶内元素太多时会扩容,
     * 而不是树形化 为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
     */
    static final int MIN_TREEIFY_CAPACITY = 64;
总结:
①、基于JDK1.8的HashMap是由数组+链表+红黑树组成,当链表长度超过 8 时会自动转
换成红黑树,当红黑树节点个数小于 6 时,又会转化成链表。相对于早期版本的 JDK 
HashMap 实现,新增了红黑树作为底层数据结构,在数据量较大且哈希碰撞较多时,能够
极大的增加检索的效率。
②、允许 key 和 value 都为 null。key 重复会被覆盖,value 允许重复。
③、非线程安全
④、无序(遍历HashMap得到元素的顺序不是按照插入的顺序)
⑤、相比于JDK1.7,1.8使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要
么是在原位置,要么是在原位置再移动2次幂的位置。我们在扩充HashMap的时候,不需
要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0
就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。

参考:以下为宝藏博主,建议收藏。
JDK1.8源码(七)——java.util.HashMap 类

44. java如何辨别线程安全与否

学习Java的时候经常会发现有很多名称相似的类,比如HashMap和Hashtable,StringBuffer和StringBuilder
等等,他们的名称相似,功能也有相似的地方;但就是不同,深究就会追溯到线程安全与否的问题,那么究竟怎么
辨别呢?

举个例子:

假设A和B同时去不同ATM上取同一张卡的1000块钱,如果是线程不安全,那么A和B可以同时取到1000块钱(两人
赚大发啦),而如果线程安全呢,就只有一个人能取出来1000块钱。

线程安全是指多个线程在执行同一段代码的时候采用加锁机制,使每次的执行结果和单线程执行的结果都是
一样的,不存在执行结果的二义性。

线程不安全就是不提供加锁机制保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据

是什么决定的线程安全问题呢?

线程安全问题都是由全局变量及静态变量引起的。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个
线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

有哪些可以解决多线程并发访问资源的安全问题呢?

有三种方式:分别是 同步代码块 、同步方法和锁机制(Lock)
(1)同步代码块:
synchronized(同步锁)
{
	 //方法体

}
2)同步方法:给多线程访问的成员方法加上synchronized修饰符
public synchronized  void test(){
     //方法体
}

以上两种该方法都用到了Java语言的关键字synchronized,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。

3)锁机制(Lock)
//Java提供的同步代码块的另一种机制,比synchronized关键字更强大也更加灵活。
//这种机制基于Lock接口及其实现类(例如:ReentrantLock)
//它比synchronized关键字好的地方:
//1、提供了更多的功能。tryLock()方法的实现,这个方法试图获取锁,
//如果锁已经被其他线程占用,它将返回false并继续往下执行代码。
//2、Lock接口允许分离读和写操作,允许多个线程读和只有一个写线程。
//3、具有更好的性能
 public class PrintQueue {
      private final Lock A=new ReentrantLock();
      //...
 }

45. 多线程的创建方式

46. 详解击穿、雪崩、穿透

其实redis的击穿、雪崩、穿透并没有那么简单,任何业务在数据量小、并发量低的时候都没什么大问题,即使代码写的很粗糙,但是随着数据量和并发量的增长,任何小问题都会带来大灾难!
在高并发、高性能的分布式系统中,Redis的地位举足轻重,在应用的过程中,就需要郑重处理缓存带来的击穿、穿透、雪崩的问题。

1.缓存击穿(只有高并发才需要特殊处理,即禁止相同的请求大量打到redis-即加锁)

如果系统中没有并发量,其实缓存击穿并不需要特殊处理,只有高并发情况下,缓存击穿才有了特殊处理的必要!!!!!!
所以缓存击穿对应的就是高并发

缓存中某个key过期了,刚好有大量的并发请求访问。
缓存失效没有读取到数据,然后去数据库中读取数据,引起数据库访问的瞬时压力过大,进而可能造成数据库的宕机。
特征:高并发下,单个key过期了,所有的线程同时将相同的请求打到数据库进行请求,这样就会导致数据库压力过大,且都是重复的请求。
重点:1)redis中单个key过期,2)高并发重复请求,3)mysql中有值

方案一:分布式锁-整个系统查询一次

使用Redisson实现的分布式锁,保证操作的原子性,且看门狗可续失效时间。

步骤:核心就是无数据先拿锁,拿到锁后先查值,没有值再请求redis。

  1. 查询redis缓存数据,如不存在,则尝试获取分布式锁,设置超时时间;
  2. 如没有获得锁,则阻塞当前线程,自旋尝试获取锁;
  3. 如获得锁,则检查数据是否已经被其他线程存入redis缓存中。
    如果redis缓存已有数据,直接返回数据,释放分布式锁;
  4. 如果缓存没有,则查询数据库。
    将数据库查询结果保存到redis缓存中,返回查询结果。

优点:

相同查询只会访问数据库1次,对数据库的压力较小。

缺点:

由于自旋阻塞操作,对应用服务器来说会占用一定内存。

方案二:本地锁-每个服务器查询一次
本地锁:又叫JVM锁。
允许各个应用服务器各查询一次数据库的前提可使用

步骤:类似分布式锁,先查询再拿锁,拿到锁再查询,查询不到再去数据库

  1. 各个应用服务器中,如果判断redis缓存不存在,则尝试本地锁;

  2. 如没有获得锁,则阻塞等待获取锁;

  3. 如获得锁,再次检查redis是否有数据,以免其他线程已经刷过缓存;

  4. 如redis已经存在数据,则直接返回数据,并释放锁;

  5. 如redis依然不存在数据,则查询数据库,将查询结果保存到redis缓存中,返回数据,并释放锁。

优点:

相对于使用分布式锁,降低了服务器内存消耗,且锁效率更高效;
实现的复杂度低,无外部依赖,系统更加稳定。

缺点:

增加了数据库查询次数。相同查询操作,多少台服务器,查询数据库多少次。和请求量无关,对于数据库的资源消耗是可控的。

方案三:多级缓存

一级缓存:本地缓存,JVM缓存,如Guava Cache、EhCache等。
二级缓存:服务器级别缓存,如Redis。

步骤:

  1. 各个应用服务器中,如果判断本地缓存存在,则直接返回数据;

  2. 如本地缓存不存在,则尝试本地锁。如未获得锁,则阻塞等待;

  3. 如获得锁,再次检查本地缓存中是否有数据,以免其他线程已刷过缓存;

  4. 如本地缓存已经存在数据,则直接返回数据,并释放锁;

  5. 如本地缓存依然不存在数据,则查询redis缓存;

  6. 如redis缓存存在,则将数据保存到本地缓存中,同时设置一个随机的过期时间,返回数据,释放锁;

  7. 如redis缓存不存在,则查询数据库,将查询结果保存到redis缓存中,再将数据保存到本地缓存中,设置随机过期时间,返回数据,释放锁。

说明:

本地缓存需要设置一个随机的过期时间,可自行评估,比如10s左右。
因多台应用服务器的本地缓存失效时间是随机值,所以很大程度上可避免同时失效,同时去查数据库的情况。
况且本地缓存,redis缓存都失效的情况可能性很小,所以数据库上相同的查询操作会很少的。

优点:

数据库相同查询的几率很小;
大部分查询结果从本地缓存中获取,更加高效;
使用本地锁,服务器内存消耗更低,且锁效率更高效。

缺点:

功能设计略微复杂。

2. 缓存雪崩(与并发量无关,任何系统都要避免-即禁止大量数据请求同时打到数据库-即错峰请求)

这个其实很简单,就是避免大量的请求同时打到MySQL,但是是因为不同的请求,因此与缓存击穿做了区分,并且严格说算是缓存击穿的进阶;因此处理方式当然也要区分!!!

原因:缓存中大量key同一时间点失效,且有大量的并发请求访问。

缓存失效没有读取到数据,然后全部去数据库中读取数据,引起数据库访问的瞬时压力过大,进而造成数据库雪崩宕机。

特征:1)多个key,2)数据缓存中没有,3)数据库中有。

区别:缓存击穿针对某一key缓存,而缓存雪崩针对大量的key缓存。

真实场景中,雪崩相对来讲更容易发生。

缓存击穿发生的条件较极端,一个key成千上万的并发,直接把数据库拖垮。而雪崩,可能一个key几百的并发,然后大量key同时过期,访问请求同时叠加累积到成千上万,把数据库拖垮。

方案一:随机设置过期时间
分散key失效时间,防止集体失效,减少并发访问到数据库。

步骤:
在原失效时间基础上增加一个随机值,根据业务自行评估,如60s左右。

优点:
简单高效。

缺点:
无法支撑必须指定时间更新数据的业务系统。

方案二:系统上线时提前预热

新系统上线时必然会导致缓存雪崩,因为此时redis中一个key都没有,所以算是一个缓存雪崩的特殊场景,处理方式也简单,就是对一些热点的key值,手动或者脚本的方式预先保存到redis中,避免系统一上线同时请求mysql。

方案三:多级缓存

前面缓存击穿中方案三已详细阐述,这里不再赘述。

3. 缓存穿透

某一个key或者多个key被高并发访问,依次从缓存和数据库中都没有查找到数据。

从而导致了大量请求到达数据库,引起数据库访问的瞬时压力过大,进而可能造成数据库宕机。

利用不存在的key频繁访问,俨然形成了安全漏洞,这时的用户很可能就是攻击者。

重点:1)一个key或者多个key,2)数据缓存中没有,3)数据库中也没有。

方案一:缓存空对象-mysql查询不到值则设置过期时间并附空值,防止短时间频繁查询

数据本身就不存在,缓存中直接将不存在的值定义为空(null或者””)。

步骤:

  1. 从缓存中获取数据,如果不存在,则直接查询数据库;

  2. 如果数据库中也不存在数据,将当前缓存中key对应的value设置为null或者””,并设置过期时间,防止key的数据后期真实存在;

  3. 返回结果。

优点:

实现方式简单。

缺点:

如遇恶意攻击,访问大量数据不存在的key或者某类key传入不同值(如key=“USER:”+id),就会浪费大量缓存空间,可能直接导致redis宕机。

注意:该方案适合key全集数据量较小,且可预测场景的系统,局限性较大,不适合实时场景。

方案二:布隆过滤器

布隆过滤器是一种比较巧妙的概率型数据结构,特点是高效地插入和查询。

相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少。

但是缺点是其返回的结果是概率性的,而不是确切的。

从容器的角度来说:

  • 如果布隆过滤器判断元素在集合中存在,不一定存在;

  • 如果布隆过滤器判断不存在,一定不存在。

从元素的角度来说:

  • 如果元素实际存在,布隆过滤器一定判断存在;

  • 如果元素实际不存在,布隆过滤器可能判断存在。

布隆过滤器事先会将所有的数据哈希到一个足够大的bitmap中,不存在的数据,大部分会被这个bitmap拦截掉(存在一定误判),从而尽可能的减少了对数据库的查询压力。

步骤:

  1. 预先加载数据库中的数据到布隆过滤器中;

  2. 先通过布隆过滤器判断数据是否存在,如果不存在,直接返回;

  3. 如果判断存在,则查询缓存中的数据是否存在,如果存在,直接返回;

  4. 如果判断不存在,则进一步查询数据库,然后返回,结束请求(误判率很低,能够很大程度降低数据库的访问)。

优点:
增加及查询高效;
布隆过滤器比其他数据结构更有空间优势。

缺点:
有误判率,即存在假阳性(False Position);
一般情况下不能从布隆过滤器中删除元素。

4. 总结

缓存击穿、缓存雪崩、缓存穿透是一类问题,都是为了处理大量的请求同时打到mysql导致mysql处理不过来,只是区分类型不同!!!

  • 从请求是否重复区分;
    1是重复的请求,2是非重复的请求,3均可

  • 从请求是否有效区分;
    12是有效,3是无效

也就是说三者对于mysql来说是一种现象,而导致这一种现象的不同原因做了区分,就是击穿和雪崩、穿透。
不同的原因便对应了不同的处理方案,既然是重复请求,那就去重,也就是加锁,便是穿透的处理方案;既然是大量的请求,无法去重,那就分批请求,也就是错峰,便是雪崩的处理方案;既然是无效请求,那就提前甄别,也就是过滤

47. 觉得Optional类怎么样

if(user!=null){
    Address address = user.getAddress();
    if(address!=null){
        String province = address.getProvince();
    }
}

java认为以上写法是比较丑陋的,为了避免上述丑陋的写法,让丑陋的设计变得优雅。JAVA8提供了Optional类来优化这种写法。
Optional可以理解为一个集合,里面只有你的对象,类似于ThreadLocal<>();并且它的用法也有些类似,其实就是先set值,然后再取值,或者做一些判断和转化。
好像代码的本质,无非就是 set、get、transf、judge

  • Optional(T value),empty(),of(T value),ofNullable(T value) :作用类似于set
  • orElse(T other),orElseGet(Supplier other)和orElseThrow(Supplier exceptionSupplier) : 作用类似于judge
  • map(Function mapper)和flatMap(Function> mapper):作用类似于transf
  • isPresent()和ifPresent(Consumer consumer) : 类似于get
// 应用场景1
public String getCity(User user)  throws Exception{
        if(user!=null){
            if(user.getAddress()!=null){
                Address address = user.getAddress();
                if(address.getCity()!=null){
                    return address.getCity();
                }
            }
        }
        throw new Excpetion("取值错误");
    }


public String getCity(User user) throws Exception{
    return Optional.ofNullable(user)
                   .map(u-> u.getAddress())
                   .map(a->a.getCity())
                   .orElseThrow(()->new Exception("取指错误"));
}


// 应用场景2
if(user!=null){
    dosomething(user);
}

 Optional.ofNullable(user)
    .ifPresent(u->{
        dosomething(u);
});


// 应用场景3
public User getUser(User user) throws Exception{
    if(user!=null){
        String name = user.getName();
        if("zhangsan".equals(name)){
            return user;
        }
    }else{
        user = new User();
        user.setName("zhangsan");
        return user;
    }
}

public User getUser(User user) {
    return Optional.ofNullable(user)
                   .filter(u->"zhangsan".equals(u.getName()))
                   .orElseGet(()-> {
                        User user1 = new User();
                        user1.setName("zhangsan");
                        return user1;
                   });
}

评价:Optional大体属于链式编程,虽然代码优雅了。但是,逻辑性没那么明显,可读性有所降低,大家项目中看情况酌情使用。

48. isEmpty和isBlank的用法区别

  • StringUtils.isEmpty()
    是否为空。可以看到 " " 空格是会绕过这种空判断,因为是一个空格,并不是严格的空值,会导致 isEmpty(" ")=false

    StringUtils.isEmpty(null) = true
    StringUtils.isEmpty("") = true
    StringUtils.isEmpty(" ") = false
    StringUtils.isEmpty(“bob”) = false
    StringUtils.isEmpty(" bob ") = false
    
  • StringUtils.isBlank()
    是否为真空值(空格或者空值)

    StringUtils.isBlank(null) = true
    StringUtils.isBlank("") = true
    StringUtils.isBlank(" ") = true
    StringUtils.isBlank(“bob”) = false
    StringUtils.isBlank(" bob ") = false
    

评价:统一使用isBlank

49. 使用大对象时需要注意什么

一个根据客户号查询客户有多少订单的内部使用接口,接口的返回是 List<订单>,看起来没啥毛病,对不对?

一般来说一个个人客户就几十上百,多一点的上千,顶天了的上万个订单,一次性拿出来也不是不可以。

但是有一个客户不知道咋回事,特别钟爱我们的平台,也是我们平台的老客户了,一个人居然有接近 10w 的订单。

然后这么多订单对象搞到到项目里面,本来响应就有点慢,上游再发起几次重试,直接触发 Full gc,降低了服务响应时间。

所以,经过这个事件,我们定了一个规矩:用 List、Map 来作为返回对象的时候,必须要考虑一下极端情况下会返回多少数据回去。即使是内部使用,也最好是进行分页查询。

list、map、json、等等可能很大的对象,使用时一定要限制size。

50. 内存溢出和内存泄漏的区别及详解

  • 内存溢出(Out Of Memory)
    是程序在申请内存时,没有足够的内存空间供其使用。比如:你需要10M的空间,内存空间只剩8M,这就会出现内存溢出。
    以栈举例:栈满时在做进栈必定产生空间溢出,叫上溢,栈空时在做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出。

  • 内存泄漏 (Memory Leak)
    是程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重。memory leak最终会导致out of memory。
    这块内存不释放,就不能再用了,就叫这块内存泄漏了。

  • 内存泄漏分类

    1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
    2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
    3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
    4. 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

    从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。
    真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较常发性和偶发性内存泄漏它更难被检测到。

  • 内存溢出的原因及解决方案
    修改JVM启动参数,直接增加内存。
    检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。
    对代码进行走查和分析,找出可能发生内存溢出的位置。
    使用内存查看工具动态查看内存使用情况。

  • 出现内存溢出和内存泄露,重点排查几点

    1. 检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条以上记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询,查多少字段用多少字段。
    2. 检查代码中是否有死循环或递归调用。
    3. 检查是否有大循环重复产生新对象实体。
    4. 检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。使用list.clear()或者map.clear()

51. 拆箱装箱原理

  • 装箱:将基本类型用它们对应的引用类型包装起来;
  • 拆箱:将包装类型转换为基本数据类型;

源码

Integer i = 10;  //装箱
int n = i;   //拆箱

字节码

   L1

    LINENUMBER 8 L1

    ALOAD 0

    BIPUSH 10

    INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;

    PUTFIELD AutoBoxTest.i : Ljava/lang/Integer;

   L2

    LINENUMBER 9 L2

    ALOAD 0

    ALOAD 0

    GETFIELD AutoBoxTest.i : Ljava/lang/Integer;

    INVOKEVIRTUAL java/lang/Integer.intValue ()I

    PUTFIELD AutoBoxTest.n : I

    RETURN

从字节码中可以看到,装箱其实就是调用了包装类的valueOf()方法,拆箱其实就是调用了包装类的 xxxValue()方法

  • Integer i = 10 等价于 Integer i = Integer.valueOf(10)
  • int n = i 等价于 int n = i.intValue();

但是:如果频繁拆装箱的话,也会严重影响系统的性能。平时应该尽量避免不必要的拆装箱操作。

52. 为什么浮点数运算的时候会有精度丢失的风险?

float a = 2.0f - 1.9f;
float b = 1.8f - 1.7f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999905
System.out.println(a == b);// false

因为计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。

比如说十进制下的 0.2 就没办法精确转换成二进制小数:

// 0.2 转换为二进制数的过程为,不断乘以 2,直到不存在小数为止,
// 在这个计算过程中,得到的整数部分从上到下排列就是二进制的结果。
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0(发生循环)
...

平时代码开发中,需要使用BigDecimal 代替浮点数的使用

53. 静态方法为什么不能调用非静态成员?

java中的绝大部分规则都是取决于JVM的规范

  • 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
  • 在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。

静态方法的调用:类名.方法名
示例方法的调用:实例.方法名

所以,虽然静态方法的调用也可以通过“实例.方法名”,这种方式非常容易造成混淆,静态方法不属于类的某个对象而是属于这个类。

54. 什么是可变长参数?

单纯可变长参数不难理解,只是遇到方法重载的情况怎么办呢?会优先匹配固定参数还是可变参数的方法呢?

答案是会优先匹配固定参数的方法,因为固定参数的方法匹配度更高。

public class VariableLengthArgument {

    public static void printVariable(String... args) {
        for (String s : args) {
            System.out.println(s);
        }
    }

    public static void printVariable(String arg1, String arg2) {
        System.out.println(arg1 + arg2);
    }

    public static void main(String[] args) {
        printVariable("a", "b");
        printVariable("a", "b", "c", "d");
    }
}

输出
ab
a
b
c
d

另外,Java 的可变参数编译后实际会被转换成一个数组,我们看编译后生成的 class文件就可以看出来了。

public class VariableLengthArgument {

    public static void printVariable(String... args) {
        String[] var1 = args;
        int var2 = args.length;

        for(int var3 = 0; var3 < var2; ++var3) {
            String s = var1[var3];
            System.out.println(s);
        }

    }
    // ......
}

55. 拷贝有几种

拷贝有三种:引用拷贝、浅拷贝、深拷贝

  • 引用拷贝:只拷贝栈上的地址
  • 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
  • 深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
// 浅拷贝
public class Address implements Cloneable{
    private String name;
    // 省略构造函数、Getter&Setter方法
    @Override
    public Address clone() {
        try {
            return (Address) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

public class Person implements Cloneable {
    private Address address;
    // 省略构造函数、Getter&Setter方法
    @Override
    public Person clone() {
        try {
            Person person = (Person) super.clone();
            return person;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

//测试
Person person1 = new Person(new Address("武汉"));
Person person1Copy = person1.clone();
// true
System.out.println(person1.getAddress() == person1Copy.getAddress());
// 深拷贝
@Override
public Person clone() {
    try {
        Person person = (Person) super.clone();
        person.setAddress(person.getAddress().clone());
        return person;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

// 测试
Person person1 = new Person(new Address("武汉"));
Person person1Copy = person1.clone();
// false
System.out.println(person1.getAddress() == person1Copy.getAddress());

在这里插入图片描述

56. 字符串拼接用“+” 还是 StringBuilder?

Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符。

String str1 = "he";
String str2 = "llo";
String str3 = "world";
String str4 = str1 + str2 + str3;

在这里插入图片描述可以看出,字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象。

不过,使用 “+” 进行字符串拼接会产生大量的临时对象的问题在 JDK9 中得到了解决。在 JDK9 当中,字符串相加 “+” 改为了用动态方法 makeConcatWithConstants() 来实现,而不是大量的 StringBuilder 了。这个改进是 JDK9 的 JEP 280open in new window 提出的,这也意味着 JDK 9 之后,你可以放心使用“+” 进行字符串拼接了。

57. try-catch-finally 如何使用

不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。

58. finally 中的代码一定会执行吗?

不一定的!在某些情况下,finally 中的代码不会被执行。
就比如说 finally 之前虚拟机被终止运行的话,finally 中的代码就不会被执行。

try {
    System.out.println("Try to do something");
    throw new RuntimeException("RuntimeException");
} catch (Exception e) {
    System.out.println("Catch Exception -> " + e.getMessage());
    // 终止当前正在运行的Java虚拟机
    System.exit(1);
} finally {
    System.out.println("Finally");
}

// 输出
Try to do something
Catch Exception -> RuntimeException

另外,在以下 2 种特殊情况下,finally 块的代码也不会被执行:

  • 程序所在的线程死亡。
  • 关闭 CPU。

59. 何时使用 try-with-resources 代替try-catch-finally?

面对必须要关闭的资源,我们总是应该优先使用 try-with-resources 而不是try-finally。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。try-with-resources语句让我们更容易编写必须要关闭的资源的代码,若采用try-finally则几乎做不到这点。

Java 中类似于InputStream、OutputStream、Scanner、PrintWriter等的资源都需要我们调用close()方法来手动关闭,一般情况下我们都是通过try-catch-finally语句来实现这个需求。

//读取文本文件的内容
//  Java 7之前,只能使用try-catch-finally
Scanner scanner = null;
try {
    scanner = new Scanner(new File("D://read.txt"));
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
} finally {
    if (scanner != null) {
        scanner.close();
    }
}
// 使用 Java 7 之后的 try-with-resources 语句改造上面的代码:
try (Scanner scanner = new Scanner(new File("test.txt"))) {
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException fnfe) {
    fnfe.printStackTrace();
}

当有多个资源需要关闭的时候可以用分号分隔,其实就相当于try()里面包裹一段正常的java代码。

try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
     BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
    int b;
    while ((b = bin.read()) != -1) {
        bout.write(b);
    }
}
catch (IOException e) {
    e.printStackTrace();
}

60. SPI 和 API 有什么区别?

在这里插入图片描述
一般模块之间都是通过接口进行通讯,那我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。

  • 当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。
  • 当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。

举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。

SPI 的优点:

  • 通过 SPI 机制能够大大地提高接口设计的灵活性

SPI 的缺点:

  • 需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。
  • 当多个 ServiceLoader 同时 load 时,会有并发问题。

5. 设计模式

1. 工厂模式的使用场景

23种设计模式分为三大类,创建型、结构型、行为型、工厂模式和单例模式、创建者模式
等,同属于创建型。主要用于创建对象。

比方说造一辆车
如果不使用工厂模式,我需要造宝马的时候,就写一个造宝马的方法,需要造奔驰的时候
就写一个造奔驰的方法,缺陷很明显,后期不易维护、代码冗余、不符合面向对象的思
想;
使用工厂模式时,我只需要写一个造汽车的方法,在内部做一些判断,如果参数是宝马我
就造宝马,如果是奔驰我就造奔驰,这样后期如果造汽车,只需要调用它的方法传入参数
就行;
其实设计模式的思想就两点,通过抽象类、接口、多态、实现类的交互使用,使开发的程
序易维护、易扩展、更简洁、23中设计模式是大家提炼出的通用设计模式,我相信,只要
是优秀的开发思想都可以凝练为设计模式。
工厂模式可以细分成简单工厂、工厂、抽象工厂,第一个是一种类型的工厂,第二个是多
种类型的工厂,第三个是产品簇。
简单工厂中包含工厂、产品和具体产品三个角色。其中工厂是整个模式的核心,这个类当
中包含有必要的判断逻辑,可以决定在什么时候创建哪一个产品类的对象。而客户端则可
以免除直接创建产品对象的责任。从这个角度上说简单工厂实现了对责任的分割。另外,
由于客户端所使用的对象都由工厂生成,并统一转型为产品类型,所以客户端无需关心自
己得到的是哪一个具体产品,这样在添加新的具体产品时,就不需要修改客户端的代码,
从某种程度上也实现了开放-封闭法则。但是每当需要添加新的具体产品时,就需要修改工
厂类。 工厂方法使用了面向对象的多态性,保留了简单工厂的优点,而且克服了它的缺
点。首先,在工厂模式当中,核心的工厂类不再负责所有产品的创建,而是将具体的产品
创建交给子类去做。这个核心类成了抽象工厂角色,仅仅负责给出具体工厂子类必须实现
的接口。这种进一步抽象的结果是,可以允许系统在不修改具体工厂角色的情况下引进新
的产品。 

在现实的开发当中,典型的例子就是在servcie层中需要得到DAO对象,通常就会抽象出
DAO接口作为产品,接口的实现类作为具体产品,然后提供工厂供service使用。这样就
可以分离service和dao的耦合,当DAO添加新的实现类是,service不需要修改。提升系
统的扩展性和维护性。

2. 简述装饰者模式和适配器模式

装饰者模式:
在不改变对象结构的情况下,动态的给对象增添一下额外的行为(或者功
能),属于结构型模式,mybatis-plus的QueryWrapper就是装饰者模式的体现,平时生
活中常见的奶茶,它的设计就是很好的装饰模式,基本茶种,加珍珠、仙草、布丁等、
后者就是装饰品。Wrapper:包装、装饰。

适配器模式:
将一个接口转换成用户需要的另一个接口,使接口不兼容的那些类可以一起
工作,也属于结构型模式,SpringMvc中的HandlerAdapter就是适配器模式的体现,一般
来说Adapter结尾的都是适配器模式。adapter:适配器

3. 实现单例设计模式(懒汉、饿汉)

//懒汉,顾名思义比较懒,在用的时候才实例化
public class Singleton {
	//创建实例,注意,此时没有new
	private static volatile Singleton instance = null;
	//构造方法私有化,无法在外部获取实例,只能通过下方的公有静态方法
	private Singleton() {}
	//公有的静态方法,返回实例对象
	public static Singleton getInstance() {
		//先看下是否存在实例,有的话就不再new了
		if (instance == null) {
			synchronized(Singleton.class){
				if(instance == null){
					//这里才new
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

//饿汉,顾名思义很饥饿,创建对象的时候就直接new
public class Singleton {
	//创建实例的时候就new
	private static Singleton instance = new Singleton();
	// 私有化构造方法,外部不能new
	private Singleton() {}
	//公有的静态方法,返回实例对象
	public static Singleton getInstance() {
		//直接将事先new好的实例返回
		return instance;
	}
}

4. 手写生产者消费者模型

关键就是volatile关键字。

//仓库:
public class Warehouse {
    private volatile int apple = 0;
    public synchronized void inCrease() {
        while (apple == 5) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        apple++;
        System.out.println("苹果生产成功!");
        notify();
    }
    public synchronized void deCrease() {
        while (apple == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        apple--;
        System.out.println("苹果消费成功!");
        notify();
    }
    public static void main(String[] args) {
        Warehouse warehouse = new Warehouse();
        Consumer con = new Consumer(warehouse);
        Producer pro = new Producer(warehouse);
        Thread t1 = new Thread(con);
        Thread t2 = new Thread(pro);
        t1.start();
        t2.start();
    }
}

//生产者
public class Producer implements Runnable {
    private Warehouse warehouse;
    public Producer(Warehouse warehouse) {
        this.warehouse = warehouse;
    }
    @Override
    public void run() {
        for( int i=0;i<10;i++)
        {
            try {
                System. out .println("pro  i:" +i);
                Thread. sleep(30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            warehouse.inCrease();
        }
    }
}

//消费者
public class Consumer implements Runnable {
    private Warehouse warehouse;
    public Consumer(Warehouse warehouse) {
        this.warehouse = warehouse;
    }
    @Override
    public void run() {
        for( int i=0;i<10;i++)
        {
            try {
                System. out .println("Con: i " +i);
                // 这里设置跟上面30不同是为了 仓库中的苹果能够增加,不会生产一个马上被消费
                Thread. sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            warehouse.deCrease();
        }
    }
}

6. Mysql

1. mysql深度分页

分页大家都懂,如果某天数据库量达到100万条,而你需要的数据恰好在最后10条,常规的分页就会变得特别慢
此时就要用些技巧,思路就是先查id后查记录,其实就是避免查询过多的 select *,这样性能最低,mysql的常规优化,走索引、避免 select * 等。

//常规分页
SELECT * FROM table_name limit 7000,10  //0.868s

//先查id ,写法很多,看个人习惯
SELECT * FROM table_name a,(SELECT id FROM table_name limit 7000,10) b WHERE a.id = b.id  //0.468

//如果你的表有自增id,就这么写,效率直接起飞,真的是项目经理看了感动,架构师看了落泪
SELECT * FROM table_name WHERE id>7000 LIMIT 10  //0.372

数据量越多,深度分页的优势就越明显。

2.说说数据库设计三范式?

第一范式:原子性
每一个字段都必须是不可再分割的最小级别,比如说个人信息,显然就违背了该范式。
第二范式:唯一性
数据表中的每一行都需要有一个字段来唯一标示,以区别于其他行的数据,比如id字段
第三范式:冗余性
任何字段不能由其他字段派生出来;主键没有直接关系的数据列必须消除,消除的办法
就是再创建一个表来存放他们,当然外键除外;

注意:
并不是非得严格按照三范式来设计,好的数据库设计一定不是这样的,而是根据实际情况
柔性处理;

3. 事务的ACID是指什么?

原子性:事务中无论那个环节出现问题,都会使整个事务回滚,也就是说,要么完全成
功,要么完全失败,真个事务是一个整体,不可分割
一致性:事务完成后整体的状态保持不变,比如银行转账,总额不能有变化
隔离性:并发的事务不能相互影响,线程隔离
持久性:事务完成之后所有的数据都会持久化,即使遇到灾难性的破坏,也可以通过日志
和备份将数据还原。

4. 事务隔离级别

事务的隔离级别主要是为了解决事务操作中容易遇见的一些问题,主要就是三个问题
1)脏读:正在执行的事务读了其他未提交的事务中的数据
2)不可重复读:事务在可读范围内多次读取数据是发现数据前后不一致,这种情况一般是
在事务的可读范围内另一个事务修改了数据并提交了。
3)虚读(幻读):事务前后读取到的数据数量不一致,有时多有时少,这种情况主要是其
他事务在这个过程中对数据进行了增加或者删除。

隔离级别:
1)读未提交:就是可以读取到未提交的事务中数据,最低隔离级别,什么问题也解决不了
2)读已提交:只能读取到已提交的事务数据,因此可以杜绝脏读,但其他两种无法解决
3)可重复读:在事务的刻度范围内将读出的数据加锁,此时其他事务也不能修改,除虚
读都可以避免,类似"select * from XXX for update",明确数据读取出来就是为了
更新用的,所以要加一把锁,防止别人修改它
4)串行化:相当于阻塞执行事务,一个事务必须等另一个事务执行完再执行,因此可以
杜绝所有的问题,隔离级别最高。

5. 常见的mysql的存储引擎?

数据库管理系统(DBMS)使用数据引擎进行表的创建销毁和数据的增删改查。不同的存
储引擎提供不同的存储机制、索引技巧、锁定水平等功能,使用不同的存储引擎,还可以 
获得特定的功能。现在许多不同的数据库管理系统都支持多种不同的数据引擎。MySql的
核心就是存储引擎。

mysql的存储引擎有很多,可以使用“show engines”命令查询所有引擎

mysql的存储引擎是插入式的,也就是说可以任意改变,为不同的表选择不同的存储引擎

常见的有:
1)myisam:最早出现的,非事务型存储引擎,不支持事务,行级锁和外键约束的功能。
细分静态、动态、压缩三种,区别就是所占空间的大小,静态的所有字段长度相同,因此
存储、修改比较快;动态就是字段长度不一,省空间,但是慢;压缩就是对前两者的压
缩,空间更省,但是无法表无法再修改。
使用场景:插入数据快,空间和内存使用比较低。如果表主要是用于插入新记录和读出记
录,那么选择MyISAM能实现处理高效率。如果应用的完整性、并发性要求比较低,也可以
使用。
2)InnoDB:现在数据库默认的引擎,也事务型数据库的首选引擎,支持事务安全表
(ACID),支持行锁定和外键,
使用场景:处理巨大数据量时可以使用,有基于内存的缓存机制,CPU效率比较高,另外
如果要提供提交、回滚、崩溃恢复能力的事物安全(ACID兼容)能力,并要求实现并发控
制,InnoDB是一个好的选择
3)memory(heap):这种类型的数据表只存在于内存中。它使用散列索引,所以数据的存
取速度非常快。因为是存在于内存中,所以这种类型常应用于临时表中。
使用场景:临时存放数据,数据量不大,并且不需要较高的数据安全性,常为临时表。
4)archive:这种类型只支持select 和 insert语句,而且不支持索引。
使用场景:常应用于日志记录和聚合分析方面。
5)MyISAM Merge:MyISAM类型的一种变种。合并表是将几个相同的MyISAM表合并为一
个虚表。
使用场景:常应用于日志和数据仓库。

6. 数据库查询慢的常见优化手段?

分为三步:
1)首先开启慢查询分析语句,这种情况下一般就是改sql和加索引,
2)然后考虑存储引擎。
3)架构层面


1)存储引擎考虑:
mysql默认的事务型存储引擎,innodb,插入和更新比较快,但是如果一个表插入数据
以后很少修改,大都是查询和计数,那么myisam会更好。适合自己的才是最好的。
2)sql语句考虑:
a. 不要使用select *,这种查询语句会查询整个表的所有数据,很耗费性能,只查询自己想
要的数据即可。
b. 注意索引失效的情况,不要在索引列上进行如下操作:not null/null、计算、模糊查询、
不等于、函数等操作,总之,索引就是为了精确查询。
c. 不要使用IN,因为IN会对子句中的表进行排序和合并,很耗性能,使用exists代替in,使
用not exists代替 not in。in的效率永远都是最低的。
d. 排序的时候尽量用升序,性能略高
e. 尽量不要用order by / group by 字段,包括在索引当中减少排序,效率会更高。
3)加查询索引:
一般来说,适当的加索引也会提升效率,但不能过多,最多5个吧,维护索引也是性能开销
4)架构层面:
分区、分表、分库和读写分离
分区:就是加一个字段进行逻辑分区,比如年级、日期等,实际还是一张表
分表:一张表变成多个表,大表变小表,提高并发能力,一般利用hash、比较复杂
分库:分表和分区都是基于同一个数据库里的数据分离技巧,分库其实就是增加服务器,
横向扩展
读写分离:当数据库读远大于写,查询多的情况,就可以考虑主数据负责写操作,从数据
库负责读操作,一主多重,从而把数据读写分离,最后还可以结合redis等缓存来配合分担
数据的读操作,大大的降低后端数据库的压力。

7. mysql默认的事务隔离级别是什么

MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)。我
们可以通过SELECT @@tx_isolation;命令来查看。

需要注意的是:与 SQL 标准不同的地方在于 InnoDB 存储引擎在 REPEATABLE-READ
(可重读) 事务隔离级别下使用的是Next-Key Lock 锁算法,因此可以避免幻读的产生,
这与其他数据库系统(如 SQL Server)是不同的。所以说InnoDB 存储引擎的默认支持的隔
离级别是 REPEATABLE-READ(可重读) 已经可以完全保证事务的隔离性要求,即达到
了SQL标准的 SERIALIZABLE(可串行化) 隔离级别。

因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 READ-
COMMITTED(读取提交内容) ,但是你要知道的是InnoDB 存储引擎默认使用 
REPEAaTABLE-READ(可重读) 并不会有任何性能损失。

InnoDB 存储引擎在 分布式事务 的情况下一般会用到 SERIALIZABLE(可串行化) 隔离级
别。

参考:mysql默认隔离级别

8. 什么是回表查询,如何避免?

定义:
mysql5.6以后默认的innodb存储引擎,该引擎的非主键索引文件和数据文件是分开存储的
叫做非聚簇索引,大概结构是,主键索引和数据文件使用B+树的数据结构保存在一起,而
非主键索引是另外一个文件,跟主键索引绑定,当以非主键索引进行查询时,需要先在一
个文件中找到id,再去另一个文件找到数据文件。多查一个表,这种现象叫回表查询。

解决:
索引覆盖,将查询sql中的字段添加到联合索引里面,只要保证查询语句里面的字段都在索
引文件中,就无需进行回表查询。

参考:mysql索引总结

9. mysql如何实现可重复读?

基于mvcc(Multi-Version Concurrency Control,多版本并发控制)

mysql默认隔离级别就是可重复读,这个隔离级别解决了不可重复读和脏读,所谓不可重复
读就是在一个事务内多次查询的结果不一样,其原因就是期间数据被另一个事务修改了;
脏读就是一个事务读取到了另一个事务未提交的数据,然而该数据回滚了,实际上并未提
交;

实现原理前置知识:
InnoDB是通过维护两个隐藏列来实现mvcc,隐藏列记录了数据行创建版本号和删除版本
号,每开始一个事务,版本号就会递增;事务开始时刻的版本号就是事务的版本号,用来
和查询到的数据行的版本号进行比较;

mvcc在可重复读级别下的具体实现:
查询:需满足两个条件,1、创建版本号小于或等于当前事务版本,这样可以确保查出来的
数据都是在本次事务开始前就已经存在,或者由本次事务创建或修改的;2、删除版本号大
于当前事务版本号或者未定义(未定义即未删除),这样可以保证在此次事务之前就存在
的行没有被删除;
插入:插入的每条数据都需要将创建版本号保存为当前事务版本号;
删除:删除的每条数据都需要将删除版本号保存为当前事务版本号;
修改:插入一条新数据,将创建版本号保存为当前事务版本号,并将原数据的删除版本号
保存为当前事务版本号;

10. 什么是mysql最左匹配原则?

比如有个表建了a、b、c三个字段的联合索引,那么哪些情况下会触发索引呢?

只要查询条件中有a字段就会走索引,不管是bac、cba、ac、ca,都可以走索引,想不到
吧,因为mysql有个sql优化器,会自己去优化顺序,但是如果你是bc或者b就不行

11. mysql读写分离和主从复制的原理?

应用场景:
1、在业务复杂的系统中,有这么一个情景,有一句sql语句需要锁表,导致暂时不能使用
读的服务,那么就很影响运行中的业务,使用主从复制,让主库负责写,从库负责读,
这样,即使主库出现了锁表的情景,通过读从库也可以保证业务的正常运作。
2、做数据的热备
3、架构的扩展。业务量越来越大,I/O访问频率过高,单机无法满足,此时做多库的存
储,降低磁盘I/O访问的频率,提高单个机器的I/O性能。	

首先:
读写分离依赖于主从复制的架构,而主从复制又离不开读写分离,所以两者其实是一个整
体。

实现原理:
mysql数据库的读写分离和主从复制,主要是通过binary_log、I/O线程、SQL线程、
relay_log、master_info、来实现的,原理并不复杂,但是自己一直有一个疑问,从
数据库终究要执行主库执行的所有sql,还要应对查询,那么效率高在哪里?

详细步骤:
master与slave之间实现整个复制过程主要由三个线程完成:两个(SQL线程和IO线程)在
slave端,一个(IO线程)在master端。slave端的IO线程负责读取master的binlog内容到
中继日志relay log里,并维持索引到master_info文件;slave端的SQL线程负责从relay log
日志里读出binlog内容,并更新到slave的数据库里。

(1)、master服务器将数据的改变记录二进制binlog日志,当master上的数据发生改变时,
则将其改变写入二进制日志中;
(2)、slave服务器会在一定时间间隔内对master二进制日志进行探测其是否发生改变,如
果发生改变,则slave上的I/O线程连接master,请求从指定日志文件的指定位置(或者从
最开始)之后的日志内容。
(3)、master收到请求,负责复制的I/O线程根据请求信息读取指定的日志,并返回(日志
文件的地址也返回,方便下次直接根据地址请求)。
(4)、slave的I/O线程收到信息后,将日志内容依次写入到slave端的relay log(中继文件)
的最末端,存master日志文件的地址。
(5)、slave的SQL线程检测到relay-log中新加内容后,马上将该log文件的内容解析成sql语
句并逐一执行,从而能保证两端的数据是一样的。最后I/O线程和SQL线程将进入睡眠状
态,等待下一次被唤醒。

注意:
1)从库同步主库数据的过程是串行化的,也就是说主库上并行的操作,在从库上会串行执
行。
2)从库从主库拷贝日志以及串行执行 SQL 的特点,在高并发场景下,从库的数据是有延
时的。


主从复制分为:异步复制,半同步复制和全同步复制

异步复制: 是MySQL默认的复制模式,主库在执行完客户端提交的事务之后会立刻将结果
返回给客户端,并不关心从库接收的结果,这样就会导致当主数据库因为某些原因宕机之
后从数据库可能没有同步到主数据库的数据,导致主从数据不一致,甚者如果将从数据库
强制转换为主数据库,可能导致数据丢失
优点:返回客户端无延迟
缺点:主从数据不一致,数据丢失
半同步复制: 半同步复制解决了主从数据库不一致的问题,原理是只有当至少一个从数据
库接收到并且写入到read log 日志中之后才会返回给客户端接收结果,这样带来的问题是
至少会带来一个 TCP/IP的往返时间的延迟
优点:保证主从数据库的最终一致性
缺点:返回客户端有延时
全同步复制: 当所有的从库接收到主数据库的数据并且执行完了其中的事务之后才会给客
户端返回
优点:主从数据库的数据强一致性
缺点:耗费性能

并行复制: 就是在半同步复制的基础上从数据库会在内部有多个SQL线程去将relay log中
的数据写入到数据库中,因为5.6x版本之前一直是单线程的,因此产生主从延迟的几率会
很大,现在多线程处理之后,能最大可能的减少主从延迟的几率
5.6X版本之前SQL线程是单线程的,IO线程支持多线程操作
5.7X版本之后SQL线程也支持多线程

主从延时问题:
mysql的主从复制都是单线程的操作,主库对所有DDL和DML产生的日志写进binlog,由于
binlog是顺序写,所以效率很高,slave的sql thread线程将主库的DDL和DML操作事件在
slave中重放。DML和DDL的IO操作是随机的,不是顺序,所以成本要高很多,另一方面,
由于sql thread也是单线程的,当主库的并发较高时,产生的DML数量超过slave的SQL 
thread所能处理的速度,或者当slave中有大型query语句产生了锁等待,那么延时就产生
了。
MySQL 有主从同步的状态信息,可以通过MySQL命令 show slave status获取,除了获知
当前是否主从同步正常工作,另外一个重要指标就是 Seconds_Behind_Master,根据输出
的Seconds_Behind_Master参数的值来判断:NULL,表示io_thread或是sql_thread有任
何一个发生故障;0,该值为零,表示主从复制良好;正值,表示主从已经出现延时,数字
越大表示从库延迟越严重。

如何解决主从延时?:
实际上主从同步延迟根本没有什么一招制敌的办法,因为所有的SQL必须都要在从服务器
里面执行一遍,但是主服务器如果不断的有更新操作源源不断的写入, 那么一旦有延迟产
生,那么延迟加重的可能性就会原来越大。 当然我们可以做一些缓解的措施。
1)分库,将一个主库拆分为多个主库,(可以是多主一从)这样每个主库的写并发会减
少。
2)单个库读写分离,一主多从,主写从读,分散压力。这样从库压力比主库高,保护主
库。、
3)MySQL 支持的并行复制,多个库并行复制。但要是单库写入并发太高,并行复制并没
有意义。
4)升级Slave硬件配置
5)服务的基础架构在业务和mysql之间加入memcache或者redis的cache层。降低mysql的
读压力。

解惑:读写分离提高性能之原因
1.物理服务器增加,负荷增加
2.主从只负责各自的写和读,极大程度的缓解X锁和S锁争用
3.从库可配置myisam引擎,提升查询性能以及节约系统开销
4.从库同步主库的数据和主库直接写还是有区别的,通过主库发送来的binlog恢复数据,但
是,最重要区别在于主库向从库发送binlog是异步的,从库恢复数据也是异步的
5.读写分离适用与读远大于写的场景,如果只有一台服务器,当select很多时,update和
delete会被这些select访问中的数据堵塞,等待select结束,并发性能不高。 对于写和读比
例相近的应用,应该部署双主相互复制
6.可以在从库启动是增加一些参数来提高其读的性能,例如--skip-innodb、--skip-bdb、--
low-priority-updates以及--delay-key-write=ALL。当然这些设置也是需要根据具体业务需
求来定得,不一定能用上
7.分摊读取。假如我们有1主3从,不考虑上述1中提到的从库单方面设置,假设现在1 分钟
内有10条写入,150条读取。那么,1主3从相当于共计40条写入,而读取总数没变,因此
平均下来每台服务器承担了10条写入和50条读取(主库不 承担读取操作)。因此,虽然写
入没变,但是读取大大分摊了,提高了系统性能。另外,当读取被分摊后,又间接提高了
写入的性能。所以,总体性能提高了,说白 了就是拿机器和带宽换性能。MySQL官方文档
中有相关演算公式:官方文档 见6.9FAQ之“MySQL复制能够何时和多大程度提高系统性
能”
8.MySQL复制另外一大功能是增加冗余,提高可用性,当一台数据库服务器宕机后能通过
调整另外一台从库来以最快的速度恢复服务,因此不能光看性能,也就是说1主1从也是可
以的。



I/O线程实现:
io线程连接到主库,与主库交换一些必要的信息,在主库上注册从库,然后请求拉取
binlog,最后循环调用read_event,将主库不断发送的binlog信息写入到relay log文件中。
整个过程很简单,但是需要考虑的细节有很多。了解io线程的主要流程,我们甚至可以自
己写一个程序,模拟io线程,从主库拉取binlog。当然前提是需要了解MySQL的
client/server的协议,或者使用现成的mysql开发库。
总之,是一个特定的协议。

参考:
Mysql——SQL查询优化解决方案
为什么数据库读写分离可以提高性能
MySQL主从复制从库IO线程源码分析

12. group by和 distinct哪个性能好

先说结论:

  • 在语义相同,有索引的情况下:
    group by和distinct都能使用索引,效率相同。因为group by和distinct近乎等价,distinct可以被看做是特殊的group by。
  • 在语义相同,无索引的情况下:
    distinct效率高于group by。原因是distinct 和 group by都会进行分组操作,但group by在Mysql8.0之前会进行隐式排序,导致触发filesort,sql执行效率低下。而从Mysql8.0开始,Mysql就删除了隐式排序,所以,此时在语义相同,无索引的情况下,group by和distinct的执行效率也是近乎等价的。

二者均可用时建议使用group by: 相比于distinct来说,group by的语义明确。且由于distinct关键字会对所有字段生效,在进行复合业务处理时,group by的使用灵活性更高,group by能根据分组情况,对数据进行更为复杂的处理,例如通过having对数据进行过滤,或通过聚合函数对数据进行运算。

再说过程:

  • distinct的使用
    单列去重时:DISTINCT 关键词用于返回唯一不同的值。放在查询语句中的第一个字段前使用,且作用于主句所有列。如果列具有NULL值,并且对该列使用DISTINCT子句,MySQL将保留一个NULL值,并删除其它的NULL值,因为DISTINCT子句将所有NULL值视为相同的值。
    多列去重时:distinct多列的去重,则是根据指定的去重的列信息来进行,即只有所有指定的列信息都相同,才会被认为是重复的信息。

  • group by的使用
    对于基础去重来说,group by的使用和distinct类似: 两者的语法区别在于,group by可以进行单列去重,group by的原理是先对结果进行分组排序,然后返回每组中的第一条数据。且是根据group by的后接字段进行去重的。

  • distinct和group by原理
    在大多数例子中,DISTINCT可以被看作是特殊的GROUP BY,它们的实现都基于分组操作,且都可以通过松散索引扫描、紧凑索引扫描来实现
    DISTINCT和GROUP BY都是可以使用索引进行扫描搜索的,所以,在一般情况下,对于相同语义的DISTINCT和GROUP BY语句,我们可以对其使用相同的索引优化手段来进行优化。

    但对于GROUP BY来说,在MYSQL8.0之前,GROUP Y默认会依据字段进行隐式排序。filesort就是排序,这个单词出现在explain中时一定要谨慎,这是一个非常耗费性能的操作。

隐形排序:

对于隐式排序,我们可以参考Mysql官方的解释:GROUP BY 默认隐式排序(指在 GROUP BY 列没有 ASC 或 DESC 指示符的情况下也会进行排序)。然而,GROUP BY进行显式或隐式排序已经过时(deprecated)了,要生成给定的排序顺序,请提供 ORDER BY 子句。

所以,在Mysql8.0之前,Group by会默认根据作用字段(Group by的后接字段)对结果进行排序。在能利用索引的情况下,Group by不需要额外进行排序操作;但当无法利用索引排序时,Mysql优化器就不得不选择通过使用临时表然后再排序的方式来实现GROUP BY了。

且当结果集的大小超出系统设置临时表大小时,Mysql会将临时表数据copy到磁盘上面再进行操作,语句的执行效率会变得极低。这也是Mysql选择将此操作(隐式排序)弃用的原因。

基于上述原因,Mysql在8.0时,对此进行了优化更新:从前(Mysql5.7版本之前),Group by会根据确定的条件进行隐式排序。在mysql 8.0中,已经移除了这个功能,所以不再需要通过添加order by null 来禁止隐式排序了,但是,查询结果可能与以前的 MySQL 版本不同。要生成给定顺序的结果,请按通过ORDER BY指定需要进行排序的字段。

7. Redis

1. 简述zset实现原理

Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)
及zset(sorted set:有序集合)。一直纳闷为什么sorted set叫做zset,翻遍全网,没有有
效答案,很奇怪,引用唯一可能的正解:Hello. Z is as in XYZ, so the idea is, 
sets with another dimension: the order. It's a far association...

好了,言归正传,redis是非结构型数据库,所以它都是k-v对,不同的类型意味着v的类型
不同或者数据存储结构不同
String:v是普通的String
Hash:v是string类型的field和value的映射表,相当于java中的对象,相较于将对象的每个字段存成单个 string类型。将一个对象存储在 hash 类型中会占用更少的内存,并且可以更方便的存取整个对象。省内存的原因是新建一个hash对象时开始是用 zipmap(又称为 small hash)来存储的。这个 zipmap其实并不是hash table,但是 zipmap相比正常的hash实现可以节省不少hash 本身需要的一些元数据存储开销。尽管 zipmap的添加,删除,查找都是 O(n),但是由于一般对象的 field数量都不太多。所以使用 zipmap 也是很快的,也就是说添加删除平均还是 O(1)。如果 field 或者 value的大小超出一定限制后, Redis会在内部自动将zipmap替换成正常的 hash实现.  这个限制可以在配置文件中指定 hash-max-zipmap-entries 64 #配置字段最多 64个 hash-max-zipmap-value 512 #配置value 最大为512字节 。
list:v是String类型的列表,双向链表结构,有序
set:v是String类型的无序集合,通过hash table 实现的,所以添加、删除和查找的复杂度都是 O(1)。hash table 会随着添加或者删除自动的调整大小。需要注意的是调整 hash table大小时候需要同步(获取写锁)会阻塞其他读写操作,可能不久后就会改用跳表(skip list)来实现,跳表已经在 sorted set 中使用了。关于 set 集合类型除了基本的添加删除操作,其他有用的操作还包含集合的取并集(union),交集(intersection),差集(difference)。通过这些操作可以很容易的实现 sns中的好友推荐和 blog的tag功能。
zset:v是String类型的有序集合,和set不同的是每个元素都会关联一个 double类型的score,score可以重复,利用score的大小进行排序。sorted set的实现是skip list和hash table 的混合体。当元素被添加到集合中时,一个元素到score 的映射被添加到hash table中,所以给定一个元素获取score 的开销是 O(1),另一个score 到元素的映射被添加到 skip list,并按照score 排序,所以就可以有序的获取集合中的元素。添加,删除操作开销都是 O(log(N))和 skip list的开销一致,redis的 skip list实现用的是双向链表,这样就可以逆序从尾部取元素。sorted set最经常的使用方式应该是作为索引来使用.我们可以把要排序的字段作为 score 存储,对象的 id当元素存储。

回到问题,数据结构真的很复杂,一两句话说不清楚,就是一种数据结构,打个比方
1、2、3、4、5、6、7、8、9
要找到8,我得遍历8次
如果我把1、3、5、7、9拎出来作为一个类似索引的东西
找8的时候,先在索引里面找,8在7和9中间,找到7以后再遍历1次就找到了8,总共遍历5
次
而这个1、3、5、7、9的索引还可以拎个1、5、9出来,以此类推
回到zset,它维护了两个元素,一个是 dict,用来维护数据到分数的关系,一个是
zskiplist,用来维护分数所在链表的关系

2. 什么是redis缓存穿透、缓存雪崩、缓存击穿?如何解决?

缓存击穿:请求了很多缓存中没有但是数据库中有的数据,一般是缓存过期导致的,会导
致请求直接冲到数据库
缓存雪崩:缓存数据大面积失效。
缓存穿透:请求了数据库中没有的数据,既然数据库中都没有,缓存中那就更没有了,也
会导致请求直接冲到数据库。

解决办法如下
缓存击穿:延长热点数据的过期时间,或者直接设置永不过期
缓存雪崩:设置key的过期时间时加上一个随机数,目的就是让key错开失效时间,不要扎
堆失效即可
缓存穿透:该问题就是需要过滤掉非法的key,没有的数据不要冲到数据库,可以用一些过
滤器,比如布隆过滤器。

3. redis的底层数据结构?

4. redis持久化原理?

###六大基本数据###

string 是Redis的最基本的数据类型,可以理解为与 Memcached 一模一样的类型,一个
key 对应一个 value。string 类型是二进制安全的,意思是 Redis 的 string 可以包含任何数
据,比如图片或者序列化的对象,一个 redis 中字符串 value 最多可以是 512M。
使用场景:
一、计数
由于Redis单线程的特点,我们不用考虑并发造成计数不准的问题,通过 incrby 命令,我
们可以正确的得到我们想要的结果。
二、限制次数
比如登录次数校验,错误超过三次5分钟内就不让登录了,每次登录设置key自增一次,并
设置该key的过期时间为5分钟后,每次登录检查一下该key的值来进行限制登录。

hash 是一个键值对集合,是一个 string 类型的 key和 value 的映射表,key 还是key,但
是value是一个键值对(key-value)。类比于 Java里面Map<String,Map<String,Object>> 
集合。
使用场景:
查询的时间复杂度是O(1),用于缓存一些信息。

list 列表,它是简单的字符串列表,按照插入顺序排序,你可以添加一个元素到列表的头部
(左边)或者尾部(右边),它的底层实际上是个链表。有序可重复
使用场景:
一、栈
通过命令 lpush+lpop
二、队列
命令 lpush+rpop
三、有限集合
命令 lpush+ltrim
四、消息队列
命令 lpush+brpop	

Redis 的 set 是 string 类型的无序集合。相对于列表,集合也有两个特点:无序和不可重
复	
使用场景:
利用集合的交并集特性,比如在社交领域,我们可以很方便的求出多个用户的共同好友,
共同感兴趣的领域等。

zset(sorted set 有序集合),和上面的set 数据类型一样,也是 string 类型元素的集合,
但是它是有序的。
使用场景:
和set数据结构一样,zset也可以用于社交领域的相关业务,并且还可以利用zset 的有序特
性,还可以做类似排行榜的业务。

Redis的作者在Redis5.0中,放出一个新的数据结构,Stream。Redis Stream 的内部,其
实也是一个队列,每一个不同的key,对应的是不同的队列,每个队列的元素,也就是消
息,都有一个msgid,并且需要保证msgid是严格递增的。在Stream当中,消息是默认持久
化的,即便是Redis重启,也能够读取到消息。那么,stream是如何做到多播的呢?其实非
常的简单,与其他队列系统相似,Redis对不同的消费者,也有消费者Group这样的概念,
不同的消费组,可以消费同一个消息,对于不同的消费组,都维护一个Idx下标,表示这一
个消费群组消费到了哪里,每次进行消费,都会更新一下这个下标,往后面一位进行偏
移。

数据结构:

Redis 是用 C 语言写的,但是对于Redis的字符串,却不是 C 语言中的字符串(即以空字
符’\0’结尾的字符数组),它是自己构建了一种名为 简单动态字符串(simple dynamic 
string,SDS)的抽象类型,并将 SDS 作为 Redis的默认字符串表示。
struct sdshdr{
 //记录buf数组中已使用字节的数量
 //等于 SDS 保存字符串的长度
 int len;
 //记录 buf 数组中未使用字节的数量
 int free;
 //字节数组,用于保存字符串
 char buf[];
}
C 语言对于字符串的定义,多出了 len 属性以及 free 属性,获取字符串长度以及杜绝缓冲
区溢出以及安全方面都有提升。

Redis链表特性:
①、双端:链表具有前置节点和后置节点的引用,获取这两个节点时间复杂度都为O(1)。
②、无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问都是
以 NULL 结束。  
③、带链表长度计数器:通过 len 属性获取链表长度的时间复杂度为 O(1)。
④、多态:链表节点使用 void* 指针来保存节点值,可以保存各种不同类型的值。

字典又称为符号表或者关联数组、或映射(map),是一种用于保存键值对的抽象数据结
构。字典中的每一个键 key 都是唯一的,通过 key 可以对值来进行查找或修改。C 语言中
没有内置这种数据结构的实现,所以字典依然是 Redis自己构建的。Redis 的字典使用哈
希表作为底层实现
渐近式 rehash:也就是说扩容和收缩操作不是一次性、集中式完成的,而是分多次、渐进
式完成的。如果保存在Redis中的键值对只有几个几十个,那么 rehash 操作可以瞬间完
成,但是如果键值对有几百万,几千万甚至几亿,那么要一次性的进行 rehash,势必会造
成Redis一段时间内不能进行别的操作。所以Redis采用渐进式 rehash,这样在进行渐进式
rehash期间,字典的删除查找更新等操作可能会在两个哈希表上进行,第一个哈希表没有
找到,就会去第二个哈希表上进行查找。但是进行 增加操作,一定是在新的哈希表上进行
的。

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其它节点的指
针,从而达到快速访问节点的目的。具有如下性质:
1、由很多层结构组成;	
2、每一层都是一个有序的链表,排列顺序为由高层到底层,都至少包含两个链表节点,分
别是前面的head节点和后面的nil节点;
3、最底层的链表包含了所有的元素;
4、如果一个元素出现在某一层的链表中,那么在该层之下的链表也全都会出现(上一层的
元素是当前层的元素的子集);
5、链表中的每个节点都包含两个指针,一个指向同一层的下一个链表节点,另一个指向下
一层的同一个链表节点;

在这里插入图片描述

总结:
大多数情况下,Redis使用简单字符串SDS作为字符串的表示,相对于C语言字符串,SDS
具有常数复杂度获取字符串长度,杜绝了缓存区的溢出,减少了修改字符串长度时所需的
内存重分配次数,以及二进制安全能存储各种类型的文件,并且还兼容部分C函数。

通过为链表设置不同类型的特定函数,Redis链表可以保存各种不同类型的值,除了用作列
表键,还在发布与订阅、慢查询、监视器等方面发挥作用(后面会介绍)。

Redis的字典底层使用哈希表实现,每个字典通常有两个哈希表,一个平时使用,另一个用
于rehash时使用,使用链地址法解决哈希冲突。

跳跃表通常是有序集合的底层实现之一,表中的节点按照分值大小进行排序。

整数集合是集合键的底层实现之一,底层由数组构成,升级特性能尽可能的节省内存。

压缩列表是Redis为节省内存而开发的顺序型数据结构,通常作为列表键和哈希键的底层实
现之一。

持久化:
1)RDB
是Redis用来进行持久化的一种方式,是把当前内存中的数据集快照写入磁盘,也
就是 Snapshot 快照(数据库中所有键值对数据)。恢复时是将快照文件直接读到内存
里。RDB 有两种触发方式,分别是自动触发和手动触发。自动是配置文件,手动是save命
令或者bgsave,阻塞和非阻塞;恢复数据时是阻塞的。
①、优势
1.RDB是一个非常紧凑(compact)的文件,它保存了redis 在某个时间点上的数据集。这种
文件非常适合用于进行备份和灾难恢复。
2.生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需
要进行任何磁盘IO操作。
3.RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
②、劣势
1、RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要执行fork
操作创建子进程,属于重量级操作,如果不采用压缩算法(内存中的数据被克隆了一份,大
致2倍的膨胀性需要考虑),频繁执行成本过高(影响性能)
2、RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式的RDB版本,存
在老版本Redis服务无法兼容新版RDB格式的问题(版本不兼容)
3、在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失最后一次快照
后的所有修改(数据有丢失)
原理:根据配置文件的算法,维持计数器。

2)AOF
Redis的持久化方式之一RDB是通过保存数据库中的键值对来记录数据库的状态。而另一种
持久化方式 AOF 则是通过保存Redis服务器所执行的写命令来记录数据库状态。
AOF持久化是Redis不断将写命令记录到 AOF 文件中,随着Redis不断的进行,AOF 的文
件会越来越大,文件越大,占用服务器内存越大以及 AOF 恢复要求时间越长。为了解决这
个问题,Redis新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动
AOF文件的内容压缩,只保留可以恢复数据的最小指令集
AOF 文件重写触发机制:通过 redis.conf 配置文件中的 auto-aof-rewrite-percentage:默
认值为100,以及auto-aof-rewrite-min-size:64mb 配置,也就是说默认Redis会记录上次
重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于
64M时触发。
优点:
①、AOF 持久化的方法提供了多种的同步频率,即使使用默认的同步频率每秒同步一次,
Redis 最多也就丢失 1 秒的数据而已。
②、AOF 文件使用 Redis 命令追加的形式来构造,因此,即使 Redis 只能向 AOF 文件写
入命令的片断,使用 redis-check-aof 工具也很容易修正 AOF 文件。
③、AOF 文件的格式可读性较强,这也为使用者提供了更灵活的处理方式。例如,如果我
们不小心错用了 FLUSHALL 命令,在重写还没进行时,我们可以手工将最后的 
FLUSHALL 命令去掉,然后再使用 AOF 来恢复数据。
缺点:
①、对于具有相同数据的的 Redis,AOF 文件通常会比 RDF 文件体积更大。
②、虽然 AOF 提供了多种同步的频率,默认情况下,每秒同步一次的频率也具有较高的性
能。但在 Redis 的负载较高时,RDB 比 AOF 具好更好的性能保证。
③、RDB 使用快照的形式来持久化整个 Redis 数据,而 AOF 只是将每次执行的命令追加
到 AOF 文件中,因此从理论上说,RDB 比 AOF 方式更健壮。官方文档也指出,AOF 的
确也存在一些 BUG,这些 BUG 在 RDB 没有存在。

那么对于 AOF 和 RDB 两种持久化方式,我们应该如何选择呢?
如果可以忍受一小段时间内数据的丢失,毫无疑问使用 RDB 是最好的,定时生成 RDB 快
照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复
的速度要快,而且使用 RDB 还可以避免 AOF 一些隐藏的 bug;否则就使用 AOF 重写。
但是一般情况下建议不要单独使用某一种持久化机制,而是应该两种一起用,在这种情况
下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件
保存的数据集要比RDB文件保存的数据集要完整。Redis后期官方可能都有将两种持久化方
式整合为一种持久化模型。

当开启混合持久化时,主进程先fork出子进程将现有内存副本全量以RDB方式写入aof文件
中,然后将缓冲区中的增量命令以AOF方式写入aof文件中,写入完成后通知主进程更新相
关信息,并将新的含有 RDB和AOF两种格式的aof文件替换旧的aof文件。
简单来说:混合持久化方式产生的文件一部分是RDB格式,一部分是AOF格式。

参考:
Redis详解(二)------ redis的配置文件介绍

8. Spring

1. spring如何解决循环依赖?

首先看什么是循环依赖,比如下面

@Data
public class A{
	private B b;
}

@Data
public class B{
	private A a;
}
当你new了一个A
spring在实例化A的时候发现依赖B
这时候spring就会先去实例化B
然后又发现B依赖了A
spring又去实例化A
。。。。。。

导致无限循环,类似一个没有出口的迭代。

如何解决

首先明确一点,循环依赖是靠Spring解决,而不是我们自己。

spring只能通过提前暴露bean来解决setter注入的循环依赖,构造器注入的循环依赖无法解决(Spring实例化一个bean的时候,是分两步进行的,首先实例化目标bean,然后为其注入属性。因此如果构造器循环依赖了,根本无法实例化)

如果出现循环依赖,一般都是设计上的问题,但凡正经点的项目都不会出现这种问题,如果出现了,不用方,我们能做的就是把构造器创建bean改成setter,剩下的交给spring;

2. 简述动态代理和静态代理

两者完全相对,类似正射和反射

静态代理:

由程序员编写或者编译工具自动生成源代码,然后对其编译,程序运行之前,代理类
的.class文件就已经生成了
静态代理一般只代理一个类,并且事先知道自己代理的是谁。

动态代理:

不是自己维护bean,程序运行时,通过反射机制动态创建生成
动态代理一般代理一个类下的多个实现类,因此运行前不知道自己实现代理的究竟是谁,
只有运行时才知道

3. Spring Boot和mybitis默认的连接池?

参考:
Mybatis的连接池
在Spring Boot中使用默认连接池HikariCP(结合MyBatis)
Springboot 2.0默认连接池HikariCP详解(效率最高)

4. Spring Cloud原理?

其实搞清楚5个核心组件就可以:

Eureka:各个服务启动时,Eureka Client都会将服务注册到Eureka Server,并且
Eureka Client还可以反过来从Eureka Server拉取注册表,从而知道其他服务在哪里

Ribbon:服务间发起请求的时候,基于Ribbon做负载均衡,从一个服务的多台机器中选择
一台

Feign:基于Feign的动态代理机制,根据注解和选择的机器,拼接请求URL地
址,发起请求

Hystrix:发起请求是通过Hystrix的线程池来走的,不同的服务走不同的线程池,实现了不
同服务调用的隔离,避免了服务雪崩的问题

Zuul:如果前端、移动端要调用后端系统,统一从Zuul网关进入,由Zuul网关转发请求给
对应的服务

在这里插入图片描述
参考:
Spring Cloud原理详解
spring cloud gateway对比 zuul2.0 主要的优势是什么?
Spring Cloud架构各组件的原理分析

5. 对spring IOC和AOP的理解

IOC:控制反转
也叫DI(依赖注入),是一种思想,不是一种技术,IOC主张把对象的控制权交由spring,
底层实现是反射+工厂方法模式,IOC容器实际上就是个Map,存放各种对象;

AOP:切面编程
面向切面编程,把一些能共用、冗余、繁琐的功能提取出来,AOP能在不改变原有业务逻
辑的情况下,增强横切逻辑代码,根本上解耦合,避免横切逻辑代码重复;常见使用场景
有事务管理、日志、全局异常处理、用户鉴权;
Authentication 权限
Caching 缓存
Context passing 内容传递
Error handling 错误处理
Lazy loading 懒加载
Debugging 调试
logging, tracing, profiling and monitoring 记录跟踪 优化 校准
Performance optimization 性能优化
Persistence 持久化
Resource pooling 资源池
Synchronization 同步
Transactions 事务

6. @Autowire和@Resource区别?

都是用来装配 java bean的

@Autowire:spring注解,按类型注入,可以搭配@Qualifie实现按名称注入;
@Resource:java注解,按名称注入,如果没有找到相同名称的Bean,则会尝试按照类型
进行匹配

注意事项:
@Autowired有个弊端,打个比方,有个userService,然后它有两个实现类
userServiceA和userServiceB,这时候用@Autowired就行不通了,因为它不知道找谁,
但是你也不能因为这个一上来就直接用@Resource,这玩意儿性能没@Autowired好,
因为@Resource要匹配两次,所以可以用@Autowire结合@Qualifie。

1. @Resource和@Autowired的起源

提到Spring依赖注入,大家最先想到应该是@Resource和@Autowired,但是大家有没有想过@Resource又支持名字又支持类型,还要@Autowired干嘛?

是的,没错,他们两个其实还是有核心区别的,了解一个东西绝不能只看其作用,一定要关注其产生的背景,从背景了解一件事才能知其然且知其所以然

  • @Resource 于 2006年5月11日随着JSR 250 发布 ,官方解释是:
    Resource 注释标记了应用程序需要的资源。该注解可以应用于应用程序组件类,或组件类的字段或方法。当注解应用于字段或方法时,容器将在组件初始化时将所请求资源的实例注入到应用程序组件中。如果注释应用于组件类,则注释声明应用程序将在运行时查找的资源。

    可以看到@Resource 其实类似一个定义,讲述了要实现的功能。其他的任何组件或框架都可以自由实现该功能。

    JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。

  • @Autowired 于 2007年11月19日随着Spring2.5发布,官方解释是:
    将构造函数、字段、设置方法或配置方法标记为由 Spring 的依赖注入工具自动装配。

    可以看到,@Autowired 是 Spring按照@Resource注解的功能自己实现的一个注解,它们的功能非常相似。那么为什么spring已经支持了@Resource,又要自己搞个@Autowired呢?

    对此,Spring2.5的官方文档有一段相关的解释,大概的意思是说,Spring2.5 支持注解自动装配啦, 现已经支持JSR-250 @Resource 基于每个方法或每个字段的命名资源的自动装配,但是只有@Resource是不行的,我们还推出了“粒度”更大的@Autowired,来覆盖更多场景了。

核心点:Resource是java的原生注解,Autowired是spring的自定义注解

2. 既生“@Resource”,何生“@Autowired”

由背景可知,粒度就是@Resource和@Autowired的和核心区别了!

  • @Autowired:类型注入
  • @Resource:名字注入优先,找不到名字找类型

论功能的“粒度”,@Resource已经包含@Autowired了啊,“粒度”更大啊。

此时很凌乱,那么“粒度”到底指的是什么?在混迹众多论坛后,其中stackoverflow的一段话引起了我的注意:

大概的意思是:Spring虽然实现了两个功能类似的,但是存在概念上的差异或含义上的差异:

  • @Resource 这按名称给我一个确定已知的资源。
  • @Autowired 尝试按类型连接合适的其他组件。

但是@Resource当按名称解析失败时会启动。在这种情况下,它会按类型解析,引起概念上的混乱,因为开发者没有意识到概念上的差异,而是倾向于使用@Resource基于类型的自动装配。

原来Spring官方说的“粒度”是指“资源范围”,@Resource找寻的是确定的已知的资源,相当于给你一个坐标,你直接去找。@Autowired是在一片区域里面尝试搜索合适的资源。

所以上面的问题答案已经基本明确了。
Spring为什么会支持两个功能相似的注解呢?

  • 它们的概念不同,@Resource更倾向于找已知资源,而Autowired倾向于尝试按类型搜索资源。

  • 方便其他框架迁移,@Resource是一种规范,只要符合JSR-250规范的其他框架,Spring就可以兼容。

既然@Resource更倾向于找已知资源,为什么也有按类型注入的功能?

  • 个人猜测:可能是为了兼容从Spring切换到其他框架,开发者就算只使用Resource也是保持Spring强大的依赖注入功能。

3. 使用@Autowired时为什么Idea会曝出黄色警告

使用@Autowired在属性上的时候Idea会曝出黄色的警告,并且推荐我们使用构造方法注入,而Resource就不会,这是为什么呢?警告如下:
在这里插入图片描述
其实Spring文档中已经给出了答案,主要有这几点:

  • 声明不了常量的属性
    基于属性的依赖注入不适用于声明为 final 的字段,因为此字段必须在类实例化时去实例化。声明不可变依赖项的唯一方法是使用基于构造函数的依赖项注入。
  • 容易忽视类的单一原则
    一个类应该只负责软件应用程序功能的单个部分,并且它的所有服务都应该与该职责紧密结合。如果使用属性的依赖注入,在你的类中很容易有很多依赖,一切看起来都很正常。但是如果改用基于构造函数的依赖注入,随着更多的依赖被添加到你的类中,构造函数会变得越来越大,代码开始就开始出现“异味”,发出明确的信号表明有问题。具有超过十个参数的构造函数清楚地表明该类有太多的依赖,让你不得不注意该类的单一问题了。因此,属性注入虽然不直接打破单一原则,但它却可以帮你忽视单一原则。
  • 循环依赖问题
    A类通过构造函数注入需要B类的实例,B类通过构造函数注入需要A类的实例。如果你为类 A 和 B 配置 bean 以相互注入,使用构造方法就能很快发现。
  • 依赖注入强依赖Spring容器
    如果您想在容器之外使用这的类,例如用于单元测试,不得不使用 Spring 容器来实例化它,因为没有其他可能的方法(除了反射)来设置自动装配的字段。

4. 使用@Resource时为什么Idea不会曝出黄色警告

在官方文档中,我没有找到答案,查了一些资料说是:@Autowired 是 Spring 提供的,一旦切换到别的 IoC 框架,就无法支持注入了. 而@Resource 是 JSR-250 提供的,它是 Java 标准,我们使用的 IoC 容器应该和它兼容,所以即使换了容器,它也能正常工作。

5. @Autowired和@Resource使用场景

记住一句话就行,@Resource倾向于确定性的单一资源,@Autowired为类型去匹配符合此类型所有资源。
如集合注入,@Resource也是可以的,但是建议使用@Autowired。idea左侧的小绿标可以看出来,不建议使用@Resource注入集合资源,本质上集合注入不是单一,也是不确定性的。
在这里插入图片描述
@Resource装配顺序:

  1. 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常。

  2. 如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常。

  3. 如果指定了type,则从上下文中找到类似匹配的唯一bean进行装配,找不到或是找到多个,都会抛出异常。

  4. 如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配。

@Resource的作用相当于@Autowired,只不过@Autowired按照byType自动注入。

7. springboot自动装配原理?

在springboot的启动类上有个

@SpringBootApplication注解,这是个组合注解,这个注
解里面有个注解叫EnableAutoConfiguration注解,

@EnableAutoConfigration 注解会导入一个自动配置选择器去扫描每个jar包的META-
INF/xxxx.factories 这个文件,这个文件是一个key-value形式的配置文件,里面存放
了这个jar包依赖的具体依赖的自动配置类。这些自动配置类又通过

@EnableConfigurationProperties 注解支持通过xxxxProperties 读取
application.properties/application.yml属性文件中我们配置的值。如果我们没有
配置值,就使用默认值,这就是所谓约定大于配置的具体落地点。

参考:@SpringBootApplication详解

9. 微服务

1. Eureka和Zookeeper的区别?

都能用来做服务注册与发现,但是两者侧重的点不一样,zk是CP架构,而eureka是AP架构

1)zk集群的master节点挂了以后,会从slave节点中选举一个出来作为master,在选举
这段时间内,zk服务不可用,为了保证一致性,牺牲了高可用;

2)而eureka的节点之间没有主仆关系,当有节点挂了以后,其它节点依然能提供服务,
只不过数据可能不一致,为了保证高可用,牺牲了一致性;

10. 分布式

1. 分布式系统如果保证接口的幂等性?

数据设置状态值
数据库设置唯一性
每个数据请求有唯一性标识

2. 分布式session如何处理?

tomcat+redis,TomcatRedisSessionManager,将所有部署的tomcat都将session存储到redis即可。使用方法不变,是Tomcat封装的类将session存储到了redis,依赖web容器
spring session +redis:spring 将session存储到redis

11. 大数据

1. 什么是CAP理论?

CAP理论是分布式系统架构设计中的一种猜想,或者说是一种理论

一致性(Consistence) : 所有节点访问的数据都是一致的;
可用性(Availability): 非故障的节点在合理的时间内返回合理的响应(不是错误或者
超时的响应);
分区容错性(Partition tolerance) : 分布式系统出现网络分区的时候,仍然能够对
外提供服务。网络分区指的是网络设备出现的丢包、阻塞、超时等问题。

误区:
我发现很多人说到CAP时,都知道分布式系统中这三者不能同时兼顾,只能满足其二,这
种说法不严谨,并不是简单的三选二,而是在一致性和可用性二者中只能选其一,分区容
错性是我们必须保证的,不能因为出现网络分区导致系统瘫痪,而一致性和可用性可以根
据我们的业务来舍弃其一,即我们只能实现AP方案或者CP方案;

2. 什么是BASE理论?

BASE理论由CAP理论演化而来,因为CAP对系统设计的要求过高,实现CAP的代价太大;
BASE实质上是对CAP 中 AP 方案的一个补充。

BA:Basically Available(基本可用)
S:Soft-state(软状态) 
E:Eventually Consistent(最终一致性)

核心思想:
牺牲强一致性,通过某些逻辑来达到数据的最终一致性

因为大多数系统对强一致性的依赖没那么强,抛开分区容错性不谈,一致性和可用性这两
者,可用性才是我们应该保证的,而对于一致性,我们往往只需要保证最终的数据一致即
可;

3. 什么是布隆过滤器?

布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。用多个哈希函数,将一个数据映射到位图结构中。

本质上布隆过滤器是一种比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。

布隆过滤器可以用于检索一个元素是否在海量数据集合中存在。

特点:1)海量数据检索是否存在;2)概率存在与否。

1. 理念

面试题:

如何在海量元素中(例如10亿无序、不定长、不重复)快速判断一个元素是否存在?

如果想判断一个元素是不是在一个集合里,一般想到的是将集合中所有元素保存起来,然后通过比较确定。链表、树、哈希表等数据结构都是这种思路。

但是随着集合中元素的增加,我们需要的存储空间越来越大。同时检索速度也越来越慢,上述三种结构的检索时间复杂度分别为:O(n),O(log n),O(n/k)。

我们假设一个元素1个字节的字段,10亿的数据大概需要 900G 的内存空间,这对于普通的服务器来说是无法承受的。

因此,我们这里引入一种节省空间的数据结构-位图(有序的数组,只有两个值(0、1),0代表不存在,1代表存在)。

还需要一个映射关系,来确定元素的位置,就要用到哈希函数了。

答:通过哈希函数计算得到相应的位置,然后再看这个位置上是0还是1,既可判断元素是否存在。

位图:

一个有序的数组,只有两个值(0、1),0代表不存在,1代表存在。

用一个bit位来标记某个元素对应的Value, 而Key即是该元素。(java中BitSet就是BitMap的实现)。

适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。位图的优点就是快速并且节省空间,但是缺点是只能映射整形。

举例说明:

假设我们对0到7内的4个元素(2,5,7,1)排序,采用bitmap的方法排序。

要表示8个数,只需要8个Bit(1Byte),首先我们初始化1Byte的空间,将这些空间的所有Bit位都置为0。
在这里插入图片描述
然后遍历这4个元素,从零开始,依次填充1。
在这里插入图片描述

哈希函数:

  • 哈希函数两个好处:
  1. 哈希函数无论输入值长度是多少,得到的输出值长度是固定的;

  2. 分布均匀。

  • 哈希函数特点:
  1. 如果根据同一个哈希函数得到的哈希值不同,那么这两个哈希值的原始输入值肯定不同;

  2. 如果根据同一个哈希函数得到的两个哈希值相等,两个哈希值的原始输入值有可能相等,有可能不相等。这种情况叫做哈希冲突或者哈希碰撞。

  • 降低哈希碰撞概率两点:
  1. 扩大位数组的容量;
    因为函数是分布均匀的,所以位图容量越大,在同一个位置发生哈希碰撞的概率就越小。但是越大的位图容量,意味着越多的内存消耗。

  2. 多几次哈希函数的计算。
    同理,但也不是越多次计算越好,因为这样很快就会填满位图,而且计算也是需要消耗时间,因此,需要在时间和空间上寻求一个平衡。

2. 原理

布隆过滤器是由一系列的哈希函数映射到二进制位数组的数据结构。

布隆过滤器 = 哈希 + 位图。

存储的每个元素只能是0或者1,每个元素都只占用1bit 。
例如:

一个100万的位数组占用大小为:
1000000 bit / 8 = 125000 byte = 125000/1024 ≈ 122kb

1. 底层存储结构是一个bit向量或者说 bit 数组,初始状态时,它的所有位置都设置为0(假设长度为8)。
在这里插入图片描述
2. 当有变量a、b添加到布隆过滤器中,通过K个映射函数将变量映射到位数组的K个点,并把这K个点的值设置为1(假设有3个映射函数)。
在这里插入图片描述

可知,a和b在位置4通过哈希产生碰撞。

3. 查询某个变量是否存在的时候,只需要通过同样的K个映射函数,找到对应的K个点。判断K个点上的值是否全都是1,如果全都是1则表示很可能存在,如果K个点上有任何一个是0,则表示一定不存在。

在这里插入图片描述
在这里插入图片描述

  • 特性:
  1. 存在一定的误判率;
  2. 不能删除元素。
  • 总结:
  1. 从容器的角度来说:
    如果布隆过滤器判断元素在集合中存在,不一定存在;
    如果布隆过滤器判断不存在,一定不存在。

  2. 从元素的角度来说:
    如果元素实际存在,布隆过滤器一定判断存在;
    如果元素实际不存在,布隆过滤器可能判断存在。

  3. 在哈希函数的个数k一定的情况下:
    位数组长度m越大,假阳性率越低;
    已插入元素的个数n越大,假阳性率越高。

  • 优点:
  1. 存储的不是完整的数据,是一个二进制向量,能节省大量的内存空间。增加和查询元素的时间复杂度为:O(K) (K为哈希函数的个数,一般比较小),与数据量大小无关;

  2. 哈希函数相互之间没有关系,方便硬件并行运算;

  3. 不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势;

  4. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能;

  5. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算。

  • 缺点:
  1. 有误判率(假阳性-False Position),即不能准确判断元素是否在集合中。存进布隆过滤器里的元素越多,误判率越高;

  2. 不能获取元素本身;

  3. 一般情况下不能从布隆过滤器中删除元素。随着使用的时间越长,存进里面的元素越多,占用内存越多,误判率越高,最后不得不重置;

  4. 不支持扩容操作。布隆过滤器建立初期必须进行严格的推算,确保后期不需要扩容,否则重建布隆过滤器的成本可能会超乎想象。

❓为什么说全都是1的情况是很可能存在,而不是一定存在呢?

因为随着增加的值越来越多,产生哈希碰撞的几率越大,同一个被置为1的bit位的映射也会越来越多。

同根据同一个哈希函数得到相同的哈希值,输入值不一定相等的道理。

我们把这种本来不存在布隆过滤器中的元素误判为存在的情况叫做假阳性(False Positive Probability,FPP)。

❓为什么布隆过滤器不能删除元素呢?

因为在位数组上的同一个点有可能有多个输入值映射,如果删除了会影响布隆过滤器里其他元素的判断结果。

❓怎么提升算法的准确度呢?

增加hash的次数;

增加bit数组的长度。

3. 实现

布隆过滤器如果要支持删除,应该怎么做呢?

Counting Bloom Filter的出现解决了这个问题,它将标准Bloom Filter位数组的每一位扩展为一个小的计数器(Counter),在插入元素时给对应的k(k为哈希函数个数)Counter的值分别加1,删除元素时给对应的k个Counter的值分别减1,判断是否存在,则是看Counter值是否大于0Counting Bloom Filter通过多占用几倍的存储空间的代价,给Bloom Filter增加了删除操作。


依赖:
<dependency>
    <groupId>com.baqend</groupId>
    <artifactId>bloom-filter</artifactId>
    <version>2.2.2</version>
</dependency>


demo:
public static void main(String[] args) {
    //int expectedElements: 容量大小
    //double falsePositiveProbability: fpp值
    //countingBits: 计数器占用的大小
    CountingBloomFilter<String> cbf = new FilterBuilder(1000000, 0.03).countingBits(4).buildCountingBloomFilter();
    //添加
    cbf.add("TOM");
    cbf.add("LILY");
    cbf.add("JACK");
    log.info("布隆过滤器判断是否存在: {}.", cbf.contains("TOM"));
    //删除
    cbf.remove("TOM");
    log.info("删除后,布隆过滤器判断是否存在: {}.", cbf.contains("TOM"));
  }

重点:countingBits参数就是计数器占用的大小,默认是16位大小,即允许65535次重复。对于大多数应用程序来说,4位就足够了。



Counting Bloom Filter虽说解决了不能删除元素的问题,但是自身仍有不少的缺陷有待完善,比如 Counter 的引入就会带来很大的资源浪费,fpp还有很大可以降低的空间, 因此在实际的使用场景中会有很多Counting Bloom Filter的升级版。

如SBF(Spectral Bloom Filter)CBF的基础上提出了元素出现频率查询的概念,将CBF的应用扩展到了 multi-set 的领域;

dlCBF(d-Left Counting Bloom Filter)利用 d-left hashing 的方法存储 fingerprint,解决哈希表的负载平衡问题;

ACBF(Accurate Counting Bloom Filter)通过 offset indexing 的方式将 Counter 数组划分成多个层级,来降低误判率。
Guava实现布隆过滤器

依赖:
 <dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>30.0-jre</version>
</dependency>


demo:
 public static void main(String[] args) {
        //设置容量大小
        int capacity = 10000000;
        //设置误判率
        double fpp = 0.02;
        BloomFilter<String> bf = BloomFilter.create(
                Funnels.stringFunnel(Charset.forName("UTF-8")), capacity, fpp);
        //添加
        bf.put("wakaka");
        bf.put("hualala");
        log.info("布隆过滤器判断是否存在: {}.", bf.mightContain("wakaka"));
        log.info("布隆过滤器判断是否存在: {}.", bf.mightContain("TOM"));
    }
Redis实现布隆过滤器

1、下载插件。
https://github.com/RedisBloom/RedisBloom.git

2、cd到文件夹RedisBloom,使用make命令编译,编译完成后生成一个redisbloom.so文件。

3、redis配置文件中加入该模块。
redis.conf
loadmodule /usr/local/redis/redisbloom-1.1.1/rebloom.so

4、启动redis生效。
redis-server redis.conf

5、测试是否安装成功。
$ ./src/redis-cli 
127.0.0.1:6379> bf.add user wakaka
(integer) 1
127.0.0.1:6379> bf.exists user wakaka
(integer) 1
127.0.0.1:6379> bf.exists user hualala
(integer) 0


基本指令含义如下:

bf.add:添加元素;
bf.exists:判断元素是否存在;
bf.madd:添加多个元素;
bf.mexists:判断多个元素是否存在;



代码实现:

// 注入配置类中创建的客户端
  @Autowired
  private RedissonClient redissonClient;

  public void filtration(String[] args) throws Exception {
    //获取过滤器对象
      RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter("user");
      //尝试初始化
      bloomFilter.tryInit(10000000, 0.03);
      //添加元素到布隆过滤器中
      bloomFilter.add("wakaka");
      bloomFilter.add("hualala");
      log.info("布隆过滤器中元素个数为:{}.", bloomFilter.count());
      log.info("布隆过滤器判断是否存在wakaka:{}." + bloomFilter.contains("wakaka"));
      log.info("布隆过滤器判断是否存在asan:{}." + bloomFilter.contains("asan"));
      redissonClient.shutdown();
  }

使用场景:

  • 缓存穿透;
  • 去重(如:爬给定网址的时候对已经爬取过的URL去重);
  • 文章是否已读(如:新闻推荐系统);
  • 邮箱的垃圾邮件过滤、黑名单等。

12. Kafka

1. kafak偏移量的问题

kafka集群中,每个主题设置的分区数和最好和节点数相同,这样可以充分发挥集群的工作
效率。
使用kafka一定会遇到偏移量的问题,其实无论哪里的偏移量,其意义都是字面意义,
kafka中的偏移量就是保存自己消费的位置,保证不要重复消费或者漏消费。

保存偏移量一般有三种方式:
1)自动提交,直接修改配置文件即可,默认5秒。但是这种方式在kafka发生重平衡时会导
致重复,因为他是按照时间频率来提交的偏移量,从流思想上来看偏移量始终是滞后的。
操作简单,容易重复消费,适用重复消费无影响的场景。
2)手动同步提交偏移量,同步提交的话是阻塞提交,虽然可以保证偏移量的绝对准确,但
是会影响效率
3)手动异步提交偏移量,异步提交的话是非阻塞,但是会导致一个问题就是假如因为网络
问题一个先提交的偏移量比后提交的偏移量更晚存储,这样还是会导致重复消费。
4)基于性能和偏移量的正确,我们一般选择的是同步和异步的一起来使用,比如每次消费
完都异步提交,然后在finally里面设置一个同步提交。

13. Elasticsearch

1. es的底层数据结构

14. Zookeeper

1. zk选举机制?

15. Mybatis

1. Mybatis简介

MyBatis 是支持普通 SQL查询,存储过程和高级映射的优秀持久层框架。MyBatis 消除了几乎所有的JDBC代码和参数的手工设置以及结果集的检索。MyBatis 使用简单的 XML或注解用于配置和原始映射,将接口和 Java 的POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。

总体流程:

  1. 加载配置并初始化
    触发条件:加载配置文件
    将SQL的配置信息加载成为一个个MappedStatement对象(包括了传入参数映射配置、执行的SQL语句、结果映射配置),存储在内存中。

  2. 接收调用请求
    触发条件:调用Mybatis提供的API
    传入参数:为SQL的ID和传入参数对象
    处理过程:将请求传递给下层的请求处理层进行处理。

  3. 处理操作请求
    触发条件:API接口层传递请求过来
    传入参数:为SQL的ID和传入参数对象
    处理过程:

    1. 根据SQL的ID查找对应的MappedStatement对象。

    2. 根据传入参数对象解析MappedStatement对象,得到最终要执行的SQL和执行传入参数。

    3. 获取数据库连接,根据得到的最终SQL语句和执行传入参数到数据库执行,并得到执行结果。

    4. 根据MappedStatement对象中的结果映射配置对得到的执行结果进行转换处理,并得到最终的处理结果。

    5. 释放连接资源。

  4. 返回处理结果将最终的处理结果返回

16. 框架

集成开发工具(IDE):Eclipse、MyEclipse、Spring Tool Suite(STS)、Intellij IDEA、NetBeans、JBuilder、JCreator
JAVA服务器:tomcat、jboss、websphere、weblogic、resin、jetty、apusic、apache
负载均衡:nginx、lvs
web层框架:Spring MVC、Struts2、Struts1、Google Web Toolkit(GWT)、JQWEB
服务层框架:Spring、EJB
持久层框架:Hibernate、MyBatis、JPA、TopLink
数据库:Oracle、MySql、MSSQL、Redis
项目构建:maven、ant
持续集成:Jenkins
版本控制:SVN、CVS、VSS、GIT
私服:Nexus
消息组件:IBM MQ、RabbitMQ、ActiveMQ、RocketMq
日志框架:Commons Logging、log4j 、slf4j、IOC
缓存框架:memcache、redis、ehcache、jboss cache
RPC框架:Hessian、Dubbo
规则引擎:Drools
工作流:Activiti
批处理:Spring Batch
通用查询框架:Query DSL
JAVA安全框架:shiro、Spring Security
代码静态检查工具:FindBugs、PMD
Linux操作系统:CentOS、Ubuntu、SUSE Linux、
常用工具:PLSQL Developer(Oracle)、Navicat(MySql)、FileZilla(FTP)、Xshell(SSH)、putty(SSH)、SecureCRT(SSH)、jd-gui(反编译)

非技术

1. 工作中的亮点?

1)自我学习能力,深入业务分析修改mysql的存储引擎。提高了查询效率innodb换成
myisam,最大的表有3百多万。kafka的流量
2)压力测试时,希望测试glzx的吞吐量,当时是特意灌装了多太jcq,然后每个jcq打上最
大的流量,这样每次都需要很多台jcq的配合,比较耽误时间,还需要别人的配合,然后我
是利用http请求把所有需要测试的接口都开发了一遍,然后直接利用多线程进行测试,省
时省力,复用性还高,用最少的时间最少的人完成自己的工作才是真正的工作。
3)暂无,马丹、很气
4)记录博客,我会用通俗易懂的语言去解释技术,然后用现象生活中的小事去剖析技术的
原因,我相信,技术来源于生活,人们的认知不会凭空的出现,只会是从一种事物嫁接到
另一种事物,并且循序渐进的优化,通过生活去观察技术,学习技术,才能更好更快地掌
握,更容易的运用,更快的举一反三。比如,RPC就像是我们打最开始电话时需要先打给
传呼机。TCP三次握手就是为了确认彼此的收发能力正常,四次握手,就是一方告诉另一
方,我要走了。池化思想,只不过是为了好钢用在刀刃上。原理也不过是一个简单的逻
辑,搞懂它的核心参数,自然就能明白判断逻辑。安全操作、事务操作、还不是来源于生
活中的锁,目的不就是把想要的东西,锁在家里不许别人碰,锁自己也在维护一个监视
器,我是谁,谁在用我,谁在等我。诸如此类。	至于数据结构,队列、栈、集合、数
组、链表、甚至是复杂的树,不全是生活中常见的事物抽象出来的吗。
5)学习能力和执行力,工作中并不会一帆风顺,我们做的是互联网,工作内容更是日新月
益,只有提高自己的学习能力,和驱动力执行力才能更好的完成本职工作,我们的工资从
来不是用嘴决定的,而是工作产出。

参考

Java面试题大全(2021版)
2021 Java面试题精心整理(持续更新)
最全的BAT大厂面试题
现在的Java面试的都这么难了吗?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lipviolet

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值