JAVA面试集锦(一)

JAVA面试集锦(一)

搭配的视频地址:
1、https://www.bilibili.com/video/BV1da411b7An?p=1
2、https://www.bilibili.com/video/BV15u411B7TJ?p=1&vd_source=3558fd85254f40ca0361146cc92d2cce

CopyOnWriteArrayList的底层原理是怎样的

  1. 首先CopyOnWriteArrayList内部也是用过数组来实现的,在向CopyOnWriteArrayList添加元素时,会复制⼀个新的数组,写操作在新数组上进行,读操作在原数组上进⾏
  2. 并且,写操作会加锁,防止出现并发写入丢失数据的问题
  3. 写操作结束之后会把原数组指向新数组
  4. CopyOnWriteArrayList允许在写操作时来读取数据,大大提高了读的性能,因此适合读多写少的应用场景

但是CopyOnWriteArrayList会比较占内存,同时可能读到的数据不是实时最新的数据,所以不适合实时性要求很高的场景

HashMap的扩容机制原理

1.7版本
1. 先生成新数组
2. 遍历老数组中的每个位置上的链表上的每个元素
3. 取每个元素的key,并基于新数组长度,计算出每个元素在新数组中的下标
4. 将元素添加到新数组中去
5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

1.8版本
1. 先生成新数组
2. 遍历老数组中的每个位置上的链表或红⿊树
3. 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
4. 如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置
a. 统计每个下标位置的元素个数
b. 如果该位置下的元素个数超过了8,则⽣成⼀个新的红黑树,并将根节点的添加到新数组的对应位置
c. 如果该位置下的元素个数没有超过8,那么则⽣成⼀个链表,并将链表的头节点添加到新数组的对应位置
5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

ConcurrentHashMap的扩容机制

1.7版本

  1. 1.7版本的ConcurrentHashMap是基于Segment分段实现的
  2. 每个Segment相对于⼀个小型的HashMap
  3. 每个Segment内部会进行扩容,和HashMap的扩容逻辑类似
  4. 先生成新的数组,然后转移元素到新数组中
  5. 扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值

1.8版本

  1. 1.8版本的ConcurrentHashMap不再基于Segment实现
  2. 当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程⼀起进行扩容
  3. 如果某个线程put时,发现没有正在进⾏扩容,则将key-value添加到ConcurrentHashMap中,然 后判断是否超过阈值,超过了则进行扩容
  4. ConcurrentHashMap是支持多个线程同时扩容的
  5. 扩容之前也先生成⼀个新的数组
  6. 在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移⼯作

ThreadLocal的底层原理

  1. ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部, 该线程可以在任意时刻、任意方法中获取缓存的数据
  2. ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对 象)中都存在⼀个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值
  3. 如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引⽤指向Entry对象,线程不被回收, Entry对象也就不会被回收,从而出现内存泄漏,解决办法是,在使⽤了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清除Entry对象
  4. ThreadLocal经典的应用场景就是连接管理(⼀个线程持有一个连接,该连接对象可以在不同的方法之间进⾏传递,线程之间不共享同⼀个连接) 在这里插入图片描述

如何理解volatile关键字

在并发领域中,存在三大特性:原子性、有序性、可见性。

volatile关键字⽤来修饰对象的属性,在并发环境下可以保证这个属性的可见性,对于加了volatile关键字的属性,在对这个属性进行修改时,会直接 将CPU高级缓存中的数据写回到主内存,对这个变量的读取也会直接从主内存中读取,从而保证了可见性。

底层是通过操作系统的内存屏障来实现的,由于使⽤了内存屏障,所以会禁止指令重排,所以同时 也就保证了有序性,在很多并发场景下,如果用好volatile关键字可以很好的提高执行效率。

ReentrantLock中的公平锁和非公平锁的底层实现

1、首先不管是公平锁和非公平锁,它们的底层实现都会使用AQS来进行排队。
它们的区别在于:线程在使用lock()方法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队, 则当前线程也进行排队,如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁。 不管是公平锁还是非公平锁,一旦没竞争到锁,都会进行排队
2、当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段。
3、另外,ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的。

ReentrantLock中tryLock()和lock()方法的区别

  1. tryLock()表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,如果加到锁则返回 true,没有加到则返回false
  2. lock()表示阻塞加锁,线程会阻塞直到加到锁,方法也没有返回值

CountDownLatch和Semaphore的区别和底层原理

CountDownLatch表示计数器,可以给CountDownLatch设置⼀个数字,⼀个线程调⽤ CountDownLatch的await()将会阻塞,其他线程可以调⽤CountDownLatch的countDown()方法来对CountDownLatch中的数字减⼀,当数字被减成0后,所有await的线程都将被唤醒。

对应的底层原理就是,调用await()方法的线程会利用AQS排队,⼀旦数字被减为0,则会将AQS中 排队的线程依次唤醒。

Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使用该信号量,通过acquire()来获取许可,如果没有许可可用则线程阻塞,并通过AQS来排队,可以通过release()方法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第⼀个线程开始依次唤醒,直到没有空闲许可。

Sychronized的偏向锁、轻量级锁、重量级锁

  1. 偏向锁:在锁对象的对象头中记录⼀下当前获取到该锁的线程ID,该线程下次如果⼜来获取该锁,就可以直接获取到了
  2. 轻量级锁:由偏向锁升级⽽来,当⼀个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻 量级锁底层是通过⾃旋来实现的,并不会阻塞线程
  3. 如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
  4. ⾃旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒 这两个步骤都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的⼀个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程⼀直在运行中,相对而言没有使用太多的操作系统资源,比较轻量。

Sychronized和ReentrantLock的区别

  1. sychronized是⼀个关键字,ReentrantLock是⼀个类
  2. sychronized会⾃动的加锁与释放锁,ReentrantLock需要程序员手动加锁与释放锁
  3. sychronized的底层是JVM层⾯的锁,ReentrantLock是API层面的锁
  4. sychronized是非公平锁,ReentrantLock可以选择公平锁或非公平锁
  5. sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识来标识锁的状态
  6. sychronized底层有⼀个锁升级的过程

线程池的底层工作原理,FixedThreadPool用的阻塞队列是什么

线程池内部是通过队列+线程实现的,当我们利⽤线程池执行任务时:

  1. 如果此时线程池中的线程数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  2. 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
  3. 如果此时线程池中的线程数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建立新的线程来处理被添加的任务。
  4. 如果此时线程池中的线程数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
  5. 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数

FixedThreadPool代表定长线程池,底层用的LinkedBlockingQueue,表示无界的阻塞队列。

你们项目如何排查JVM问题

对于还在正常运行的系统:

  1. 可以使⽤jmap来查看JVM中各个区域的使用情况
  2. 可以通过jstack来查看线程的运行情况,比如哪些线程阻塞、是否出现了死锁
  3. 可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc比较频繁,那么就得进行调优了
  4. 通过各个命令的结果,或者jvisualvm等⼯具来进行分析
  5. 首先,初步猜测频繁发送fullgc的原因,如果频繁发⽣fullgc但是又⼀直没有出现内存溢出,那么表示fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免 这些对象进⼊到老年代,对于这种情况,就要考虑这些存活时间不⻓的对象是不是比较大,导致年轻代放不下,直接进⼊到了老年代,尝试加大年轻代的大小,如果改完之后,fullgc减少,则证明修改有效
  6. 同时,还可以找到占用CPU最多的线程,定位到具体的方法,优化这个方法的执行,看是否能避免某些对象的创建,从而节省内存

对于已经发生了OOM的系统:
1、⼀般生产系统中都会设置当系统发⽣了OOM时,生成当时的dump文件(- XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base)
2、我们可以利用jsisualvm等工具来分析dump文件
3、根据dump文件找到异常的实例对象,和异常的线程(占用CPU高),定位到具体的代码
4、然后再进行详细的分析和调试
总之,调优不是⼀蹴而就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题

说说类加载器双亲委派模型

JVM中存在三个默认的类加载器:

  1. BootstrapClassLoader
  2. ExtClassLoader
  3. AppClassLoader
    AppClassLoader的父加载器是ExtClassLoader,ExtClassLoader的父加载器是 BootstrapClassLoader。

JVM在加载⼀个类时,如果AppClassLoader这个类加载器加载过这个类就直接返回,如果没有加载过这个类,AppClassLoader的loadClass中会先使⽤ ExtClassLoader来加载类
同样ExtClassLoader这个类加载器加载过这个类就直接返回,如果没有加载过,则ExtClassLoader的loadClass方法中会先使用 BootstrapClassLoader来加载类
如果BootstrapClassLoader这个类加载器加载过这个类就直接返回,如果没有加载过,则BootstrapClassLoader会尝试加载,加载到了就直接成功,如果 BootstrapClassLoader没有加载到,那么ExtClassLoader就会自己尝试加载该类,如果还没有加载到, 那么则会由AppClassLoader来加载这个类。
所以,双亲委派指得是,JVM在加载类时,会委派给Ext和Bootstrap进行加载,如果没加载到才由自己进行加载。

为什么会有双亲委派机制:

  • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改。
  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证加载类的唯一性。

Tomcat中为什么要使用自定义类加载器

⼀个Tomcat中可以部署多个应用,而每个应用中都存在很多类,并且各个应用中的类是独立的,全类名是可以相同的

比如⼀个订单系统中可能存在com.zhouyu.User类,⼀个库存系统中可能也存在 com.zhouyu.User类,⼀个Tomcat,不管内部部署了多少应用,Tomcat启动之后就是⼀个Java进程, 也就是⼀个JVM,所以如果Tomcat中只存在⼀个类加载器,比如默认的AppClassLoader,那么就只能加载⼀个com.zhouyu.User类,这是有问题的,而在Tomcat中,会为部署的每个应⽤都生成⼀个类加载器实例,名字叫做WebAppClassLoader,这样Tomcat中每个应用就可以使用自己的类加载器去加载自己的类,从而达到应用之间的类隔离,不出现冲突。

另外Tomcat还利用自定义加载器实现了热加载功能,热加载功能:如果一个已经被加载的类重新修改之后,那么会重新生成一个WebAppClassLoader实例,之前那个WebAppClassLoader实例就会销毁,而由之前的WebAppClassLoader实例已经加载的类是不会有什么影响的。对已经修改的类和未被加载的类,就会重新被新的WebAppClassLoader实例加载。

Tomcat如何进行优化?

对于Tomcat调优,可以从两个方面来进行调整:内存和线程。

  1. 首先启动Tomcat,实际上就是启动了⼀个JVM,所以可以按JVM调优的方式来进行调整,从而达到 Tomcat优化的目的。
  2. 另外Tomcat中设计了⼀些缓存区,比如appReadBufSize、bufferPoolSize等缓存区来提高吞吐量。
  3. 还可以调整Tomcat的线程,比如调整minSpareThreads参数来改变Tomcat空闲时的线程数,调整 maxThreads参数来设置Tomcat处理连接的最⼤线程数。
  4. 并且还可以调整IO模型,比如使⽤NIO、APR这种相比于BIO更加高效的IO模型

浏览器发出一个请求到收到响应经历了哪些步骤?

  1. 浏览器解析用户输入的URL,生成⼀个HTTP格式的请求
  2. 先根据URL域名从本地hosts文件查找是否有映射IP,如果没有就将域名发送给电脑所配置的DNS进行域名解析,得到IP地址
  3. 浏览器通过操作系统将请求通过四层网络协议发送出去
  4. 途中可能会经过各种路由器、交换机,最终到达服务器
  5. 服务器收到请求后,根据请求所指定的端口,将请求传递给绑定了该端⼝的应应程序,比如8080被 tomcat占用了
  6. tomcat接收到请求数据后,按照http协议的格式进行解析,解析得到所要访问的servlet
  7. 然后servlet来处理这个请求,如果是SpringMVC中的DispatcherServlet,那么则会找到对应的 Controller中的方法,并执行该方法得到结果
  8. Tomcat得到响应结果后封装成HTTP响应的格式,并再次通过网络发送给浏览器所在的服务器
  9. 浏览器所在的服务器拿到结果后再传递给浏览器,浏览器则负责解析并渲染

跨域请求是什么?有什么问题?怎么解决?

跨域是指浏览器在发起网络请求时,会检查该请求所对应的协议、域名、端口和当前网页是否⼀致,如果不一致则浏览器会进行限制。

比如在www.baidu.com的某个网页中,如果使用ajax去访问 www.jd.com是不行的,但是如果是img、iframe、script等标签的src属性去访问则是可以的,之所以浏览器要做这层限制,是为了用户信息安全。但是如果开发者想要绕过这层限制也是可以的:

  1. response添加header,比如resp.setHeader(“Access-Control-Allow-Origin”, “*”);表示可以访问所有网站,不受是否同源的限制
  2. jsonp的方式,该技术底层就是基于script标签来实现的,因为script标签是可以跨域的
  3. 后台自己控制,先访问同域名下的接口,然后在接口中再去使⽤HTTPClient等工具去调用目标接口
  4. 网关,和第三种方式类似,都是交给后台服务来进行跨域访问

Spring中的Bean创建的生命周期有哪些步骤

Spring中⼀个Bean的创建⼤概分为以下几个步骤:

  1. 推断构造方法
  2. 实例化
  3. 填充属性,也就是依赖注⼊
  4. 处理Aware回调
  5. 初始化前,处理@PostConstruct注解
  6. 初始化,处理InitializingBean接⼝
  7. 初始化后,进行AOP

Spring中Bean是线程安全的吗

Spring本身并没有针对Bean做线程安全的处理,所以:

  1. 如果Bean是无状态的,那么Bean则是线程安全的
  2. 如果Bean是有状态的,那么Bean则不是线程安全的

另外,Bean是不是线程安全,跟Bean的作用域没有关系,Bean的作⽤域只是表示Bean的生命周期范围,对于任何生命周期的Bean都是⼀个对象,这个对象是不是线程安全的,还是得看这个Bean对象本身。

ApplicationContext和BeanFactory有什么区别

BeanFactory是Spring中非常核心的组件,表示Bean工厂,可以生成Bean,维护Bean。

ApplicationContext继承了BeanFactory,所以ApplicationContext拥有BeanFactory所有的特点,也 是⼀个Bean工厂,但是ApplicationContext除开继承了BeanFactory之外,还继承了诸如 EnvironmentCapable、MessageSource、ApplicationEventPublisher等接口,从而ApplicationContext还有获取系统环境变量、国际化、事件发布等功能,这是BeanFactory所不具备的。

ApplicationContext和BeanFactory都是接口。

Spring中的事务是如何实现的

  1. Spring事务底层是基于数据库事务和AOP机制的
  2. 首先对于使用了@Transactional注解的Bean,Spring会创建⼀个代理对象作为Bean
  3. 当调用代理对象的方法时,会先判断该方法上是否加了@Transactional注解
  4. 如果加了,那么则利用事务管理器创建⼀个数据库连接
  5. 并且修改数据库连接的autocommit属性为false,禁止此连接的自动提交,这是实现Spring事务非常重要的⼀步
  6. 然后执行当前方法,方法中会执行sql
  7. 执行完当前方法后,如果没有出现异常就直接提交事务
  8. 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
  9. Spring事务的隔离级别对应的就是数据库的隔离级别
  10. Spring事务的传播机制是Spring事务自己实现的,也是Spring事务中最复杂的
  11. Spring事务的传播机制是基于数据库连接来做的,一个数据库连接⼀个事务,如果传播机制配置为需要新开⼀个事务,那么实际上就是先建立⼀个数据库连接,在此新数据库连接上执行sql

Spring中什么时候@Transactional会失效

  1. 因为Spring事务是基于代理来实现的,所以某个加了@Transactional的方法只有是被代理对象调用时, 那么这个注解才会生效,所以如果是被代理对象来调用这个方法,那么@Transactional是不会失效的。
  2. 同时如果某个方法是private的,那么@Transactional也会失效,因为底层cglib是基于父子类来实现的,子类是不能重载父类的private方法的,所以无法很好的利用代理,也会导致@Transactianal失效。

Spring容器启动流程是怎样的

在创建Spring容器,也就是启动Spring时:

  1. 首先会进行扫描,扫描得到所有的BeanDefinition对象,并存在⼀个Map中
  2. 然后筛选出⾮懒加载单例BeanDefinition进行创建Bean,对于多例Bean不需要在启动过程中去进行创建,对于多例Bean会在每次获取Bean时利⽤BeanDefinition去创建
  3. 利用BeanDefinition创建Bean就是Bean的创建⽣命周期,这期间包括了合并BeanDefinition、推断构造方法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中AOP就是发⽣在初始化后这⼀步骤中
  4. 单例Bean创建完了之后,Spring会发布⼀个容器启动事件
  5. Spring启动结束

在源码中会更复杂,比如源码中会提供⼀些模板方法,让子类来实现,比如源码中还涉及到⼀些 BeanFactoryPostProcessor和BeanPostProcessor的注册,Spring的扫描就是通过 BenaFactoryPostProcessor来实现的,依赖注入就是通过BeanPostProcessor来实现的。

在Spring启动过程中还会去处理@Import等注解

SpringMVC的底层工作流程

  1. ⽤户发送请求至前端控制器DispatcherServlet。
  2. DispatcherServlet 收到请求调用 HandlerMapping 处理器映射器。
  3. 处理器映射器找到具体的处理器(可以根据 xml 配置、注解进行查找),生成处理器及处理器拦截器 (如果有则生成)⼀并返回给 DispatcherServlet。
  4. DispatcherServlet 调用 HandlerAdapter 处理器适配器。
  5. HandlerAdapter 经过适配调用具体的处理器(Controller,也叫后端控制器)
  6. Controller 执行完成返回 ModelAndView。
  7. HandlerAdapter 将 controller 执行结果 ModelAndView 返回给 DispatcherServlet。
  8. DispatcherServlet 将 ModelAndView 传给 ViewReslover 视图解析器。
  9. ViewReslover 解析后返回具体 View。
  10. DispatcherServlet 根据 View 进行渲染视图(即将模型数据填充至视图中)。
  11. DispatcherServlet 响应用户。

SpringBoot中常用注解及其底层实现

  1. @SpringBootApplication注解:这个注解标识了一个SpringBoot⼯程,它实际上是另外三个注解的组合,这三个注解是:
    a. @SpringBootConfiguration:这个注解实际就是一个@Configuration,表示启动类也是⼀个 配置类
    b. @EnableAutoConfiguration:向Spring容器中导⼊了一个Selector,⽤来加载ClassPath下 SpringFactories中所定义的⾃动配置类,将这些自动加载为配置Bean
    c. @ComponentScan:标识扫描路径,因为默认是没有配置实际扫描路径,所以SpringBoot扫描的路径是启动类所在的当前目录

  2. @Bean注解:⽤来定义Bean,类似于XML中的标签,Spring在启动时,会对加了@Bean注解的方法进行解析,将方法的名字做为beanName,并通过执行方法得到bean对象

  3. @Controller、@Service、@ResponseBody、@Autowired都可以说

SpringBoot是如何启动Tomcat的

  1. 首先,SpringBoot在启动时会先创建⼀个Spring容器
  2. 在创建Spring容器过程中,会利用@ConditionalOnClass技术来判断当前classpath中是否存在Tomcat依赖,如果存在则会生成⼀个启动Tomcat的Bean
  3. Spring容器创建完之后,就会获取启动Tomcat的Bean,并创建Tomcat对象,并绑定端口等,然后启动Tomcat。

SpringBoot中配置文件的加载顺序是怎样的?

优先级从⾼到低,⾼优先级的配置覆盖低优先级的配置,所有配置会形成互补配置。

  1. 命令⾏参数。所有的配置都可以在命令⾏上进⾏指定;
  2. Java系统属性(System.getProperties());
  3. 操作系统环境变量 ;
  4. jar包外部的application-{profile}.properties或application.yml(带spring.profile)配置⽂件
  5. jar包内部的application-{profile}.properties或application.yml(带spring.profile)配置⽂件 再来加 载不带profile
  6. jar包外部的application.properties或application.yml(不带spring.profile)配置⽂件
  7. jar包内部的application.properties或application.yml(不带spring.profile)配置⽂件
  8. @Configuration注解类上的@PropertySource

Mybatis存在哪些优点和缺点

优点:

  1. 基于 SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL单独写,解除 SQL与程序代码的耦合,便于统⼀管理
  2. 与 JDBC 相比,减少了 50%以上的代码量,消除了 JDBC 大量冗余的代码,不需手动开关连接
  3. 很好的与各种数据库兼容(因为 MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)
  4. 能够与 Spring 很好的集成;
  5. 提供映射标签,支持对象与数据库的 ORM 字段关系映射; 提供对象关系映射标签,支持对象关系组件维护

缺点:

  1. SQL 语句的编写工作量较大, 尤其当字段多、关联表多时, 对开发人员编写SQL 语句的功底有一定要求。
  2. SQL 语句依赖于数据库, 导致数据库移植性差, 不能随意更换数据库

Mybatis中#{}和${}的区别是什么?

  1. #{}是预编译处理、是占位符, ${}是字符串替换、是拼接符
  2. Mybatis 在处理#{}时,会将 sql 中的#{}替换为?号,调⽤ PreparedStatement 来赋值
  3. Mybatis 在处理${}时, 就是把${}替换成变量的值,调⽤ Statement 来赋值
  4. 使⽤#{}可以有效的防⽌SQL注⼊,提⾼系统安全性
name="zhouyu"
password="1 or 1=1" 

select * from user where name = #{name} and password = #{password} 
将转为 
select * from user where name = 'zhouyu' and password = '1 or 1=1' 

select * from user where name = ${name} and password = ${password} 
将转为 
select * from user where name = zhouyu and password = 1 or 1=1

什么是CAP理论

CAP理论是分布式领域中非常重要的⼀个指导理论,C(Consistency)表示强⼀致性
A(Availability)表示可用性,P(Partition Tolerance)表示分区容错性,CAP理论指出在目前的硬件条件下,⼀个分布式系统是必须要保证分区容错性的,而在这个前提下,分布式系统要么保证CP,要么保证AP,无法同时保证CAP。

  • 分区容错性表示,⼀个系统虽然是分布式的,但是对外看上去应该是⼀个整体,不能由于分布式系统内部的某个结点挂点,或网络出现了故障,而导致系统对外出现异常。所以,对于分布式系统而言是⼀定要保证分区容错性的。
  • 强⼀致性表示,⼀个分布式系统中各个结点之间能及时同步数据,在数据同步过程中,是不能对外提供服务的,不然就会造成数据不⼀致,所以强⼀致性和可用性是不能同时满足的。
  • 可用性表示,⼀个分布式系统对外要保证可用

什么是BASE理论

由于不能同时满足CAP,所以出现了BASE理论:

  1. BA:Basically Available,表示基本可用,表示可以允许⼀定程度的不可拥,比如由于系统故障, 请求时间变长,或者由于系统故障导致部分非核心功能不可用,都是允许的
  2. S:Soft state:表示分布式系统可以处于⼀种中间状态,比如数据正在同步
  3. E:Eventually consistent,表示最终⼀致性,不要求分布式系统数据实时达到⼀致,允许在经过⼀段时间后再达到⼀致,在达到⼀致过程中,系统也是可用的

什么是RPC

RPC,表示远程过程调用,对于Java这种面向对象语言,也可以理解为远程方法调用,RPC调用和 HTTP调用是有区别的,RPC表示的是⼀种调用远程方法的方式,可以使用HTTP协议、或直接基于TCP 协议来实现RPC,在Java中,我们可以通过直接使用某个服务接口的代理对象来执行方法,而底层则通过构造HTTP请求来调用远端的方法,所以,有⼀种说法是RPC协议是HTTP协议之上的⼀种协议,也是可以理解的。

个人理解
远程方法调用:A进程的一个方法调用B进程的另外一个方法。本地方法调用:同一个进程之间的方法调用。

A进程将参数等数据通过HTTP协议或者直接通过TCP协议传给B进程,然后将远端B进程方法执行的结果通过HTTP协议返回给A进程。

分布式ID是什么?有哪些解决方案?

在开发中,我们通常会需要⼀个唯⼀ID来标识数据,如果是单体架构,我们可以通过数据库的主键,或 直接在内存中维护⼀个自增数字来作为ID都是可以的,但对于⼀个分布式系统,就会有可能会出现ID冲突,此时有以下解决方案:

  1. uuid,这种方案复杂度最低,但是会影响存储空间和性能(uuid 长度是128bit(16字节(128位)))
  2. 利用单机数据库自增主键,作为分布式ID的⽣成器,复杂度适中,ID长度较之uuid更短,但是受到单机数据库性能的限制,并发量大的时候,此方案也不是最优方案。(所有服务器都需要连接该数据库,因此该数据库压力很大)
  3. 利用redis、zookeeper的特性来生成id,比如redis的自增命令、zookeeper的顺序节点,这种方案和单机数据库(mysql)相比,性能有所提高,可以适当选用
  4. 雪花算法,⼀切问题如果能直接用算法解决,那就是最合适的,利⽤雪花算法也可以生成分布式 ID,底层原理就是通过某台机器在某⼀毫秒内对某⼀个数字自增,这种方案也能保证分布式架构中的系统id唯⼀,但是只能保证趋势递增。业界存在tinyid、leaf等开源中间件实现了雪花算法

分布式锁的使用场景是什么?有哪些实现方案?

在单体架构中,多个线程都是属于同⼀个进程的,所以在线程并发执行时,遇到资源竞争时,可以利用ReentrantLock、synchronized等技术来作为锁,来控制共享资源的使⽤。

而在分布式架构中,多个线程是可能处于不同进程中的,而这些线程并发执行遇到资源竞争时,利用ReentrantLock、synchronized等技术是没办法来控制多个进程中的线程的,所以需要分布式锁,意思就是,需要⼀个分布式锁生成器,分布式系统中的应用程序都可以来使⽤这个生成器所提供的锁,从而达到多个进程中的线程使用同一把锁。

目前主流的分布式锁的实现方案有两种:

  1. zookeeper:利用的是zookeeper的临时节点、顺序节点、watch机制来实现的,zookeeper分布式锁的特点是高⼀致性,因为zookeeper保证的是CP,所以由它实现的分布式锁更可靠,不会出现混乱
  2. redis:利⽤redis的setnx、lua脚本、消费订阅等机制来实现的,redis分布式锁的特点是⾼可⽤, 因为redis保证的是AP,所以由它实现的分布式锁可能不可靠,不稳定(⼀旦redis中的数据出现了 不⼀致),可能会出现多个客户端同时加到锁的情况

什么是分布式事务?有哪些实现方案?

在分布式系统中,⼀次业务处理可能需要多个应用来实现,比如用户发送⼀次下单请求,就涉及到订单系统创建订单、库存系统减库存,而对于⼀次下单,订单创建与减库存应该是要同时成功或同时失败的,但在分布式系统中,如果不做处理,就很有可能出现订单创建成功,但是减库存失败,那么解决这类问题,就需要用到分布式事务。常用解决方案有:

  1. 本地消息表:创建订单时,将减库存消息加入到本地事务中,⼀起提交到数据库存⼊本地消息表,然后调⽤库存系统,如果调⽤成功则修改本地消息状态为成功,如果调⽤库存系统失败,则由后台 定时任务从本地消息表中取出未成功的消息,重试调用库存系统
  2. 消息队列:目前RocketMQ中⽀持事务消息,它的⼯作原理是:
    a. 生产者订单系统先发送⼀条half消息到Broker,half消息对消费者而言是不可见的
    b.再创建订单,根据创建订单成功与否,向Broker发送commit或rollback
    c.并且⽣产者订单系统还可以提供Broker回调接口,当Broker发现⼀段时间half消息没有收到任 何操作命令,则会主动调此接口来查询订单是否创建成功
    d.⼀旦half消息commit了,消费者库存系统就会来消费,如果消费成功,则消息销毁,分布式事务成功结束
    e.如果消费失败,则根据重试策略进行重试,最后还失败则进⼊死信队列,等待进⼀步处理
  3. Seata:阿⾥开源的分布式事务框架,⽀持AT、TCC等多种模式,底层都是基于两阶段提交理论来 实现的

什么是ZAB协议

ZAB协议是Zookeeper用来实现⼀致性的原子广播协议,该协议描述了Zookeeper是如何实现⼀致性的,分为三个阶段:

  1. 领导者选举阶段:从Zookeeper集群中选出⼀个节点作为Leader,所有的写请求都会由Leader节点 来处理
  2. 数据同步阶段:集群中所有节点中的数据要和Leader节点保持⼀致,如果不⼀致则要进行同步
  3. 请求广播阶段:当Leader节点接收到写请求时,会利用两阶段提交来广播该写请求,使得写请求像事务⼀样在其他节点上执行,达到节点上的数据实时⼀致。

个人理解
Leader节点接收到写请求之后,生成日志发送给Follow节点,如果Follow节点可以持久化完成,则发送ACK给Leader节点,当接收到超过一半的Follow节点发送的ACK时,Leader节点会给自己和Follow节点发送Commit,刷新内存,刷新内存完之后再发送给客户端。

但值得注意的是,Zookeeper只是尽量的在达到强一致性,实际上仍然只是最终一致性的。

为什么Zookeeper可以用来作为注册中心

可以利用Zookeeper的临时节点和watch机制来实现注册中心的自动注册和发现,另外Zookeeper中的数据都是存在内存中的,并且Zookeeper底层采用了NIO多线程模型,所以Zookeeper的性能也是比较高的,所以可以用来作为注册中心

但是如果考虑到注册中心应该是注重可用性的话,那么Zookeeper 则不太合适,因为Zookeeper是CP的,它注重的是一致性,所以集群数据不一致时,集群将不可用,所以用Redis、Eureka、Nacos来作为注册中心将更合适。

什么是服务雪崩?什么是服务限流?

  1. 当服务A调用服务B,服务B调用C,此时大量请求突然请求服务A,假如服务A本身能抗住这些请 求,但是如果服务C抗不住,导致服务C请求堆积,从而服务B请求堆积,从而服务A不可用,这就是服务雪崩,解决方式就是服务降级和服务熔断
  2. 服务限流是指在高并发请求下,为了保护系统,可以对访问服务的请求进行数量上的限制,从而防止系统不被大量请求压垮,在秒杀中,限流是非常重要的。

什么是服务熔断?什么是服务降级?区别是什么?

  1. 服务熔断是指,当服务A调用的某个服务B不可用时,上游服务A为了保证自己不受影响,从而不再调用服务B,直接返回⼀个结果,减轻服务A和服务B的压力,直到服务B恢复。
  2. 服务降级是指,当发现系统压力过载时,可以通过关闭某个服务,或限流某个服务来减轻系统压力,这就是服务降级。

相同点:

  1. 都是为了防止系统崩溃
  2. 都让⽤户体验到某些功能暂时不可⽤

不同点:熔断是下游服务故障触发的,降级是为了降低系统负载

SOA、分布式、微服务之间有什么关系和区别?

  1. 分布式架构是指将单体架构中的各个部分拆分,然后部署不同的机器或进程中去,SOA和微服务基本上都是分布式架构的。
  2. SOA是⼀种面向服务的架构,系统的所有服务都注册在总线上,当调用服务时,从总线上查找服务信息,然后调用。
  3. 微服务是⼀种更彻底的面向服务的架构,将系统中各个功能个体抽成⼀个个小的应用程序,基本保持⼀个应用对应的⼀个服务的架构。

BIO、NIO、AIO分别是什么

  1. BIO:同步阻塞IO,使⽤BIO读取数据时,线程会阻塞住,并且需要线程主动去查询是否有数据可读,且需要处理完一个Socket之后才能处理下⼀个Socket。
  2. NIO:同步非阻塞IO,使用NIO读取数据时,线程不会阻塞,但需要线程主动的去查询是否有IO事件
  3. AIO:也叫做NIO 2.0,异步非阻塞IO,使用AIO读取数据时,线程不会阻塞,并且当有数据可读时会通知给线程,不需要线程主动去查询。

零拷贝是什么

零拷贝指的是,应用程序在需要把内核中的⼀块区域数据转移到另外⼀块内核区域去时,不需要经过先复制到用户空间,再转移到目标内核区域去了,而直接实现转移。

Redis有哪些数据结构?分别有哪些典型的应用场景?

Redis的数据结构有:

  1. 字符串:可以用来做最简单的数据,可以缓存某个简单的字符串,也可以缓存某个json格式的字符串,Redis分布式锁的实现就利用了这种数据结构,还包括可以实现计数器、Session共享、分布式 ID
  2. 哈希表:可以用来存储⼀些key-value对,更适合用来存储对象
  3. 列表:Redis的列表通过命令的组合,既可以当做栈,也可以当做队列来使用,可以用来缓存类似微信公众号、微博等消息流数据
  4. 集合:和列表类似,也可以存储多个元素,但是不能重复,集合可以进行交集、并集、差集操作, 从而可以实现类似,我和某人共同关注的⼈、朋友圈点赞等功能
  5. 有序集合:集合是无序的,有序集合可以设置顺序,可以用来实现排行榜功能

Redis主从复制的核心原理

Redis的主从复制是提高Redis的可靠性的有效措施,主从复制的流程如下:

  1. 集群启动时,主从库间会先建立连接,为全量复制做准备
  2. 主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载,这个过程依赖于内存快照 RDB
  3. 在主库将数据同步给从库的过程中,主库不会阻塞,仍然可以正常接收请求。否则,redis的服务就被中断了。但是,这些请求中的写操作并没有记录到刚刚生成的RDB文件中。为了保证主从库的数据⼀致性,主库会在内存中用专门的replication buffer,记录RDB文件生成收到的所有写操作
  4. 最后,也就是第三个阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库。具体的操作是,当主库完成RDB文件发送后,就会把此时replication buffer中修改操作发送给从库,从库再执行这些操作。这样⼀来,主从库就实现同步了
  5. 后续主库和从库都可以处理客户端读操作,写操作只能交给主库处理,主库接收到写操作后,还会将写操作发送给从库,实现增量同步

Redis集群策略

  1. 主从模式:这种模式比较简单,主库可以读写,并且会和从库进行数据同步,这种模式下,客户端直接连主库或某个从库,但是但主库或从库宕机后,客户端需要手动修改IP,另外,这种模式也比较难进行扩容,整个集群所能存储的数据受到某台机器的内存容量,所以不可能支持特大数据量(个人看法:一主多从)。
  2. 哨兵模式:这种模式在主从的基础上新增了哨兵节点,当主库节点宕机后,哨兵会发现主库节点宕机, 然后在从库中选择⼀个库作为进的主库,另外哨兵也可以做集群,从而可以保证但某⼀个哨兵节点宕机后,还有其他哨兵节点可以继续工作,这种模式可以比较好的保证Redis集群的高可用,但是仍然不能很好的解决Redis的容量上限问题(个人看法:一主多从) 。
  3. Cluster模式:Cluster模式是用得比较多的模式,它支持多主多从,这种模式会按照key进行槽位的分配,可以使得不同的key分散到不同的主节点上,利用这种模式可以使得整个集群支持更大的数据容量,同时每个主节点可以拥有自己的多个从节点,如果该主节点宕机,会从它的从节点中选举⼀个新的主节点。
    对于这三种模式,如果Redis要存的数据量不大,可以选择哨兵模式,如果Redis要存的数据量大,并且需要持续的扩容,那么选择Cluster模式。

缓存穿透、缓存击穿、缓存雪崩分别是什么

缓存中存放的大多都是热点数据,目的就是请求可以直接从缓存中获取到数据,而不用访问 Mysql。

  1. 缓存雪崩:如果缓存中某⼀时刻大批热点数据同时过期,那么就可能导致大量请求直接访问Mysql 了,解决办法就是在过期时间上增加一点随机值,另外如果搭建⼀个高可用的Redis集群也是防止缓存雪崩的有效手段
  2. 缓存击穿:和缓存雪崩类似,缓存雪崩是⼤批热点数据失效,而缓存击穿是指某一个热点key突然失效,也导致了大量请求直接访问Mysql数据库,这就是缓存击穿,解决方案就是考虑这个热点key不设过期时间
  3. 缓存穿透:假如某⼀时刻访问redis的大量key都在redis中不存在(比如黑客故意伪造⼀些乱七八糟的key),那么也会给数据造成压力,这就是缓存穿透,解决方案是使用布隆过滤器,它的作用就是如果它认为一个key不存在,那么这个key就肯定不存在,所以可以在缓存之前加一层布隆过滤器来拦截不存在的key

Redis和MySQL如何保证数据一致

  1. 先更新Mysql,再更新Redis,如果更新Redis失败,可能仍然不一致
  2. 先删除Redis缓存数据,再更新Mysql,再次查询的时候在将数据添加到缓存中,这种方案能解决1 方案的问题,但是在高并发下性能较低,而且仍然会出现数据不⼀致的问题,比如线程1删除了 Redis缓存数据,正在更新Mysql,此时另外⼀个查询再查询,那么就会把Mysql中老数据又查到 Redis中
  3. 延时双删,步骤是:先删除Redis缓存数据,再更新Mysql,延迟几百毫秒再删除Redis缓存数据, 这样就算在更新Mysql时,有其他线程读了Mysql,把老数据读到了Redis中,那么也会被删除掉, 从而把数据保持一致

Explain语句结果中各个字段分表表示什么

列名描述
id查询语句中每出现一个SELECT关键字,MySQL 就会为它分配一个唯一的id值,某些子查询会被优化为join查询,那么出现的id会一样
select_typeSELECT关键字对应的那个查询的类型
table表名
partitions匹配的分区信息
type针对单表的查询⽅式(全表扫描、索引)
possible_keys可能⽤到的索引
key实际上使⽤的索引
key_len实际使用到的索引⻓度
ref当使⽤索引列等值查询时,与索引列进⾏等值匹配的对象信息
rows预估的需要读取的记录条数
filtered某个表经过搜索条件过滤后剩余记录条数的百分比
Extra⼀些额外的信息,比如排序等

索引覆盖是什么

索引覆盖就是⼀个SQL在执行时,可以利用索引来快速查找,并且此SQL所要查询的字段在当前索引对应的字段中都包含了,那么就表示此SQL走完索引后不用回表了,所需要的字段都在当前索引的叶子节点上存在,可以直接作为结果返回了。

最左前缀原则是什么

当⼀个SQL想要利用索引是,就⼀定要提供该索引所对应的字段中最左边的字段,也就是排在最前面的字段,比如针对a,b,c三个字段建立了⼀个联合索引,那么在写⼀个sql时就⼀定要提供a字段的条件,这样才能用到联合索引,这是由于在建立a,b,c三个字段的联合索引时,底层的B+树是按照a,b,c三个字段从左往右去比较大小进行排序的,所以如果想要利用B+树进行快速查找也得符合这个规则

Innodb是如何实现事务的

Innodb通过Buffer Pool,LogBuffer,Redo Log,Undo Log来实现事务,以⼀个update语句为例:

  1. Innodb在收到⼀个update语句后,会先根据条件找到数据所在的页,并将该页缓存在Buffer Pool 中
  2. 执行update语句,修改Buffer Pool中的数据,也就是内存中的数据
  3. 针对update语句⽣成⼀个RedoLog对象,并存入LogBuffer中
  4. 针对update语句⽣成undolog日志,⽤于事务回滚
  5. 如果事务提交,那么则把RedoLog对象进行持久化,后续还有其他机制将Buffer Pool中所修改的数据页持久化到磁盘中
  6. 如果事务回滚,则利用undolog日志进行回滚

B树和B+树的区别,为什么Mysql使用B+树

B树的特点:

  1. 节点排序
  2. 一个节点了可以存多个元素,多个元素也排序了

B+树的特点:

  1. 拥有B树的特点
  2. 叶子节点之间有指针
  3. 非叶⼦节点上的元素在叶⼦节点上都冗余了,也就是叶子节点中存储了所有的元素,并且排好顺序

Mysql索引使用的是B+树,因为索引是用来加快查询的,而B+树通过对数据进行排序所以是可以提高查询速度的,然后通过⼀个节点中可以存储多个元素,从而可以使得B+树的高度不会太高,在Mysql中⼀ 个Innodb页就是⼀个B+树节点,⼀个Innodb页默认16kb,所以⼀般情况下⼀颗两层的B+树可以存2000万行左右的数据,然后通过利用B+树叶子节点存储了所有数据并且进行了排序,并且叶子节点之间有指针,可以很好的支持全表扫描,范围查找等SQL语句。

MySQL锁有哪些,如何理解

按锁粒度分类:

  1. 行锁:锁某行数据,锁粒度最小,并发度高
  2. 表锁:锁整张表,锁粒度最大,并发度低
  3. 间隙锁:锁的是⼀个区间

还可以分为:

  1. 共享锁:也就是读锁,⼀个事务给某行数据加了读锁,其他事务也可以读锁,但是不能写锁
  2. 排它锁:也就是写锁,⼀个事务给某行数据加了写锁,其他事务不能加读锁,也不能写锁

还可以分为:

  1. 乐观锁:并不会真正的去锁某行记录,而是通过⼀个版本号来实现的
  2. 悲观锁:上面说的行锁、表锁等都是悲观锁

在事务的隔离级别实现中,就需要利用锁来解决幻读

MySQL慢查询该如何优化?

  1. 检查是否走了索引,如果没有则优化SQL利用索引
  2. 检查所利用的索引,是否是最优索引
  3. 检查所查字段是否都是必须的,是否查询了过多字段,查出了多余数据
  4. 检查表中数据是否过多,是否应该进行分库分表了
  5. 检查数据库实例所在机器的性能配置,是否太低,是否可以适当增加资源

MySQL数据库中,什么情况下设置了索引但无法使用?

  1. 没有符合最左前缀原则
  2. 字段进行了隐式数据类型转化
  3. 走索引没有全表扫描效率高

消息队列有哪些作用

  1. 解耦:使用消息队列来作为两个系统之间的通讯方式,两个系统不需要相互依赖了
  2. 异步:系统A给消息队列发送完消息之后,就可以继续做其他事情了
  3. 流量削峰:如果使用消息队列的方式来调用某个系统,那么消息将在队列中排队,由消费者自己控制消费速度

死信队列是什么?延时队列是什么?

  1. 死信队列也是⼀个消息队列,它是用来存放那些没有成功消费的消息的,通常可以用来作为消息重试
  2. 延时队列就是用来存放需要在指定时间被处理的元素的队列,通常可以用来处理⼀些具有过期性操作的业务,比如十分钟内未支付则取消订单

个人理解
经历过重试策略之后如果还是没有成功,进入死信队列。一般情况下消息即时消费

JDK1.7到JDK1.8 Java虚拟机发生了什么变化

1.7中存在永久代,1.8中没有永久代,替换它的是元空间,元空间所占的内存不是在虚拟机内部,而是本地内存空间,这么做的原因是,不管是永久代还是元空间,他们都是方法区的具体实现,之所以元空间所占的内存改成本地内存,官方的说法是为了和JRockit统一,不过额外还有一些原因,比如方法区所存储的类信息通常是比较难确定的,所以对于方法区的大小是比较难指定的,太小了容易出现方法区溢出,太大又会占用了太多虚拟机的内存空间,而转移到本地内存后则不会影响虚拟机所占用的内存

Java死锁如何避免?

造成死锁的几个原因:

  1. ⼀个资源每次只能被⼀个线程使用
  2. ⼀个线程在阻塞等待某个资源时,不释放已占有资源
  3. ⼀个线程已经获得的资源,在未使用完之前,不能被强行剥夺
  4. 若干线程形成头尾相接的循环等待资源关系

这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满足其中某⼀个条件即可。⽽其中前3个 条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。

在开发过程中:

  1. 要注意加锁顺序,保证每个线程按同样的顺序进行加锁
  2. 要注意加锁时限,可以针对所设置⼀个超时时间
  3. 要注意死锁检查,这是⼀种预防机制,确保在第⼀时间发现死锁并进行解决

深拷贝和浅拷贝

深拷贝和浅拷贝就是指对象的拷贝,⼀个对象中存在两种类型的属性,⼀种是基本数据类型,⼀种是实例对象的引用。

  1. 浅拷贝是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制⼀份引⽤地址所指向的对象,也就是浅拷贝出来的对象,内部的类属性指向的是同⼀个对象
  2. 深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的属性指向的不是同⼀个对象

如果你提交任务时,线程池队列已满,这时会发生什么

  1. 如果使用的无界队列,那么可以继续提交任务时没关系的
  2. 如果使用的有界队列,提交任务时,如果队列满了,如果核⼼线程数没有达到上限,那么则增加线程, 如果线程数已经达到了最大值,则使用拒绝策略进行拒绝

Maven中Package和Install的区别

  1. Package是打包,打成Jar或War
  2. Install表示将Jar或War安装到本地仓库中

泛型中extends和super的区别

  1. <? extends T>表示包括T在内的任何T的⼦类
  2. <? super T>表示包括T在内的任何T的⽗类

并发编程三要素?

  1. 原子性:不可分割的操作,多个步骤要保证同时成功或同时失败
  2. 有序性:程序执行的顺序和代码的顺序保持⼀致
  3. 可见性:⼀个线程对共享变量的修改,另⼀个线程能立马看到

图的深度遍历和⼴度遍历

  1. 图的深度优先遍历是指,从⼀个节点出发,⼀直沿着边向下深⼊去找节点,如果找不到了则返回上⼀层找其他节点
  2. 图的广度优先遍历只是,从⼀个节点出发,向下先把第⼀层的节点遍历完,再去遍历第⼆层的节点,直 到遍历到最后⼀层

二叉搜索树和平衡二叉树有什么关系?

平衡二叉树也叫做平衡二叉搜索树,是二叉搜索树的升级版,二叉搜索树是指节点左边的所有节点都比该 节点小,节点右边的节点都比该节点⼤,而平衡二叉搜索树是在二叉搜索的基础上还规定了节点左右两边的子树高度差的绝对值不能超过1

强平衡二叉树和弱平衡二叉树有什么区别

强平衡二叉树AVL树,弱平衡⼆叉树就是我们说的红黑树。

  1. AVL树比红黑树对于平衡的程度更加严格,在相同节点的情况下,AVL树的高度低于红黑树
  2. 红黑树中增加了⼀个节点颜色的概念
  3. AVL树的旋转操作比红黑树的旋转操作更耗时

AQS队列

AQS全称AbstractQueuedSynchronizer,AQS 是多线程同步器,它是 J.U.C 包中多个组件的底层实现,如 Lock、CountDownLatch、Semaphore 等都用到了 AQS.

从本质上来说,AQS 提供了两种锁机制,分别是排它锁,和共享锁。

排它锁,就是存在多线程竞争同一共享资源时,同一时刻只允许一个线程访问该共享资源,也就是多个线程中只能有一个线程获得锁资源,比如 Lock 中的 ReentrantLock 重入锁实现就是用到了 AQS 中的排它锁功能。

共享锁也称为读锁,就是在同一时刻允许多个线程同时获得锁资源,比如 CountDownLatch 和 Semaphore 都是用到了 AQS 中的共享锁功能。

设计AQS整个体系需要解决的三个核心的问题:
①互斥变量的设计以及多线程同时更新互斥变量时的安全性
②未竞争到锁资源的线程的等待以及竞争到锁资源的线程释放锁之后的唤醒
③锁竞争的公平性和非公平性。

对应三个核心问题的解决方案:
①AQS采用了一个int类型的互斥变量state用来记录锁竞争的一个状态,0表示当前没有任何线程竞争锁资源,而大于等于1表示已经有线程正在持有锁资源。一个线程来获取锁资源的时候,首先判断state是否等于0,如果是(无锁状态),则把这个state更新成1,表示占用到锁。此时如果多个线程进行同样的操作,会造成线程安全问题。AQS采用了CAS机制来保证互斥变量state的原子性。
②未获取到锁资源的线程通过Unsafe类中的park方法对线程进行阻塞,把阻塞的线程按照先进先出的原则加入到一个双向链表的结构中,当获得锁资源的线程释放锁之后,会从双向链表的头部去唤醒下一个等待的线程再去竞争锁。
③另外关于公平性和非公平性问题,AQS的处理方式是,在竞争锁资源的时候,公平锁需要判断双向链表中是否有阻塞的线程,如果有,则需要去排队等待;而非公平锁的处理方式是,不管双向链表中是否存在等待锁的线程,都会直接尝试更改互斥变量state去竞争锁。

进程与端口的关系

一个应用可以有多个进程,一个进程可以有多个端口,而一个端口只能有一个进程。原因socket=ip+port来寻找每个进程的,ip表示目标主机的地址,port表示主机上的进程位置,所以socket可以唯一标识一个程序。如果一个端口被多个进程占用,服务器就不知道把数据发给哪个应用了。但是一个进程可以有多个端口,不同端口连接不同服务。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值