大厂面经汇总

https://jinshuju.net/f/XDyaUn


做个广告

AE物流技术部门直推来了,这是我所在的团队,AE物流技术现在正在快速发展中,作为跨国团队,我们团队不仅有国内的同学,还有乌克兰、俄罗斯等地的同事,未来我们将走出中国,来到俄罗斯,美国等全球各个地方。此时此刻非我莫属,现在正是加入菜鸟国际化的好时候。对我们感兴趣的小伙伴可以直接点击我们的调查问卷,越早投递越有可能获得提前面试的机会,未通过还可以继续内推。时不我待,大家抓紧时间填写调查问卷吧。还有疑惑可以添加我的微信ljo0412。调查问卷地址:https://jinshuju.net/f/XDyaUn  推荐人请填写 伍图。

下面的面经是当年找工作时候收集和总结的,有问题的地方欢迎大家指出。

一、多线程篇

1.0 进程状态

一共有五种状态,新建-》就绪-》运行-》休眠-》销毁 

1.1 进程和线程的区别

进程是程序运行的最小单元,一个进程中可以包含多个线程,多个线程可以共享一个进程资源。

线程是CPU调度的最小单元


image.png


1.2 并发和并行的区别

并发是单位时间内,能处理事件的能力;

并行是同一时刻,能够处理事件的能力;

1.3 辨析stop,interrupt,suspend

stop和suspend都是不被推荐的方法。

stop是立刻停止线程,而不管线程是运行到何处,所以没有留给线程释放资源的时间。

suspend是会让出cpu资源,但是不会释放锁,一直持有到resume执行,才会继续执行该程序,容易造成死锁。

interrupt是推荐的停止线程的方法,interrupt停止线程不是强制的,而是协助式的,调用该方法,将发出通知,具体是否停止,如何停止由程序自己决定,所以更加推荐该方法。

1.4 对象锁和类锁的区别

类锁,锁的对象是class文件,一个虚拟机中只有一个class文件,锁是jvm内唯一的;

对象锁,锁的对象是new出来的对象,不唯一。

通过synchronized修饰的静态方法 和synchronized输入类.class是类锁,其他情况是对象锁。

1.5 volatile的作用

volatile只能保证可见性,但是不保证原子性。适合一个写,多个读的场景。

1.6 对ThreadLocal的理解(如何保证自己的变量不被其他线程修改)

ThreadLocal是一个数据结构,有些类似HashMap,可以存储k,v键值对,但是一个ThreadLocal只能保存一个,每个线程都有一个副本,各个线程数据互不干扰。

在源码中,实际上存在一个ThreadLocalMap的内部类,通过具体线程获取对应线程存储的ThreadLocalMap,进而获取到对应的值。ThreadLocalMap看似是一个Map,但实际上是一个初始大小为16的Entry数组。插入的时候ThreadLocal对象把自己当如key,放进了这个ThreadLocalMap数组中。

但是这个Entry是WeakReference,所以没有办法像HashMap存在链表。没有链表结构,那么如何解决hash冲突呢?

源码中是这样解决的,根据ThreadLocal对象的hash值定位到数组中的i,如果i没有人,那就直接插入;如果有,那么比较下,看看hash值是否相同,如果相同就替换该位置的value;如果,比较,hash值不同,那么就根据规则找下一个位置,直到能插入进去。get反其道行之,所以如果冲突严重,那么插入和查找的效率是非常低的。

由此也产生了一个问题,Entry使用了虚引用,当第一次GC发生时,ThreadLocal在外部没有强引用时,将会回收引用。这也就导致了内存泄漏,如果创建ThreadLocal的线程一直运行,那么Entry中的对象将一直不能被回收。

图片.png

相应的解决办法,ThreadLocal本身的set和get方法可能会清除掉key为null的对象,也可以使用remove方法直接删除,用完记得删除,要养成良好的习惯。

为什么会出现内存泄露,key会被回收,但是value不会,这样就会造成一直持有value的强引用,最后导致value无法被回收,只有当线程结束,ThreadLocal才能被回收。

为什么使用WeakReference,这是避免将我们创建的threadLocal对象置为null时,Entry中存在强引用,无法回收该对象,只有等ThreadLocal的线程结束才能回收。

参考链接

1.7 线程之间的通信问题

常见问题:两个线程交替打印奇数和偶数

实际就是wait()、notify()、notifyAll()的问题。

还可以用来实现数据库连接池等操作,即没有可用线程时使用wait等待,当其他线程执行完成之后使用notifyAll()通知其他线程那资源。

1.8 辨析yield(),sleep(),wait(),notify()

yield()是让出CPU资源,但是不意味着他就不执行了,同时yield()也不释放锁;

sleep()线程进入休眠状态,不释放锁,直到休眠时间到了才可以继续执行;

wait()线程释放锁,并让出cpu资源。

notify()通知wait()重新获得锁,继续执行,但本身并不释放锁,所以一般放在同步语句块最后一句。

1.9 对Fork/Join框架的理解

Fork/Join是Jdk1.7添加的计算框架。fork/join的主要思想就是,将一个大任务分解(fork)成多个小任务,然后利用多线程并行去处理小任务。处理完成之后再进行join合并,得到我们最终想要的结果。fork/join可以充分压榨CPU多线程的资源。 fork/join的特别之处在于使用了work-stealing(工作幂取)算法,这种算法的设计思路在于使用了一个双端队列,线程即可以从队列头获取任务,也可以从队列尾部获取任务。当一个线程完成了自己的队列任务,可以从其他线程任务队列队尾获取任务进行计算,也正是因为如此,所有线程都在运行中,直到全部任务都被计算完成,充分利用了多线程资源。

1.10 CountDownLatch和CyclicBarrier

CountDownLatch和CyclicBarrier都是JDK1.5被引入的线程同步类。

CountDownLatch是通过一个计数器实现的,计数器初始值为线程的数量。每个线程完成自己的工作,可以通过调用countDown()进行-1,当计数器到达0的时候,其他使用await等待的线程将继续运行。

缺点:CountDownLatch是一次性的,使用一次之后无法再使用。

CyclicBarrier有些类似有CountDownLatch,也是一个计数器,计数器初始值同样为线程的数量。同样通过await()暂停线程,但是不同的是,无需在第三方线程进行计数器的递减。每有一个线程调用了await的同时计数器减一,当计数器到0,全部放行,继续执行。类似长途大巴车,人满发车的概念。

缺点:CyclicBarrier如果有一个线程出错,CyclicBarrier将永远不能满足条件,将一直被阻塞。

Phaser jdk1.7引入,推荐的用法。

1.11 什么是信号量(Semaphore)

信号量本质上也是一个计数器,用来限制线程的数量。当一个线程开始时,通过acquire方法申请一个证书,对应计数器减1,当线程结束,通过release()返回证书,对应计数器减1.

底层是基于自旋+原子操作实现的。

1.12 对Future类的理解

future类是我们经常用的非阻塞的并发模型。Future可能是一个还没有完成的异步任务结果,可以通过get方法获取结果,isDone方法判断异步任务是否完成,isCancel()方法可以判断当前方法是否取消。

1.13 Runnable和Callable接口的区别

两者都是用来编写多线程程序的接口。都通过调用Thread.start()启动线程。

区别是:Runnable接口的任务线程不能返回结果,而Callable接口的任务线程能返回执行结果。同时Runnable不能向上抛出异常,只能内部处理,而Callable可以向上抛出异常。

1.14 辨析乐观锁和悲观锁,常见的锁类型,适用场景

悲观锁,总是假设最坏的情况,即每次获取数据的时候都认为会被别人修改,所以在每次获取数据之前都会都数据加锁,获取完成之后再释放锁。悲观锁,将阻塞其他线程,所以效率较低。

乐观锁,总是假设最好的情况,即认为每次获取数据都不会有人修改,所以不使用锁,而是通过CAS算法的自旋方式实现,每次在更新的时候会判断下在此期间别人有没有更新这个数据。省去了锁的开销,效率更高,但是在写竞争压力大的时候讲有不断的自旋发生,消耗更多的CPU资源。

因此,乐观锁适合写比较少的情况,即冲突少的情况下,省去了锁的开销,加大了系统的整个吞吐量。但是写竞争明显的情况,会导致乐观锁不断重试,消耗更多cpu资源,降低了性能,所以写竞争明显的情况下,使用悲观锁比较合适。

1.15 CAS原理及问题

基本算法:

for(;;) {  int current = get();  int next = current + 1;  if(compareAndSet(current, next)) {  return next;  } }

该算法就是先获取到当前值,然后使用原子操作compareAndSet(),比较当前值是否被更改,如果没有被更改,则进行修改,并返还新的值;如果已经被修改,进行重试,重新执行该操作,直到修改到当前值。

compareAndSet()调用的是Unsafe类的方法,Unsafe类是通过JNI调用操作系统的原生程序进行的,直接在内存上操作数据,所以保证了操作的原子性。

CAS存在ABA问题,比如两个线程同时去修改一个数据,都同时拿到了A,但第一个线程,先将A修改成了B,然后又修改成了A,这时第二个线程,比较发现A没有变化,所以可以进行修改。这里就产生了问题,虽然A的值没有发生改变,但实际上的数据已经发生了变化,所以第二个线程发生错误。

解决这个问题可以通过对数据加上版本号来解决。jdk中具体提供了AtomicMarkableReference和AtomicStampedReference两个原子操作类来解决这个问题。

1.16 什么是可重入锁

不可重入锁即当前的线程执行的某个方法已经获取到了该锁,那么在该方法中,如果尝试再获取该锁,将会被阻塞,如果递归调用锁,就会发生死锁。

可重入锁从广义上讲指的就是可重复可递归调用的锁,在外层使用锁,在内层仍然可以使用,并且不会发生死锁。ReentrantLock和synchronized都是可重用锁。

1.17 synchronized和lock的异同

  1. synchronized是java内置的关键字,Lock是java类
  2. synchronized无法判断是否获取锁的状态,Lock可以判断是否获取锁
  3. synchronized会自动释放锁,是隐式锁,而Lock需要在finally中手动释放锁,是显示锁
  4. synchronized获取不到锁将会阻塞,而Lock可以尝试获取锁,并可以设置超时时间
  5. synchronized的锁可重入,不可中断,非公平;Lock锁可重入,可判断、可公平锁
  6. Lock更加灵活,适合大量的同步代码,而synchronized适合代码少量的同步问题。

1.18 公平锁和非公平锁

1.19 共享锁和排它锁

1.20 LockSupport类

LockSupport类是Java6引入的一个类,提供了两个函数,park()方法用于阻塞当前线程,unpark()方法用于停止指定线程的阻塞。底层都是通过调用Unsafe类的函数实现的。unpark()相当于给指定线程发了一个许可证,所以可以先与park()方法进行调用,非常灵活。park/unpark最核心是解耦了线程之间的同步,线程之间不再需要一个Object或者其他变量来存储状态,不再需要关心对方的状态。可以在任何地方调用park/unpark方法。

1.21 HashTable,HashMap,ConcurrentHashMap的异同

HashTable:

  • 底层数组+链表实现,无论key还是value都不能为null,线程安全,在实现线程安全时修改数据需要锁住整个HashTable,效率低。
  • 初始容量为11,扩容阈值0.75,扩容oldsize*2 + 1
  • 使用synchronized保障线程安全,效率非常低下。

HashTable通过key计算出hash值,再得到index,遍历Entry数组中对应位置的链表,如果entry hash值和key值相等,则覆盖对应位置存储的数据,并返回旧数据;如果没有,则添加到对应的链表中。

HashMap:

  • 底层数组+链表实现,可以存在null键和null值,线程不安全。
  • 初始容量为16,扩容因素:0.75,扩容newSize = oldSize * 2
  • 扩容针对整个Map,每次扩容时,原数组中的元素依次重新计算存储位置,并重新插入。
  • 插入元素后判断该不该扩容,有可能是无效扩容。
  • 当Map中元素超过75%,触发扩容操作,减少链表长度,元素分配更加均匀;
  • 多线程会导致HashMap的Entry链表形成环形数据结构,一旦循环将产生死循环。
    • 多个线程在同时重新分配数组中数据容易产生死循环

ConcurrentHashMap在jdk1.7和1.8中有两种实现方案:

详细见:下一节描述

  • 初始容量16
  • 扩容因子0.75
  • 并发度16.segment数组的长度
  • 扩容 newSize = oldSize*2

重新总结HashMap和HashTable之间的异同


  1. 作者不同,HashMap是 Doug Lea,java并发大神开发的,而HashTable是由Hoff(亚瑟饭霍夫)开发,这个人参与Java早期的大量开发工作,创立了多家成功的公司
  2. 产生时间不同,HashMap jdk1.2产生,虽然出现较晚,但是Hashtable基本上被弃用了,可能一是线程安全,太慢,二是,没有驼峰命名法。
  3. 继承父类不同,HashMap继承AbstractMap,Hashtable继承Dictionary,但是都实现了map,Cloneable,Serializable三个接口。Dictionary已经被弃用了。
  4. 二者对外的接口不同
  5. 对Null key value支持不同
  6. 线程安全性不同
  7. 遍历方式内部实现不同,都使用了Iterator进行迭代,但是Hashtable还使用了Enumeration。HashMap的Iterator使用了fail-fast迭代器,遍历中迭代会报错。JDK8,Hashtable也使用了fail-fast;
  8. 初始容量和每次扩容大小不同 Hashtable默认11,HashMap默认容量16,hashTable扩容,2oldSize + 1,HashMap 2 oldSize.(为什么?Hashtable设计重点是哈希更加均匀,hash表为素数时,直接简单的取模结果更加均匀。而HashMap是为了加速hash速度,2的次幂,做位运算更快,但也因此导致hash分布不均匀,所以hash算法做了一些修改)
  9. hash值计算方法不同

1.22 ConcurrentHashMap如何保证高并发下的线程安全并同时提高了性能

jdk1.7中的ConcurrentHashMap是由Segment数组和HashEntry数组组成。Segment继承可重入锁,在ConcurrentHashMap中扮演锁的结构。HashEntry用于存储k,v数据。一个ConcurrentHashMap中包含一个Segment数组,每个Segment包含一个HashEntry数组,每个HashEntry就是一个链表。

在这个结构中,最重要的的就是Segment数组,实际上使用了锁分离技术,它使用了多个锁来控制对Hash表中的不同部分进行修改。只要修改发送在不同的segment段,就可以并发的进行修改。因此保证线程安全的前提下提高了性能。

jdk1.7中是采用Segment + HashEntry + ReentrantLock的方式进行实现的

jdk1.8中取消了segment数组,直接使用table保存数据,锁的粒度更小。同时使用链表+红黑树进行存储数据。

1.23 ConcurrentHashMap1.8中的变化

jdk1.7中是采用Segment + HashEntry + ReentrantLock的方式进行实现的,jdk1.8中使用Node+CAS+synchronized来保障线程安全。具体说:

  • 取消了segment数组,直接用table保存数据
  • 存储数据用红黑树+链表的形式,当同一个链表中的长度超过8个转用红黑树。

1.24 SkipList 跳表

空间换时间,在原链表的基础上形成多层索引,随机生成,又称为概率数据结构。

1.25 Copy-on-Write 写时复制容器

添加元素时,复制一个新的容器,向其中添加元素,添加完成后,再讲原容器的引用指向新的容器。这样在读的时候可以不用加锁,可以对容器进行并发的读,但是会存在时延,只能保障最终一致性。适合读多写少的场景。本质上还是读写分离的思想。

问题:占用内存过多,不保证数据实时一致性。

1.26 常见的阻塞队列

  • ArrayBlockingQueue 有界
  • LinkedBlockingQueue 有界
    • ArrayBlockingQueue只用了一个锁,而LinkedBlockingQueue用了2个
    • ArrayBlockingQueue使用了一个头指针一个尾指针,入队出队都是O(1)

  • PriorityBlockingQueue 无界
  • DelayQueue 无界 --- 用于实现订单到期
  • SynchronousQueue:不存储元素的阻塞队列,只用来同步
  • LinkedTransferQueue:无界,jdk1.7新加入的队列,无界阻塞队列。也是基于链表实现的。但是和普通队列不同的是,LinkedTranserQueue生成者会关心放入队列的数据是不是被消费,而不像以前完全不关心。
  • LinkedBlockingDeque:双向队列,jdk1.6引入,但是没有使用读写分离。

1.27 对AQS的理解

AQS是AbstractQueuedSynchronizer的简称。AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架。所谓框架,AQS使用了模板方法的设计模式,为我们屏蔽了诸如内部队列等一系列复杂的操作,让我们专注于对锁相关功能的实现。

获取锁

既然涉及到锁竞争的问题,必然需要一个标志位来表示锁的状态,AQS中提供了state这样一个成员变量,为了安全的操作state,我们需要使用原子操作。将state从0修改为1就代表这个线程已经持有了这把锁。

但竞争锁的线程绝对不会只是一个,其他未竞争到锁的线程该如何进行处理?

第一个答案可能是重试,重试虽好,但是可不能贪杯,如果竞争很严重,无数的线程在不断的重新尝试获取锁,我们的CPU早晚会吃不消。

第二个比较好的方式就是排队,持有锁的线程释放锁之后,通知下一个线程去获取锁,避免了不必要的CPU损失。但是值得注意的是,即使是从队列中被唤醒的线程去获取锁也依旧可能获取不到的,因为无时无刻都有新加入的线程来竞争锁。

AQS实际上就是使用了双端队列来解决了这个问题的。

public final void acquire(int arg) {  if (!tryAcquire(arg) &&  acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  selfInterrupt(); }

tryAcquire()如果失败将执行acquireQueued()中的addWaiter()方法,即尝试加入等待队列。这个等待队列使用了双端队列进行实现,在AQS中定义了一个Node的数据结构,AQS中维护着head和tail两个成员变量。

在单线程中插入队列尾部很简单,只需要将原来的tail的next指向新插入的节点,并且将tail重新设置为新插入的节点。但是在多线程环境中,很有可能发生多个线程同时插入尾部的现象,而上述的插入过程不具有原子性,同时插入的过程必将出现多个操作顺序的混乱,最终导致等待队列的tail节点

AQS在插入tail节点时使用原子操作来保证了插入的可靠。

private Node addWaiter(Node mode) {  Node node = new Node(Thread.currentThread(), mode);  // Try the fast path of enq; backup to full enq on failure  Node pred = tail;  if (pred != null) {  node.prev = pred;  if (compareAndSetTail(pred, node)) {  pred.next = node;  return node;  }  }  enq(node);  return node; }

插入成功的直接返回了node,而没有插入成功的则执行了enq()函数,在enq()中使用了CAS进行插入。

private Node enq(final Node node) {  for (;;) {  Node t = tail;  if (t == null) { // Must initialize  if (compareAndSetHead(new Node()))  tail = head;  } else {  node.prev = t;  if (compareAndSetTail(t, node)) {  t.next = node;  return t;  }  }  } }

经历这个CAS插入,最后全部的节点都将被插入到队列尾部。

现在,没有获取到锁的线程已经被放进队列了,但是放入队列也代表着我们可以忘了初心。我们的目标是获取锁,而不是进入队列。acquireQueued()就在尝试为我们获取锁。

final boolean acquireQueued(final Node node, int arg) {  boolean failed = true;  try {  boolean interrupted = false;  for (;;) {  final Node p = node.predecessor();  if (p == head && tryAcquire(arg)) {  setHead(node);  p.next = null; // help GC  failed = false;  return interrupted;  }  if (shouldParkAfterFailedAcquire(p, node) &&  parkAndCheckInterrupt())  interrupted = true;  }  } finally {  if (failed)  cancelAcquire(node);  } }

简单说就是,检查自己是不是head节点的下一个节点,如果是的话,尝试获取获取锁;如果不是的话,将使用LockSupport的park方法阻塞当前线程,避免造成CPU的浪费。

释放锁

释放锁的过程可以分成两大部分:

  1. 恢复AQS的状态为无锁状态
  2. 唤醒等待队列中下一个等待的节点

在第一个过程中,没有排在队头的节点都已经被阻塞了,而唤醒的时机就是前一个节点已经释放锁,所以可以说这个等待队列,实际上是一个唤醒链。

public final boolean release(int arg) {  if (tryRelease(arg)) {  Node h = head;  if (h != null && h.waitStatus != 0)  // 使用unpark唤醒下一个线程  unparkSuccessor(h);  return true;  }  return false; }

总结

AQS为我们提供了:

  • status 状态同步标示
  • Node双端队列 存储竞争锁线程
  • 基于Node双端队列的线程唤醒机制

我觉得AQS精华在于,将原来N个线程并发竞争锁降低为1+M(新加入)个。在我们自己实现类似的资源竞争算法中,也可以通过加入队列来降低竞争的并发度,降低CPU的负载压力。

1.28 为什么需要线程池

  1. 降低资源消耗,创建线程和销毁线程都需要消耗一定的资源
  2. 提高响应速度,免去线程创建时间,所以更快。
  3. 提高线程的可管理性。

1.29 shutdown 和 shutdownNow的区别

shutdown 会中断所有没有执行任务的线程;

shutdownNow 会尝试停止正在运行或者正在暂停的线程。

1.30 对CompletionService的理解,或者与ExecutorService的区别

为了解决Future的get阻塞问题,CompletionService以异步方式一边submit提交执行任务,一边take获取已完成的任务结果。可以将执行任务和处理任务结果的程序分离开同时处理,提高效率。

1.31 保证线程安全的手段

  1. 栈封闭
  2. 无状态,没有任何成员变量
  3. 让类不可变,加final,不提供修改的地方
  4. volatile
  5. 加锁和CAS
  6. 安全发布,不发布不安全的对象出去
  7. ThreadLocal

1.32 常用线程池适用的场景

 进阶问题

1.33 volatile的实现原理

https://www.cnblogs.com/awkflf11/p/9218414.html

volatile只能保证对数据的可见性但是不保证原子性。在OpenJDK中的unsafe.cpp源码中,被volatile修饰的变量会存在一个“lock:”的前缀。Lock前缀不是一种内存屏障,但是他可以实现类似内存屏障的功能。如果进行volatile写操作,Lock会对CPU总线和高速缓存加锁,可以理解为一种CPU指令级的一种锁。该指令会使当前处理器缓存行中的数据直接写到系统内存中同时会使其他其他CPU里缓存了该地址的数据无效

Volatile的使用优化


著名的Java并发编程大师Doug lea在JDK7的并发包里新增一个队列集合类LinkedTransferQueue,他在使用Volatile变量时,用一种追加字节的方式来优化队列出队和入队的性能。

追加字节能优化性能?这种方式看起来很神奇,但如果深入理解处理器架构就能理解其中的奥秘。让我们先来看看LinkedTransferQueue这个类,它使用一个内部类类型来定义队列的头队列(Head)和尾节点(tail),而这个内部类PaddedAtomicReference相对于父类AtomicReference只做了一件事情,就将共享变量追加到64字节。我们可以来计算下,一个对象的引用占4个字节,它追加了15个变量共占60个字节,再加上父类的Value变量,一共64个字节。

为什么追加64字节能够提高并发编程的效率呢 ? 因为对于英特尔酷睿i7,酷睿, Atom和NetBurst, Core Solo和Pentium M处理器的L1,L2或L3缓存的高速缓存行是64个字节宽,不支持部分填充缓存行,这意味着如果队列的头节点和尾节点都不足64字节的话,处理器会将它们都读到同一个高速缓存行中,在多处理器下每个处理器都会缓存同样的头尾节点,当一个处理器试图修改头接点时会将整个缓存行锁定,那么在缓存一致性机制的作用下,会导致其他处理器不能访问自己高速缓存中的尾节点,而队列的入队和出队操作是需要不停修改头接点和尾节点,所以在多处理器的情况下将会严重影响到队列的入队和出队效率。Doug lea使用追加到64字节的方式来填满高速缓冲区的缓存行,避免头接点和尾节点加载到同一个缓存行,使得头尾节点在修改时不会互相锁定。


1.34 synchronized的实现原理

synchronized底层实现原理分为两种,第一种是同步代码块,同步代码块的底层实现涉及到了monitorenter,monitorexit两条指令,执行monitorenter指令之后相当于获取了对对象Monitor的所有权,其他再想要获取的只有等待执行monitorexit指令之后才可以获取。而第二章同步方法,实际上并没有使用这两个指令来实现同步,相对于普通的方法,同步方法在常量池中多了一个ACC_SYNCHRONIZED标识符。

1.35 锁的状态和理解

锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,这四种锁会伴随竞争情况而进行升级。锁只可以升级不可以降级,目的是为了提高获取锁和释放锁的效率。

偏向锁:跟他的名字一样,偏向于第一个访问锁的线程,如果运行过程中,只有一个线程竞争资源,那么线程是不需要触发同步的,减少加锁、释放锁的操作,提高效率。如果出现其他线程抢占的情况,持有锁的线程会被挂起,JVM会消除他身上的偏向锁,将锁恢复到标准的轻量级锁状态。

轻量级锁运行在一个线程进入同步块的情况下,第二个线程加入竞争锁的时候,偏向锁会升级为轻量级锁。在竞争锁的过程使用CAS操作进行竞争。

当轻量级锁竞争多次仍然失败,证明有多个线程同时竞争锁的时候,锁升级为重量级锁,重量级线程指针指向竞争线程,竞争线程进入阻塞状态,等待轻量级线程释放锁后唤醒他,后续的等待锁的线程也要进入阻塞状态。

1.36 Java8中的LongAdder

LongAdder相比AtomicLong具有更好性能。AtomicLong底层使用CAS自旋提供并发性,但在并发量大,线程冲突严重的时候,会出现大量失败并且不断自旋的情况,CAS自旋就会成为性能瓶颈。

LongAdder就是为了解决这个问题,基本思想是分散热点,有些类似锁的读写分离,底层实际上使用了一个数组,不同的线程会映射到不同的数组槽上,这样也就降低了冲突,CAS自旋也就得以缓解。待获取value值时,只需要累加数组中的全部value即可,进而提高了性能。

1.37 Java8中的StampLock

1.38 Disruptor了解吗?原理是什么?为什么高性能?

在稳定性和性能要求高的系统中,为了防止生成者速度过快,导致内存溢出,只能选择有界队列,同时为了减少Java垃圾回收次数,尽量要选择Array,所以jdk中提供的只有ArrayBlockingQueue,但是ArrayBlockingQueue是通过加锁保证线程安全,同时因为伪共享问题存在性能瓶颈。

Disruptor是英国的一家公司开发的高性能线程间异步通信框架。性能之所以高是采用了环状数组结构,采用覆盖的方式避免垃圾回收,同时在属性上使用对其填充,通过添加额外的无用信息,避免伪共享问题。


1.39 线程池

newFixedThreadPool(int nThreads)

corePoolSize == maxPoolSize(固定线程数量),阻塞队列选择LinkedBlockingQueue,几乎无界的队列,所以maxPoolSize和keepAliveTime都没有意义。

newSingleThreadExecutor()

也是固定线程数量的线程池,与newFixedThreadPool()相比,newSingleThreadExecutor限制线程数量为1,也就保证了全部任务都安装放入的顺序来进行执行。

newCachedThreadPool()

corePoolSize=0,maxPoolSize=Integer.MAX_VALUE,keepalive=60s,阻塞队列使用了SynchronousQueue.这个队列不存储计算任务,而是直接提交给线程进行计算,不保持任务。 

SynchronousQueue仅用于生成者和消费者之间直接传递消息

ThreadPoolExecutor参数,工作原理,handler

  • corePoolSize
  • maximumPoolSize
  • keepAliveTime
  • unit
  • workQueue
  • threadFactory
  • handler

工作原理,线程池启动将创建corePoolSize个线程的线程池,如果有计算任务超过corePoolSize将会将计算任务放入到workQueue中;如果workQueue已满,线程池将创建新的线程并执行计算任务;当线程池中任务数量到达了maximumPoolSize将执行对应的RejectExecutionHandler处理方法。

ThreadPoolExecutor提供了四种handler实现:

  • Reject:直接抛出exception异常
  • Discard:直接忽略该runnable
  • DiscardOldest:丢弃队列中最老的任务
  • CallsRun:调用主线程执行该任务

除了这四种实现,还支持我们实现自定义的Handler方法。

二、JVM面试题


2.1 JVM内存是如何划分的?


JVM内存区域分为以下五部分:


可以由不同线程共享的有:


  • 方法区


不可共享的区域有:


  • 本地方法栈
  • Java栈
  • 程序计数器


程序计数器存储当前线程执行的字节码行号指示器;Java栈存储java方法执行的内存模型;本地方法栈存储当前线程使用的Native方法的内存模型;java堆存储存放几乎所有的对象实例和数组,是垃圾收集器的主要区域,因此也被称为GC堆。是Java虚拟机中管理的内存最大的一块;方法区存储已经被类加载的类信息,常量,静态变量,即时编译后产生的代码等数据;在Java1.6及以前,常量池在方法区中,java1.7改为堆中,1.8改为meta space元空间,直接放在了本地内存中。(为什么要迁移讷)


2.2  谈谈垃圾回收机制?为什么引用计数器判定对象是否回收不可行?知道哪些垃圾回收算法?


判断是否回收对象,主要有两种方法:

  1. 引用计数法
  2. 可达性分析法


引用计数法,在对象中添加一个引用计数器,每次有一个对象引用它,便加1,引用失效时便建议。这种方法简单容易理解,实现也更加快速,但是存在一个相互引用的问题,即当两个对象相互引用时,将导致计数器不断增加,无法有GC回收。所以引入了可达性分析:


可达性分析就是先标记一些对象为GC Root节点,然后以这些节点为起始节点进行遍历,向下搜索,搜索所走过的路称为引用链,当一个对象到GC Roots没有任何引用链时,就证明该对象是不可达的,可以进行GC回收。可以作为GC Root节点的对象,java栈中引用的对象,本地方法栈中的native方法引用的对象,方法区中静态属性引用的对象,方法区中常量引用的对象。


2.3 Java中引用有几种类型?在Android中常用于什么情景?


一共有四种,具体包括:


  1. 强引用
  2. 软引用
  3. 弱引用
  4. 虚引用


强引用就是我们日常通过new生成的对象引用,即便内存不足,发生OOM,JVM也不会回收具有强引用的对象。软引用通过SoftReference封装的引用,只被软引用关联的对象,可以通过软引用获取到对象,在OOM发生前会被GC回收。通常被用来实现对内存敏感度的高速缓存。弱引用,只被弱引用关联的对象。可以获取到原对象,但是只能存活到下一次GC,无论内存是否足够都要被回收。通常用于解决内存泄漏的问题。虚引用,仅持有虚引用的对象,在任何时候都可能被GC回收,无法获取到原对象,只能在对象被回收前,做一个通知,通常和引用队列一起使用。


2.4 类加载的全过程是怎样的?什么是双亲委派模型?


类加载的全过程包括:


  1. (loading)加载
  2. (verification) 验证
  3. (preparation)准备
  4. (Resolution)解析
  5. (Initialization)初始化

5个过程。其中加载过程主要是通过全限定名来获取对应类的二进制字节流,将二进制字节流中所代表的静态内存结构转换成对应的运行时内存的数据结构,并生成一个代表的Class对象,对外提供一些访问接口;验证阶段,主要验证class文件的二进制流是否符合当前虚拟机的要求;准备阶段为类变量分配内存,是类对象分配在方法区,而不是实例对象,设置一些初始值;解析阶段,将常量池中的一些符号引用替换为直接引用(符号引用和直接引用)。初始化阶段,是类加载的最后一个步骤,开始真正执行类中定义的字节码。


五种情况才会进行初始化,

  1. 调用new ,调用static方法
  2. 反射调用类
  3. 初始化类,父类没初始化
  4. 执行main类初始化主类
  5. 动态语言支持


双亲委派模型,表示了类加载器之间的层次关系,当父类无法加载类,使用子类开始加载。


类加载器从上到下主要分为:


  1. 启动类加载器(bootstrap ClassLoader)
  2. 扩展类加载器
  3. 应用程序类加载器
  4. 自定义类加载器


虽然叫做父类加载器,但是并不是java中的继承关系,而是组合关系。当一个类加载器收到一个类加载请求时,将先通过父类加载器加载,父类加载器再递交上层加载。当父类加载器无法完成加载时,才会递交给子类加载器进行加载。


2.5 工作内存和主内存的关系?在Java内存模型有哪些可以保证并发过程的原子性、可见性和有序性的措施?


2.5.1 工作内存和主内存


这个问题要从计算机的发展历程讲起,首先我们知道最初的计算机就是单核的,也就没有并发的概念。后来出现了多核,只有多任务并行处理才能真正的发挥多核cpu的能力。起初CPU的速度和磁盘I/O速度差不多,后来cpu发展快速,速度远超于磁盘速度,为了不影响整体的速度。在cpu内核和磁盘中引入了一层高速缓存,即运算时将数据缓存到缓存中,运行完成后再写入磁盘。但是高速缓存带来了新的问题,就是每一个内核都有一个高速缓存,所以带来了一致性问题,因此又加入了一个一致性协议,用来保证数据的一致性。因此这里的内存模型只得是,对高速缓存进行读写的一个过程抽象。


JVM为了保证在不同机器上都能运行,引入的Java Memory Model,JMM,就是为了屏蔽掉各种硬件和操作系统的访问内存的差异,使其在不同平台都能达到一致性内存访问的效果。而其方法同计算机的内存模型一致,不实际存在,只是定义了对一些变量的访问规则,对存储在内存中的变量的读写规则。


JMM定义了主内存和工作内存:


主内存存储全部“变量“,直接存在硬件内存上,工作内存,每个线程都用,用于拷贝该线程用到的变量的主内存副本,以便获取到更快的速度,所以JVM可能会允许工作内存优先存储在寄存器或高速缓存上。


2.5.2 可见性


各个线程的内存上的副本都是隔离的,因此带来了可见性的问题。为了解决这个问题,jvm允许我们使用volatile或者加锁来解决。比如使用synchronized可以保证,两次内存数据的修改是顺序,所以也就不会产生不可见问题。所谓的顺序执行,java规范提案中提出了Happens-before的概念来阐述操作之间的可见性。其中定义了一些原则规则,比如volatile的写操作要先行发生于后面对这个变量的读操作,等等。


2.5.3 有序性


除了可见性问题,为了提高性能,编译器和处理器常常会对指令进行重排序,所以带来了第二个问题,重排序问题。而重排序要受依赖性的影响,具体包括数据依赖性,控制依赖性。这些情况,不管是不是在多线程,都要单线程下的正确顺序,因此提出了一个as-if-serial的概念,即不管顺序怎么变,结果不能变。所以jvm不会对as-if-serial语句进行重排序。单线程下可以通过as-if-serial语义进行重排序,但是到了并发环境下,又遇到了重排序的麻烦。


为了解决这些问题,java编译器引入了内存屏障,内存屏障的存在将禁止重排序,并会影响一些数据的可见性,强制刷出缓存数据,以保证各个线程获得数据的最新版本。对于可以重排序的代码引入了临界区,在进入和离开临界区处进行特殊处理,进而使用重排序提升了执行效率。


2.5.4 原子性


JMM保证原子性变量包括原子类的操作,更大范围的原子性需要lock,unlock,syshronized来保障。


2.6 Java中堆和栈的区别?


堆和栈都是用来存放运行时数据的,但是:


栈内存主要用来存放基本的数据类型和局部变量,当超出变量作用域时将会自动释放。


堆用来存放运行时创建的对象,需要由虚拟机的自动垃圾回收器来回收。


2.7 怎样获取堆和栈的dump文件?

Java Dump实际上是JVM的运行快照,可以将JVM运行时的状态和信息保存到文件上。

通过 jmap可以获取堆的dump,而通过jstack可以获取线程的调用栈dump。


问题分析:很有可能是一个场景题:

  1. 如果程序内存不足或者频繁发送GC,很有可能存在内存泄露的情况。
  2. 通过jmap可以查看当前堆的快照
  3. 可以先使用jmap -heap命令查看堆的使用情况,看一下各个堆空间中的占用情况
  4. 使用jmap -histo:[live] 可以查看堆内存中对象的情况。如果有大量对象被持续引用,并没有被释放掉,那就可能产生了内存泄露,需要结合代码,把不用的对象释放掉。

jmap -histo:live 这个命令执行,JVM会先触发gc,然后再统计信息

  1. 也可以使用jmap -dump:format=b,file=<filename>命令将堆信息保存到一个文件中,再借助jhat命令查看详细内容;
  2. 在内存出现泄露、溢出或者其他前提条件下,建议多dump几次内存,把内存文件进行编号归档,便于后续内存整理分析。


2.7 回收算法比较


比较Serial/Serial Old,ParNew/Serial Old,Parallel Scavenge/Parallel Old,CMS,G1

2.8 解释逃逸分析、标量替换、栈上分配、同步消除

来源:https://www.jianshu.com/p/580f17760f6e


三、计算机网络面试题

3.1 简述TCP三次握手和四次挥手流程

3.2 为什么连接的时候是三次握手,关闭的时候是四次

因为Server端接收到Client端的SYN连接请求后,可以直接发送SYN+ACK报文,在发送同步报文的同时也可以发送确认报文。而关闭连接时,当Server端接收到FIN报文时,很有可能不会立即关闭socket连接,所以只能先返回ACK确认报文。只有当Server端接收完全部的报文,才能发送FIN报文,因此需要四次挥手。

3.3 为什么TIME_WAIT状态需要经历2MSL(最大报文生存时间)才能返回CLOSE状态

  • 第一,为了保证A发送的最有一个ACK报文段能够到达B。这个ACK报文段有可能丢失,因而使处在LAST-ACK状态的B收不到对已发送的FIN和ACK报文段的确认。B会超时重传这个FIN和ACK报文段,而A就能在2MSL时间内收到这个重传的ACK+FIN报文段。接着A重传一次确认。
  • 第二,就是防止上面提到的已失效的连接请求报文段出现在本连接中,A在发送完最有一个ACK报文段后,再经过2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。

3.4 为什么不能两次握手进行连接?

3.5 如果已经建立了连接,但是客户端突然发生了故障会出现什么状况?

TCP还设置有一个保活计时器,如果客户端出现问题,服务器不能一直等待下去,服务器没收到一个客户端的请求都将复位这个计时器。时间通常是设置为2小时,若两小时还没有接收到客户端任何数据,服务器会发送一个探测报文段,以后每隔75s都将发送一次,若一连发送10个探测报文还没有反应,服务端认为客户端出现了故障,接着就关闭连接。

3.6 TCP和UDP的区别

  • TCP协议是有连接的,有连接的意思是开始传输实际数据之前TCP的客户端和服务器端必须通过三次握手建立连接,会话结束之后也要结束连接。而UDP是无连接的
  • TCP协议保证数据按序发送,按序到达,提供超时重传来保证可靠性,但是UDP不保证按序到达,甚至不保证到达,只是努力交付,即便是按序发送的序列,也不保证按序送到。
  • TCP协议所需资源多,TCP首部需20个字节(不算可选项),UDP首部字段只需8个字节。
  • TCP有流量控制和拥塞控制,UDP没有,网络拥堵不会影响发送端的发送速率
  • TCP是一对一的连接,而UDP则可以支持一对一,多对多,一对多的通信。
  • TCP面向的是字节流的服务,UDP面向的是报文的服务。

3.7 TCP协议中KeepAlive(结合3.3)

首先介绍一下 HTTP 协议中 KeepAlive 与 TCP 中 KeepAlive 的区别:

  • HTTP 协议(七层)的 KeepAlive 意图在于连接复用,希望可以短时间内在同一个连接上进行多次请求/响应。举个例子,你搞了一个好项目,想让马云爸爸投资,马爸爸说,"我很忙,最多给你3分钟”,你需要在这三分钟内把所有的事情都说完。核心在于:时间要短,速度要快。
  • TCP 协议(四层)的 KeepAlive 机制意图在于保活、心跳,检测连接错误。当一个 TCP 连接两端长时间没有数据传输时(通常默认配置是 2 小时),发送 KeepAlive 探针,探测链接是否存活。例如,我和厮大聊天,开了语音,之后我们各自做自己的事,一边聊天,有一段时间双方都没有讲话,然后一方开口说话,首先问一句,"老哥,你还在吗?”,巴拉巴拉..。又过了一会,再问,"老哥,你还在吗?”。核心在于:虽然频率低,但是持久。

回到 TCP KeepAlive 探针,对于一方发起的 KeepAlive 探针,另一方必须响应。响应可能是以下三种形式之一:

  • 对方回应了 ACK。说明一切 OK。如果接下来 2 小时还没有数据传输,那么还会继续发送 KeepAlive 探针,以确保连接存活。
  • 对方回复 RST,表示这个连接已经不存在。例如一方服务宕机后重启,此时接收到探针,因为不存在对应的连接。
  • 没有回复。说明 Socket 已经被关闭了。

但是TCP的KeepAlive机制存在一些问题:

  • KeepAlive只能检测连接是否存活,不能检测连接是否可用,比如一端发生了死锁,无法在连接上进行任何读写操作,但是操作系统仍然可以响应网络层 KeepAlive 包。
  • TCP KeepAlive 机制依赖于操作系统的实现,灵活性不够,默认关闭,且默认的 KeepAlive 心跳时间是 两个小时, 时间较长。 
  • 代理(如 Socks Proxy)、或者负载均衡器,会让 TCP KeepAlive 失效

因此实际上需要在应用层实现功能更强的心跳机制。

3.8 TCP如何实现的可靠连接?

确认机制、重传机制、滑动窗口

3.9 UDP可以实现可靠连接吗?

https://blog.csdn.net/huangyimo/article/details/80343376

UDP它不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。

         传输层无法保证数据的可靠传输,只能通过应用层来实现了。实现的方式可以参照tcp可靠性传输的方式,只是实现不在传输层,实现转移到了应用层。

         实现确认机制、重传机制、窗口确认机制。

         如果你不利用linux协议栈以及上层socket机制,自己通过抓包和发包的方式去实现可靠性传输,那么必须实现如下功能:

         发送:包的分片、包确认、包的重发

         接收:包的调序、包的序号确认

         目前有如下开源程序利用udp实现了可靠的数据传输。分别为RUDP、RTP、UDT。

四、 数据库面试题

4.1 count(*),count(1), count(字段),count(索引)

性能:count(字段)< count(索引) < count(1) 约等于 count(*)

数据库针对count(*)进行了优化

4.2 Hash索引

Hash索引:

  • 基于hash算法将键值换算成新的哈希值,只需一次就可以直接定位到相应的位置,速度快
  • hash索引只能支持等值查询,不能使用范围查询
  • hash索引无法被用来避免数据的重排序(hash不保证对应的hash值依旧保持原来的顺序关系)
  • hash索引不支持多列联合索引的最左前缀匹配规则(hash索引在计算时是通过几个索引一起算出来的hash值,并不是单个索引进行计算)
  • hash索引在任何时候都不能避免表扫描(因为hash冲突,必须通过访问表中实际数据,进行比较)
  • hash索引效率可能是极低的,因为存在哈希碰撞的问题。

4.3 B+树比B-树的优势

  1. IO次数更少
  2. 查询性能稳定
  3. 范围查询简便

附:MySQL笔记

  1. TPS和QPS计算方法
    1. TPS:(COM_COMMIT + COM_ROLLBACK)/UPDATE 每秒传输事务数量
    1. QPS:(QUESTIONS/UPTIME) 每秒查询数量
  1. MySqlSlap:压测工具,测试MYSQL压力(5.1.4之后提供)
  1. MySQL用户连接过程概述
    1. 连接层
    1. SQL处理层 (缓存存储内容:1. 缓存执行过的SQL语句和SQL执行计划(默认开启)2. 数据(不是默认的)
      1. 开启数据库数据缓存(query_cache_type),query_cache_size:缓存大小
        set GLOBAL query_cache_size="123452134"
      1. 数据库缓存可能有一些脏读(why?)
    1. 解析
    1. 优化SQL语句
      1. EXPAIN(查看执行计划) select * ... 可以查看真正的语句,可以把没有用的语句去掉
    1. 存储层(见下一节)
  1. MyISAM(5.5之前默认支持的)
    • 数据库文件:frm,myd,myi(非聚集索引)
    • 表级锁
    • 支持全文索引
    • 支持数据压缩---myisampack.exe
    • 使用场景:
      • 不支持事务-适合非事务类型(报表,日志)
      • 查询熟读快,适合只读类应用
      • 空间类应用(GIS)
  1. Innnodb(5.5及以后默认)
    • 数据库文件:frm,ibd(数据+索引)(独立表空间时(5.6以后默认),系统表空间只有frm
    • 独立表空间(方便收缩文件)(可以向对个文件刷数据)/系统表空间(IO瓶颈)(innodb_file_per_table)
    • 支持事务
    • 支持行级锁 (适合高并发的操作)
    • 适合大多数OLTP应用
  1. CSV
    • .frm .csv .csm(多少个数据量)
    • 所有列都不能为null
    • 不支持索引(不适合大表,在线处理)
    • 可以对数据文件直接编辑,改完csv文件之后要flushDB(财务上可能会用)
  1. Archive(日志文件适合)
    1. .frm .ARZ
    1. 只支持insert和select
    1. 只允许自增列做索引Memory
  1. Memory
    • 全部存在内存中(也叫heap引擎),只有一个frm
    • 与临时表的异同
      • 临时表(create temporary table...)?】只在一个session内有效,而memory引擎是都有效的。
      • 系统使用临时表:超限制使用Myisam,未超过使用Memory
    • 支持Hash索引和BTree索引
    • 所有字段都是固定长度
    • 不支持Blog和text字段
    • 表级锁
    • 最大大小由max_heap_table_size决定
  1. Ferderated 需要在my.ini ferderated = 1(相当于一个代理)
    • 远程访问MySQL上的表
    • 本地不存数据
    • 存储表结构
    • 偶尔的统计分析和手工查询可能用的到
  1. 数据库锁 --保证多用户的顺序访问数据
    1. 锁的类型:
      • 表级锁 开销小,加锁快,不会出现死锁,粒度大,冲突概率高,并发度低(更适合以查询为主的业务)
      • 行级锁 适合大量索引条件下,发生少量更新的情况
      • 页面锁
    1. MyISAM表锁
      1. 表共享读锁  (lock table 表名 read)-->(unlock tables)
        1. 同一个session对表进行修改会报错,其他session可以更新数据,但是需要等待
        1. 在上锁的session中更新其他的表也会报错
        1. 别名查询不支持
      1. 表独占写锁(lock table 表名 write)
        1. insert 其他表,也不行
    1. InnoDb行锁(以事务为单元(BEGIN) -->(COMMIT)
      1. 读锁
        1. select * from 表 where 条件 lock in share mode;
      1. 写锁
        1. select * from 表 where 添加 for update;
  1. 面试题:系统运行时间长,数据量很大,然后表A要增加一个字段,白天晚上并发访问量都特别大,请问怎么修改?
    1. 创建一个和你要执行alter的表结构一样的空表
    1. 执行我们赋予的表结构的修改,然后copy原表数据到新表中
    1. 在原表创建一个触发器在数据copy中将新更新的值也copy到新的表中
    1. copy完成,执行rename table新表,代替原表。

      触发器怎么写,使用percona-toolkit工具集合

  1. 事务:只有InnoDb支持。(查看表的存储引擎:show create table account;)
    1. 事务的特性(ACID含义)
      • A:一个事务必须被视为不可分割的最小单元
      • C:事务将数据库从一种一致性转换到另外一种一致性状态, 在事务开始之前和事务结束之后数据库中的数据完整性没有发生变化
      • D:事务一旦提交,所做修改就会永久保存到数据库中
      • I:隔离性要求一个事务对数据库中数据的修改,在未提交前对于其他事务是不可见的(tx_isolation)

        set SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

        • 未提交读-脏读(读到没提交的读,然后回滚)(READ UNCOMMITTED)(隔离性最差)
        • 已提交读-不可重复读(B事务更新过程中,A重复读,数据不一致)(行锁)(READ COMMITTED)(A事务修改,只要提交,其他的Session中事务就可以查询到)(可能出现幻读)
        • 可重复读(默认)(REPEATABLE READ)(常考)(A事务修改并提交,B事务看到的还是旧的)(隔离性更强,别的事务都提交了,还是看不到)(什么情况行锁会升级成表锁?-》隔离事务为可重复读时,以索引为条件更新数据时,会锁住一行,如果没有索引将升级为整张表)
        • 可串行化(SERIALlIZABLE)(要按照事务顺序执行,先修改的必须提交才可以)(并发性低)(读写都会锁住整张表)

幻读:A更新,B插入,然后A以为没有更新(解决:锁表)

    1. 事务语法:
      1. begin/start transaction(推荐)/begin work   rollback commit savepoint(rollback全部回滚,rollback to savepoint s2)
  1. 范式化设计 image 减少冗余,跟新操作比反范式化更新快,表小。多表优化难,级联多。
    1. 第一大范式:
      • 数据库表中所有字段都只具有单一属性
      • 单一属性的类都是基本数据类型
      • 设计出来的表都是二维表
    1. 第二大范式
      • 表中只有一个业务主键(通过中间表实现关联),一行数据只做一件事,不出现重复数据。
    1. 第三大范式
      • 每个非非主属性既不部分依赖于也不传递依赖于业务主键(列跟列之间不要有关联关系)(看不明白)
      • 数据不能有传递关系,每个属性跟主键都有直接关联。

即使按照三大范式,性能也可能很低,不能完全按照范式进行设计

  1. 反范式化设计 减少关联,可以索引优化,可能冗余出现异常不一致,修改成本高。
    1. 为了性能和读取效率适当对数据库设计范式要求进行违反
    1. 运行存在少量冗余,(以空间换时间)
  1. 物理设计
    1. 命名规范-可读-表意性-长名原则(不要缩写)
    1. 搜索引擎(查询多-MyISAM,事务-InnoDB)
    1. 字段选取合适的数据类型,优先:数字->日期,时间->字符->相同级别选用空间小的数据
    1. 浮点类型:财务:优先选用Decimal,防止丢失数据。
    1. timestamp和datetime区别:
      • Datetime(5.5:8字节,5.6:5字节),TIMESTAMP:4字节
      • timestamp跟时区有关,而datetime无关。
  1. 慢查询
    1. 慢查询日志,记录mysql中执行时间超过long_query_time参数设定的时间阈值。
    1. 启动:slow_query_log-->存放日志slow_query_log_file-->long_query_time(默认10s)-->log_output=[file,table]
    1. 记录:查询语句,数据修改语句,回滚语句set GLOBAL slow_query_log=1
    1. 分析工具:
      1. mysqldumpslow -s [c,r,t,l,r] -t 10(条) slow.log(没办法等服务器,没有办法看执行计划)
      1. pt-query_digest --explain h=127.0.0.1,u=root,p=password slow.log->重点去看执行计划
  1. 索引和执行计划
    1. 索引本身就是一个数据结构 ---快速定位,索引就是一个BTree(默认-Innodb只支持),B+Tree.(可能会问,BTree,B+Tree,二叉树区别)
    1. 分类(这里分类有些问题)
      1. 普通索引 一个索引只包含单个列
      1. 唯一索引 索引值必须唯一,允许空值
      1. 复合索引:一个索引包含多个列
      1. 聚簇索引:并不是一个单独索引类型,而是一个数据存储方式
      1. 非聚簇索引:myisam

        show index from table_name

        create [unique] index indexName ON mytable(columnname(length));

        alter table .. ADD [unique] index [indexName] ON (columnname(length));

        drop index [indexName] on myTable\

    1. 索引分类:
      1. 普通索引 :没有任何限制
      1. 唯一索引 :索引列的值必须唯一,但允许有空值,如果是组合索引,列值组合必须唯一
      1. 主键索引 :一种特殊的唯一索引,一个表只能有一个主键,并且不能为空
      1. 组合索引 :多个索引,遵循最左前缀集合
      1. 全文索引 :用来查找文本中的关键字,而不是直接与索引中的值相比较。fulltext跟其他索引不同,更像是搜索引擎,不能通过where语句的参数匹配。要配合match againsts操作仪器使用。

        缺点:

        1. 虽然提高了查询速度,同时却会降低更新表的速度,如对表进行insert、update、delete操作。因为更新表时,不仅要保存数据,还要保存索引文件。
        1. 建立索引会占用磁盘空间的索引文件。
  1. 执行计划
    1. 使用EXPLAIN关键字可以模拟优化器执行SQL查询语句,从而知道MYSQL是如何处理SQL语句。进而查询性能瓶颈。
    1. 作用:
      1. 表的读取顺序等。
    1. id:执行sql的顺序,id越大,越先,相同,前边的先执行

      select_type:查询的类型,普通,联合,子查询等

      type : system(只有一行)>const(索引一次就到)>eq_ref(对于每个索引都只有一个与之匹配 a.id=b.id)>ref(非唯一的索引)>range(in >)>index(不当查询列全为索引列,也是全表扫描的一种)>ALL(All全表查询,这里需要优化到range)

      key:实际使用的索引

      possible:可能使用的key

      key_len:显示的索引字段的最大可能长度(latin1占用一个字节,gbk占用2个,utf-8占用3个)索引最好不要null

      • char:(10) = 10*3 +1(null) = 31 (允许null)
      • varchar(10) = 10*3 + 2(长度)
      • 复合索引,是多个的和,可以通过长度判断复合索引是否生效

      ref:显示之前哪一列使用了索引

      rows:估计找到目标位置,扫描了多少行。

      Extra:包含不适合显示在其他列中但是非常重要的值

      • Using filesort: 使用文件排序,而没有使用索引排序
      • Using temporary table
      • Using Index(使用了覆盖索引using where 和 using index生效了,效率挺高的)
      • Using Join buffer
      • Using Impossible WHERE

索引选择:

  1. 某一列相对唯一
  1. 经常用来查询显示的列
  1. 经常用来关联的列,where中,join on 用到的列
  1. 优化方案
    1. 尽量把索引都用上,全值查找
    1. 最佳左前缀原则,指从索引的最左前列开始并且不跳过索引列(让索引不失效的一个策略)
    1. 不要再索引上做任何操作(计算,函数,类型转换)
    1. 范围条件要放最后列(如果使用了范围查询,会导致后边的索引失效)
    1. 平时尽量使用覆盖索引,少用*
    1. != 要慎用,会导致全表扫描
    1. Null/Not 可能对索引有影响(not null 前提下 使用 is not null会失效),如果索引失效可以使用覆盖索引进行优化
    1. like查询要当心,可能会导致全表查询。(‘%like百分号在前会导致失效,在后边不会)
    1. 字符串类型要加引号,如果忘了加引号,可能会导致索引失效。
    1. OR改UNION效率高,or导致索引失效,使用UNION就没事。
  1. 批量导入 (1,2百万数据要插入)
    1. 方案一:批量插入
      1. 提交前关闭自动提交
      1. 尽量使用批量insert语句
      1. 使用MyISAM引擎
    1. 方案二:LOAD DATA INFILE,比insert快20倍。
      1. 导出select * into OUTFILE 'filename' from product_info(200w,2s)
      1. 导入load data INFILE 'filename' into table product_info_db(200w,7s)


五、Spring面试题


5.1 Spring中使用的设计模式


  1. 简单工厂模式:beanFacotry,根据传入的参数决定创建哪个对象
  1. 工厂方法模式:spring中的工厂bean,都是通过工厂模式new出来的bean
  1. 单例模式:spring默认的bean都是单例的,单例模式无处不在
  1. 适配器模式:在spring的Aop中,使用Advice来增强被代理的功能,AOP的原理就是使用了代理模式(JDK和CGLIB进行代理),生成被代理的类。
  1. 包装器模式:spring的包装器模式主要有两个表现,一个类名中包含Wrapper,另一种是Decorator。在实例化bean的过程中就有使用到,主要是用来给bean对象添加一些额外的功能、
  1. 代理模式,JdkDynamicAopProxy和Cglib2AopProxy就是典型的案例,主要用来提供一个代理以控制对这个对象的访问。
  1. 模板方法模式:简单说就是定义一个算法的骨架,在ApplicationContext init()就是一个典型的案例。


六、Redis面试题

6.1 Redis的特点

6.2 Redis数据类型


6.3 简述Redis的RESP协议


6.4 Redis的慢查询你了解吗?

6.5 Redis 批处理?

6.6 Redis事务?

6.7 Lua脚本的好处

6.8 Redis持久化知道多少

6.9 Redis的复制过程,完整复制和部分复制?

6.10 Redis哨兵机制

6.11 Redis分布式集群

6.12 缓存过期和一致性问题了解多少

6.13 缓存过期和缓存雪崩?

6.14 标准分布式事务XA了解吗?

6.15 2PC,3PC,TCC?

6.16 如何解决缓存和数据库不一致的问题?

6.17 

6.1 常用的数据结构


6.2 Redis && Memcached对比


七、设计模式面试题


八、 数据结构面试题


8.1 常见排序算法原理及其时间复杂度


8.2 B树

B树可以看做是对2叉查找树的一种扩展,即他允许每个节点都有M-1个子节点。B树在对磁盘IO的优化更好,通过设置一个节点的大小等于磁盘单位页面的大小,以实现最优化大块数据的读和写操作。

一个M阶的B树,特性如下:

  • 每个节点最多有个m棵子树;
  • 除根节点外,其他每个分支至少有个m/2棵子树
  • 更节点至少有两棵子树
  • 所有叶节点在同一层。
  • 有j个孩子的非叶节点恰好有j-1个关键码,关键码按递增次序排列。


下边不确定正确性


B树结构和红黑树原理上有些类似,但是B树在对磁盘IO的优化更好。它与红黑树的不同之处在于,B树的节点可以有很多个孩子,从数个到上千个都有可能。每个含有n个节点的红黑树,高度为O(lgn),所以可以在lgn的时间内完成一些动态集合的操作。


B树之所以可以降低磁盘IO,是由于B树充分考虑到了磁盘的结构特点。磁盘在读取数据,需要移动磁臂,转圈圈,速度特别慢,所以引入了页面概念,即一些连续的存储空间,来减少IO操作。一页的长度大约在211到214字节,而我们经常看到的B数的分支因子50~2000正是根据对应一页的大小。这样便可以最大程度上降低对磁盘的I/O操作次数,进而提升性能。


更多,查看B树的插入等原理

简述:每个节点满了的话,对半分,留中间的那个放在父节点,如果父节点满了,分父节点


8.3 B+树


B树的变型,他把数据都存储在叶子节点上,内部只存储关键字和孩子指针,以叶子中最小值作为索引。B+数遍历高效,所有叶子节点串联成链表即可从头到尾遍历。

  • 有n棵子树的节点含有n个关键字,每个关键字都不会保存数据,只会用来索引,所有数据都保存在叶子节点。
  • 所有叶子节点包含所有关键字信息以及指向关键字的指针,关键字顺序从小到大。


为什么B+树比B树更适合做系统的数据库索引和文件索引


  1. B+树的磁盘读写代价更低
    因为B+树内部节点没有指向关键字具体信息的指针,内部节点比B树小
  1. B+树的查询更加稳定
    所有关键字查询的长度都是一样的。


8.4 红黑树

红黑树实际上是一种自平衡的二叉树,在二叉树基础上引入颜色的概念,具有以下特性:

  • 节点非红即黑
  • 根节点是黑
  • 每个叶子节点都是黑色的空节点(NULL)
  • 每个红色节点的两个子节点都是黑色的
  • 从任意节点触发到叶子节点的路径上都包含相同数量的黑色节点

8.5 字典树


九、分布式理论面试题


9.1 辨析AIO、BIO、NIO


BIO,是Blocking IO,即同步阻塞IO。采用BIO通信模型的服务端,通常是由一个独立的Acceptor线程负责监听客户端的连接,他接收到客户端连接请求之后为每个客户端创建一个新的线程进业务处理,通过对输出流返回应答给客户端,返回成功后线程销毁,即典型的应答-请求模型。


BIO模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,线程数急剧增加,性能急剧下降,甚至导致系统崩溃。当然可以使用线程池来管理这些线程,这种方法也被人称作伪异步I/O模型。但是并没有从根本上改变阻塞等待的问题。


NIO,是同步非阻塞IO,适用于连接数目多且比较短的架构。在jdk1.4开始提供支持。NIO采用的是一种多路复用的机制。基于NIO的服务端,通常采用单线程的Reactor模式流程,服务端的Reactor是一个线程对象,该线程会启动循环,并使用Selector来实现IO复用。同时注册一个Accptor事件到Reactor中,Acceptor事件处理器所关注的事件是ACCEPT事件。当客户端发起连接后,服务端监听到Acceptor事件,获取到对应的SocketChannel,并将该连接所关注的READ事件,以及对象的事件处理器注册到Reactor中,当监听到对应的读写请求时,将由指定的事件处理器处理。


9.2 异步的优势


  • 提高系统可用性
  • 加快网站响应速度
  • 消除并发访问高峰


异步可能会对用户体验、业务流程造成影响。


9.3 软件架构关注


本身功能之外包括性能、可用性、扩展性、伸缩性、安全性五个要素


9.4 应用服务器性能优化


缓存、异步、集群等(LRU)


9.5 资源复用


单例,对象池


十、MyBatis基础

image

10.1 一级缓存和二级缓存


MySql在执行sql命令之前需要编译sql,当重复查询相同的sql语句时,如果每次都需要重新编译将造成资源的巨大浪费,因此Mybatis提供了一级缓存方法,来优化在与数据库会话间重复查询的问题。每一个sqlSession在持有了自己的Executor都会有一个localCache。在执行查询的时候将会将当前执行的MappedStatement生成一个key,并去localCache中查询,查询到再去数据库中进行查询。一句话概括的话,可以说一级缓存缓存的是编译好的sql语句。


image


二级缓存,一级缓存的范围是一个sqlSession,而二级缓存就是用来让多个SqlSession共享缓存。开启二级缓存之后,将会在CacheExecutor装饰Executor,在查询之前会现在CachingExecutor进行二级缓存查询。


image


更多请查看简书博客-mybatis

10.1 Mybatis是如何进行分页的?分页插件原理是什么?

  1. MyBatis使用RowBounds对象进行分页,也可以只写编写sql实现分页,也可以使用分页插件进行分页
  2. 分页插件实现的原理是,通过实现Mybatis提供的接口,实现自定义插件,拦截待执行的sql,并在其中执行sql,然后重写sql。

10.2 简述Mybatis插件运行的原理,以及如何实现一个插件

  1. Mybatis只可以编写支持ParemeterHandler、ResultSetHandler、StatementHandler,Executor这4个接口的插件,MyBatis通过动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,将进入拦截方法。
  2. 通过实现Mybatis的Interceptor接口并复习intercept()方法,然后在给插件开发一个注解,指定要拦截哪一个方法的具体接口的那些方法就可以。

10.3 Mybatis动态sql是什么?有哪些动态sql?简述下动态sql的执行原理?

  1. MyBatis的动态sql可以让我们在xml映射文件中以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能。
  2. MyBatis提供了9种动态的sql标签:|trim|where|set|foreach|if|choose|when|otherwise|bind|
  3. 动态sql使用OGNL从sql参数对象中计算表达式的值,并根据表达式的值动态拼接sql。

10.4 #{}和${}的区别?

  1. ${}是字符串替换而#{}是预编译处理
  2. Mybatis在处理#{}时会将sql中的#{}替换称为?,调用PreparedStatement的set方法进行赋值;
  3. Mybatis在处理${}时,就是把${}替换成对应变量的值
  4. #{}可以防止sql注入,提高系统安全性

10.5 为什么说Mybatis是半自动ORM映射工具

主要区别在于Mybatis需要编写sql,而与之对应全自动ORM完全不需要编写SQL语句。

10.6 MyBatis是否支持延迟加载?如果支持,原理?

  1. Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指一对一,collection指一对多查询。在Mybatis配置文件中可以通过配置lazyLoadingEnabled=true|false开启或关闭。
  2. 原理是:使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()为null值,就会发送之前保存好的实现语句查询B对象,再调用a.setB(b),于是a的对象b就有值了,接着完成之前的调用,这就是延迟加载的基本原理。

10.7 Mybatis好处?

  1. 把sql语句从Java代码中独立出来,方便维护
  2. 封装了底层JDBC API的调用细节,并将结果自动转成Java Bean对象,大大简化了Java数据库编程的重复性,
  3. 因为MyBatis需要程序员自己去编写sql语句,更加灵活,可以做到更优的效率。

10.8 Mybatis的Xml映射文件和Mybatis内部数据结构之间的映射关系?

Mybatis将所有的XML配置信息都封装到了Configuration内部。在XML映射文件中,<parameterMap><resultMap>等都会被解析成同名的对象。而<select><insert><update><delete>等标签都会被解析成MappedStatement对象,标签内的sql会被解析成BoundSql对象。

10.9 什么是MyBatis的接口绑定,有什么好处?

接口绑定就是在MyBatis中把定义的接口中的方法和SQL语句绑定在一起,我们直接调用接口方法就可以,这样比通过SqlSession调用更加灵活和简单。

10.10 接口绑定有几种实现方式,都是怎么实现的?

有两种方式。一种是在接口方法上写@Select@Insert注解等,另一种是通过在xml中编写sql语句来绑定,其中要求xml的namespace必须为接口的全路径名。

10.11 MyBatis实现一对一有几种方式?具体怎么操作的?

有两种方式,联合查询和嵌套查询。联合查询就是几个表级联查询,只查询一次,通过在resultMap中配置association一对一的类就可以完成;嵌套查询是先查询一个表,在根据表中的外键id,去另外一个表里面查询数据,也是通过association配置,但是另外一个表通过select属性进行配置。

联合查询是 from user,article where user.id=article.userid ... 这种不会产生N+1问题

嵌套查询是select * from user where id = #{},association中设置select属性,这种通过外键查询的方式,容易产生N+1问题。

10.12 通常一个xml映射文件,都会写一个Dao接口与之对应,Dao的工作原理,是否可以重载?

不能重载,因为Dao寻找Xml对应的sql是通过全限定名+方法名的寻找策略,接口实现的原理是jdk的动态代理原理,运行时会为dao生成一个proxy代理对象,代理对象会拦截这些接口方法,去执行对应的sql并返回数据。

10.13 Mybatis有哪些Executor执行器?

  1. SimpleExecutor,每次执行update、select,就开启一个statement对象,用完就销毁
  2. ReuseExecutor,执行update、select,存在statement对象就使用,不存在就创建,用完不销毁,并存放到一个map中
  3. BatchExecutor,完成批处理。

Mapped Statement也是mybatis一个底层封装对象,它包装了mybatis配置信息及sql映射信息等。mapper.xml文件中一个sql对应一个Mapped Statement对象,sql的id即是Mapped statement的id。

10.14 MyBatis如何选择Executor

配置文件中可以指定,也可以在创建SqlSession时传递参数ExecutorType。

10.15 MyBatis可以映射到Enum枚举类吗

通过实现一个TypeHandler可以实现任何对象到表的映射。

10.16 如何自动生成主键

设置usegeneratedKeys = true

10.17 mapper中如何传递多个参数

  1. 方法中正常传递,xml中使用#{0}#{1}
  2. 使用@Param注解,xml中使用#{name}

Dubbo面试题


dubbo是什么


dubbo是一个分布式框架,远程服务调用的分布式框架,其核心部分包含: 集群容错:提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持。 远程通讯: 提供对多种基于长连接的NIO框架抽象封装,包括多种线程模型,序列化,以及“请求-响应”模式的信息交换方式。 自动发现:基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器。


dubbo能做什么


透明化的远程方法调用,就像调用本地方法一样调用远程方法,只需简单配置,没有任何API侵入。 软负载均衡及容错机制,可在内网替代F5等硬件负载均衡器,降低成本,减少单点。 服务自动注册与发现,不再需要写死服务提供方地址,注册中心基于接口名查询服务提供者的IP地址,并且能够平滑添加或删除服务提供者。


1、默认使用的是什么通信框架,还有别的选择吗?


答:默认也推荐使用 netty 框架,还有 mina。


2、服务调用是阻塞的吗?


答:默认是阻塞的,可以异步调用,没有返回值的可以这么做。


3、一般使用什么注册中心?还有别的选择吗?


答:推荐使用 zookeeper 注册中心,还有 Multicast注册中心, Redis注册中心, Simple注册中心.


ZooKeeper的节点是通过像树一样的结构来进行维护的,并且每一个节点通过路径来标示以及访问。除此之外,每一个节点还拥有自身的一些信息,包括:数据、数据长度、创建时间、修改时间等等。


4、默认使用什么序列化框架,你知道的还有哪些?


答:默认使用 Hessian 序列化,还有 Duddo、FastJson、Java 自带序列化。 hessian是一个采用二进制格式传输的服务框架,相对传统soap web service,更轻量,更快速。


Hessian原理与协议简析:


http的协议约定了数据传输的方式,hessian也无法改变太多:


  1. hessian中client与server的交互,基于http-post方式。
  1. hessian将辅助信息,封装在http header中,比如“授权token”等,我们可以基于http-header来封装关于“安全校验”“meta数据”等。hessian提供了简单的”校验”机制。
  1. 对于hessian的交互核心数据,比如“调用的方法”和参数列表信息,将通过post请求的body体直接发送,格式为字节流。
  1. 对于hessian的server端响应数据,将在response中通过字节流的方式直接输出。


hessian的协议本身并不复杂,在此不再赘言;所谓协议(protocol)就是约束数据的格式,client按照协议将请求信息序列化成字节序列发送给server端,server端根据协议,将数据反序列化成“对象”,然后执行指定的方法,并将方法的返回值再次按照协议序列化成字节流,响应给client,client按照协议将字节流反序列话成”对象”。


5、服务提供者能实现失效踢出是什么原理?


答:服务失效踢出基于 zookeeper 的临时节点原理。


6、服务上线怎么不影响旧版本?


答:采用多版本开发,不影响旧版本。在配置中添加version来作为版本区分


7、如何解决服务调用链过长的问题?


答:可以结合 zipkin 实现分布式服务追踪。


8、说说核心的配置有哪些?


核心配置有:


  1. dubbo:service/
  1. dubbo:reference/
  1. dubbo:protocol/
  1. dubbo:registry/
  1. dubbo:application/
  1. dubbo:provider/
  1. dubbo:consumer/
  1. dubbo:method/


9、dubbo 推荐用什么协议?


答:默认使用 dubbo 协议。


10、同一个服务多个注册的情况下可以直连某一个服务吗?


答:可以直连,修改配置即可,也可以通过 telnet 直接某个服务。


11、dubbo 在安全机制方面如何解决的?


dubbo 通过 token 令牌防止用户绕过注册中心直连,然后在注册中心管理授权,dubbo 提供了黑白名单,控制服务所允许的调用方。


12、集群容错怎么做?


答:读操作建议使用 Failover 失败自动切换,默认重试两次其他服务器。写操作建议使用 Failfast 快速失败,发一次调用失败就立即报错。


13、在使用过程中都遇到了些什么问题? 如何解决的?


  1. 同时配置了 XML 和 properties 文件,则 properties 中的配置无效


只有 XML 没有配置时,properties 才生效。


  1. dubbo 缺省会在启动时检查依赖是否可用,不可用就抛出异常,阻止 spring 初始化完成,check 属性默认为 true。


测试时有些服务不关心或者出现了循环依赖,将 check 设置为 false


  1. 为了方便开发测试,线下有一个所有服务可用的注册中心,这时,如果有一个正在开发中的服务提供者注册,可能会影响消费者不能正常运行。


解决:让服务提供者开发方,只订阅服务,而不注册正在开发的服务,通过直连测试正在开发的服务。设置 dubbo:registry 标签的 register 属性为 false。


  1. spring 2.x 初始化死锁问题。


在 spring 解析到 dubbo:service 时,就已经向外暴露了服务,而 spring 还在接着初始化其他 bean,如果这时有请求进来,并且服务的实现类里有调用 applicationContext.getBean() 的用法。getBean 线程和 spring 初始化线程的锁的顺序不一样,导致了线程死锁,不能提供服务,启动不了。


解决:不要在服务的实现类中使用 applicationContext.getBean(); 如果不想依赖配置顺序,可以将 dubbo:provider 的 deplay 属性设置为 - 1,使 dubbo 在容器初始化完成后再暴露服务。


  1. 服务注册不上


检查 dubbo 的 jar 包有没有在 classpath 中,以及有没有重复的 jar 包


检查暴露服务的 spring 配置有没有加载


在服务提供者机器上测试与注册中心的网络是否通


  1. 出现 RpcException: No provider available for remote service 异常


表示没有可用的服务提供者,


a. 检查连接的注册中心是否正确


b. 到注册中心查看相应的服务提供者是否存在


c. 检查服务提供者是否正常运行


  1. 出现” 消息发送失败” 异常


通常是接口方法的传入传出参数未实现 Serializable 接口。


14、dubbo 和 dubbox 之间的区别?


答:dubbox 是当当网基于 dubbo 上做了一些扩展,如***务可 restful 调用,更新了开源组件等。


15、你还了解别的分布式框架吗?


答:别的还有 spring 的 spring cloud,facebook 的 thrift,twitter 的 finagle 等。


16、Dubbo 支持哪些协议,每种协议的应用场景,优缺点?


dubbo: 单一长连接和 NIO 异步通讯,适合大并发小数据量的服务调用,以及消费者远大于提供者。传输协议 TCP,异步,Hessian 序列化;


rmi: 采用 JDK 标准的 rmi 协议实现,传输参数和返回参数对象需要实现 Serializable 接口,使用 java 标准序列化机制,使用阻塞式短连接,传输数据包大小混合,消费者和提供者个数差不多,可传文件,传输协议 TCP。 多个短连接,TCP 协议传输,同步传输,适用常规的远程服务调用和 rmi 互操作。在依赖低版本的 Common-Collections 包,java 序列化存在安全漏洞;


webservice:基于 WebService 的远程调用协议,集成 CXF 实现,提供和原生 WebService 的互操作。多个短连接,基于 HTTP 传输,同步传输,适用系统集成和跨语言调用;http: 基于 Http 表单提交的远程调用协议,使用 Spring 的 HttpInvoke 实现。多个短连接,传输协议 HTTP,传入参数大小混合,提供者个数多于消费者,需要给应用程序和浏览器 JS 调用; hessian: 集成 Hessian 服务,基于 HTTP 通讯,采用 Servlet 暴露服务,Dubbo 内嵌 Jetty 作为服务器时默认实现,提供与 Hession 服务互操作。多个短连接,同步 HTTP 传输,Hessian 序列化,传入参数较大,提供者大于消费者,提供者压力较大,可传文件;


memcache: 基于 memcached 实现的 RPC 协议 redis: 基于 redis 实现的 RPC 协议


17、Dubbo 集群的负载均衡有哪些策略


Dubbo 提供了常见的集群策略实现,并预扩展点予以自行实现。


Random LoadBalance: 随机选取提供者策略,有利于动态调整提供者权重。截面碰撞率高,调用次数越多,分布越均匀;


RoundRobin LoadBalance: 轮循选取提供者策略,平均分布,但是存在请求累积的问题;


LeastActive LoadBalance: 最少活跃调用策略,解决慢提供者接收更少的请求; ConstantHash LoadBalance: 一致性 Hash 策略,使相同参数请求总是发到同一提供者,一台机器宕机,可以基于虚拟节点,分摊至其他提供者,避免引起提供者的剧烈变动;


18、服务调用超时问题怎么解决


dubbo在调用服务不成功时,默认是会重试两次的。这样在服务端的处理时间超过了设定的超时时间时,就会有重复请求,比如在发邮件时,可能就会发出多份重复邮件,执行注册请求时,就会插入多条重复的注册数据,那么怎么解决超时问题呢?如下


对于核心的服务中心,去除dubbo超时重试机制,并重新评估设置超时时间。 业务处理代码必须放在服务端,客户端只做参数验证和服务调用,不涉及业务流程处理 全局配置实例


image


当然Dubbo的重试机制其实是非常好的QOS保证,它的路由机制,是会帮你把超时的请求路由到其他机器上,而不是本机尝试,所以 dubbo的重试机器也能一定程度的保证服务的质量。但是请一定要综合线上的访问情况,给出综合的评估。


十一、zookeeper面试题


11.1 zookeeper是什么框架?


分布式的、开源的分布式应用程序协调服务,原本是Hadoop、HBase的一个重要组件。它为分布式应用提供一致***的软件,包括:配置维护、域名服务、分布式同步、组服务等。它能提供基于类似于文件系统的目录节点树方式的数据存储, Zookeeper 作用主要是用来维护和监控存储的数据的状态变化,通过监控这些数据状态的变化,从而达到基于数据的集群管理。简单说,zk=文件系统数据结构+watch通知机制。


11.2 应用场景


  1. 数据发布订阅
  1. 负载均衡
  1. 命名服务
  1. Master选举
  1. 集群管理
  1. 配置管理
  1. 分布式队列
  2. 分布式锁

11.3 简述Paxos算法和zab算法


这里举个开早会的例子吧。


第一阶段:


zab:


  • 消息广播:广播了大多数节点就认为是一致的
  • 崩溃恢复:包含以下流程


  1. 每个server先给自己投一票具体包括:(myid,ZXID(事务id))
  1. 收集这些投票进行统计
  1. 处理投票信息,并重投(先比较事务id,相同再比较myid,选出下一个要投票的)
  1. 统计,多数成为leader
  1. 选举完成,改变状态


11.4 选举算法流程


11.5 zk节点类型


  • 持久节点
  • 临时节点
  • 持久顺序节点
  • 临时顺序节点


11.6 watch的节点是永久的吗?

不是。官方声明:一个Watch事件是一个一次性的触发器,当被设置了Watch的数据发生了改变的时候,则服务器将这个改变发送给设置了Watch的客户端,以便通知它们。

为什么不是永久的,举个例子,如果服务端变动频繁,而监听的客户端很多情况下,每次变动都要通知到所有的客户端,这太消耗性能了。


一般是客户端执行getData(“/节点A”,true),如果节点A发生了变更或删除,客户端会得到它的watch事件,但是在之后节点A又发生了变更,而客户端又没有设置watch事件,就不再给客户端发送。

在实际应用中,很多情况下,我们的客户端不需要知道服务端的每一次变动,我只要最新的数据即可。

11.7 部署方式,角色,个数

单机,集群。Leader、Follower。集群最低3(2N+1)台,保证奇数,主要是为了选举算法。

11.8 支持动态添加集群吗?

zk3.5版本提供了这个功能,之前还不支持,我们只能一次修改一个,轮流的修改集群配置。

11.9 2PC和3PC,你了解数据库集群的一致性吗


2PC:是两阶段提交


  • 第一阶段:Prepare阶段,向数据库发送写入undo和redo请求,全部返回ok之后进入第二阶段
  • 第二阶段:Commit阶段,想数据库提交commit/或者rollback操作


优点:简单,实现方便(TCC)


缺点:


  1. 同步阻塞
  1. 单点故障
  1. 数据一致性问题(Commit过程断网)
  1. 容错不好


3PC:


  • canCommit? 先查询是否能够执行事务
  • preCommit? 写入undo和redo
  • DoCommit?执行commit操作


优点:改善同步阻塞和单点故障


缺点:同样问题,没有从根本上改变问题。


11.10 ZAB协议

ZAB协议具体可以分为两个部分:

  1. 消息广播 多数收到消息就认为数据一致的。
  2. 崩溃恢复

崩溃恢复

当leader崩溃或者leader失去大多数follower,这个时候进入zk的恢复状态,需要重新选择一个新的leader,并让全部的server都恢复到一个正确的状态。基本过程:

  1. 每个人先投自己一票(myid, ZXID(事务id))
  2. 收集投票
  3. 处理投票,未成功重新投(先比较ZXID,找到ZXID最大的,再比较myid)
  4. 统计,少数服从多数,多数成为leader。


十二、消息中间件

1. JMS对象模型包括

image

  1. JMS连接工厂
  2. JMS连接
  3. JMS会话
  4. JMS生产者、JMS消费者
  5. JMS目的
  6. JMS消息

2. AMQP协议组成

image

问题:

12.1 RabbitMQ中的broker是啥?cluster优势什么?

broker是一个或者多个erlang node的逻辑分组,且node上运行着RabbitMQ应用程序。cluster是在broker基础上,增加了node之间共享元数据的约束。

十三、Java基础篇

13.1 字符型常量和字符串常量的区别

  1. 形式上,字符型常量通过单引号包围一个字符,二字符串是双引号多个字符
  2. 含义上,字符常量相当于一个整形(ASCII值),可以参加表达式运算 字符串常量代表一个地址值
  3. 空间上,字符常量只占2个字节,字符串占多个

13.2 构造器能不能被override

不能,但是可以被重载

13.3 重写和重载的区别

overload:发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时

override:发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法。

13.4 Java三大特性是什么?

13.5 StringBuffer,StringBuilder,String的区别,为什么String是不可变的

String是不可变的,String类中使用final关键字字符数组保持字符串,所以String对象是不可变的。

  1. StringBuffer和StringBuilder都是继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组来保存字符串但是并没有使用final进行修饰,所以这两种对象是可变的。
  2. StringBuffer方法加了同步锁,所以是线程安全的。StringBuilder没有,线程不安全。
  3. String每次修改都会创建 一个新的String对象,然后将新的指针指向新的String对象,性能低。StringBuiler比StringBuffer性能提高10%~15%.


13.6 自动装箱和拆箱了解吗?

装箱:将基本数据类型用它们对应的引用类型包装起来,使用valueOf()进行装箱,会对-128~127的数据进行缓存;

拆箱:将包装类型转换为基本数据类型,两个Integer在进行四则运算时会出现这个现象。

https://zhidao.baidu.com/question/373251301430667724.html

13.7 在Java中定义了一个不做事情且没有参数的构造方法的作用,或者问super()的作用?

13.8 == 和equals的区别

13.9 hashCode和equals的区别

13.10 unckeck Exception 和 checked Exception的区别

13.11 Error和Exception的区别

13.14 default 和 protected的区别

类内部 本包 子类 外部包
public
protected ×
default × ×
private × × ×

13.15 看代码


try {  // 发生异常 } catch(Exception e) {  return 1; } finally {  return 2; }


Integer.valueOf(10) == Integer.valueOf(10); // true new Integer(10) == Integer.valueOf(10); // false new Integer(10) == new Integer(10); // false 


public class C {   private static class A {  public void print() {  System.out.println("a");  }  }   private static class B extends A {  public void print() {  System.out.println("B");  }  }   public void print(A a) {  a.print();  }   public void print(B a) {  a.print();  }   public static void main(String[] args) {  A a = new B();  a.print(); // B  C c = new C();  c.print(a); // B


十四、Java Web相关

14.1 HTTP了解吗?HTTPS呢?



全部评论

相关推荐

风中翠竹:真的真的真的没有kpi。。。面试官是没有任何kpi的,捞是真的想试试看这个行不行,碰碰运气,或者是面试官比较闲现在,没事捞个人看看。kpi算HR那边,但是只有你入职了,kpi才作数,面试是没有的。
双非有机会进大厂吗
点赞 评论 收藏
分享
评论
1
10
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务