1、受检异常和非受检异常的区别?
所有的异常都是继承至Throwable,包括Error和Exception两个大类
Error: 不用捕获,通常是一些底层和硬件的错误,与程序本身无关
Exception:
非受检异常:程序本身的异常,如果不主动捕获的,会由jvm去进行处理
受检异常:IOException/SQLException ,必须要去捕获的异常
当发生异常时,可以通过try catch去捕获异常,或通过throws抛出去。
eg:在用户修改密码时,对于开放的临时用户的密码时不允许修改的,修改的时候可以抛出RejectException
这样去做,之前调用的代码就会发生变化,因为异常是主逻辑的补充逻辑,修改一个补充逻辑就改了主逻辑,这样是不行的
实现类的变更会影响到调用者,这样破坏了封装性。因为一行代码多个捕获条件,使用降低了代码的可读性。
这是在使用受检异常时可能会引发的问题。
什么时候使用受检异常?
例如 IOException/SQLException ,必须要去捕获的异常 。 受检异常可以转化为非受检异常,当受检异常威胁到系统的安全性、稳定性的时候必须要去处理。
2、软引用、弱引用、虚引用、强引用的对比?
强引用:new Object(); 不会被回收,宁愿抛出OOM异常也不去回收。
public class ReferenceDemo {
static Object strongRef = new Object();
public static void main(String[] args) {
Object obj = strongRef;
strongRef=null;
System.gc();
System.out.println("gc之后:"+obj);
}
}
之前当前这个类会卸载的时候才会被回收,当obj=null的时候可以被回收。
回收的机制有两个:脱离作用域、这个对象被设置为null的时候。
软引用:SoftReference,用来描述一些有用但并不是必需的对象,对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。
弱引用:WeakReference,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
WeakReference<String> sr = new WeakReference<String>(new String("hello"));
System.out.println(sr.get());
System.gc(); //通知JVM的gc进行垃圾回收
System.out.println(sr.get());
虚引用:(PhantomReference)并不影响对象的生命周期,在任何时候都可能被垃圾回收器回收。。
可以用来跟踪对象看时候被释放,释放之后可以做一些处理。
3、Integer的原理?
对于Integer,如果想要交换a和b的值,那么如果用下面的swap方法是无法进行交换的。因为java中是按值传递的。如果传递的是基本类型,那么函数接收的是函数原始值的副本,因此如果改变这个函数的值,只是改变了原始值的副本,而原始值不变.如果传的是引用类型,那么函数接收的是原始引用类型的内存地址而不是值的副本,因此如果改变这个函数的值,会改变原始值.
public class Swap {
public static void main(String[] args) {
Integer a=1,b=2;
System.out.println("before:a="+a+",b="+b);
swap(a,b);
System.out.println("after:a="+a+",b="+b);
}
//交换两个数的值
public static void swap(Integer i1,Integer i2){
Integer tmp = i1;
i1=i2;
i2=tmp;
}
}
结果:
before:a=1,b=2
after:a=1,b=2
Integer源码中:定义了value是final的。
private final int value;
解决方案:使用反射来处理
//交换两个数的值
public static void swap(Integer i1,Integer i2) throws Exception {
Field field=Integer.class.getDeclaredField("value");
field.setAccessible(true);
int tmp=i1.intValue();
field.set(i1,i2.intValue());
field.set(i2,tmp);
}
结果:
before:a=1,b=2
after:a=2,b=2
但是反射也会出现一个问题,就是只换成功了一个,另一个却还是2,交换后应该是1了。
我们可以看到Integer a=1;其实有一个装箱操作,相当于Integer a=Integer.valueOf(1);
找到到Integer中的这个valueOf里面的操作:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
这里可以看到有一个IntegerCache,如果i在这个范围内,就直接从缓存中取值,缓存中的范围是-128到127
static final int low = -128;
int h = 127;
引申:在-128在127之间,是true,其他的都是false
Integer i1=-129;
Integer i2=-129;
System.out.println(i1==i2); false
Integer i1=1;
Integer i2=1;
System.out.println(i1==i2); true
最终解决方案:避免进行自动装箱操作
//交换两个数的值
public static void swap(Integer i1,Integer i2) throws Exception {
Field field=Integer.class.getDeclaredField("value");
field.setAccessible(true);
Integer tmp=new Integer(i1.intValue());
field.set(i1,i2.intValue());
field.set(i2,tmp);
}
或者:
field.setInt(i1,i2.intValue());
field.setInt(i2,tmp);
4、动态代理的原理?
jdk动态代理必须要实现接口,cglib并不需要。
jdk动态代理
public class DynamicProxy implements InvocationHandler {
private Object target;
public Object bind(Object target){
this.target = target;
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("记录日志开始");
Object result = method.invoke(target,args);
System.out.println("记录日志结束");
return result;
}
}
CGLIB
public class CglibDynamicProxy implements MethodInterceptor {
private Object target;
public Object getInstance(Object target){
this.target=target;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(this.target.getClass());
enhancer.setCallback(this);
return enhancer.create();
}
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("记录日志开始");
Object result = methodProxy.invoke(o,objects);
System.out.println("记录日志结束");
return result;
}
}
5、设计模式的落地
6大原则:
单一职责(一个类只做一个事情)、李氏替换(子类可以扩展父类的功能,但不能改变父类原有的功能)、依赖倒置、接口隔离、开闭原则(对扩展开放)、迪米特法则(类之间互相知道的越少越好,将逻辑封装到类的内部)
策略模式:应用场景:定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独
立于使用它的客户而变化。 特点 :行为型模式 最终执行结果是固定的。执行过程和执行逻辑不一样。
6、List和Set的区别?
List: 有序并且允许重复
Set: 不允许重复且无序
ArrayList: 可变长度动态数组,可以动态扩容
ArrayList的内部结构:
transient Object[] elementData;
绕过transient进行序列化,因为通过重写了
readObject(java.io.ObjectInputStream s)和writeObject来对elementData进行序列化,数组长度大于0的时候才进行序列化操作。
如果这个数组没有长度,则默认是空的,初始化是10。动态扩容每次会递增1.5倍,当删除一个元素的时候让右边的元素每次左移一位,同时将元素size减1 。
List允许重复:每次是以数据下标的方式去递增,不会覆盖掉之前的数据。
Set的数据结构:
常用的是HashSet和TreeSet
7、ClassLoader加载机制
程序在启动时不是一次性加载所有的类到内存中,而是根据需要的时候再去加载。
类装载器经过验证、准备、解析,最后初始化。
类装载器分为启动类、扩展类、系统类装载器、自定义装载器。
启动顺序是bootstrap->extension->application->user classload
分别装载核心类库、/lib/ext下目录的类、classpath。
装载类的时候会有双亲委派装载。
8、乐观锁和悲观锁
锁的目的是保证在多线程并行执行的时候对共享资源访问的安全性。
乐观锁:它认为每次去获取共享资源的时候是不会有冲突的,不会主动去加锁,是通过CAS或者加versoin来实现。
CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
version机制:
update from table set name=?,version=version+1 where version=0${version} and id=?
悲观锁:
每次都加锁,会阻塞线程。
9、AIO、BIO、NIO
BIO:Blocking IO 同步阻塞IO(JDK1.4之前都是这种)
同步IO:是否要亲自去监听操作,不断进行关注是否已经完成对应的操作
改进方案:在IO操作的时候进行创建线程处理。
NIO:Non-Blocking IO 同步非阻塞IO。Channel、Buffer、Selector
先通过Channel登记,等需要进行IO操作的时候再去创建一个Thread进行读写操作。
阻塞的理解:在单线程的环境中,如果IO操作没有完成,那么当前线程就一定会等待在这边,不能做其他操作。
非阻塞的理解: 无论当前IO是否完成,都会去返回一个结果。通过一个缓存来保证。
Buffer: 通过数组的方式来实现 ,记录整个数据的移动过程。可以临时去保存这些数据。
总之:
同步,要自己不间断的询问是否已经完成
非阻塞:IO操作不需要再阻塞在那边,而是直接返回。
AIO: aysnc IO 异步非阻塞IO
用户如果触发了IO操作之后,程序这个时候就委托给操作系统来完成,当操作系统完成这个IO操作之后,再告诉自己。
10、spring中对象注入的几种方式和区别?
注解注入属性、构造方法注入、set方法注入
11、mysql的事务隔离级别?
ACID:原子性,一致性,隔离性,持久性
脏读、不可重复读、幻读
12、 HashMap源码分析?
数组加链表的存储方式
transient Node[] table;
hash算法的作用:为了node节点落点的一个前戏计算
经过map组装后的,会优先进入数组中,HashMap的默认数组大小是16,位置是落在0-15之间,通过hash算法,得到数组的下标的整型数,key.hashCode,得到了int32的数,通过高16位和低16位进行一个异或运算。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
put的过程(key,value这边,put时候的流程是什么样的)
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
先通过hash算法得到一个result,然后去判断一下当前node节点的数组是否为空,如果为空就先去初始化这个数组的大小,将数组的默认大小16赋值给newCap。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
然后根据key,value值组装成node节点,通过hash计算出来的结果与上01111(15),算出来的结果是在0000-01111之间的,也就是0-15之间。这里为什么不用取模, hash%16 和hash&15的性能对比:对于计算机来说,用&的效率会更加高。
然后不为null,如果key相同,只需要把value值进行覆盖就行了;红黑树:在jdk1.8开始使用,因为当链表不断加长之后,当下次put元素之后,要不断对比的值,这样路程会很长,会影响效率,所以不要让链表的长度过长;当过长之后,当超过8之后,就把这个形式转换为红黑树,类似于二叉树,就是会把节点颜色变成红色或者黑色,
最后还需要判断数组的大小,如果数组过大时,还需要进行扩容,扩容因子是0.75,如果使用的容量大于等于12的时候,就进行扩容,2倍扩容,原因是因为需要位置尽可能均匀的分散在每个数组上,要进行异或运算,必须是n的2次幂,让数组的大小为2*n,可以使使用效率增加。
对这个新的数据进行使用,需要将原来数组中的元素移动到新的数组上,
1) 数组下面有元素,下面为空的,就直接计算hash值,然后放到新数组上面去,
2) 如果数组位置有元素,是红黑树的话, 就将这个红黑树进行拆分
((TreeNode)e).split(this, newTab, j, oldCap);
3)如果数组下面的是链表,就对它循环遍历,e.hash & oldCap的方式是用于计算在数组中下标的位置,移动的标准是原来的hash倒数第5位为0时,就等于多了一个16的大小,直接省略掉了计算的过程,这样新的链表的节点的位置只有可能在两个地方:1 是在原来的位置 2 原来的位置加上oldCap.
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
全部源码如下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
//如果是红黑树了结构就直接使用红黑树进行处理
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//如果长度超过8,就讲这个链表转为红黑树处理
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
红黑树的方式是从jdk1.8中开始引用,在jdk7中:
当我们给put()方法传递键和值时,HashMap会由key来调用hash()方法,返回键的hash值,计算Index后用于找到bucket(哈希桶)的位置来储存Entry对象。
当get时要按顺序遍历链表的每个 Entry,直到找到想获取的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那HashMap必须循环到最后才能找到该元素。 这和红黑树的方式使用相比性能开销很大。
13、红黑树算法
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
它的时间复杂度是O(lgn),
14、如何停止一个线程
1、interrupt方式
private static int i;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
while(!Thread.currentThread().isInterrupted()){
i++;
}
System.out.println("num-"+i);
});
thread.start();
TimeUnit.SECONDS.sleep(2);
thread.interrupt();
}
2、设置flag的方式
private static int i;
private volatile static boolean stop = false;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
while(!stop){
i++;
}
System.out.println("num-"+i);
});
thread.start();
TimeUnit.SECONDS.sleep(2);
stop=true;
}
15、Thread.join的实现原理
join:等待线程执行完成,获得一个线程执行结果。
原理:
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
16、ThreadLocal的分析
ThreadLocal的实例代表了一个线程局部的变量,每条线程都只能看到自己的值,并不会意识到其它的线程中也存在该变量。每个线程都是一个一个引用副本。
它采用采用空间来换取时间的方式,解决多线程中相同变量的访问冲突问题。
原理:
每个Thread的对象都有一个ThreadLocalMap,当创建一个ThreadLocal的时候,就会将该ThreadLocal对象添加到该Map中,其中键就是ThreadLocal,值可以是任意类型。
在该类中,我觉得最重要的方法就是两个:set()和get()方法。当调用ThreadLocal的get()方法的时候,会先找到当前线程的ThreadLocalMap,然后再找到对应的值。set()方法也是一样。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
17、volatile
用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。
volatile具有可见性、有序性,不具备原子性。
可见性:当多个线程访问同一个变量x时,线程1修改了变量x的值,线程1、线程2…线程n能够立即读取到线程1修改后的值。
有序性:即程序执行时按照代码书写的先后顺序执行。在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。volatile会禁止指令重排。
volatile不会让线程阻塞,响应速度比synchronized高,这是它的优点。
原理是通过内存屏障去实现是,是基于jvm代码中,多了ACC_VOLATILT。
18、缓存穿透的原理
当缓存体系建立起来之后,如果每次查询的都不在缓存中,那么数据库就会收到大量的并发请求,例如当在大流量流入时,可能因为频繁访问存储层导致DB直接宕机,这样会形成被人利用不存在的key频繁攻击应用的漏洞。
解决方案:
最为常简的是采用布隆过滤器,将所有可能存在的数据哈希到一个足够发的 bigmap 中,一个一定不存在的数据会被该 bigmap 拦截掉,从而避免对底层存储系统造成查询压力。
另一种更为简单的方法,如果一个查询返回的数据为空(无论数据为空,或是系统故障),将空结果进行缓存,设置一个最长不超过五分钟的过期时间。
缓存雪崩
设置缓存时采用了相同的过期时间,导致缓存在某时刻同时失效,请求全部转向DB,DB瞬时压力过重雪崩。
Redis宕机,导致客户端的请求之间流向DB,拖垮DB。
解决方案:
简单方案就是将缓存失效时间分散开,我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
保持缓存层服务器的高可用。 监控、集群、哨兵。当一个集群里面有一台服务器有问题,让哨兵踢出去。
依赖隔离组件为后端限流并降级。
19、幂等
一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外)。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。
业务开发中,经常会遇到重复提交的情况,无论是由于网络问题无法收到请求结果而重新发起请求,或是前端的操作抖动而造成重复提交情况。 在交易系统,支付系统这种重复提交造成的问题有尤其明显,比如:
用户在APP上连续点击了多次提交订单,后台应该只产生一个订单;
向支付宝发起支付请求,由于网络问题或系统BUG重发,支付宝应该只扣一次钱。 很显然,声明幂等的服务认为,外部调用者会存在多次调用的情况,为了防止外部多次调用对系统数据状态的发生多次改变,将服务设计成幂等。
20、数据库和缓存双写如何保证数据一致性
最经典的做法:
1、读的时候,先读缓存,缓存没有的话,那么就读数据库,然后取出数据后放入缓存,同时返回响应;
2、更新的时候,先删除缓存,然后再更新数据库。