程序员最近都爱上了这个网站  程序员们快来瞅瞅吧!  it98k网:it98k.com

本站消息

站长简介/公众号

  出租广告位,需要合作请联系站长


+关注
已关注

分类  

暂无分类

标签  

暂无标签

日期归档  

2023-06(3)

java基础面经

发布于2021-06-12 14:01     阅读(488)     评论(0)     点赞(2)     收藏(5)


1.请你谈谈你对volatile的理解?

volatile是JVM提供的轻量级的同步机制

1.保证可见性

2.不保证原子性

3.禁止指令重排

<1> volatile保证可见性:(JMM内存模型)

JMM(Java内存模型java memory model)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范。

JMM关于同步的规定:

  1. 线程解锁前,必须把共享变量的值刷新回主内存
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存
  3. 加锁和解锁是同一把锁

由于JVM运行程序的实体是线程,而每个线程创建是JVM都会为其创建一个工作内存(栈空间),工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有的线程都可以访问。线程对变量的操作(读取赋值等)都必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,不同的线程之间无法访问对方发的工作内存。

而这就可能存在 当一个线程AAA修改了共享变量X的值但还未写回主内存时,另一个线程BBB又对主内存中同一个变量X进行操作,但此时AAA线程工作内存中的变量X对线程BBB来讲并不课件,这种工作内存与主内存同步延迟现象就造成了可见性问题。

<2> volatile不保证原子性:

原子性:不可分割,完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者分割,需要整体完成,要么同时成功要么同时失败。

  1. class MyData2 {
  2. /**
  3. * volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
  4. */
  5. volatile int number = 0;
  6. public void addPlusPlus() {
  7. number ++;
  8. }
  9. }
  10. public class VolatileAtomicityDemo {
  11. public static void main(String[] args) {
  12. MyData2 myData = new MyData2();
  13. // 创建10个线程,线程里面进行1000次循环
  14. for (int i = 0; i < 20; i++) {
  15. new Thread(() -> {
  16. // 里面
  17. for (int j = 0; j < 1000; j++) {
  18. myData.addPlusPlus();
  19. }
  20. }, String.valueOf(i)).start();
  21. }
  22. // 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
  23. // 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
  24. while(Thread.activeCount() > 2) {
  25. // yield表示不执行
  26. Thread.yield();
  27. }
  28. // 查看最终的值
  29. // 假设volatile保证原子性,那么输出的值应该为: 20 * 1000 = 20000
  30. System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);
  31. }
  32. }

最后的结果总是小于20000。

number++在多线程下是非线程安全的。

我们可以将代码编译成字节码,可看出number++被编译成3条指令。

假设我们没有加 synchronized那么第一步就可能存在着,三个线程同时通过getfield命令,拿到主存中的 n值,然后三个线程,各自在自己的工作内存中进行加1操作,但他们并发进行 iadd 命令的时候,因为只能一个进行写,所以其它操作会被挂起,假设1线程,先进行了写操作,在写完后,volatile的可见性,应该需要告诉其它两个线程,主内存的值已经被修改了,但是因为太快了,其它两个线程,陆续执行 iadd命令,进行写入操作,这就造成了其他线程没有接受到主内存n的改变,从而覆盖了原来的值,出现写丢失,这样也就让最终的结果少于20000。
问题解决:

  • 可以加synchronized解决,但它是重量级同步机制,性能上有所顾虑。
  • 如何不加synchronized解决number++在多线程下是非线程安全的问题? 使用AtomicInteger
  1. import java.util.concurrent.atomic.AtomicInteger;
  2. class MyData2 {
  3. /**
  4. * volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
  5. */
  6. volatile int number = 0;
  7. AtomicInteger number2 = new AtomicInteger();
  8. public void addPlusPlus() {
  9. number ++;
  10. }
  11. public void addPlusPlus2() {
  12. number2.getAndIncrement();
  13. }
  14. }
  15. public class VolatileAtomicityDemo {
  16. public static void main(String[] args) {
  17. MyData2 myData = new MyData2();
  18. // 创建10个线程,线程里面进行1000次循环
  19. for (int i = 0; i < 20; i++) {
  20. new Thread(() -> {
  21. // 里面
  22. for (int j = 0; j < 1000; j++) {
  23. myData.addPlusPlus();
  24. myData.addPlusPlus2();
  25. }
  26. }, String.valueOf(i)).start();
  27. }
  28. // 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
  29. // 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
  30. while(Thread.activeCount() > 2) {
  31. // yield表示不执行
  32. Thread.yield();
  33. }
  34. // 查看最终的值
  35. // 假设volatile保证原子性,那么输出的值应该为: 20 * 1000 = 20000
  36. System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);
  37. System.out.println(Thread.currentThread().getName() + "\t finally number2 value: " + myData.number2);
  38. }
  39. }

输出结果为:

  1. main finally number value: 18766
  2. main finally number2 value: 20000

 <3>volatile禁止指令重排

计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种:

单线程环境里确保程序最终执行结果和代码顺序执行的结果一致。

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线性中使用的变量能否保障一致性是无法确定的,结果无法预测。

  1. public class ReSortSeqDemo{
  2. int a = 0;
  3. boolean flag = false;
  4. public void method01(){
  5. a = 1;//语句1
  6. flag = true;//语句2
  7. }
  8. public void method02(){
  9. if(flag){
  10. a = a + 5; //语句3
  11. }
  12. System.out.println("retValue: " + a);//可能是6或1或5或0
  13. }
  14. }

多线程环境中线程交替执行method01()method02(),由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。 

 

volatile实现禁止指令重拍优化,从而避免多线程环境下程序出现乱序执行的现象。

先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:

  1. 保证特定操作的执行顺序,
  2. 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。

由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

对volatile变量进行写操作时,会在写操作后加入一条store屏障指令,将工作内存中的共享变量值刷新回到主内存。

对Volatile变量进行读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。

 

2.CAS你知道吗?

CAS的全称为Compare-And-Swap,比较并交换,是一条CPU并发原语。

CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

  1. public class CASDemo{
  2. public static void main(string[] args){
  3. AtomicInteger atomicInteger = new AtomicInteger(5);// mian do thing. . . . ..
  4. System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t current data: "+atomicInteger.get());
  5. System.out.println(atomicInteger.compareAndset(5, 1024)+"\t current data: "+atomicInteger.get());
  6. }
  7. }

输出结果为

  1. true 2019
  2. false 2019

 CAS底层原理?谈谈你对UnSafe的理解?

atomiclnteger.getAndIncrement();源码

  1. public class AtomicInteger extends Number implements java.io.Serializable {
  2. private static final long serialVersionUID = 6214790243416807050L;
  3. // setup to use Unsafe.compareAndSwapInt for updates
  4. private static final Unsafe unsafe = Unsafe.getUnsafe();
  5. private static final long valueOffset;
  6. static {
  7. try {
  8. valueOffset = unsafe.objectFieldOffset
  9. (AtomicInteger.class.getDeclaredField("value"));
  10. } catch (Exception ex) { throw new Error(ex); }
  11. }
  12. private volatile int value;
  13. /**
  14. * Creates a new AtomicInteger with the given initial value.
  15. *
  16. * @param initialValue the initial value
  17. */
  18. public AtomicInteger(int initialValue) {
  19. value = initialValue;
  20. }
  21. /**
  22. * Creates a new AtomicInteger with initial value {@code 0}.
  23. */
  24. public AtomicInteger() {
  25. }
  26. ...
  27. /**
  28. * Atomically increments by one the current value.
  29. *
  30. * @return the previous value
  31. */
  32. public final int getAndIncrement() {
  33. return unsafe.getAndAddInt(this, valueOffset, 1);
  34. }
  35. ...
  36. }

1. UnSafe是CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地(native)方法来访问,而基于UnSafe类可以直接操作特定内存的数据,UnSafe类存在于sun.misc包中,其内部方法操作可以像c的指针一样直接操作内存,因为java中CAS操作的执行依赖于UnSafe类的方法。

2.变量valueOffset 表示该变量值在内存中的偏移地址,因为UnSafe就是根据内存偏移地址获取数据的。

3.变量value用volatile修饰,保证了多线程之间的内存可见性。

CAS是什么?

CAS的全称为Compare-And-Swap,比较并交换,是一条CPU并发原语。
他的功能是判断内存某个位置的值是否为预期值,如果是则更新为新的值,这个过程是原子的。

CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。(原子性)

上面类似自旋锁

UnSafe.getAndAddInt()源码解释:

  • var1 AtomicInteger对象本身。
  • var2 该对象值得引用地址。
  • var4 需要变动的数量。(步长)
  • var5是用过var1,var2找出的主内存中真实的值。
  • 用该对象当前的值与var5比较:        如果相同,更新var5+var4并且返回true, 如果不同,继续取值然后再比较,直到更新完成。

假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上) :

  1. Atomiclnteger里面的value原始值为3,即主内存中Atomiclnteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
  2. 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
  3. 线程B也通过getintVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
  4. 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值己经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
  5. 线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwaplnt进行比较替换,直到成功

CAS缺点:

1.循环时间长开销很大

  1. // ursafe.getAndAddInt
  2. public final int getAndAddInt(Object var1, long var2, int var4){
  3. int var5;
  4. do {
  5. var5 = this.getIntVolatile(var1, var2);
  6. }while(!this.compareAndSwapInt(varl, var2, var5,var5 + var4));
  7. return var5;
  8. }

可以看到getAndAddInt方法执行时,有个do while,如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

2.只能保证一个共享变量的原子操作

当对一个共享变量执行操作是,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

3.CAS引来ABA问题

3.原子类AtomicInteger的ABA问题谈谈?原子类更新引用知道吗?

CAS会导致“ABA问题”(狸猫换太子).

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作变成了B,然后线程two又将V位置的数据变成了A,这时候线程one进行CAS操作的时候发现内存中仍然是A,然后线程one操作成功

尽管线程one的CAS操作成功,但不代表这个过程是没有问题的。

AtomicReference原子引用(自定义的类,原理和AtomicInteger差不多)

  1. import java.util.concurrent.atomic.AtomicReference;
  2. class User{
  3. String userName;
  4. int age;
  5. public User(String userName, int age) {
  6. this.userName = userName;
  7. this.age = age;
  8. }
  9. @Override
  10. public String toString() {
  11. return String.format("User [userName=%s, age=%s]", userName, age);
  12. }
  13. }
  14. public class AtomicReferenceDemo {
  15. public static void main(String[] args){
  16. User z3 = new User( "z3",22);
  17. User li4 = new User("li4" ,25);
  18. AtomicReference<User> atomicReference = new AtomicReference<>();
  19. atomicReference.set(z3);
  20. System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString());
  21. System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString());
  22. }
  23. }

输出结果

  1. true User [userName=li4, age=25]
  2. false User [userName=li4, age=25]

 AtomicStampedReference版本号原子引用:

 原子引用 + 新增一种机制,那就是修改版本号(类似时间戳),它用来解决ABA问题。

  1. import java.util.concurrent.TimeUnit;
  2. import java.util.concurrent.atomic.AtomicReference;
  3. import java.util.concurrent.atomic.AtomicStampedReference;
  4. public class ABADemo {
  5. /**
  6. * 普通的原子引用包装类
  7. */
  8. static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
  9. // 传递两个值,一个是初始值,一个是初始版本号
  10. static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
  11. public static void main(String[] args) {
  12. System.out.println("============以下是ABA问题的产生==========");
  13. new Thread(() -> {
  14. // 把100 改成 101 然后在改成100,也就是ABA
  15. atomicReference.compareAndSet(100, 101);
  16. atomicReference.compareAndSet(101, 100);
  17. }, "t1").start();
  18. new Thread(() -> {
  19. try {
  20. // 睡眠一秒,保证t1线程,完成了ABA操作
  21. TimeUnit.SECONDS.sleep(1);
  22. } catch (InterruptedException e) {
  23. e.printStackTrace();
  24. }
  25. // 把100 改成 101 然后在改成100,也就是ABA
  26. System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
  27. }, "t2").start();
  28. /
  29. try {
  30. TimeUnit.SECONDS.sleep(2);
  31. } catch (Exception e) {
  32. e.printStackTrace();
  33. }
  34. /
  35. System.out.println("============以下是ABA问题的解决==========");
  36. new Thread(() -> {
  37. // 获取版本号
  38. int stamp = atomicStampedReference.getStamp();
  39. System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);
  40. // 暂停t3一秒钟
  41. try {
  42. TimeUnit.SECONDS.sleep(1);
  43. } catch (InterruptedException e) {
  44. e.printStackTrace();
  45. }
  46. // 传入4个值,期望值,更新值,期望版本号,更新版本号
  47. atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(),
  48. atomicStampedReference.getStamp() + 1);
  49. System.out.println(Thread.currentThread().getName() + "\t 第二次版本号" + atomicStampedReference.getStamp());
  50. atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(),
  51. atomicStampedReference.getStamp() + 1);
  52. System.out.println(Thread.currentThread().getName() + "\t 第三次版本号" + atomicStampedReference.getStamp());
  53. }, "t3").start();
  54. new Thread(() -> {
  55. // 获取版本号
  56. int stamp = atomicStampedReference.getStamp();
  57. System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);
  58. // 暂停t4 3秒钟,保证t3线程也进行一次ABA问题
  59. try {
  60. TimeUnit.SECONDS.sleep(3);
  61. } catch (InterruptedException e) {
  62. e.printStackTrace();
  63. }
  64. boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
  65. System.out.println(Thread.currentThread().getName() + "\t 修改成功否:" + result + "\t 当前最新实际版本号:"
  66. + atomicStampedReference.getStamp());
  67. System.out.println(Thread.currentThread().getName() + "\t 当前实际最新值" + atomicStampedReference.getReference());
  68. }, "t4").start();
  69. }
  70. }

输出结果

  1. ============以下是ABA问题的产生==========
  2. true    2019
  3. ============以下是ABA问题的解决==========
  4. t3     第一次版本号1
  5. t4     第一次版本号1
  6. t3     第二次版本号2
  7. t3     第三次版本号3
  8. t4     修改成功否:false     当前最新实际版本号:3
  9. t4     当前实际最新值100

反射的实现与作用

Java语言编译之后会生成一个.class文件,反射就是通过字节码文件找到某一个类,类中的方法以及属性等。反射的实现主要借助以下四个类:

Class:类的对象

Constructor:类的构造方法

Field:类中的属性对象

Method:类中的方法对象

作用:反射机制指的是程序在运行时能够获取自身的信息。在java中,只要给定类的名字,那么就可以通过反射机制来获取类的所有信息。

注解的原理

注解本质是一个继承了Annotation 的特殊接口,其具体实现类是Java 运行时生成的动态代理类。而我们通过反射获取注解时,返回的是Java 运行时生成的动态代理对象$Proxy1。通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler 的invoke 方法。该方法会从memberValues 这个Map 中索引出对应的值。而memberValues 的来源是Java 常量池。

Synchronized和lock

synchronized是Java的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。JDK1.5以后引入了自旋锁、锁粗化、轻量级锁,偏向锁来有优化关键字的性能。

(1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

(2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

(3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

(4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

Integer和int=5的区别:

Java是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java为每一个基本数据类型都引入了对应的包装类型(wrapper class),int的包装类就是Integer,从Java 5开始引入了自动装箱/拆箱机制,使得二者可以相互转换。

Java 为每个原始类型提供了包装类型:

- 原始类型: boolean,char,byte,short,int,long,float,double

- 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double

如:

class AutoUnboxingTest {

    public static void main(String[] args) {

        Integer a = new Integer(3);

        Integer b = 3;                  // 将3自动装箱成Integer类型

        int c = 3;

        System.out.println(a == b);     // false 两个引用没有引用同一对象

        System.out.println(a == c);     // true a自动拆箱成int类型再和c比较

    }

}

 

请你解释一下类加载机制,双亲委派模型,好处是什么?

某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

使用双亲委派模型的好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行。

Java的容器:

容器是一个java所编写的程序,原先必须自行编写程序以管理对象关系,容器会自动帮您做好。

常用的容器:WebSphere,Weblogic,Resinm,Tomcat,Glassfish

Java容器类包含:list ,Arraylist,Vector及map, hashTable, HashMap, HashSet

wait和sleep:

sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。

wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。

hashSet的底层实现原理:

hashset是无序的,不可重复的。

Hashset底层使用了哈希表(哈希表是将数组和单向链表的优点集成在一起)实现的。特点是存储快

往hashset添加元素的时候,hashset会先调用元素的hashcode方法得到元素的哈希值,然后通过与水泥素的哈希值经过异或或移位等运算,就可以算出该元素在哈希表中的存储位置。

运行原理:

如果算出的元素的存储的位置目前没有任何元素储存,那么该元素可以直接存储在该位置上,如果算出的元素的存储位置上目前已经有了其他的元素没那么还会调用该元素的equals方法,与该位置的元素进行比较一次,如果equals方法返回的是true,那么该位置上的元素就会被视为重复元素,不允许被添加,如果false,则允许添加。

实现原理

Hashset是基于hashmap实现的,默认构造函数是构建一个初始容量为16,负载因子为0.75的hashmap。封装了一个hashmap对象来储存所有的集合元素,所有放在hashset中的集合元素实际上由hashmap的key来保存。

hashSet和treeset有什么区别:

hashset是由一个hash表来实现的,因此它的元素是无序的,add,remove,contains方法的时间复杂度是 O(1)

treeset是由一个树形结构来实现的,它里面的元素是有序的,因此,add,remove,contains方法的时间复杂度是 O(logn)

二叉树,多叉树,B树,B+树,B*树

二叉树的操作效率较高,但存在问题。

  1. 二叉树需要加载到内存,如果二叉树的节点少,那没什么问题,但是如果二叉树的节点很多(1亿),就会出现问题。
  2. 问题1:在构建二叉树时,需要多次进行I/O操作(海量数据存在数据库或者文件中),节点海量,构建二叉树时,速度有影响、。
  3. 问题2:节点海量,也会造成二叉树的高度很大,会降低操作速度。

 

多叉树:在二叉树中,每个节点有数据项,最多有两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化。(2-3树,2-3-4树)

 

 

B树(B-tree/B-树)(就是多叉树的一种):B树通过重新组织节点,降低了树的高度,广泛应用于文件系统和数据库系统中。

B即Balanced,平衡的意思。

特点:在叶子节点和非叶子节点都存放数据。

 

B树为什么要设置成多路?

答:为了进一步降低树的高度。但不能设计成无限多路,因为如果不限制路数,B树就退化成一个有序数组了,而文件系统和数据库索引都是存在硬盘上的,并且数据量大的话,不一定能一次性加载到内存中。

B树做文件系统的索引比较多。

 

B+树(B树的变体)(也是多一种多路搜索树):

特点:(1)所有的数据(关键字)都放在叶子节点上。

          

 

B+树做数据库的索引比较多。

这是和业务场景相关的,数据库中select数据,不一定只选一条,很多时候会选多条,比如按照id排序后选10条。使用B树需要做局部的中序遍历,可能要跨层访问。而B+树由于所有数据都在叶子结点,不用跨层,同时由于有链表结构,只需要找到首尾,通过链表就能把所有数据取出来了。

 

面试题:B+树查询的时间和树的高度有关,大概是log(n),而使用hash存储索引,查询的平均时间是O(1),既然hash比B+树更快,为啥mysql还用b+树来存索引呢?

答:这和业务场景有关。如果只选一个数据,那确实是hash更快。但是数据库中经常会选择多条,这时候由于B+树索引有序,并且又有链表相连,它的查询效率比hash就快很多了。而且数据库中的索引一般是在磁盘上,数据量大的情况可能无法一次装入内存,B+树的设计可以允许数据分批加载,同时树的高度较低,提高查找效率。

B*树(B+树的变体)(在B+树的非根和非叶子结点再增加指向兄弟的指针):

 

1.怎么快速的把一个list集合中的元素去重?

(1)利用HashSet去重

  1. package com.ggqq;
  2. import java.util.ArrayList;
  3. import java.util.HashSet;
  4. import java.util.Iterator;
  5. import java.util.List;
  6. public class Test01 {
  7. public static void main(String[] args) {
  8. List list = new ArrayList();
  9. list.add("123");
  10. list.add("123");
  11. list.add("234");
  12. list.add("789");
  13. /*//遍历
  14. //方法一:for循环
  15. for(int i = 0; i<list.size();i++){
  16. System.out.println(list.get(i));
  17. }
  18. //方法二:iterator迭代器
  19. Iterator iterator = list.iterator();
  20. while(iterator.hasNext()){
  21. System.out.println(iterator.next());
  22. }*/
  23. //方法三:增强for
  24. for(Object s:list){
  25. System.out.println(s);
  26. }
  27. //list添加到HashSet中去重
  28. HashSet hashSet = new HashSet(list);
  29. //清空list
  30. list.clear();
  31. //将HashSet添加到list中
  32. list.addAll(hashSet);
  33. //遍历
  34. for(int i = 0; i<list.size();i++){
  35. System.out.println(list.get(i));
  36. }
  37. }
  38. }

 (2)通过List的contains()方法去重

  1. package com.ggqq;
  2. import java.util.ArrayList;
  3. import java.util.HashSet;
  4. import java.util.Iterator;
  5. import java.util.List;
  6. public class Test02 {
  7. public static void main(String[] args) {
  8. List list = new ArrayList();
  9. list.add("123");
  10. list.add("123");
  11. list.add("234");
  12. list.add("789");
  13. //遍历
  14. Iterator iterator = list.iterator();
  15. while(iterator.hasNext()){
  16. System.out.println(iterator.next());
  17. }
  18. System.out.println("------------------------");
  19. //新建一个tempList,用于存放去重后的
  20. List tempList = new ArrayList();
  21. for(int i = 0 ; i <list.size();i++){
  22. if(!tempList.contains(list.get(i))){
  23. tempList.add(list.get(i));
  24. }
  25. }
  26. //遍历
  27. Iterator iterator2 = tempList.iterator();
  28. while(iterator2.hasNext()){
  29. System.out.println(iterator2.next());
  30. }
  31. }
  32. }

(3) 通过两层for循环判断

  1. package com.ggqq;
  2. import java.util.ArrayList;
  3. import java.util.Iterator;
  4. import java.util.List;
  5. public class Test03 {
  6. public static void main(String[] args) {
  7. List list = new ArrayList();
  8. list.add("123");
  9. list.add("123");
  10. list.add("234");
  11. list.add("789");
  12. //遍历
  13. Iterator iterator = list.iterator();
  14. while(iterator.hasNext()){
  15. System.out.println(iterator.next());
  16. }
  17. System.out.println("------------------------");
  18. //从list中索引为0开始往后遍历
  19. for(int i = 0 ; i <list.size()-1;i++){
  20. for(int j = list.size()-1; j>i; j-- ){
  21. if(list.get(i).equals(list.get(j))){
  22. //去重
  23. list.remove(j);
  24. }
  25. }
  26. }
  27. //遍历
  28. Iterator iterator2 = list.iterator();
  29. while(iterator2.hasNext()){
  30. System.out.println(iterator2.next());
  31. }
  32. }
  33. }

2.ThreadLocal是什么?有哪些使用场景?

ThreadLocal 是线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。

经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。

ThreadLocal 使用例子:

  1. package com.ggqq;
  2. public class TestThreadLocal {
  3. //线程本地存储变量
  4. private static final ThreadLocal<Integer> THREAD_LOCAL_NUM = new ThreadLocal<Integer>() {
  5. @Override
  6. protected Integer initialValue() {
  7. return 0;
  8. }
  9. };
  10. public static void main(String[] args) {
  11. for (int i = 0; i < 3; i++) {//启动三个线程
  12. Thread t = new Thread() {
  13. @Override
  14. public void run() {
  15. add10ByThreadLocal();
  16. }
  17. };
  18. t.start();
  19. }
  20. }
  21. /**
  22. * 线程本地存储变量加 5
  23. */
  24. private static void add10ByThreadLocal() {
  25. for (int i = 0; i < 5; i++) {
  26. Integer n = THREAD_LOCAL_NUM.get();
  27. n += 1;
  28. THREAD_LOCAL_NUM.set(n);
  29. System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n);
  30. }
  31. }
  32. }

 结果:

3.什么是死锁?

 

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。是操作系统层面的一个错误,是进程死锁的简称。

4.分布式锁的实现方式

 (1)为什么需要分布式锁?

首先我们应该先了解一下分布式锁的使用场景,然后再来理解为什么需要分布式锁。现我举两个例子来进行阐述:
应用场景:
1:银行转账问题(该场景不太好解释):A在上海,B在北京同时在建行转账给杭州C,A转账时,会修改C处服务器的表,B不能在此刻转账,同理,B转账时,A不能做处理,A,B的转账操作时同步,必须保证数据的一致性,这就需要分布式锁来进行处理。
2:取任务问题:某服务提供一组任务,A系统请求随机从任务组中获取一个任务;B系统请求随机从任务组中获取一个任务。 在理想的情况下,A从任务组中挑选一个任务,任务组删除该任务,B从剩下的的任务中再挑一个,任务组删除该任务。 同样的,在真实情况下,如果不做任何处理,可能会出现A和B挑中了同一个任务的情况。

(2)为什么分布式系统中不能用普通锁呢?那么普通锁和分布式锁有什么区别呢?

普通锁:单一系统中,同一个应用程序是有同一个进程,然后多个线程并发会造成数据安全问题,他们是共享同一块内存的,所以在内存某个地方做标记即可满足需求,例如synchronized和volatile+cas一样对具体的代码做标记,对应的就是在同一块内存区域作了同步的标记。
分布式锁:分布式系统中,最大的区别就是不同系统中的应用程序都是在各自机器上不同的进程中处理的,这里的线程不安全可以理解为多进程造成的数据安全问题,他们不会共享同一台机器的同一块内存区域,因此需要将标记存储在所有进程都能看到的地方。例如zookeeper作分布式锁,就是将锁标记存储在多个进程共同看到的地方,redis作分布式锁,是将其标记公共内存,而不是某个进程分配的区域。

(3)分布式锁的三种实现方式

a:zookeeper实现分布式锁(用的最多)

实现方式:
方案1:利用节点名称的唯一性来实现共享锁。
算法思路: 利用名称唯一性,加锁操作时,只需要所有客户端一起创建/test/Lock节点,只有一个创建成功,成功者获得锁。解锁时,只需删除/test/Lock节点,其余客户端再次进入竞争创建节点,直到所有客户端都获得锁。
方案2:利用临时顺序节点实现共享锁。(主要是用这种方式实现)
算法思路:对于加锁操作,可以让所有客户端都去/lock目录下创建临时顺序节点,如果创建的客户端发现自身创建节点序列号是/lock/目录下最小的节点,则获得锁。否则,监视比自己创建节点的序列号小的节点(比自己创建的节点小的最大节点),进入等待。
比如创建节点:/lock/0000000001、/lock/0000000002、/lock/0000000003。则节点/lock/0000000001会先获得锁,因为zk上的节点是有序的,且都是最小的节点先获得锁。
注:临时顺序节点比持久顺序节点的好处是:当zookeeper宕机后,临时顺序节点会自动删除,获取锁的客户端会释放锁,不会一直造成锁等待,而持久节点会造成锁等待。
两种方式的区别
方案1会产生惊群效应:假如许多客户端在等待一把锁,当锁释放时候所有客户端都被唤醒,然后竞争分布式锁,仅仅有一个客户端得到锁。
方案2是按照创建顺序排队的实现,多个客户端共同等待锁,当锁释放时只有一个客户端会被唤醒,在zk上注册节点最小的客户端会被唤醒,避免了惊群效应。

b:redis实现分布式锁(用的次之)

redis实现分布式锁主要靠四个命令:
setnx(set if not exits 维护着是乐观锁):当不存在key的时候,才为key设置值为value。setnx与set的区别:set是存在key,则去覆盖value;setnx是不存在key,则重新给key和value赋值。
getset:根据key得到旧的值,并set新的值。
expire:设置过期时间。
del:删除

实现方式:
1:获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
2:获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
3:释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

c:数据库实现分布式锁(用的最少)

实现方式:利用的是乐观锁和悲观锁
乐观锁:在表中添加版本号的字段,每次更新前都先查询出带版本号的数据,然后再更新的时候where条件语句后带版本号条件,更新成功表示锁已占用,更新不成功表示锁没被占用。
悲观锁:利用select...for update(X锁)/select...lock in share mode(S锁),一般来说用X锁的较多,因为后续多会做写功能的实现。
注:当实现悲观锁的时候,需要关闭数据库的事务自动提交机制不然不会生效。因此java代码中应该选择主动关闭数据库的事务自动提交功能。

 

5.说一下HashMap的实现原理

 hashset是无序的,不可重复的。

Hashset底层使用了哈希表(哈希表是将数组和单向链表的优点集成在一起)实现的。特点是存储快

往hashset添加元素的时候,hashset会先调用元素的hashcode方法得到元素的哈希值,然后通过与水泥素的哈希值经过异或或移位等运算,就可以算出该元素在哈希表中的存储位置。

运行原理:

如果算出的元素的存储的位置目前没有任何元素储存,那么该元素可以直接存储在该位置上,如果算出的元素的存储位置上目前已经有了其他的元素没那么还会调用该元素的equals方法,与该位置的元素进行比较一次,如果equals方法返回的是true,那么该位置上的元素就会被视为重复元素,不允许被添加,如果false,则允许添加。

实现原理

Hashset是基于hashmap实现的,默认构造函数是构建一个初始容量为16,负载因子为0.75的hashmap。封装了一个hashmap对象来储存所有的集合元素,所有放在hashset中的集合元素实际上由hashmap的key来保存。

6.在Queue中poll()和remove()有什么区别?

poll()和remove()都将移除并且返回队头,但是在poll()在队列为空时返回null,而remove()会抛出NoSuchElementException异常。 

7.JDK1.8的新特性:

https://blog.csdn.net/Mcdull__/article/details/112464042 

8.application和bootstrap的应用场景:

application:配置文件这个容易理解,主要用于SpringBoot项目的自动化配置。

bootstrap:配置文件有以下几个应用场景:

  • 使用SpringCloud Config配置中心时,这时需要在bootstrap配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息;
  • 一些固定的不能被覆盖的属性;
  • 一些加密/解密的场景。
application.yml 是用户级别的配置,而bootstrap.yml 是系统级别的配置

9.HashMap和HashTable的key和value是否可以为null?

HashMap可以存储一个Key为null,多个value为null的元素,但是Hashtable却不可以存储

(看源码)

10.什么是Spring?

Spring是于2003年兴起的一个轻量级java开发框架,他是为了解决企业应用开发的复杂性而创建的。Spring的核心是控制反转(IOC)和面向切面编程(AOP)。

Spring的作用就是为代码解耦合,降低代码的耦合度。就是让对象和对象(模块和模块)之间关系不是使用代码关联,而是通过配置来说明,

IOC又称自动注入,注入即赋值,IOC使的主业务在相互调用的过程中,不用再自己维护关系了,即不用自己再创建要使用的对象了,而是右Spring容器统一管理。

AOP是动态代理的规范化,AOP是Spring框架中的一个重要内容,利用AOP可以对业务逻辑的各个部分进行隔离,从而使业务逻辑的各个部分之间的耦合度降低,提高程序的可重复性,同时提高了开发的效率。

 

SpringMVC谁调谁的问题

在SpringMVC中,强化了注解的使用,在Controller,Service,Dao层都可以使用注解。

  • 使用@Controller创建处理器对象
  • @Service创建业务对象
  • @Autowired或者@Resource在Controller类中注入Service,在Service类中注入Dao.

使用@Controller注解的处理器的处理器方法,其返回值类型常用的有四种类型: 

第一种:ModelAndView          (传递数据+传递视图(即跳转到其他资源))

第二种:String                         (视图)

第三种: 无返回值void            (处理Ajax)

第四种:返回自定义类型对象  (数据) 

区分返回值String是数据还是视图,看有没有@ResponseBody注解,如果有,则是数据,否则是视图。

2、cookie和session的区别?

session是服务器端用于验证用户权限的一把钥匙,存于服务端,在进行数据交互时使用的,我们经常遇到的登录失效这一类场景,就是因为session在中间起到了作用。

cookie是保存在本地的数据(客户端),可以简单地理解,在页面中输入账号,自动弹出密码,这个密码之所以会弹出就是因为本地cookie的原因,包括历史记录这些,之所以会有记录,就是因为内容存储在本地的cookie文件中。

 

  • 1. 由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是Session.典型的场景比如购物车,当你点击下单按钮时,由于HTTP协议无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的Session,用于标识这个用户,并且跟踪用户,这样才知道购物车里面有几本书。这个Session是保存在服务端的,有一个唯一标识。在服务端保存Session的方法很多,内存、数据库、文件都有。集群的时候也要考虑Session的转移,在大型的网站,一般会有专门的Session服务器集群,用来保存用户会话,这个时候 Session 信息都是放在内存的,使用一些缓存服务比如Memcached之类的来放 Session。
  • 2. 思考一下服务端如何识别特定的客户?这个时候Cookie就登场了。每次HTTP请求的时候,客户端都会发送相应的Cookie信息到服务端。实际上大多数的应用都是用 Cookie 来实现Session跟踪的,第一次创建Session的时候,服务端会在HTTP协议中告诉客户端,需要在 Cookie 里面记录一个Session ID,以后每次请求把这个会话ID发送到服务器,我就知道你是谁了。有人问,如果客户端的浏览器禁用了 Cookie 怎么办?一般这种情况下,会使用一种叫做URL重写的技术来进行会话跟踪,即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数,服务端据此来识别用户。
  • 3. Cookie其实还可以用在一些方便用户的场景下,设想你某次登陆过一个网站,下次登录的时候不想再次输入账号了,怎么办?这个信息可以写到Cookie里面,访问网站的时候,网站页面的脚本可以读取这个信息,就自动帮你把用户名给填了,能够方便一下用户。这也是Cookie名称的由来,给用户的一点甜头。

 

  • 所以,总结一下:
  1. Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;
  2. Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。

3、springmvc和springboot的区别

Spring 框架就像一个家族,有众多衍生产品例如 boot、security、jpa等等。但他们的基础都是Spring 的 ioc和 aop ioc 提供了依赖注入的容器 aop ,解决了面向横切面的编程,然后在此两者的基础上实现了其他延伸产品的高级功能。Spring MVC是基于 Servlet 的一个 MVC 框架 主要解决 WEB 开发的问题,因为 Spring 的配置非常复杂,各种XML、 JavaConfig、hin处理起来比较繁琐。于是为了简化开发者的使用,从而创造性地推出了Spring boot,约定优于配置,简化了spring的配置流程。

说得更简便一些:Spring 最初利用“工厂模式”(DI)和“代理模式”(AOP)解耦应用组件。大家觉得挺好用,于是按照这种模式搞了一个 MVC框架(一些用Spring 解耦的组件),用开发 web 应用( SpringMVC )。然后有发现每次开发都写很多样板代码,为了简化工作流程,于是开发出了一些“懒人整合包”(starter),这套就是 Spring Boot。

 

Spring MVC的功能

Spring MVC提供了一种轻度耦合的方式来开发web应用。

Spring MVC是Spring的一个模块,式一个web框架。通过Dispatcher Servlet, ModelAndView 和 View Resolver,开发web应用变得很容易。解决的问题领域是网站应用程序或者服务开发——URL路由、Session、模板引擎、静态Web资源等等。

 

Spring Boot的功能

Spring Boot实现了自动配置,降低了项目搭建的复杂度。

众所周知Spring框架需要进行大量的配置,Spring Boot引入自动配置的概念,让项目设置变得很容易。Spring Boot本身并不提供Spring框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于Spring框架的应用程序。也就是说,它并不是用来替代Spring的解决方案,而是和Spring框架紧密结合用于提升Spring开发者体验的工具。同时它集成了大量常用的第三方库配置(例如Jackson, JDBC, Mongo, Redis, Mail等等),Spring Boot应用中这些第三方库几乎可以零配置的开箱即用(out-of-the-box),大部分的Spring Boot应用都只需要非常少量的配置代码,开发者能够更加专注于业务逻辑。

Spring Boot只是承载者,辅助你简化项目搭建过程的。如果承载的是WEB项目,使用Spring MVC作为MVC框架,那么工作流程和你上面描述的是完全一样的,因为这部分工作是Spring MVC做的而不是Spring Boot。

对使用者来说,换用Spring Boot以后,项目初始化方法变了,配置文件变了,另外就是不需要单独安装Tomcat这类容器服务器了,maven打出jar包直接跑起来就是个网站,但你最核心的业务逻辑实现与业务流程实现没有任何变化。

所以,用最简练的语言概括就是:

Spring 是一个“引擎”;

Spring MVC 是基于Spring的一个 MVC 框架 ;

Spring Boot 是基于Spring4的条件注册的一套快速开发整合包。

5、你所知道的微服务技术栈有哪些?列举一二

  • 服务开发:springboot spring springmvc
  • 服务配置与管理:Netfix公司的Archaiusm ,阿里的Diamond
  • 服务注册与发现:Eureka,Zookeeper
  • 服务调用:Rest RPC gRpc
  • 服务熔断器:Hystrix
  • 服务负载均衡:Ribbon Nginx
  • 服务接口调用:Fegin
  • 消息队列:Kafka Rabbitmq activemq
  • 服务配置中心管理:SpringCloudConfig
  • 服务路由(API网关)Zuul
  • 事件消息总线:SpringCloud Bus
微服务技术条目落地技术
服务开发SpringBoot、Spring、SpringMVC等
服务配置与管理Netfix公司的Archaius、阿里的Diamond等
服务注册与发现Eureka、Consul、Zookeeper等
服务调用Rest、PRC、gRPC
服务熔断器Hystrix、Envoy等
负载均衡Ribbon、Nginx等
服务接口调用(客户端调用服务的简化工具)Fegin等
消息队列Kafka、RabbitMQ、ActiveMQ等
服务配置中心管理SpringCloudConfig、Chef等
服务路由(API网关)Zuul等
服务监控Zabbix、Nagios、Metrics、Specatator等
全链路追踪Zipkin、Brave、Dapper等
数据流操作开发包SpringCloud Stream(封装与Redis,Rabbit,Kafka等发送接收消息)
时间消息总栈SpringCloud Bus
服务部署Docker、OpenStack、Kubernetes等

6、 SpringCloud 和 Dubbo有那些区别?

对比结果:

 DubboSpringCloud
服务注册中心ZookeeperSpring Cloud Netfilx Eureka
服务调用方式RPCREST API
服务监控Dubbo-monitorSpring Boot Admin
断路器不完善Spring Cloud Netfilx Hystrix
服务网关Spring Cloud Netfilx Zuul
分布式配置Spring Cloud Config
服务跟踪Spring Cloud Sleuth
消息总栈Spring Cloud Bus
数据流Spring Cloud Stream
批量任务Spring Cloud Task

 

最大的区别:SpringCloud抛弃了Dubbb的RPC通信,采用的是Http的REST方式

Rest和RPC对比:严格来说,这两种方式各有优劣,虽然从一定程度上说,Rest牺牲了服务调用的性能,但也避免了原生RPC带来的问题,而且REST相比RPC更为灵活,服务提供方和调用方只依靠一纸契约,不存在代码级别的强依赖,这个有点在当下强调快速演化的微服务环境下,显得更为合适。

文档质量和社区活跃度对比:SpringCloud社区活跃度远高于Dubbo,毕竟由于梁飞团队的原因导致Dubbo停止更新迭代五年,而中小型公司无法承担技术开发的成本导致Dubbo社区严重低落,而SpringCloud异军突起,迅速占领了微服务的市场,背靠Spring混的风生水起
Dubbo经过多年的积累文档相当成熟,对于微服务的架构体系各个公司也有稳定的现状

品牌机和组装机的区别:Dubbo是组装机,SpringCloud是品牌机

总结:二者解决的问题领域不一样,Dubbo的定位是一款RPC框架,而SpringCloud的目标是微服务架构下的一站式解决方案。

 

7、 SpringBoot 和 SpringCloud,请谈谈你对他们的理解

  • SpringBoot专注于快速方便的开发单个个体微服务(打jar包)
  • SpringCloud是关注全局的微服务协调整理治理框架,他将SpringBoot开发的一个个单体微服务,整合并管理起来,为各个微服务之间提供:配置管理,服务发现,断路器,路由,为代理,事件总栈,全局锁,决策竞选,分布式会话等等集成服务。
  • SpringBoot可以离开SpringCloud独立使用,开发项目,但是SpringCloud离不开SpringBoot,属于依赖关系
  • SpringBoot专注于快速、方便的开发单个个体微服务,SpringCloud关注全局的微服务治理框架

 

8、 Eureka和Zookeeper都可以提供服务注册与发现的功能,请说说两者的区别

 回顾CAP原则

RDBMS (MySQL\Oracle\sqlServer) ===> ACID

NoSQL (Redis\MongoDB) ===> CAP

 

ACID是什么?(orcal mysql等数据库的性质)

  • A (Atomicity) 原子性
  • C (Consistency) 一致性
  • I (Isolation) 隔离性
  • D (Durability) 持久性

 

CAP是什么?

  • C (Consistency) 强一致性(保证数据的一致性)
  • A (Availability) 可用性
  • P (Partition tolerance) 分区容错性

CAP的三进二:要么CA、要么AP、要么CP(无法保证该三个性质都满足)

 

CAP原则的核心

  • 一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求
  • 根据CAP原理,将NoSQL数据库分成了满足CA原则,满足CP原则和满足AP原则三大类
    • CA:单点集群,满足一致性,可用性的系统,通常可扩展性较差
    • CP:满足一致性,分区容错的系统,通常性能不是特别高
    • AP:满足可用性,分区容错的系统,通常可能对一致性要求低一些

 

作为分布式服务注册中心,Eureka比Zookeeper好在哪里?

著名的CAP理论指出,一个分布式系统不可能同时满足C (一致性) 、A (可用性) 、P (容错性),由于分区容错性P在分布式系统中是必须要保证的,因此我们只能再A和C之间进行权衡。

  • Zookeeper 保证的是 CP —> 满足一致性,分区容错的系统,通常性能不是特别高
  • Eureka 保证的是 AP —> 满足可用性,分区容错的系统,通常可能对一致性要求低一些

Zookeeper保证的是CP(一致性,容错性,可用性差)

​ 当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的注册信息,但不能接收服务直接down掉不可用。也就是说,服务注册功能对可用性的要求要高于一致性。但zookeeper会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新进行leader选举。问题在于,选举leader的时间太长,30-120s,且选举期间整个zookeeper集群是不可用的,这就导致在选举期间注册服务瘫痪。在云部署的环境下,因为网络问题使得zookeeper集群失去master节点是较大概率发生的事件,虽然服务最终能够恢复,但是,漫长的选举时间导致注册长期不可用,是不可容忍的。

Eureka保证的是AP(可用性,容错性,一致性差)

​ Eureka看明白了这一点,因此在设计时就优先保证可用性。Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而Eureka的客户端在向某个Eureka注册时,如果发现连接失败,则会自动切换至其他节点,只要有一台Eureka还在,就能保住注册服务的可用性,只不过查到的信息可能不是最新的,除此之外,Eureka还有之中自我保护机制,如果在15分钟内超过85%的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,此时会出现以下几种情况:

  • Eureka不在从注册列表中移除因为长时间没收到心跳而应该过期的服务
  • Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点上 (即保证当前节点依然可用)
  • 当网络稳定时,当前实例新的注册信息会被同步到其他节点中

因此,Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像zookeeper那样使整个注册服务瘫痪

11.关于数组的时间复杂度

 
算法时间复杂度
线性查找O(N)
二分查找O(logN)
无序数组的插入O(1)
有序数组的插入O(N)
无序数组的删除O(N)
有序数组的删除O(N)

12.线程启动的四种方式:

https://blog.csdn.net/Mcdull__/article/details/115444499

原文链接:https://blog.csdn.net/Mcdull__/article/details/117734159



所属网站分类: 技术文章 > 博客

作者:快起来搬砖啦

链接:http://www.javaheidong.com/blog/article/222295/6a3d77e50b9bbf48febd/

来源:java黑洞网

任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任

2 0
收藏该文
已收藏

评论内容:(最多支持255个字符)