赞
踩
java.lang.Object
下面是对应方法的含义。
clone 方法
保护方法,实现对象的浅复制,只有实现了 Cloneable 接口才可以调用该方法,否则抛出 CloneNotSupportedException 异常,深拷贝也需要实现 Cloneable,同时其成员变量为引用类型的也需要实现 Cloneable,然后重写 clone 方法。
finalize 方法
该方法和垃圾收集器有关系,判断一个对象是否可以被回收的最后一步就是判断是否重写了此方法。
equals 方法
该方法使用频率非常高。一般 equals 和 == 是不一样的,但是在 Object 中两者是一样的。子类一般都要重写这个方法。
hashCode 方法
该方法用于哈希查找,重写了 equals 方法一般都要重写 hashCode 方法,这个方法在一些具有哈希功能的 Collection 中用到。
一般必须满足 obj1.equals(obj2)==true
。可以推出 obj1.hashCode()==obj2.hashCode()
,但是 hashCode 相等不一定就满足 equals。不过为了提高效率,应该尽量使上面两个条件接近等价。
wait 方法
配合 synchronized 使用,wait 方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait() 方法一直等待,直到获得锁或者被中断。wait(long timeout) 设定一个超时间隔,如果在规定时间内没有获得锁就返回。
调用该方法后当前线程进入睡眠状态,直到以下事件发生。
此时该线程就可以被调度了,如果是被中断的话就抛出一个 InterruptedException 异常。
notify 方法
配合 synchronized 使用,该方法唤醒在该对象上等待队列中的某个线程(同步队列中的线程是给抢占 CPU 的线程,等待队列中的线程指的是等待唤醒的线程)。
notifyAll 方法
配合 synchronized 使用,该方法唤醒在该对象上等待队列中的所有线程。
总结
只要把上面几个方法熟悉就可以了,toString 和 getClass 方法可以不用去讨论它们。该题目考察的是对 Object 的熟悉程度,平时用的很多方法并没看其定义但是也在用,比如说:wait() 方法,equals() 方法等。
Class Object is the root of the class hierarchy.Every class has Object as a superclass. All objects, including arrays, implement the methods of this class.
大致意思:Object 是所有类的根,是所有类的父类,所有对象包括数组都实现了 Object 的方法。
面试扩散
上面提到了 wait、notify、notifyAll 方法,或许面试官会问你为什么 sleep 方法不属于 Object 的方法呢?因为提到 wait 等方法,所以最好把 synchronized 都说清楚,把线程状态也都说清楚,尝试让面试官跟着你的节奏走。
这题目看似简单,要好好回答起来还是有点小复杂的,我们来看看,到底有哪些方式可以创建对象?
\1. 使用 new 关键字,这也是我们平时使用的最多的创建对象的方式,示例:
User user=new User();
\2. 反射方式创建对象,使用 newInstance(),但是得处理两个异常 InstantiationException、IllegalAccessException:
User user=User.class.newInstance();
Object object=(Object)Class.forName("java.lang.Object").newInstance()
3.使用 clone 方法,前面题目中 clone 是 Object 的方法,所以所有对象都有这个方法。
4.使用反序列化创建对象,调用 ObjectInputStream 类的 readObject() 方法。
我们反序列化一个对象,JVM 会给我们创建一个单独的对象。JVM 创建对象并不会调用任何构造函数。一个对象实现了 Serializable 接口,就可以把对象写入到文件中,并通过读取文件来创建对象。
总结
创建对象的方式关键字:new、反射、clone 拷贝、反序列化。
搞清楚类对象和实例对象,但都是对象。
第一种:通过类对象的 getClass() 方法获取,细心点的都知道,这个 getClass 是 Object 类里面的方法。
User user=new User();
//clazz就是一个User的类对象
Class<?> clazz=user.getClass();
第二种:通过类的静态成员表示,每个类都有隐含的静态成员 class。
//clazz就是一个User的类对象
Class<?> clazz=User.class;
第三种:通过 Class 类的静态方法 forName() 方法获取。
Class<?> clazz = Class.forName("com.tian.User");
面试扩散
可能面试官会问相关的题目,比如:
Class.forName 和 ClassLoader.loadClass 的区别是什么?
参考:
ArrayList
LinkedList
适用场景分析
如果容量固定,并且只会添加到尾部,不会引起扩容,优先采用 ArrayList。
当然,绝大数业务的场景下,使用 ArrayList 就够了,但需要注意避免 ArrayList 的扩容,以及非顺序的插入。
只要是搞 Java 的肯定都会回答“用过”。所以,回答题目的后半部分——ArrayList 的特点。可以从这几个方面去回答:
Java 集合框架中的一种存放相同类型的元素数据,是一种变长的集合类,基于定长数组实现,当加入数据达到一定程度后,会实行自动扩容,即扩大数组大小。
底层是使用数组实现,添加元素。
高并发的情况下,线程不安全。多个线程同时操作 ArrayList,会引发不可预知的异常或错误。
ArrayList 实现了 Cloneable 接口,标识着它可以被复制。注意:ArrayList 里面的 clone() 复制其实是浅复制。
通常我们在使用的时候,如果在不明确要插入多少数据的情况下,普通数组就很尴尬了,因为你不知道需要初始化数组大小为多少,而 ArrayList 可以使用默认的大小,当元素个数到达一定程度后,会自动扩容。
可以这么来理解:我们常说的数组是定死的数组,ArrayList 却是动态数组。
fail-fast 机制是 Java 集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生 fail-fast 事件。
例如:当某一个线程 A 通过 iterator 去遍历某集合的过程中,若该集合的内容被其他线程所改变了,那么线程 A 访问集合时,就会抛出 ConcurrentModificationException 异常,产生 fail-fast 事件。这里的操作主要是指 add、remove 和 clear,对集合元素个数进行修改。
解决办法:建议使用“java.util.concurrent 包下的类”去取代“java.util 包下的类”。
可以这么理解:在遍历之前,把 modCount 记下来 expectModCount,后面 expectModCount 去和 modCount 进行比较,如果不相等了,证明已并发了,被修改了,于是抛出 ConcurrentModificationException 异常。
本来不想这么写标题的,但是无奈,面试官都喜欢这么问 HashMap。
key==null
,则其 hash=0
;至于 value 是否为 null,根本没有判断过。另外在 Hashtable 源码注释中有这么一句话:
Hashtable is synchronized. If a thread-safe implementation is not needed, it is recommended to use HashMap in place of Hashtable . If a thread-safe highly-concurrent implementation is desired, then it is recommended to use ConcurrentHashMap in place of Hashtable.
大致意思:Hashtable 是线程安全,推荐使用 HashMap 代替 Hashtable;如果需要线程安全高并发的话,推荐使用 ConcurrentHashMap 代替 Hashtable。
这个回答完了,面试官可能会继续问:HashMap 是线程不安全的,那么在需要线程安全的情况下还要考虑性能,有什么解决方式?
这里最好的选择就是 ConcurrentHashMap 了,但面试官肯定会叫你继续说一下 ConcurrentHashMap 数据结构以及底层原理等。
平时可能大家使用的最多的就是使用 String 作为 HashMap 的 key,但是现在我们想使用某个自定义类作为 HashMap 的 key,那就需要注意以下几点:
为了能让 HashMap 存数据和取数据的效率高,尽可能地减少 hash 值的碰撞,也就是说尽量把数据能均匀的分配,每个链表或者红黑树长度尽量相等。
我们首先可能会想到 %
取模的操作来实现。
下面是回答的重点哟:
取余(%)操作中如果除数是 2 的幂次,则等价于与其除数减一的与(&)操作(也就是说
hash % length == hash &(length - 1)
的前提是 length 是 2 的 n 次方)。并且,采用二进制位操作&
,相对于%
能够提高运算效率。
这就是为什么 HashMap 的长度需要 2 的 N 次方了。
紧接上个问题,面试官很有可能会问红黑树,下面把红黑树的几个特征列出来:
如果面试官还要继续问红黑树具体是怎么添加节点和删除节点的,推荐看:
try-catch-finally
抛出异常→捕获异常→捕获成功(当 catch 的异常类型与抛出的异常类型匹配时,捕获成功)→异常被处理,程序继续运行 抛出异常→捕获异常→捕获失败(当 catch 的异常类型与抛出异常类型不匹配时,捕获失败)→异常未被处理,程序中断运行
在开发过程中会使用到自定义异常,在通常情况下,程序很少会自己抛出异常,因为异常的类名通常也包含了该异常的有用信息,所以在选择抛出异常的时候,应该选择合适的异常类,从而可以明确地描述该异常情况,所以这时候往往都是自定义异常。
自定义异常通常是通过继承 java.lang.Exception 类,如果想自定义 Runtime 异常的话,可以继承 java.lang.RuntimeException 类,实现一个无参构造和一个带字符串参数的有参构造方法。
在业务代码里,可以针对性的使用自定义异常。比如说:该用户不具备某某权限、余额不足等。
public class FinallyDemo {
public String method111() {
String ret = "hello";
try {
return ret;
} finally {
ret = "world";
}
}
}
把 FinallyDemo.java 编译成 class 文件后,找到该 class 文件的当前目录,执行 cmd 命令:
javap -verbose FinallyDemo.class >>test.txt
然后打开 test.txt,关键部分内容如下:
发现在字节码指令中,将 hello 保存在本地变量 2 中,然后直到把本地变量 2 加载到操作数栈中,然后就直接出栈,return 回去了,所以本题的返回去的是 hello,但是 finally 代码块也执行了,执行完 finally 模块后再返回一个临时变量 2。
类加载的过程包括了:
加载、验证、准备、解析、初始化。
new 创建一个普通对象的过程如下:
下面用一张图来描述:
注意类生命周期和对象声明周期,类生命周期主要有以下几个阶段:
可以写,能编译,但是不能 run。禁止使用包名:java.开头的包名
。
定义一个普通类:
package java.lang; public class MyTest { public MyTest() { } public MyTest(String str, int a) { } public int length(){ return 10; } public static void main(String[] args) { MyTest myTest =new MyTest("lang",1); myTest.length(); } }
运行:
具体校验的源码地方:
结论就是定义包目录的时候,不能以 java. 开头。
此题我想用我的方法说,不像网上一堆一堆抄书上的,希望能对大家有所帮助,如果没多大帮助,那可以网上找个看看,只能说抱歉了。
下面我们直入主题:
如果你在 Java 代码里创建一个线程,相应 JVM 虚拟机中就创建与之对应的程序计数器、Java 虚拟机栈、本地方法栈,同时方法区和堆是在虚拟机启动就已经有了。
程序计数器
可以简单理解为:程序计数器是记录执行到你代码的的第几行了,每个线程各自对应自己的程序计数器。
Java 虚拟机栈
虚拟机会为每个线程分配一个虚拟机栈,每个虚拟机栈中都有若干个栈帧,每个栈帧中存储了局部变量表、操作数栈、动态链接、返回地址等。一个栈帧就对应 Java 代码中的一个方法,当线程执行到一个方法时,就代表这个方法对应的栈帧已经进入虚拟机栈并且处于栈顶的位置,每一个 Java 方法从被调用到执行结束,就对应了一个栈帧从入栈到出栈的过程。一个线程的生命周期和与之对应的 Java 虚拟机栈的生命周期相同。
一个线程进来就创建虚拟机栈,该线程调用的方法就是栈帧,进入方法,栈帧就入栈(虚拟机栈),出方法就是出虚拟机栈。可以通过下面两张图进行理解:
本地方法栈
和 Java 虚拟机栈类似,Java 虚拟机栈针对的是 Java 方法,而本地方法栈针对的 native 修饰的方法。
堆
JVM 几乎所有的对象的内存分配都在堆里。由于对象是有生命周期的,所以把堆又分成了新生代和老年代。
新生代和老年代大小比例 = 1:2(默认)。新生代又分为 Eden、S0、S1 区域,Ede:S0:S1=8:1:1。
大多数对象在 Eden 区出生和死亡。Eden 区存活下来的对象进入 S0 区,S0 区活下来的对象放到 S1,S1 区活下来的对象放到 S0 区,这过程中 S0 和 S1 至少有一个区域是空着的。并且对象每次倒腾一次自己的年龄就加 1,直达加到 15 岁的时候,就直接入老年代了。有的大对象可以直接进入老年代,条件是把该对象的大小以及达到了能直接进入老年代的条件了(阈值可以设置)。
方法区
先按照图中的关键字回答。但是方法区由于 JDK 版本有所变动。
回答的时候,一定要说一下方法区由于 JDK 版本有所变动。版本变动情况如下:
之前有小伙伴也问过我,方法区和永久代到底是什么区别?
这么说吧:永久代又叫 Perm 区,只存在于 HotSpot JVM 中,并且只存在于 JDK 1.7 和之前的版本中,JDK 1.8 中已经彻底移除了永久代,JDK 1.8 中引入了一个新的内存区域叫 metaspace。
因此,我们可以说,在 JDK 1.7 中永久代是方法区的一种实现,当然,在 HotSpot JDK 1.8 中 metaspace 可以看成是方法区的一种实现。
简单回答就行:
Java 虚拟机栈、本地方法栈、Java 堆、方法区,其实就是除了程序计数器以外的几个都会发生 OOM。
建议把阈值对应的几个区也简要的说一下:
在 JDK 1.8 之前的 HotSpot 实现中,类的元数据如方法数据、方法信息(字节码、栈和变量大小)、运行时常量池、已确定的符号引用和虚方法表等被保存在永久代中,32 位默认永久代的大小为 64M,64 位默认为 85M,可以通过参数 -XX:MaxPermSize
进行设置,一旦类的元数据超过了永久代大小,就会抛出 OOM 异常。
虚拟机团队在 JDK 1.8 的 HotSpot 中,把永久代从 Java 堆中移除了,并把类的元数据直接保存在本地内存区域(堆外内存),称之为元空间 metaspace(本地内存)。即就是说 JDK 1.8 之前,永久代是在虚拟机中的,而 JDK 1.8 引入的元空间是系统的内存的一部分。理论上取决于 32 位/64 位系统可虚拟的内存大小,可见也不是无限制的,需要配置参数。
另外一方面,咱们在对永久代进行调优的时候是相当费劲,因为永久代的大小不好控制,涉及到很多因素,比如:类的总数、常量池的大小、方法数量等,最无语的是永久代的内存不够了可能会伴随着一次 Full GC。
下面是 JDK 几个版本中方法区和堆存储的信息的关系:
总结
整个堆内存大小
-Xms
(初始堆大小)、-Xmx
(最大堆大小),为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最大、最小设置为相同的值。
新生代空间大小
NewRadio:年轻代和年老代将根据默认的比例(1:2)分配堆内存,建议设置为 2 到 4,可以通过调整二者之间的比率 NewRadio 来调整二者之间的大小。也可以针对回收代,比如年轻代,通过 -XX:newSize -XX:MaxNewSize
来设置其绝对大小。同样,为了防止年轻代的堆收缩,我们通常会把 -XX:newSize -XX:MaxNewSize
设置一样大小。
方法区(元空间)
JDK 1.8:-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m
,根据实际情况调整, 可以使用命令 jstat -gcutil pid
查看当前使用率,M 对应的列,根据使用率来定制一个具体的值,建议两个值设置成同样大小。
JDK 1.7:-XX:MaxPermSize=256m -XX:MaxPermSize=256m
永久带。
GC 日志
-Xloggc:$CATALINA_BASE/logs/gc.log
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
记录 GC 日志并不会特别地影响 Java 程序性能,推荐你尽可能记录日志。
GC 算法
-XX:+UseParNewGC
-XX:+CMSParallelRemarkEnabled
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75
一般来说推荐使用这些配置,但是根据程序不同的特性,其他的也有可能更好。
任何一个 JVM 参数的默认值可以通过
java -XX:+PrintFlagsFinal -version |grep JVMParamName
获取,例如:
java -XX:+PrintFlagsFinal -version |grep MetaspaceSize
对象引用类型有四类:强引用、软引用、弱引用、虚引用。
垃圾回收算法共四种:其实我更愿意说成三种,因为分代回收其实不是算法。
目前常见的有如下几种:
Serial 收集器
ParNew 收集器
Parallel scavenge 收集器
Serial Old 收集器
CMS=Concurrent Mark Sweep 收集器
Parallel Old 收集器
G1=Garbage-First 收集器
垃圾收集器整合
分代收集器对应
其实关于 Dubbo 的面试题,我觉得最好的文档应该还是官网,因为官网有中文版,照顾了很多阅读英文文档吃力的小伙伴。但是官网内容挺多的,于是这里就结合官网和平时面试被问的相对较多的题目整理了一下。
基本工作流程:
上图中角色说明:
工作原理分 10 层:
这是个很坑爹的面试题,但是很多面试官又喜欢问,你真的要背么?你能背那还是不错的,我建议不要背,你就想想 Dubbo 服务调用过程中应该会涉及到哪些技术,把这些技术串起来就 OK 了。
面试扩散
如果让你设计一个 RPC 框架,你会怎么做?其实你就把上面这个工作原理中涉及的到技术点总结一下就行了。
还有三种,混个眼熟就行:Memcached 协议、Redis 协议、Rest 协议。
上图基本上把序列化的方式也罗列出来了。
详细请参考:Dubbo 官网。
可以。因为刚开始初始化的时候,consumer 会将需要的所有提供者的地址等信息拉取到本地缓存,所以注册中心挂了可以继续通信。但是 provider 挂了,那就没法调用了。
关键字:consumer 本地缓存服务列表。
服务订阅通常有 pull 和 push 两种方式:
Dubbo ZooKeeper 注册中心采用是事件通知与客户端拉取方式。服务第一次订阅的时候将会拉取对应目录下全量数据,然后在订阅的节点注册一个 watcher。一旦目录节点下发生任何数据变化,ZooKeeper 将会通过 watcher 通知客户端。客户端接到通知,将会重新拉取该目录下全量数据,并重新注册 watcher。利用这个模式,Dubbo 服务就可以做到服务的动态发现。
注意:ZooKeeper 提供了“心跳检测”功能,它会定时向各个服务提供者发送一个请求(实际上建立的是一个 socket 长连接),如果长期没有响应,服务中心就认为该服务提供者已经“挂了”,并将其剔除。
failover cluster 模式
provider 宕机重试以后,请求会分到其他的 provider 上,默认两次,可以手动设置重试次数,建议把写操作重试次数设置成 0。
failback 模式
失败自动恢复会在调用失败后,返回一个空结果给服务消费者。并通过定时任务对失败的调用进行重试,适合执行消息通知等操作。
failfast cluster 模式
快速失败只会进行一次调用,失败后立即抛出异常。适用于幂等操作、写操作,类似于 failover cluster 模式中重试次数设置为 0 的情况。
failsafe cluster 模式
失败安全是指,当调用过程中出现异常时,仅会打印异常,而不会抛出异常。适用于写入审计日志等操作。
forking cluster 模式
并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2"
来设置最大并行数。
broadcacst cluster 模式
广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。
默认使用 javassist 动态字节码生成,创建代理类,但是可以通过 SPI 扩展机制配置自己的动态代理策略。
这是很多面试官喜欢问的问题,本人认为其实他们没什么关联之处,但是硬是要问区别,那就说说吧。
回答的时候主要围绕着四个关键点来说:通信方式、注册中心、监控、断路器,其余像 Spring 分布式配置、服务网关肯定得知道。
通信方式
Dubbo 使用的是 RPC 通信;Spring Cloud 使用的是 HTTP RestFul 方式。
注册中心
Dubbo 使用 ZooKeeper(官方推荐),还有 Redis、Multicast、Simple 注册中心,但不推荐。;
Spring Cloud 使用的是 Spring Cloud Netflix Eureka。
监控
Dubbo 使用的是 Dubbo-monitor;Spring Cloud 使用的是 Spring Boot admin。
断路器
Dubbo 在断路器这方面还不完善,Spring Cloud 使用的是 Spring Cloud Netflix Hystrix。
分布式配置、网关服务、服务跟踪、消息总线、批量任务等。
Dubbo 目前可以说还是空白,而 Spring Cloud 都有相应的组件来支撑。
综上所述:
HTTP 协议的长连接和短连接,实质上是 TCP 协议的长连接和短连接。
短连接
在 HTTP/1.0 中默认使用短链接,也就是说,浏览器和服务器每进行一次 HTTP 操作,就建立一次连接,但任务结束就中断连接。如果客户端访问的某个 HTML 或其他类型的 Web 资源,如 JavaScript 文件、图像文件、CSS 文件等。当浏览器每遇到这样一个 Web 资源,就会建立一个 HTTP 会话。
长连接
从 HTTP/1.1 起,默认使用长连接,用以保持连接特性。在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输 HTTP 数据的 TCP 连接不会关闭。如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。Keep-Alive 不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如 Apache)中设定这个时间。
一级缓存
在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的 SQL,MyBatis 提供了一级缓存的方案优化这部分场景,如果是相同的 SQL 语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。
每个 SqlSession 中持有了 Executor,每个 Executor 中有一个 LocalCache。当用户发起查询时,MyBatis 根据当前执行的语句生成 MappedStatement,在 Local Cache 进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入 Local Cache,最后返回结果给用户。具体实现类的类关系图如下图所示:
二级缓存
在上文中提到的一级缓存中,其最大的共享范围就是一个 SqlSession 内部,如果多个 SqlSession 之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用 CachingExecutor 装饰 Executor,进入一级缓存的查询流程前,先在 CachingExecutor 进行二级缓存的查询,具体的工作流程如下所示。
二级缓存开启后,同一个 namespace 下的所有操作语句,都影响着同一个 Cache,即二级缓存被多个 SqlSession 共享,是一个全局的变量。
当开启缓存后,数据的查询执行的流程为:
二级缓存 -> 一级缓存 -> 数据库
\1. 装载相应的数据库的 JDBC 驱动并进行初始化:
Class.forName("com.mysql.jdbc.Driver");
\2. 建立 JDBC 和数据库之间的 Connection 连接:
Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test?characterEncoding=UTF-8", "root", "123456");
\3. 创建 Statement 或者 PreparedStatement 接口,执行 SQL 语句:
//查询用户信息 public List<User> findUserList(){ String sql = "select * from t_user order by user_id"; Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; //创建一个List用于存放查询到的User对象 List<User> userList = new ArrayList<>(); try { conn = DbUtil.getConnection(); pstmt =(PreparedStatement) conn.prepareStatement(sql); rs =(ResultSet) pstmt.executeQuery(); while(rs.next()){ int courseId = rs.getInt("user_id"); String courseName = rs.getString("user_name"); //每个记录对应一个User对象 User user = new User(); user.setUserId(courseId); user.setUserName(courseName); //将对象放到集合中 userList.add(course); } } catch(SQLException e) { e.printStackTrace(); }finally{ //资源关闭 DbUtil.close(pstmt); DbUtil.close(conn); } return userList; }
\4. 处理和显示结果。
\5. 释放资源。
动态 SQL 是 MyBatis 的主要特性之一,在 mapper 中定义的参数传到 xml 中之后,在查询之前 MyBatis 会对其进行动态解析。
MyBatis 为我们提供了两种支持动态 SQL 的语法:#{}
以及 ${}
。
#{}
是预编译处理,${}
是字符替换。在使用 #{} 时,MyBatis 会将 SQL 中的 #{}
替换成 ?
,配合 PreparedStatement 的 set 方法赋值,这样可以有效的防止 SQL 注入,保证程序的运行安全。
建议能不要用就不要用,“常在河边走哪能不湿鞋”。
UserMapper.xml 中:
<mapper namespace="com.tian.UserMapper">
反射生成 namespace 的对象:
boundType = Resources.classForName(namespace);
JDK 动态代理:
Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
总结:XML 中的 namespace="com.user.UserMapper"
接口 com.user.UserMapper 本身反射 JDK 动态代理实现接口。
select name from t_user where id=1
MySQL 的典型的三层结构(连接器 + Server + 执行器):
int(11) 中的 11,不影响字段存储的范围,只影响展示效果。
对于 SELECT COUNT(*) FROM table 语句,在没有 WHERE 条件的情况下,InnoDB 比 MyISAM 可能会慢很多,尤其在大表的情况下。因为,InnoDB 是去实时统计结果,会全表扫描;而 MyISAM 内部维持了一个计数器,预存了结果,所以直接返回即可。
面试扩散
此题还有另外一种问法:SELECT COUNT(*) FROM table
在使用存储引擎 InnoDB 和 MyISAM,谁更快,为什么?
建议:一般情况下,个人建议优先选择 InnoDB 存储引擎,并且尽量不要将 InnoDB 与 MyISAM 混合使用。
索引,类似于书籍的目录,想找到一本书的某个特定的主题,需要先找到书的目录,定位对应的页码。
MySQL 中存储引擎使用类似的方式进行查询,先去索引中查找对应的值,然后根据匹配的索引找到对应的数据行。
使用方法,在 SELECT 语句前加上 EXPLAIN 执行,查看其执行计划, 可以帮助选择更好的索引和写出更优化的查询语句,explain 执行计划对应每一列的详情,这里就不用再提了,网上一堆资料,但还是推荐看官网:
英语好的就直接看官网的说明,英语不好的可以使用浏览器搞个自动翻译的小插件,结合英文同步来看。
多版本并发控制(MVCC=Multi-Version Concurrency Control),是一种用来解决读 - 写冲突的无锁并发控制。也就是为事务分配单向增长的时间戳,为每个修改保存一个版本。版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照(复制了一份数据)。这样在读操作不用阻塞写操作,写操作不用阻塞读操作的同时,避免了脏读和不可重复读。
在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能。同时还可以解决脏读、幻读、不可重复读等事务隔离问题,但不能解决更新丢失问题。
MVCC 的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3 个隐式字段、undo 日志、Read View 来实现的。
生活中:吃饭使用一双筷子,但是如果你我各一支,你想吃饭我也想吃饭,你的那支不愿意给我,我的那支也不愿给你,这会就死锁了。
虽然进程在运行过程中,可能发生死锁,但死锁的发生也必须具备一定的条件。
死锁的发生必须具备以下四个必要条件:
关于 MySQL 的锁机制,可能会问很多问题,不过这也得看面试官在这方面的知识储备。
MySQL 中有共享锁和排它锁,也就是读锁和写锁。
悲观锁
说的是数据库被外界(包括本系统当前的其他事物以及来自外部系统的事务处理)修改保持着保守态度,因此在整个数据修改过程中,将数据处于锁状态。悲观的实现往往是依靠数据库提供的锁机制,也只有数据库层面提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统汇总实现了加锁机制,也是没有办法保证系统不会修改数据。
在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。
乐观锁
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。
而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本(Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个“version”字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
异步
场景:需要把 A 系统信息发给 B、C、D 系统。
用户发起请求总耗时 380,调用 A 系统耗时 80ms,A 系统调用 B 系统 100ms,A 系统调用 C 系统耗时 110ms,A 系统调用 D 系统耗时 90ms。其中 A 系统只管把数据推送出去就行。
如果引入 RabbitMQ 后,该请求总耗时 = A 系统耗时 + 发送 RabbitMQ 耗时,总耗时从 380ms 到 120ms 了。
可以看到通过 RabbitMQ 的异步功能,可以大大提高接口的性能。
解耦
场景:业务需要把用户信息从 A 系统推送到 B 系统和 C 系统,流程如下:
但是这回业务变化了,还需要把 A 系统把用户信息推送给 D 系统和 E 系统。如果还按照上面系统那种模式,那么 A 系统得又得新开发。就变成下面这样:
如果引入 RabbitMQ 的话:
往后不管有多少个系统需要 A 系统推送用户信息,A 系统就不用变,只是对应系统去 RabbitMQ 取数据就可以搞定了。
削峰
场景:A 系统常规情况下,每秒并发 200,但是高峰时间可能会到几千或者上万,但是数据库每秒只能处理 1000 左右,多了会把数据库搞死。正常情况下:
如果引入 RabbitMQ 的话,就是先把消息给 RabbitMQ,然后慢慢入库,这样数据库就不会有太大压力了。
削峰就类似于银行办理业务,正常情况下,几个窗口够用,但是在节假日去银行办理业务的人太多了,几个窗口就支持不了,窗口边会站满了人。于是就可以采取取号制度,先拿号码去大厅坐着,我们一个一个来。(如有觉得场景不妥的,望谅解)
上面三种场景能回答上来,基本上就可以避免“我们领导叫我们这么用的”尴尬场面。
真实场景:
到这里相信大家面试的时候,就算你没用过,编也能编出一个你项目中的场景了吧。
这里就得把常见的消息队列都列出来进行对比,如果是做技术选型的话,这也是必须要考察的。短期为了面试,长期为了在做架构时的技术选型。
DLX=Dead-Letter-Exchange。利用 DLX,当消息在一个队列中变成死信(dead message)之后,它能被重新 publish 到另一个 Exchange,这个 Exchange 就是 DLX。
从字面上就看得出,死信就是无法被消费的消息。producer 将消息传给 broker 或者 queue 中,consumer 从 queue 取出消息进行消费。但是在某些特殊情况下会导致 queue 中部分消息无法被 consumer 消费,这样一直没有被消费的消息就变成了私信,既然有私信了,相应也有了死信队列。
消息变成死信一般有以下几种情况:
关于死信的出现既然不可避免,那么就需要从实际业务场景去考虑这个问题。对这些私信怎么处理,常见的处理办法有以下几种:
有三种场景会导致消息丢失:
总结为:生产者搞丢数据、RabbitMQ 搞丢数据、消费者搞丢数据。
生产者搞丢数据
事务功能机制
使用 RabbitMQ 的事务功能,就是生产者发送数据之前开启 RabbitMQ 事务 channel.txSelect,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务 channel.txRollback,然后重试发送消息;如果收到了消息,那么可以提交事务 channel.txCommit。
但是这种方案是存在问题的,即 RabbitMQ 事务机制(同步)一搞,基本上吞吐量会下来,因为太耗性能。
confirm 机制
在生产者那里设置开启 confirm 模式后,你每次写的消息都会分配一个唯一的 ID。如果写入 RabbitMQ 成功后会回传一个 ack 消息,告诉你这个消息已经到达 RabbitMQ 了;如果没收到你的消息或者失败了,则会回调你的一个 nack,告诉你这个消息 RabbitMQ 接受失败,然后你就可以继续重试发送,而且你可以结合这个机制在内存里维护一个 ID 的状态。如果超过一定时间没收到回调,那么就可以再次发送消息。所以一般在生产者这方避免数据丢失,都是使用 confirm 机制。
事务机制 PK confirm 模式
事务机制是同步的,提交事务后会阻塞在那里等待。
confirm 是异步的,发送这个消息后就可以发送下一个消息了。消息被 RabbitMQ 接收之后会异步回调一个通知,告知你这个消息已接收到了。
RabbitMQ 搞丢数据
RabbitMQ 接收到消息,默认是放在内存里,如果系统挂了或者重启,那对应的消息就会丢失。所以选择开启持久化,把消息写入磁盘中,这样就算系统挂了或者重启都不会丢失消息。
持久化的两个步骤:
上面两个持久化必须同时设置才行。这个 RabbitMQ 就算挂了,再次重启的时候也会从磁盘上重启恢复 queue 和 queue 里的数据。
持久化和 confirm 模式一起使用,就算在消息持久化之前,RabbitMQ 挂了、数据丢了、生产者收不到 ack 的通知时,咱们也可以选择重新发送数据。
消费端搞丢消息数据
消费端代码中可能有 bug,异常没有处理导致消费失败,或者系统重启、挂了等,那么 RabbitMQ 认为咱们已经消费了,所以对应消息数据就会丢失了。
这时候我们就得使用 RabbitMQ 的 ack 机制。得把自动 ack 关闭,有个 api 直接调用,然后在自己代码里,确保消费者真的成功消费完成后,再进行一个手动 ack。
**总结 **
单机模式
不属于高可用,单机模式就是启动单个 RabbitMQ 节点,一般用于本地开发或者测试环境。实际生产上,基本不会使用这种单节点模式。
普通集群模式
不属于高可用,普通集群模式就是在多台机器上启动多个 RabbitMQ 实例,每个机器各自启动一个。
你创建的 queue,只会放在一个 RabbitMQ 实例上,但是每个实例都是同步 queue 的元数据(元数据可以认为是 queue 的一些配置基本信息,通过元数据,可以找到 queue 所在的实例)。
你消费的时候,如果连接到另外一个实例,那个实例就会从 queue 所在的实例上把数据拉取过来。
上面这种普通集群方式确实很麻烦,给人的感觉不是很好,没有做到真正的分布式,就是一个普通的集群。
因为这导致要么消费者每次都链接一个实例然后拉取消息数据,要么固定连接那个 queue 所在实例消费数据。前者有数据拉取的开销,后者导致实例性能瓶颈。
如果消息放的 queue 挂了,会导致接下来其他实例无法从该实例上拉取消息数据。如果开启了消息持久化,让 RabbitMQ 本地持久化,消息不一定会丢。得等到这个实例重启恢复后,才可以继续从这个 queue 上拉取消息数据。
所以上面这种模式,就没有所谓的高可用。这方案主要是提高吞吐量的,就是说让集群中多个节点来服务 queue 的读写操作。
镜像集群模式
高可用模式,镜像集群模式才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你所创建的 queue,无论元数据还是 queue 里的消息数据都会存在于多个实例上。也就是说,每个 RabbitMQ 都有 queue 的一个完整镜像,包含 queue 的全部数据。每次写消息到 queue 的时候,都会自动把消息数据同步到多个实例的 queue 上。
那么,要如何才能开启这个镜像集群模式呢?其实很简单,RabbitMQ 有很好的管理控制台,在后台新增一个策略,这个策略就是镜像集群模式的策略。指定的时候可以要求数据同步到所有节点,也可以要求同步到指定数量的节点上。再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的结点上了。
可以看到,不管任何一个机器挂了、宕了,都没影响。其他机器或者节点上还包括这个 queue 的完整数据,其他消费者都可从其他节点上去拉取消息数据进行消费。但有坏处:
所以,以上三种模式都不是绝对的高可用模式,只是相对的。
与 Kafka 和 RocketMQ 不同,Kafka 不存在类似 Topic 的概念,而是真正的一条一条队列,并且每个队列可以被多个消费者拉取消息。这是一个非常大的差异。
在 RabbitMQ 中,一个 queue,多个 consumer。比如:生产者向 RabbitMQ 里发送了三条数据,顺序依次为是 data1/data2/data3,压入的是 RabbitMQ 的内存队列里。有三个消费者分别从 RabbitMQ 中消费三条数据中的一条,结果消费者 2 先执行完操作,把 data2 存入数据库,然后是 data1/data3。这不明显乱了,没有按照顺序消费消息。
应对上面的顺序消费问题有两种方案:
方案一
拆分多个 queue,每个 queue 对应一个 consumer,就是多一些 queue 而已,确实也麻烦了些。
这种方式,有点类似于 Kafka 和 RocketMQ 中 Topic 的概念。比如说,原先一个 queue 叫 aaa,那么多个 queue,我们就可以搞成 aaa-01,aaa-02,aaa-03 等,相同前缀,不同后缀。
方法二
就一个 queue,但对应一个 consumer,consumer 内部用内存做队列、做排队,然后分发给底层不同的 worker 来处理。这种方式就是将一个 queue 里的,相同 key 交给同一个 worker 来执行。因为 RabbitMQ 是可以单条消息来 ack,所以较为方便。
从上面两个方案可以看出,前提都是一个 queue 只能启动一个 consumer 对应。
通常遇到这种情况,只能是搞个临时扩容,具体步骤和思路如下:
下面大致搞一个图来表示。
解决之前的架构一:
解决消息堆积的临时架构二:
问题处理完了,还得把架构改成架构一。
使用缓存的目的就是提升读写性能。而实际业务场景下,更多的是为了带来更好的性能、更高的并发量。Redis 的读写性能比 MySQL 好的多,我们就可以把 MySQL 中的热点数据缓存到 Redis 中,提升读取性能,同时也减轻了 MySQL 的读取压力。
这时候可以回答 Memcached 与 Redis 区别:
问这问题是因为前面回答的时候提到了“Redis 是基于非阻塞的 IO 复用模型”。如果这个问题回答不上来,就相当于前面的回答是给自己挖坑,面试官对你的印象可能也会打点折扣。
Redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。
文件事件处理器的结构包含 4 个部分:
多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件。但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队。事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
来看客户端与 Redis 的一次通信过程:
下面来大致说一下这个图:
这样便完成了一次通信。不要怕这段文字,结合图看,一遍不行两遍,实在不行可以网上查点资料结合着看。一定要搞清楚,否则前面吹的牛逼就白费了。
优点
缺点
Redis 提供两种持久化机制 RDB 和 AOF 机制:
RDB 持久化方式
指用数据集快照的方式半持久化模式记录 redis 数据库的所有键值对,在某个时间点将数据写入一个临时文件。持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。
优点:
缺点:
数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化期间 Redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候。
AOF=Append-only file 持久化方式
是指所有的命令行记录,以 Redis 命令请求协议的格式完全持久化存储,保存为 AOF 文件。
优点:
缺点:
一般使用 list 结构作为队列,rpush 生产消息,lpop 消费消息。当 lpop 没有消息的时候,要适当 sleep 一会再重试。
面试扩散:很多面试官上来就直接这么问: Redis 如何实现延时队列?
\1. Redis Sentinel
体量较小时,选择 Redis Sentinel,单主 Redis 足以支撑业务。
\2. Redis Cluster
Redis 官方提供的集群化方案,体量较大时,选择 Redis Cluster,通过分片,使用更多内存。
\3. Twemprox
Twemprox 是 Twtter 开源的一个 Redis 和 Memcached 代理服务器,主要用于管理 Redis 和 Memcached 集群,减少与 Cache 服务器直接连接的数量。
\4. Codis
Codis 是一个代理中间件,当客户端向 Codis 发送指令时,Codis 负责将指令转发到后面的 Redis 来执行,并将结果返回给客户端。一个 Codis 实例可以连接多个 Redis 实例,也可以启动多个 Codis 实例来支撑,每个 Codis 节点都是对等的,这样可以增加整体的 QPS 需求,还能起到容灾功能。
\5. 客户端分片
在 Redis Cluster 还没出现之前使用较多,现在基本很少人使用了。在业务代码层实现,启动几个毫无关联的 Redis 实例,在代码层,对 Key 进行 hash 计算,然后去对应的 Redis 实例操作数据。这种方式对 hash 层代码要求比较高,考虑部分包括节点、失效后的替代算法方案、数据震荡后的自动脚本恢复、实例的监控等等。
解决方案
上面这方案解决了数据不一致的问题,主要是使用了串行化,每次操作进来必须按照顺序进行。如果某个队列元素积压太多,可以针对读请求进行过滤,提示用户刷新页面,重新请求。
潜在的问题,留给大家自己去想了,因为这属于发散性问题。如下给出两点思考提示:
独立运行 Spring 项目
Spring Boot 可以以 jar 包形式独立运行,运行一个 Spring Boot 项目只需要通过 java -jar xx.jar 来运行。
内嵌 Servlet 容器
Spring Boot 可以选择内嵌 Tomcat、Jetty 或者 Undertow,这样我们无须以 war 包形式部署项目。
提供 Starter 简化 Maven 配置
Spring 提供了一系列的 starter pom 来简化 Maven 的依赖加载,比如:spring-boot-starter-web。
自动配置 Spring Bean
Spring Boot 检测到特定类的存在,就会针对这个应用做一定的配置,进行自动配置 Bean,这样会极大地减少我们要使用的配置。当然,Spring Boot 只考虑大多数的开发场景,并不是所有的场景,若在实际开发中我们需要配置 Bean,而 Spring Boot 没有提供支持,则可以自定义自动配置进行解决。
准生产的应用监控
Spring Boot 提供基于 HTTP、JMX、SSH 对运行时的项目进行监控。
无代码生成和 XML 配置
Spring Boot 没有引入任何形式的代码生成,它使用的是 Spring 4.0 的条件 @Condition 注解以实现根据条件进行配置。同时使用了 Maven /Gradle 的依赖传递解析机制来实现 Spring 应用里面的自动配置。
package cn.tian.spring.boot.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
@SpringBootApplication 注解,就是 Spring Boot 的核心注解。
@SpringBootApplication 源码:
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan( excludeFilters = {@Filter( type = FilterType.CUSTOM, classes = TypeExcludeFilter.class}), @Filter( type = FilterType.CUSTOM,classes = {AutoConfigurationExcludeFilter.class})}) public @interface SpringBootApplication { @AliasFor( annotation = EnableAutoConfiguration.class) Class<?>[] exclude() default {}; @AliasFor(annotation = EnableAutoConfiguration.class) String[] excludeName() default {}; @AliasFor(annotation = ComponentScan.class,attribute = "basePackages") String[] scanBasePackages() default {}; @AliasFor(annotation = ComponentScan.class,attribute = "basePackageClasses") Class<?>[] scanBasePackageClasses() default {}; }
这个注解主要由三个注解组合:
太多了 starter 了,这里只是例举几个。
依赖管理是任何复杂项目的关键部分。以手动的方式来实现依赖管理不太现实,你得花更多时间,这样你在项目其他重要的方面能付出的时间就会变得越少。Starter 主要用来简化依赖用的。
比如我们之前做 MVC 时要引入日志组件,那么需要去找到 log4j 的版本,然后引入。现在有了 Starter 之后,直接用这个之后,log4j 就自动引入了,也不用关心版本这些问题。
比如我们要在 Spring Boot 中引入 Web MVC 的支持时,我们通常会引入这个模块 spring-boot-starter-web,而这个模块如果解压出来会发现里面什么都没有,只定义了一些 POM 依赖。
类图
包目录不同
国际化
BeanFactory 是不支持国际化功能的,因为 BeanFactory 没有扩展 Spring 中 MessageResource 接口。相反,由于 ApplicationContext 扩展了 MessageResource 接口,因而具有消息处理的能力(i18N)。
强大的事件机制(Event)
基本上牵涉到事件(Event)方面的设计,就离不开观察者模式,ApplicationContext 的事件机制主要通过 ApplicationEvent 和 ApplicationListener 这两个接口来提供的,和 Java swing 中的事件机制一样。即当 ApplicationContext 中发布一个事件时,所有扩展了 ApplicationListener 的 Bean 都将接受到这个事件,并进行相应的处理。
底层资源的访问
ApplicationContext 扩展了 ResourceLoader(资源加载器)接口,从而可以用来加载多个 Resource,而 BeanFactory 是没有扩展 ResourceLoader。
对 Web 应用的支持
与 BeanFactory 通常以编程的方式被创建,ApplicationContext 能以声明的方式创建,如使用 ContextLoader。
当然你也可以使用 ApplicationContext 的实现方式之一,以编程的方式创建 ApplicationContext 实例。
延迟加载
可以看到,ApplicationContext 继承了 BeanFactory,BeanFactory 是 Spring 中比较原始的 Factory,它不支持 AOP、Web 等 Spring 插件。而 ApplicationContext 不仅包含了 BeanFactory 的所有功能,还支持 Spring 的各种插件,还以一种面向框架的方式工作以及对上下文进行分层和实现继承。
BeanFactory 是 Spring 框架的基础设施,面向 Spring 本身;而 ApplicationContext 面向使用 Spring 的开发者,相比 BeanFactory 提供了更多面向实际应用的功能,几乎所有场合都可以直接使用 ApplicationContext,而不是底层的 BeanFactory。
常用容器
BeanFactory 类型的有 XmlBeanFactory,它可以根据 XML 文件中定义的内容,创建相应的 Bean。
ApplicationContext 类型的常用容器有:
这个题目在面试的时候被问到的概率很大,主要考察咱们对 bean 的整个生命周期的了解程度,其实平时工作中很少需要你关注 bean 的生命周期。
Bean 的声明周期为 bean 的创建、应用、销毁。
创建过程
应用
销毁过程
销毁过程会依次调用 DisposableBean 的 destroy 方法和 Bean 自身定制的 destroy 方法。
网上找到一张图:
注:网络上很多文章说有 Global-session 级别,它是 Portlet 模块独有,目前已经废弃,在 Spring 5 中是找不到的。
AOP 的设计:
整体分析
\1. 代理的创建
注意:创建代理对象时,同时会创建一个外层拦截器,这个拦截器就是 spring 内核的拦截器,用于控制整个 AOP 流程。
\2. 代理的调用
咋一看觉得动态代理和装饰器模式很像,都是功能增加,但是,这两个模式的本质是不一样的,代理重在访问权限的控制,装饰器模式重在动态地对原本的功能进行改变。
简单工厂模式:Spring 中的 BeanFactory 就是简单工厂模式的体现。根据传入一个唯一的标识来获得 Bean 对象,但是在传入参数后创建还是传入参数前创建,要根据具体情况来定。
工厂模式:Spring 中的 FactoryBean 就是典型的工厂方法模式,实现了 FactoryBean 接口的 bean 是一类叫做 factory 的 bean。其特点是,spring 在使用 getBean() 调用获得该 bean 时,会自动调用该 bean 的 getObject() 方法,所以返回的不是 factory 这个 bean,而是这个 bean.getOjbect() 方法的返回值。
单例模式:在 spring 中用到的单例模式有:scope="singleton"
,注册式单例模式,bean 存放于 Map 中。bean name 当做 key,bean 当做 value。
原型模式:在 spring 中用到的原型模式有:scope="prototype"
,每次获取的是通过克隆生成的新实例,对其进行修改时对原有实例对象不造成任何影响。
迭代器模式:在 Spring 中有个 CompositeIterator 实现了 Iterator,Iterable 接口和 Iterator 接口,这两个都是迭代相关的接口。可以这么认为,实现了 Iterable 接口,则表示某个对象是可被迭代的。Iterator 接口相当于是一个迭代器,实现了 Iterator 接口,等于具体定义了这个可被迭代的对象时如何进行迭代的。
代理模式:Spring 中经典的 AOP,就是使用动态代理实现的,分 JDK 和 CGlib 动态代理。
适配器模式:Spring 中的 AOP 中 AdvisorAdapter 类,它有三个实现:MethodBeforAdviceAdapter、AfterReturnningAdviceAdapter、ThrowsAdviceAdapter。Spring 会根据不同的 AOP 配置来使用对应的 Advice,与策略模式不同的是,一个方法可以同时拥有多个 Advice。Spring 存在很多以 Adapter 结尾的,大多数都是适配器模式。
观察者模式:Spring 中的 Event 和 Listener。spring 事件:ApplicationEvent,该抽象类继承了 EventObject 类,JDK 建议所有的事件都应该继承自 EventObject。spring 事件监听器:ApplicationListener,该接口继承了 EventListener 接口,JDK 建议所有的事件监听器都应该继承 EventListener。
模板模式:Spring 中的 org.springframework.jdbc.core.JdbcTemplate 就是非常经典的模板模式的应用,里面的 execute 方法,把整个算法步骤都定义好了。
责任链模式:DispatcherServlet 中的 doDispatch() 方法中获取与请求匹配的处理器 HandlerExecutionChain,this.getHandler() 方法的处理使用到了责任链模式。
Spring 框架并没有对单例 Bean 进行任何多线程的封装处理。
当然,但实际上,大部分的 Spring Bean 并没有可变的状态,所以在某种程度上说 Spring 的单例 Bean 是线程安全的。如果你的 Bean 有多种状态的话,就需要自行保证线程安全。最浅显的解决办法,就是将多态 Bean 的作用域(Scope)由 Singleton 变更为 Prototype。
整个流程大致如下:
ZooKeeper 是一个开放源码的分布式协调服务,它是集群的管理者,监视着集群中各个节点的状态。根据节点提交的反馈进行下一步合理操作,最终将简单易用的接口和性能高效、功能稳定的系统提供给用户。ZooKeeper 是 Chubby 的开源实现,使用 ZAB 协议(Paxos 算法的变种)。
分布式应用程序可以基于 ZooKeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。
统一命名服务
命名服务是指通过指定的名字来获取资源或服务的地址,利用 ZooKeeper 创建一个全局的路径,即时唯一的路径。这个路径就可以作为一个名字,指向集群中机器或者提供服务的地址,又或者一个远程的对象等。
分布式服务
相对来说,这个功能使用还是比较广泛的。ZooKeeper 实现的分布式锁的可靠性比 Redis 实现的高,当然相对性能来说,ZooKeeper 性能稍弱,但其实已经很牛了。
配置管理
Spring Cloud Config ZooKeeper 就是基于 ZooKeeper 来实现的,提供配置中心的服务。
注册与发现
是否有新的机器加入或者是有机器退出(挂了)。所有机器约定在父目录下创建临时节点,然后监听父节点下的子节点变化。一旦有机器挂机,该机器与 ZooKeeper 的链接断开,其所创建的临时目录节点也被删除,所有其他机器都收到对应的通知:某个结点被删除了。Dubbo 就是典型应用案例。
Master 选举
基于 ZooKeeper 实现分布式协调,从而实现主从的选举。比如:Kafka、Elastic-job 等中间件都有使用到。
队列管理
ZooKeeper 有两种类型的队列:
同步队列:当一个队列的成员都聚齐时,这个队列才可用,否则一直等待。在约定的目录下创建临时目录节点,再监听节点数目是否是我们要求的数据。
队列按照先进先出 FIFO 方式进行入队和出队操作。和分布式锁服务中心的控制时序的场景基本原理相同,入列和出列都有编号。创建 PERSISTENT_SEQUENTIAL 节点,创建成功时 Watcher 通知等待的队列,队列删除序列号最小的节点以消费。此场景下,znode 用于消息存储,znode 存储的数据就是消息队列中的消息内容,SEQUENTIAL 序列号就是消息的编号,按序取出即可。由于创建的节点是持久化的,所以不必担心队列消息丢失的问题。
分布式锁
有了 ZooKeeper 的一致性文件系统,锁的问题就变得简单多了。锁服务可以分为保持独占和控制时序。
ZooKeeper 允许客户端向服务端的某个 znode 注册一个 Watcher 监听,当服务端的一些指定事件触发了这个 Watcher,服务端会向指定客户端发送一个事件通知来实现分布式的通知功能,然后客户端根据 Watcher 通知状态和事件类型做出业务上的改变。
大致分为三个步骤:
客户端注册 Watcher
服务端处理 Watcher
客户端回调 Watcher
不是,一次性的。无论是服务端还是客户端,一旦一个 Watcher 被触发, ZooKeeper 都会将其从相应的存储中移除。这样的设计有效地减轻了服务端的压力,不然对于更新非常频繁的节点,服务端会不断地向客户端发送事件通知,无论对于网络还是服务端的压力都非常大。
在一个集群中,最少需要 3 台。或者保证 2N+1 台,即奇数。为什么保证奇数?主要是为了选举算法。
LOOKING
寻找 Leader 状态。当服务器处于该状态时,它会认为当前集群中没有 Leader,因此需要进入 Leader 选举状态。
FOLLOWING
跟随者状态。表明当前服务器角色是 Follower。
LEADING
领导者状态。表明当前服务器角色是 Leader。
OBSERVING
观察者状态。表明当前服务器角色是 Observer。
当 Leader 崩溃了,或者失去了大多数的 Follower,这时候 ZooKeeper 就进入恢复模式,恢复模式需要重新选举一个新的 Leader,让所有的 Server 都恢复到一个状态 LOOKING。
ZooKeeper 有两种选举算法:基于 basic paxos 实现和基于 fast paxos 实现。默认为 fast paxos。
由于篇幅问题,这里推荐:选举流程。
发出事务执行请求,如果超过半数的机器都能执行并且能够成功,那么就会开始执行。
Leader 服务器会和每一个 Follower/Observer 服务器都建立 TCP 连接,同时为每个 Follower/Observer 都创建一个叫做 LearnerHandler 的实体。
如果有客户端 1、客户端 2 等 N 个客户端争抢一个 ZooKeeper 分布式锁。大致如下:
而且用临时顺序节点的另外一个用意就是,如果某个客户端创建临时顺序节点之后,不小心自己宕机了也没关系。ZooKeeper 感知到那个客户端宕机,会自动删除对应的临时顺序节点,相当于自动释放锁,或者是自动取消自己的排队。
创建线程的常用四种方式:
通过继承 Thread 类或者实现 Runnable 接口、Callable 接口都可以实现多线程,不过实现 Runnable 接口与实现 Callable 接口的方式基本相同,只是 Callable 接口里定义的方法返回值,可以声明抛出异常而已。因此将实现 Runnable 接口和实现 Callable 接口归为一种方式。这种方式与继承 Thread 方式之间的主要差别如下:
采用实现 Runnable、Callable 接口的方式创建线程的优缺点
采用继承 Thread 类的方式创建线程的优缺点:
先来看一张图:
这六个状态就对应线程的生命周期。下图为线程对应状态以及状态出发条件:
JVM 基于进入和退出 Monitor 对象来显示方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用 monitorenter 和 monitorexit 指令实现的,而方法同步是使用另外一种方式实现的。
monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法的结束处和异常处,JVM 保证每个 monitorenter 必须有对应的 monitorexit 与之配对。
try{--objectLock.lock();}---finally--{objectLock.unlock();}
,synchronized 隐形释放(方法或者代码块执行完、异常)。按照《Java 并发编程实战》(Java Concurrency in Practice) 的定义就是:线程安全是一个多线程环境下正确性的概念,也就是保证多线程环境下共享的、可修改的状态的正确性,这里的状态反映在程序中其实可以看作是数据。
通俗易懂的说法:当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替执行,我们在主程序中不需要做任何同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。
线程之间的通信有两种方式:共享内存和消息传递。
共享内存
在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写 - 读内存中的公共状态来隐式进行通信。典型的共享内存通信方式,就是通过共享对象进行通信。
例如上图线程 A 与 线程 B 之间如果要通信的话,那么就必须经历下面两个步骤:
消息传递
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。在 Java 中典型的消息传递方式,就是 wait() 和 notify(),或者 BlockingQueue。
调用 run 方法不会再启一个线程,跟着主线程继续执行,和调普通类的方法一样;
调用 start 方法表示启动一个线程。
面试扩散
下面代码将输出什么内容?不清楚的建议自己去试试。
public class ThreadDemo {
public static void main(String[] args) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("test start");
}
});
thread.start();
thread.start();
}
}
通过 java.util.concurrent.Executors 来创建以下常见线程池:
也可以通过 java.util.concurrent.ThreadPoolExecutor 来创建自定义线程池,其中核心的几个参数:
int corePoolSize, //核心线程数量
int maximumPoolSize, //最大线程数
long keepAliveTime, //超时时间,超出核心线程数量以外的线程空余存活时间
TimeUnit unit, //存活时间单位
BlockingQueue<Runnable> workQueue, //保存执行任务的队列
ThreadFactory threadFactory,//创建新线程使用的工厂
RejectedExecutionHandler handler //当任务无法执行的时候的处理方式
线程池原理:
推荐阅读:
Semaphore
是一种新的同步类,它是一个计数信号。从概念上讲,从概念上讲,信号量维护了一个许可集合。
信号量常常用于多线程的代码中,比如数据库连接池。
CountDownLatch
字面意思是减小计数(CountDown)的门闩(Latch)。它要做的事情是,等待指定数量的计数被减少,意味着门闩被打开,然后进行执行。CountDownLatch 默认的构造方法是 CountDownLatch(int count),其参数表示需要减少的计数,主线程调用 await() 方法告诉 CountDownLatch 阻塞等待指定数量的计数被减少,然后其它线程调用 CountDownLatch 的 CountDown() 方法,减小计数(不会阻塞)。等待计数被减少到零,主线程结束阻塞等待,继续往下执行。
CyclicBarrier
字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 await() 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞,直到 parties 个线程到达,结束阻塞。
CyclicBarrier 可以重复使用,而 CountdownLatch 不能重复使用。
CountdownLatch 其实可以把它看作一个计数器,只不过这个计数器的操作是原子操作。可以向 CountdownLatch 对象设置一个初始的数字作为计数值,任何调用这个对象上的 await() 方法都会阻塞,直到这个计数器的计数值被其他的线程减为 0 为止。所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次 —— 计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier。
CountdownLatch 的一个非常典型的应用场景是:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行。假如我们这个想要继续往下执行的任务调用一个 CountdownLatch 对象的 #await() 方法,其他的任务执行完自己的任务后调用同一个 CountdownLatch 对象上的 countDown() 方法,这个调用 #await() 方法的任务将一直阻塞等待,直到这个 CountdownLatch 对象的计数值减到 0 为止。 CyclicBarrier 一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点(common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环的 barrier。
这里肯定是巴拉巴拉一堆设计模式,但是记住最好说清楚,自己最熟悉那几个设计模式,这样面试官就可以问你最熟悉的了。
比如:
模板模式推荐:
代理模式详解推荐:
代理模式 VS 装饰模式:
工厂方法模式 VS 建造者模式:
初中级应对的方法就是搞几个自己很熟悉的,然后面试的时候强调一下自己最熟悉的,然后结合 Spring 中或者 MyBatis 中的来说一下。高级可能就没那么简单的,至少得知道的多点,尤其是要学会对比,比如说:工厂方法模式 VS 建造者模式有什么区别。
建议熟悉一下常用设计模式实际使用案例,例如 Spring 篇、MyBatis 篇中。
推荐在线阅读:
所以一共称了两次。
在高并发场景的架构里,幂等性是必须得保证的。比如说支付功能,用户发起支付,如果后台没有做幂等校验,刚好用户手抖多点了几下,于是后台就可能多次收到同一个订单请求。不做幂等很容易就让用户重复支付了,这样用户是肯定不能忍的。
解决方案
\1. 查询和删除不在幂等讨论范围,查询肯定没有幂等的说法,删除:第一次删除成功后,后面来删除直接返回 0,也是返回成功。
\2. 建唯一索引:唯一索引或唯一组合索引来防止新增数据存在脏数据 (当表存在唯一索引,并发时新增异常时,再查询一次就可以了,数据应该已经存在了,返回结果即可)。
\3. token 机制:由于重复点击或者网络重发,或者 nginx 重发等情况会导致数据被重复提交。前端在数据提交前要向后端服务的申请 token,token 放到 Redis 或 JVM 内存,token 有效时间。提交后后台校验 token,同时删除 token,生成新的 token 返回。Redis 要用删除操作来判断 token,删除成功代表 token 校验通过,如果用 select+delete 来校验 token,存在并发问题,不建议使用。
\4. 悲观锁:
select id ,name from table_# where id='##' for update;
悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用(另外还要考虑 id 是否为主键,如果 id 不是主键或者不是 InnoDB 存储引擎,那么就会出现锁全表)。
\5. 乐观锁,给数据库表增加一个 version 字段,可以通过这个字段来判断是否已经被修改了。
update table_xxx set name=#name#,version=version+1 where version=#version#
\6. 分布式锁,比如 Redis、ZooKeeper 的分布式锁。单号为 key,然后给 Key 设置有效期(防止支付失败后,锁一直不释放),来一个请求使用订单号生成一把锁,业务代码执行完成后再释放锁。
\7. 保底方案:先查询是否存在此单,不存在此支付单据(上锁),存在就直接返回支付结果。
\1. DNS 解析(通过访问的域名找出其 IP 地址,递归搜索)。
\2. HTTP 请求,当输入一个请求时,建立一个 Socket 连接发起 TCP 的 3 次握手。
如果是 HTTPS 请求,会略微有不同。等到 HTTPS 小节,我们再来讲。
\3. 客户端向服务器发送请求命令(一般是 GET 或 POST 请求),客户端发送请求头信息和数据。
这个是补充内容,面试一般不用回答。
客户端的网络层不用关心应用层或者传输层的东西,主要做的是通过查找路由表确定如何到达服务器,期间可能经过多个路由器,这些都是由路由器来完成的工作,我不做过多的描述,无非就是通过查找路由表决定通过哪个路径到达服务器。
客户端的链路层,包通过链路层发送到路由器,通过邻居协议查找给定 IP 地址的 MAC 地址,然后发送 ARP 请求查找目的地址。如果得到回应后就可以使用 ARP 的请求应答交换的 IP 数据包,然后发送 IP 数据包到达服务器的地址。
\4. 服务器发送应答头信息,服务器向客户端发送数据。
\5. 服务器关闭 TCP 连接(4 次挥手)。
这里是否关闭 TCP 连接,也根据 HTTP Keep-Alive 机制有关。
同时,客户端也可以主动发起关闭 TCP 连接。
\6. 客户端根据返回的 HTML、CSS、JS 进行渲染。
下面使用《图解 HTTP》里的一张图:
更多方案详情,推荐阅读:
常用 JVM 调优工具分为两类:
JDK 自带监控工具:jconsole 和 jvisualvm 2、第三方有:MAT(Memory Analyzer Tool)、GChisto。
内存溢出
即 OutOfMemoryError,当没有足够的空闲内存可供程序使用时出现。
内存泄漏
内存使用后未得到及时释放,又不能被 GC 回收,导致虚拟机不能再次使用该内存,此时这段内存就泄露了。
常见避免方法:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。