Java高级特性、集合、框架、中间件、JVM、多线程、设计模式、数据库、网络通信

文章目录

    • 克隆,深克隆和浅克隆
    • 谈谈对Spring IOC的理解
    • Spring AOP原理
    • SpringMVC运行流程
    • 分析springboot运行机制
    • SpringCloud原理
    • MyBatis中#{}和${}区别
    • MyBatis的分页方式
    • 谈谈MyBatis缓存
    • MyBatis延迟加载
    • 谈谈Redis缓存穿透和缓存雪崩
    • Servlet生命周期
    • Java内存区域(JVM内存结构)
    • Java引用类型
    • JVM垃圾回收机制(判断对象是否存活)
    • 垃圾收集算法
    • Java为什么需要性能调优
    • 类加载时机
    • Java类加载的过程
    • 数组的加载
    • 什么是类加载器
    • 类加载器的分类
    • 类加载器双亲委派模型
    • Java多线程实现原理
    • 线程状态
    • ThreadLocal内存泄漏
    • wait、sleep、yield、join、interrupt
    • 线程间通信方式
    • 主存和工作内存交互时虚拟机保证的天然原子性操作有哪些
    • 谈谈volatile关键字
    • synchronized关键字原理
    • 乐观锁与悲观锁
    • 乐观锁出现的问题
    • 互斥同步和非阻塞同步
    • CAS原理
    • ReenterantLock和synchronized关键字对比
    • 静态代理、JDK动态代理、CGLIB动态代理的区别
    • 设计模式-策略模式
    • 谈谈对数据库索引的理解
    • 谈谈聚集索引和非聚集索引
    • 一次web请求的过程
    • TCP/IP组成或者模型
    • TCP和UDP
    • TCP三次握手
    • https工作原理
    • 二分法查找

克隆,深克隆和浅克隆

某种场景下想使用已有对象的属性,由于new出来和反射出来的新对象是全新的对象,直接赋值又会影响到原有对象,克隆就是为了解决此类问题的。克隆又分为浅克隆和深克隆

  1. 浅克隆:被克隆类实现Cloneable接口,重写克隆方法。属性中的基本数据类型是直接赋值,引用类型克隆后的引用指向的还是同一个对象,改变原对象(P)和克隆对象(T)中任意的引用对象中的属性会彼此影响,这是为什么要使用深克隆。
  2. 深克隆:P和T之间任何属性不会彼此相互影响,彼此对立的个体。实现方法有两种:
    1. 类和类中的引用对象全部实现Cloneable接口并且重写clone方法,并且在克隆方法中逐级调用引用属性的克隆方法。
    2. 使用Java IO的对对象进行序列化和反序列化。
      Java高级特性、集合、框架、中间件、JVM、多线程、设计模式、数据库、网络通信_第1张图片

谈谈对Spring IOC的理解

谈谈对Spring IOC的理解

Spring AOP原理

AOP(Aspect Oriented Programming) 面向切面编程。在编程中,我们希望将日志记录,性能统计,事务处理,异常处理等代码逻辑相似又不影响正常业务流程的代码提取出来,然后通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。假设把应用程序想成一个立体结构的话,OOP的利刃是纵向切入系统,把系统划分为很多个模块(如:用户模块,文章模块等等),而AOP的利刃是横向切入系统,提取各个模块可能都要重复操作的部分(如:权限检查,日志记录等等)。由此可见,AOP是OOP的一个有效补充。AOP不是一种技术,实际上是编程思想。凡是符合AOP思想的技术,都可以看成是AOP的实现。
参考:SpringAOP原理分析

SpringMVC运行流程

  1. 用户发起请求
  2. DispatcherServlet 收到请求
  3. DispatcherServlet 获取处理器执行链,包括一个handler即Controller和一系列HandlerInterceptor(拦截处理器)
  4. DispatcherServlet 获取HandlerAdapter
  5. HandlerInterceptor调用preHandle方法进行前置拦截处理,返回结果为false,停止往下走,返回为true继续
  6. HandlerAdapter适配Controller进行请求处理返回ModelAndView
  7. HandlerInterceptor调用postHandle方法进行前置拦截处理
  8. DispatcherServlet处理结果 处理结果,返回View
  9. 视图通过Themleaf、Freemarker等模板引擎渲染返回给用户前端

分析springboot运行机制

  1. 首先主类上@SpringBootApplication注解,点进去主要有三个重要注解:@Configuration、@ComponentScan、@EnableAutoConfiguration。
  2. @Configuration将该类标记为配置类;
  3. @ComponentScan没有指定basePackages的话就以当前类所在的包为basePackages,这就是为什么将Bean放于主类所在包范围之外无法扫描到的原因;
  4. @EnableAutoConfiguration有个注解@Import({AutoConfigurationImportSelector.class}),而AutoConfigurationImportSelector最终实现了ImportSelector接口,该接口selectImports方法返回一组bean全类名数组,将实现对导入类的收集。 那么导入的类从哪来呢?
    AutoConfigurationImportSelector调用SpringFactoriesLoader的loadSpringFactories 方法,该方法会加载class路径下META-INF/spring.factories配置文件里所有的配置类
    Java高级特性、集合、框架、中间件、JVM、多线程、设计模式、数据库、网络通信_第2张图片

SpringCloud原理

SpringCloud的核心组件有:Eureka、Ribbon、Feign、Hystrix、Zuul。

  1. 微服务将模块服务化,他们之间会相互调用,随着业务增多,服务增多服务间管理逐渐复杂,Eureka Server提供服务注册功能,服务启动后会将自己的服务名、ip、端口信息注册到Eureka Server上,Eureka Client进行服务调用时就会从Eureka Server上拉取服务信息。

  2. Eureka Client调用某个具有多个实例服务时,应该从众多服务中进行选择,Ribbon提供服务的负载均衡,Ribbon内置了一些负载均衡算法(轮询、随机等),用户也可以自定义算法。

  3. SpringCloud的服务调用可以直接通过自行封装Http发送请求,但是每次服务调用都需要大量代码去封装发送和解释返回结果。Java都推崇面向接口编程,使用Feign发送远程请求就像SpringMVC的前端请求后端一样简单,原理如下

    1. 在启动时Feign会对使用了@FeignClient注解的接口进行扫描生成动态代理类注册到Spring容器中。
    2. 然后当调用Feign中的接口时,代理类根据接口上的@RequestMapping等注解,来动态封装HTTP请求,发送请求
    3. 请求结果返回后,代理类会对结果进行解码返回给调用者
  4. 当某个服务在被调用时发生网络故障或者宕机时,服务调用者由于等不到响应会阻塞直到超时,如果有很多服务调用该服务那么所有的服务都将被阻塞。Hystrix会为每个服务提供独立的线程池,服务调用先打到Hystrix中,某个服务发生故障不会影响到其它服务调用,并且Hystix提供服务降级功能,某个服务挂掉时Hystix可以通过fallback直接构造返回结果,并且处理失败结果,比如说将失败信息保存起来以便进行恢复。

  5. 随着服务的增多,几十个、几百个甚至是几千个服务,每次调用服务都需要记住服务名。在前后端分离开发的应用中,前端工程师就需要知道每个服务名,这是不切实际的。所有的服务通过zuul配置路径后,发送的请求都通过zuul向服务转发,实现服务访问统一管理。zuul还可以实现统一服务降级、身份权限认证、限流等功能

MyBatis中#{}和${}区别

#{}是预编译处理,MyBatis会将#{}替换为?,配合PreparedStatement的set方法赋值,防止SQL注入。${}直接是字符串替换,不推荐使用。

MyBatis的分页方式

  1. 逻辑分页:使用自带的RowBounds分页,一次查很多数据(不是全部数据),然后在这些数据里面检索。本质上是使用DB的limit进行分页,表里数据量小速度较快,数据量大就很慢。比如:limit 100 offset 20就会查询满足120条的数据,然后取出20条,可想而知随着limit后的数值增大越来越慢。
  2. 物理分页:使用分页插件PageHelper,直接去数据库里查询指定条数的数据。

谈谈MyBatis缓存

参考:简述MyBatis的一级缓存、二级缓存原理

MyBatis延迟加载

开启延迟加载

<setting name="lazyLoadingEnabled" value="true"/>

默认是侵入式延迟加载机制:如果只查询主表数据而不进行使用,级联表的数据不会被查询;如果使用了主表数据,即使级联表的数据没有使用,也会查询
关闭侵入式延迟加载机制:使用到数据才会去查找相关表

<setting name="aggressiveLazyLoading" value="false"/>

谈谈Redis缓存穿透和缓存雪崩

  1. 缓存穿透:同一时间客户端大量的请求在Redis和数据库中都不存在的数据会导致每次请求都会查DB。解决办法:
    • 将从DB查询出来的空值进行缓存“null”
    • 使用布隆过滤器。(有一定的误判率,谷歌guava的默认误判率为0.03)
  2. 缓存雪崩:大量的key在同一时间过期,同一时间收到大量的请求,流量会直接到达DB,造成DB宕机。解决办法:
    • 设置热点数据key永不过期
    • 设置过期时间不要集中在一起

Servlet生命周期

Java高级特性、集合、框架、中间件、JVM、多线程、设计模式、数据库、网络通信_第3张图片

  1. 容器启动将Servlet Class加载到虚拟机
  2. 第一个请求到达时,实例化Servlet 调用init()初始化方法,调用service()方法
  3. 第二个以及第二个请求之后的请求到达时,调用service()方法
  4. Servlet容器正常关闭时,调用destroy()方法。

Java内存区域(JVM内存结构)

  1. 程序计数器:线程私有的(为了保证线程切换后能恢复到正确位置),当前线程执行字节码的行号指示器。字节码解释器就是通过改变这个计数器的值来选取下条需要执行的字节码指令(分支、循环、跳转、异常处理、线程恢复等)。程序计数器是此区域是唯一一个没有规定OutOfMemoryError的区域。
  2. Java虚拟机栈:线程私有,描述的是Java方法执行的内存模型,每个方法执行的时都会创建一个栈帧用于储存局部变量表 1、操作数栈、动态链接、方法出口等信息。如果线程请求栈深度大于虚拟机允许,抛出StackOverflowError;如果无法申请到足够内存抛出OutOfMemoryError
  3. 本地方法栈:线程私有,与Java虚拟机栈相似,只不过是为虚拟机使用到的native方法服务的
  4. Java堆:线程共享,Java虚拟机管理的内存最大的一块区域,在虚拟机启动时创建。此区域唯一目的是存放对象实例,几乎所有对象实例都在这里分配内存2。无法申请所需内存时抛出OutOfMemoryError
  5. 方法区:线程共享,储存被虚拟机加载的类信息、常亮、静态变量、即时编译器编译的代码等数据。无法申请所需内存时抛出OutOfMemoryError。方法区还包含运行时常量池,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息-常量池,用于存放编译期生成的各种字面量和符号引用,这部分信息将存放于运行时常量池中。

参考:《深入理解Java虚拟机》

Java引用类型

JDK1.2之前Java中引用只有引用和没被引用两种状态,过于狭隘,对于“食之无味弃之可惜”的对象无能为力。我们希望某些对象在内存足够时保留,内存不足时抛弃。JDK1.2之后对引用进行了扩充,由以下4中以此减弱:

  1. 强引用(Strong Reference):类似“Object obj=new Object()”,只要强引用存在对象永远不会被回收
  2. 软引用(Soft Reference):有用,非必须对象。软引用关联的对象会在内存将要溢出时被系统列入回收范围,进行二次回收
  3. 弱引用(Weak Reference):被弱引用关联的对象无论内存是否足够,只能存活到下次垃圾回收发生之前
  4. 虚引用(Phantom Reference):被虚引用关联的对象,其生存完全不受引用影响,也无法通过该引用获取对象实例。唯一作用是在对象被回收时收到一个通知

参考:《深入理解Java虚拟机》

JVM垃圾回收机制(判断对象是否存活)

  1. 判断对象是否可用
    1. 引用计数算法:给对象添加一个引用计数器,每当一个地方引用它计数器就加1,每当一个引用失效时计数器减1,任意时刻计数器为0时,该对象不可用。缺点是:无法解决循环引用问题。
    2. 可达性分析算法:通过一系列成为“GC Roots”对象作为起点向下搜索,搜索走过的路径被称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,改对象不可用。Java中,可作为GC Roots对象包括:
      ① 虚拟机栈(栈帧中本地变量表)中引用的对象
      ②方法区中静态属性引用的对象
      ③方法区中常量引用的对象
      ④本地方法栈中JNI引用的对象
  2. 判断对象是否死亡
    如果根搜索算法中判断对象不可用,并不代表对象真正死亡。对象真正死亡要经历两次标记:
    1. 如果对象不可达那么将会被第一次标记,并且进行一次筛选,筛选条件是是否需要执行finalize()方法(对象没有覆盖finalize()方法,或者执行过了finalize()就没必要执行,任何对象的finalize()只会执行一次)
    2. 如果对象有必要执行finalize()方法,将会被放进F-Queue队列中。然后GC将对该对象进行第二次标记,对象如果在执行finalize()方法时成功自救(重新与引用链上任意对象建立关联),将被移除即将回收的集合,否则就离死不远了

参考:《深入理解Java虚拟机》

垃圾收集算法

  1. 标记-清除:首先标记处所有需要回收的对象,标记完成后统一进行回收。有两个不足:
    1. 标记和清除效率低下
    2. 标记清除后产生大量不连续的内存碎片
  2. 复制算法:将内存等分为两块,每次只使用其中一块,当一块内存用完了就将活着的对象复制到另一块,然后清除掉。优点是实现简单,效率高,内存连续。缺点是内存使用率低,代价高。

    现在的商业虚拟机使用这种方式回收新生代,新生代百分之98是朝生夕死对象,所以不需要1:1分配内存,而是将内存分为一块较大的Eden空间和两块较小的Survior(Survior from、Survior to)空间,回收时将Eden和Servior From存活对象复制到Servior to上,然后清理掉自己。HotSpot默认Eden和Servior为8:1,我们没法保证每次回收存活对象不多于10%,当内存不够时需要依赖老年代进行分配担保,也就是当Servior to没有足够内存存放上一次新生代的存活对象时,这些对象将通过分配担保机制进入老年代。

  3. 假设对象存活率在100%(老年代完全有可能),那么复制算法就不适合了,所以提出来标记-整理方法。标记过程如同标记-清除算法,然后将标记的对象向一侧移动,最后一次清理掉边界之外的内存
    Java高级特性、集合、框架、中间件、JVM、多线程、设计模式、数据库、网络通信_第4张图片
  4. 分代收集算法:当代虚拟机都采用这种方法,将Java堆分为新生代、老年代。新生代每次收集都会有大量的对象死亡,采用复制算法。老年代对象存活率高、没有额外空间对它进行担保,采用标记-清理或者标记-整理算法
    参考:《深入理解Java虚拟机》

Java为什么需要性能调优

Java应用随着用户量增大等原因会导致需要的内存不断增大,一旦所需内存大于物理机可分配内存就会导致系统崩溃,因此就需要对JVM内存进行配置限制,一旦到达临界点就会进行内存回收释放,系统永远不会因为内存问题而导致崩溃。

类加载时机

Java高级特性、集合、框架、中间件、JVM、多线程、设计模式、数据库、网络通信_第5张图片
加载、验证、准备、初始化、卸载这5个阶段顺序是固定的。为了支持Java运行时绑定,解析阶段可以在初始化之后进行。以下5种情况必须初始化:

  1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用JDK 1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且该方法句柄所对应的类没有初始化过,则先触发初始化。

Java类加载的过程

  1. 加载:在加载阶段,虚拟机需要完成3件事情:
    1. 通过一个类的全限定名来获取定义此类的二进制字节流;

      获取二进制流途径:

      1. 从ZIP包获取,是JAR、EAR、WAR格式基础
      2. 网络中获取,如:Applet
      3. 运行时计算机生成,这种场景使用最多的是动态代理技术,在java.lang.reflect.Proxy就是使用ProxyGenerator.generateProxyClass来为特定的接口生成二进制字节流。
      4. 其他文件生成,如JSP
      5. 从数据库中读取,如某些中间件服务器将代码安装到数据库中来实现代码在集群中分发
    2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
    3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
  2. 验证:验证阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。整体来看,验证阶段大致分为4个验证动作:
    1. 文件格式验证:第一阶段是验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。该阶段是基于二进制字节流验证的,只有通过了这个阶段的验证,字节流才会进入内存的方法去中存储,后面的3个验证都是基于方法区的存储结构进行的。
      这一阶段可能的验证点:

      a.是否以魔数0xCAFEBABE开头
      b.主、次版本号是否在当前虚拟机处理范围内
      c.常量池的常量数据类型是否被支持(检查常亮tag标志)
      d. 指向常量的各种索引值是否有指向不存在或者复合类型的常亮
      e. CONSTANT_utf8_info型常量中是否有不符合utf8编码的数据
      f. Class文件中各个部分以及文件本身是否有被删除或者附件的其他信息

    2. 元数据验证:元数据验证是对字节码描述信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。这个阶段可能的验证点:

      a. 是否有父类
      b. 是否继承了不被允许继承的类
      c. 如果该类不是抽象类,是否实现了其父类或接口要求实现的所有方法
      d. 类中的字段、方法是否与父类产生矛盾(类如覆盖父类final字段,或者错误方法重载)

    3. 字节码验证:字节码验证的主要目的是通过数据流和控制流分析,确定程序语义的合法性和逻辑性。该阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事情。这个阶段可能的验证点:

      a. 保证任何时候操作数栈的数据类型与指令代码序列的一致性,不会出现这种情况:在操作栈中放置了一个int类型数据,使用时却按照long类型加载入本地变量表中;
      b.跳转指令不会跳转到方法体以外的字节码指令上

    4. 符号引用验证:符号引用验证的主要目的是保证解析动作能正常执行,如果无法通过符号引用验证,则会抛出异常。这个阶段可能的验证点:

      a. 符号引用的类、字段、方法的访问性(public、private等)是否可被当前类访问
      b. 指定类是否存在符合方法的字段描述符以及简单名称所描述的方法和字段

c. 符号引用中的类、字段、方法的访问性(private、protected…)是否可被当前类访问

数组的加载

数组是直接由Java虚拟机创建的,但是数组组件是由类加载器创建的,一个数组创建遵循以下规则:

  1. 如果数组组件是引用类型,就是用上节讲到的加载过程去递归加载,数组将在加载该组件的类加载器的类名空间上被标识(一个类必须与类加载器一起确定唯一性)
  2. 如果数组组件不是引用类型(如int[]数组),Java虚拟机将会把数组标识为与引导类加载器关联
  3. 数组类的可见性和组件可见性一致,如果组件不是引用类型,那么数组可见性默认为public。

什么是类加载器

为了让应用程序自己去决定如何获取自己需要的类,将通过一个全类名来获取类的二进制字节流的这个动作放到Java虚拟机外部去实现。实现这个动作的代码块就是类加载器。

类加载器的分类

  1. 启动类加载器(Bootstrap ClassLoader):负责加载存放在\lib目录中或者被-Xbootclasspath指定路径中并且是Java虚拟机识别的类库加载到虚拟机内存中。开发者不可使用该类加载器
  2. 拓展类加载器(Extension ClassLoader):由sun.misc.Launcher$ExtClassLoader实现,负责加载存放在\lib\ext目录中或者被系统变量java.ext.dirs所指定的路径的所有类库加载到虚拟机内存中。开发者可以直接使用该类加载器
  3. 应用程序类加载器(Application ClassLoader):由sun.misc.Launcher$AppClassLoader实现,是ClassLoader中getSystemClassLoader()方法的返回值,所以也成为系统类加载器。负责加载用户类路径classpath上的类。开发者可以直接使用,这是程序中使用的默认类加载器,用户可以自定义类加载器。

类加载器双亲委派模型

Java高级特性、集合、框架、中间件、JVM、多线程、设计模式、数据库、网络通信_第6张图片

  1. 定义:如果一个类加载器收到加载类请求,它首先不会自己去加载这个类,而是将该类加载工作委派给父加载器,因此所有的类加载请求最终都会传到顶层Bootstrap ClassLoader进行加载。只要父加载器反馈自己无法完成这个加载请求(它在自己的搜索范围内没找到所需的类)时,子加载器才会尝试自己加载
  2. 意义:Java类随着类加载器一起具有优先级关系。例如java.lang.Object,存放在rt.jar总,无论哪个类加载器加载它最终都会委派给处于模型最顶层类加载器进行加载,因此Object在程序中各种类加载器加载的结构都是同一个类。反之,若没有使用双亲委派模型,如果用户自己编写一个java.lang.Object类,放于classpath路径下,再新建了个类加载器去加载它,系统中就会出现不同的Object对象。

参考:《深入理解Java虚拟机》

Java多线程实现原理

Java多线程是通过线程轮流切换并分配处理器执行时间来实现的,在任何时刻,一个处理器只能执行一条线程中的指令

线程状态

jdk1.8中为线程设置了6个状态:

  1. NEW:新创建的线程,未调用start()方法
  2. RUNNABLE:可能是正在运行,也可能是在等待cpu进行调度,可以理解为READY(start())和RUNNING
  3. BLOCKED:一般是线程等待获取一个锁,来继续执行下一步的操作,例如使用synchronized修饰的代码块,等待获取锁的线程就是处于这种状态
  4. WAITING:调用以下方法进入这种状态:Thread.sleep(long)、Object.wait(long、)Thread.join(long)、LockSupport.parkNanos(long)、LockSupport.parkUntil()
  5. TERMINATED:线程执行结束之后的状态

ThreadLocal内存泄漏

Java高级特性、集合、框架、中间件、JVM、多线程、设计模式、数据库、网络通信_第7张图片

  1. 实现原理:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal 实例本身,value 是真正需要存储的Object。也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
  2. 为什么会内存泄漏:ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
  3. 解决内存泄漏办法:调用get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value

wait、sleep、yield、join、interrupt

  1. Object.wait:线程会释放掉它所占有的锁,从而使别的线程有机会抢占该锁。
    当前线程必须拥有当前对象锁,否则会抛出IllegalMonitorStateException异常。唤醒当前对象锁的等待线程使用notify或notifyAll方法,也必须拥有相同的对象锁,否则也会抛出IllegalMonitorStateException异常。wait()和notify()必须在synchronized函数或synchronized block中进行调用。如果在non-synchronized函数或non-synchronized block中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常。
  2. Thread.Sleep:在指定时间内使当前线程进入BLOCKED状态,不会释放锁
  3. Thread.yield:作用是让步,不会释放锁。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!
  4. Thread.join:等待调用join方法的线程结束,再继续执行。
  5. Thread.interrupt:改变中断状态而已,它不会中断一个正在运行的线程。这一方法实际完成的是,给受阻塞的线程发出一个中断信号,这样受阻线程就得以退出阻塞的状态。更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个 InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。如果线程没有被阻塞,这时调用 interrupt()将不起作用,直到执行到wait(),sleep(),join()时,才马上会抛出InterruptedException。interrupt()方法的简单理解

线程间通信方式

  1. 使用volatile关键字:volatile保证了被修饰的变量对所有线程的可见性
  2. 使用Object类的wait() 和 notify() 方法,wait和 notify必须配合synchronized使用,wait方法释放锁,notify方法不会释放锁直到代码执行完
  3. 使用JUC工具类 CountDownLatch,基于AQS框架,相当于也是维护了一个线程间共享变量state
  4. 使用 ReentrantLock 结合 Condition
  5. 基本LockSupport实现线程间的阻塞和唤醒

参考:线程间通信的几种实现方式

主存和工作内存交互时虚拟机保证的天然原子性操作有哪些

lock(锁定)、unclock(解锁)、read(读取)、load(载入)、use(使用)、assign(赋值)、store(储存)、write(写入)

  1. 虚拟机未将lock、unlock直接开放给用户,但是提供了更高层次的字节码指令monitorenter、monitorexit来隐式使用这两个指定,反映到Java就是synchronized关键字,因此synchronized修饰的代码块具备原子性
  2. 我们可以认为基本数据类型的访问读写是具有原子性的(long、double例外,但是大部分商用虚拟机都将它们读写当做原子性对待,平时在写long、double变量时不需要声明为volatile)

谈谈volatile关键字

当一个变量定义为 volaiile 之后,它将具备两种特性:

  1. 第一是保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的,在各线程的工作内存中变量也存在不一致的情况,但是由于每次使用变量前都需要先刷新,执行引擎看不到不一致的情况,因此可以认为不存在不一致的情况。而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成,例如,线程 A 修改一个普通变量的值,然后向主内存进行回写,另外一条线程 B 在线程 A 回写完成了之后再从主内存进行读取操作,新变量值才会对线程 B 可见。

    不过,无法保证非原子性操作的变量线程安全,例如i++问题,对以下代码进行反编译:

    private static int i = 0;
    public static void increase() {
        i++;
    }
    

    反编译结果:

     public static void increase();
     Code:
        0: getstatic     #2                  // Field i:I
        3: iconst_1
        4: iadd
        5: putstatic     #2                  // Field i:I
        8: return
    

    getstatic将i的值取到操作栈顶时,volatile保证此时变量是正确的的,但是当执行iconst_1、iadd这些操作时,其它线程已经对i的值进行了修改,putstatic就会将较小的值同步回主内存

  2. 禁止指令重排:指令重排是指CPU在正确处理指令依赖情况以保证程序得出正确结果的前提下,不按程序规定的顺序将多条指令分开发给不同的电路单元处理。被volatile修饰的变量,会在赋值后多执行一步相当于添加内存屏障的操作,指令重排时不能将后面的指令重排到内存屏障之前。

synchronized关键字原理

程序编译后会在添加synchronized关键字代码块的前后分别添加monitorenter和monitorexit字节码指令,这两个指令都需要同一个reference类型的参数来指明要锁定和解锁的对象。执行monitorenter指令是就会尝试获取对象的锁。如果对象没有被锁定或者当前线程已经拥有对象的锁,就把锁的计数器加1,因此对同一个线程来说在synchronized中是可重入的,不会自己把自己锁死。相应的,在执行monitorexit指令时就将锁计数器减1,当计数器为0时释放锁。

乐观锁与悲观锁

  1. 悲观锁,正如其名,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。
  2. 乐观锁会假设整个数据处理过程中数据不会被修改,只有当操作提交操作时才检查数据是否被修改,如果发生冲突了就返回错误信息,反之提交成功。Java使用CAS实现乐观锁。

乐观锁出现的问题

ABA问题,假设有个变量a,线程1读到的值为2,然后进行修改3操作,线程b将a修改为4然后又改回为2,线程1提交时发现数据还是2,提交成功,这就是ABA问题,线程1读取了脏数据。
解决办法就是添加版本号,每次提交时获取最新版本号和之前版本号进行对比,一致就提交。
JUC包通过提供一个带有标记的原子引用类“AomicStampedReference”来解决ABA问题,它可以通过控制变量值的版本来保证CAS正确性,不过目前来说这个类比较鸡肋,大部分情况ABA问题不会影响并发正确性,要解决ABA问题改用互斥同步更高效

互斥同步和非阻塞同步

  1. 互斥同步:多个线程并发访问同一个数据时,保证同一时刻只被一个线程访问,是一种悲观并发策略。互斥同步手段:synchronized是原生语法的互斥锁;ReenterantLock是API层面的互斥锁。
  2. 非阻塞同步:这是一种基于冲突检测的乐观并发策略,先进性操作如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就采取其它补偿措施(例如:不断尝试直到成功),这种乐观的并发策略许多实现都不需要将线程挂起,因此被称为非阻塞同步(Non-Blocking Synchronization)。

    由于需要保证操作和冲突检测两个步骤具备原子性,如果依靠互斥同步就失去了意义,只能依靠硬件指令集的发展,硬件保证一个从语义上看起来需要多次操作的行为通过一条处理器指令就能完成,常用的指令有:

    1. 测试并设置(Test-and-Set)
    2. 获取并增加(Fetch-and-Increment)
    3. 交换(Swap)
    4. 比较并交换(Compare-and-Swap,CAS)
    5. 加载链接/条件储存(Load-linked/Store-Conditional,LL/SC)

CAS原理

CAS:Compare and Swap,即比较再交换。CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

ReenterantLock和synchronized关键字对比

ReenterantLock需要lock()和unlock()配合try catch finally使用,相比synchronized关键字ReenterantLock增加了以下高级功能:

  1. 等待可中断:正在等待的线程可以放弃等待,改为处理其他事情。
  2. 实现公平锁:多个线程等待一个锁是可以按照申请时间顺序依次获取锁,synchronized是非公平的,ReenterantLock默认是非公平锁,可以通过带boolean的构造函数使用公平锁
  3. 绑定多个条件:一个ReenterantLock可以同时绑定多个Condition对象,而synchronized中锁对象的wait()、notify()、notifyAll()可以实现一个隐含条件,如果要和多于一个条件关联就必须再加一个锁,ReenterantLock只需要多次调用newCondition()即可。
    性能方面,1.6之前单核synchronized性能高,多核ReenterantLock性能高。1.6之后对synchronized大大优化,它们性能基本持平synchronized甚至优之,所以现在性能不是选择ReenterantLock的理由

静态代理、JDK动态代理、CGLIB动态代理的区别

代理方式 特点 缺点
静态代理 需要定义父类或者接口,代理对象和被代理对象需要同时继承父类或者实现该接口,一次代理一个类 随着代理类增多,出现大量重复代码,难维护,造成类膨胀
jdk动态代理 目标类需要实现至少一个接口,代理对象通过JAVA的API动态生成,可以代理一个借口的多个实现 只能够代理实现了接口的目标类
cglib动态代理 代理类要实现MethodInterceptor接口,通过Enhancer创建目标类的子类为代理对象,所有也是通过继承关系创建代理类的,然后通过实现intercept(Object o, Method method, Object[] objects, MethodProxy proxy)方法对所有的方法进行拦截,添加增强处理,注意该方法中要通过代理类的invokeSuper调用父类的方法 不能代理final修饰的类

参考:设计模式-代理模式(Proxy Pattern)

设计模式-策略模式

Java高级特性、集合、框架、中间件、JVM、多线程、设计模式、数据库、网络通信_第8张图片

  1. 解决问题:在有多种算法相似的情况下,使用 if…else 所带来的复杂和难以维护。
  2. 概念:在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。
  3. 角色:Context: 环境类、Strategy: 抽象策略类、ConcreteStrategy: 具体策略类
  4. 代码
    1. 抽象策略类Sort
      public interface Sort {
        void sort();
      }
      
    2. 具体策略类ConcreteSort1
       public class ConcreteSort1 implements Sort {
       
           @Override
           public void sort() {
               System.out.println("使用快速排序");
           }
       
       }
      
    3. 具体策略类ConcreteSort2
       public class ConcreteSort2 implements Sort {
       
           @Override
           public void sort() {
               System.out.println("使用归并排序");
           }
       
       }
      
    4. 定义Context环境类
       public class Context {
       
           public AbstractSort method;
       
           public Context(AbstractSort abstractSort) {
               this.method = abstractSort;
           }
       
           public void contextSort() {
               method.sort();
           }
       }
      
    5. 客户端类Main
       public class Main {
       
           public static void main(String[] args) {
               //传入不同的具体策略即可
               Context context = new Context(new ConcreteSort2());
               context.contextSort();
       
           }
       }
      
  5. 在JDK中的使用:ThreadPoolExecutor中的四种拒绝策略
    1. AbortPolicy:直接抛出异常。
    2. CallerRunsPolicy:只用调用者所在线程来运行任务。
    3. DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
    4. DiscardPolicy:不处理,丢弃掉。

参考:设计模式之–策略模式及其在JDK中的应用

谈谈对数据库索引的理解

数据库索引设计的初衷是可以通过索引快速查找表中数据。索引是建立某种数据结构和表中数据一种关系,这种数据结构必须能够快速查找到目标值,然后通过这种关系定位到所需的数据行。索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息。以MySQL的MyISAM为例,每个表对应的数据库文件有三个,一个保存表信息,一个保存索引信息,一个保存数据。保存索引的数据结构是B+Tree,如果根据某个字段获取数据MySQL首先判断该字段是否建立了索引,如果有索引,就先通过在B+Tree上快速查找目标值,如果找到目标值,则会通过该索引对应的物理地址定位到数据文件中的数据,获得查询结果。

谈谈聚集索引和非聚集索引

聚集索引就是将索引和数据存在一个文件中,查找数据时找到索引值后直接能获取到数据。非聚集索引是将索引和数据分开储存,索引文件储存的时索引值和对应数据的物理地址,找到索引后还需要根据物理地址找到对应数据。对比直线聚集索引比非聚集索引效率要高。

一次web请求的过程

Java高级特性、集合、框架、中间件、JVM、多线程、设计模式、数据库、网络通信_第9张图片

  1. 在web客户端中输入网址
  2. web客户端通过DNS将域名解析成ip
  3. 根据ip在互联网上找到对应的服务器,建立tcp连接
  4. web客户端向服务器发起http请求,获取服务器资源。一般在客户端和服务器间会使用Nginx进行请求转发和静态资源处理
  5. 应用服务器处理接收到的请求,进行业务处理,将处理结果返回给web客户端
  6. web客户端服务器断开连接(Http1.1之后是长连接,不一定是请求完成后就断开连接,这取决于服务器的操作。)
  7. web客户端拿到请求结果后进行界面渲染

参考一次web请求过程

TCP/IP组成或者模型

Java高级特性、集合、框架、中间件、JVM、多线程、设计模式、数据库、网络通信_第10张图片

  1. 应用层(细分为应用层、表示层、会话层):定义数据格式并按照对应的格式解读数据
  2. 传输层:定义端口,标识应用程序身份,实现端口到端口的通信
  3. 网络层:定义网络地址、区分网段、子网内MAC寻址、对于不同子网的数据包进行路由
  4. 网络接口层(细分为物理层、数据链路层):对电信号进行分组并形成具有特定意义的数据帧,然后以广播的形式通过物理介质发送给接收方。

参考:TCP/IP协议

TCP和UDP

  1. TCP面向连接;UDP是一种无连接协议
  2. TCP提供可靠的服务,使用阻塞控制和流量控制;UDP不建立连接,不关心消息是否被接受到,无视网络状况数据以恒定速率想发就发,无法提供可靠的服务,不使用阻塞控制和流量控制
  3. TCP1对1通信;UDP支持1对1,1对多,多对1,多对多通信
  4. TCP面向字节流;UDP面向报文
  5. TCP慢;UDP快
  6. TCP适用于可靠传输的应用(文件传输);UDP适用于实时应用(IP电话、视频会议、直播等)

参考:一文搞懂TCP与UDP的区别

TCP三次握手

Java高级特性、集合、框架、中间件、JVM、多线程、设计模式、数据库、网络通信_第11张图片

  1. 客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态。
  2. 服务器端收到SYN报文,回应一个SYN (SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。
  3. 客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态。
  • SYN(Synchronize Sequence Numbers):同步序列编号
  • SEQ:初始序号

https工作原理

https=http+SSL / TLS,https相当于安全的http,https工作过程如下

  1. 首先采用https协议的服务器首先要安装安全证书,这套证书就是一对公钥(包含证书的颁发机构,过期时间等信息)和私钥。
  2. 收到客户端请求时服务器将公钥发给客户端
  3. 客户端收到证书信息后由TLS来解析公钥,首先会验证公钥是否有效,比如颁发机构,过期时间等等,如果发现异常,则会弹出一个警告框,提示证书存在问题。如果证书没有问题,那么就生成一个随即值。然后用公钥对该随机值进行加密,只有拥有对应的私钥才能解密。
  4. 客户端将加密后的随机值传给服务器,随后客户端服务器数据交互就用该随机值(解密私钥)进行加密解密
  5. 服务器收到解密私钥后用私钥进行解密得到随机值,然后将返回内容用该随机值进行算法加密后发给客户端。

    只有知道解密私钥才能对内容进行解密。所以只要算法足够高深和解密私钥足够复杂数据就很安全。

  6. 客户端收到数据后利用解密私钥进行数据还原

二分法查找

  1. 算法:二分法查找适用于数据量较大时,但是数据需要先排好顺序。
  2. 主要思想是:(设查找的数组区间为array[low, high])
    • 确定该区间的中间位置K
    • 将查找的值T与array[k]比较。若相等,查找成功返回此位置;否则确定新的查找区域,继续二分查找。
    • 区域确定如下:如果a.array[k] > T 由数组的有序性可知array[k,k+1,……,high] > T,故新的区间为array[low,……,K-1];如果b.array[k]
  3. 示例
    public static int indexedBinarySearch(List<Integer> list, int key) {
        if (list.isEmpty()) {
            throw new RuntimeException("List can`t be empty !");
        }
        //排序
        list.sort(Integer::compareTo);
        System.out.println(Arrays.toString(list.toArray()));
        int low = 0;
        int high = list.size() - 1;
    
        while (low <= high) {
            int mid = (low + high) >>> 1;
            Integer midVal = list.get(mid);
            if (midVal < key)
                low = mid + 1;
            else if (midVal > key)
                high = mid - 1;
            else
                return mid; // key found
        }
        return -(low + 1);  // key not found
    }
    

  1. 局部变量表存放编译器可知的各种基本变量类型、对象引用、返回类型(指向一条字节码指令的地址)。其中64位的long和double占两个局部变量空间(slot)。局部变量表所需内存空间编译期间完成分配,当进入某方法时这个方法需要在栈帧中分配的局部变量表空间是完全确定的,方法运行时不会改变。 ↩︎

  2. 随着JIT编译器的发展和逃逸分析技术的成熟,栈上分配、标量替换优化技术将会导致一些微妙变化,所有对象实例在堆上分配内存变得不“绝对”了。 ↩︎

你可能感兴趣的:(Java知识,面试)