自用的笔记

一、java 基础
1.1 请你说明 String 和 StringBuffer 的区别
String是一种被final修饰的类,一旦创建就不能修改,对string对象的修改其实都是新建一个新的string对象 而Stringbuffer是可变对象,不会像string只要修改就创建新的对象,它的初始值是null 可以用append方法追加值,并且是线程安全的(所有修改数据的方法都被synchronized修饰)
1.2 请你说明一下 int 和 Integer 有什么区别
Int是基本数据类型而integer是一个包装类对象,int可以直接使用,而integer需要先创建对象赋值
1.3 数组(Array)和列表(ArrayList)的区别?什么时候应该使用 Array 而不是 ArrayList?
数组在创建是需要指定长度和类型并且只能存放相同类型的对象,而数组集合需要先创建再存放对象并且可以存放不同的对象。知道所需长度的时候用数组,不知道大小的时候用数组集合。
1.4 什么是值传递和引用传递?
值传递只是将一个复制过后的值传递过去,这个值发生了改变不会影响原有数据,
而引用传递则是将对象在堆中的地址传递过去,如果发生改变则会改变原有的数据
1.5Java 支持的数据类型有哪些?什么是自动拆装箱?
Byte char Boolean short int long float double
将基本数据类型转换包装类称为装箱,反之叫做拆箱
1.6 为什么会出现 4.0-3.6=0.40000001 这种现象?
因为二进制的原因
1.7java8 的新特性吗,请简单介绍一下
Lambda表达式,Stream流 ,Option,日期类型
1.8 你说明符号“==”比较的是什么?
比较在堆中存放的地址是否一样,而eqauls则是比较存放的值
1.9Object 若不重写 hashCode()的话,hashCode()如何计算出来的?
调用本地方法 直接返回该对象的内存地址
1.10 为什么重写 equals 还要重写 hashcode?
因为equals相等hashcode一定相同,而hashcode相同equals不一定相同
1.11 若对一个类不重写,它的 equals()方法是如何比较的?
直接比较内存地址是否相同,如果相同为true,反之为false
二、关键字
2.1Java 里面的 final 关键字是怎么用的?
修饰类、变量、方法,被修饰的对象不能进行修改,被修饰的方法不能被重写,被修饰的类不能被继承
2.2 谈谈关于 Synchronized 和 lock
Synchronized是java中的关键字,是悲观锁,而lock是一个类,是一个乐观锁,Synchronized在代码执行完过后会自动释放锁而lock需要手动释放,Synchronized性能没有lock好。
2.3 请你介绍一下 volatile?
是一个关键字,修饰变量对所有线程可见
2.4 请你介绍一下 Syncronized 锁,如果用这个关键字修饰一个静态方法,锁住了什么?如果修饰成员方法,锁住了什么?
如果是静态方法则是锁住了这个类的class对象,而成员方法则是当前实例
三.面向对象
3.1Java 中的方法覆盖(Overriding)和方法重载(Overloading)是什么意思?
覆盖发生于子类中,将父类中的方法保持方法签名不变,实现与父类不同的代码(多态的表现)
而重载发生与普通类中,通过不同的方法签名去实现不同的方法(编译时绑定)
3.2 如何通过反射获取和设置对象私有字段的值?
setAccessable(true)
3.3 请说明内部类可以引用他包含类的成员吗,如果可以,有没有什么限制吗?
可以,前提这个内部类不是静态的,那么它可以访问包含他的外部类的所有属性方法,如果是静态的内部类,那么就只能访问外部类的所有静态属性
3.4 当一个对象被当作参数传递给一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?
值传递,因为Java中只有值传递方式,其实就是将传递的对象的副本传过去,这个副本对象指向的也是传递对象的地址,所以就算这个副本对象的内容发生改变,但是对象的引用也不会发生改变
3.5 什么是泛型?
当不确定传入对象的类型的时候可以用泛型来约束传入对象,可以通过上限和下限来确定范围,泛型只存在与编译时期。
3.6 解释一下类加载机制,双亲委派模型,好处是什么?
虚拟机将class文件加载到内存中,然后效验其中的数据并解析,然后进行初始化,接下来就可以使用了,使用完毕后就会卸载。
双亲委派模型就是表现了类加载器的层次关系,好处是节约了内存,只有当父加载器不能胜任时候才会用当前类加载器去加载
3.7”static”关键字是什么意思?Java 中是否可以覆盖(override)一个 private 或者是 static 的方法?
Static表示静态的,可以修饰方法,属性,被修饰的属性随类一起加载,被修饰的方法可以调用非静态方法,而非静态方法不能调用静态方法。不能。
3.8 列举你所知道的 Object 类的方法并简要说明。
3.9 类和对象的区别
对象是类的体现,类是对象的抽象,类不占内存,对象占内存
4.0
Java中sleep和wait的区别?
Sleep是thread的方法而wait是object的方法,Sleep不会释放锁而wait会释放锁,sleep需要异常捕获
4.1
简述jvm内存模型?
程序计数器:线程私有,各线程之间独立储存,互不影响,若当前执行的是Java方法,则记录的就是当前执行指令的地址,若是native方法,则为空;
java虚拟机栈:线程私有,每个方法在执行时都会创建一个栈帧,方法执行过程就是栈帧在虚拟机栈中从入栈到出栈的过程,入栈表示方法开始被调用,出栈表示方法执行完毕,栈帧用于保存方法内部局部变量、操作数、方法返回值、动态链接;我们平时说的栈其实一般就是指局部变量区:用于存放方法参数、方法内定义的局部变量,还有已知的八大基本数据类型、对象引用、返回值地址;
本地方法栈:线程私有,和虚拟机栈相似,区别在于虚拟机栈的服务对象是java方法,而本地方法栈是本地方法;
堆:线程共享,在虚拟机启动的时候创建,用于存放对象实例,堆是GC管理的主要区域;
方法区:线程共享,其实方法区也是堆的物理组成部分,用于存放常量、静态变量 、 类信息(构造方法/接口定义) 、运行时常量池;注意, 实例变量在堆内存中,和方法区无关。
(jdk1.8之前,方法区的实现是永久代,从1.8开始,用元空间代替了永久代,注意一点,方法区还是那个方法区)
4.2、什么是公平锁?什么是非公平锁?
公平锁:当前获得锁的线程释放锁后,其它所有等待中的线程会按照来的顺序执行,不会造成锁竞争;
非公平锁:当前获得锁的线程释放锁后,其它所有等待中的线程会全部参与锁竞争;
4.3什么是redis缓存穿透、缓存击穿、缓存雪崩?如何解决?
从事态严重性来讲:穿透 > 雪崩 > 击穿
缓存穿透:请求数据库中根本就不存在的数据,既然数据库中都没有,缓存中更没有,导致每次请求直接怼到数据库;
缓存雪崩:缓存大面积失效;
缓存击穿:请求了很多缓存中没有但是数据库中真实存在的数据,一般是缓存过期导致,也导致请求直接怼到数据库;
解决办法:
缓存穿透:最简单的就是利用布隆过滤器过滤非法key,我写了个 demo来分析具体原理,请移步布隆过滤器原理
缓存雪崩:设置key过期时间的时候加上一个随机数,关键点就在于让key错开时间失效;
缓存击穿:延长热点数据过期时间,或者直接设置永远不过期;
4.4简单阐述Java中的io、nio、bio
i/o即input/output,就是指读写操作

面试官经常问io和nio的区别,如果把io和nio放一起比较的话,那这里的io其实可以理解为bio,即blocking-io:
bio:同步阻塞
bio是java传统的io模型,他是同步阻塞io,一个线程触发io操作后,必须等待这个io操作执行完成,期间不能去做其他事情;
nio:同步非阻塞
nio(non-blocking-io)是同步非阻塞io,一个线程触发io操作后它可以立即返回,但是他需要不断地轮询去获取返回结果;
aio:异步非阻塞
aio(Asynchronous-io)是异步非阻塞io,一个线程触发io操作后她可以立马返回去做其他事情,内核系统将io操作执行完成后会通知线程;
多路复用io:异步阻塞
io多路复用:可以理解为异步阻塞io,但官方没这么叫,一个线程可以管理多个连接,不用来回切换;
4.5事务的ACID是指什么?
原子性(Atomic):事务中各项操作,要么全做要么全不做,任何一项操作的失败都会导致整个事务的失败;
一致性(Consistent):事务结束后系统状态是一致的;
隔离性(Isolated):并发执行的事务彼此无法看到对方的中间状态;
持久性(Durable):事务完成后所做的改动都会被持久化,即使发生灾难性的失败,通过日志和同步备份可以在故障发生后重建数据。
4.6事务隔离级别
隔离级别⭐
未提交读 READ UNCOMMITTED
事务中的修改即使没有提交,对其他事务也是可见的。事务可以读取其他事务修改完但未提交的数据,这种问题称为脏读。这个级别还存在不可重复读和幻读,很少使用。
提交读 READ COMMITTED
多数数据库的默认隔离级别,事务只能看见已提交事务的修改。存在不可重复读,两次执行同样的查询可能会得到不同结果。
可重复读 REPEATABLE READ(MySQL默认的隔离级别)
解决了不可重复读,保证同一个事务中多次读取同样的记录结果一致,InnoDB 通过 MVCC 解决。但无法解决幻读,幻读指当某个事务在读取某个范围内的记录时,会产生幻行。
可串行化 SERIALIZABLE
最高隔离级别,通过强制事务串行执行避免幻读。在读取的每一行数据上都加锁,可能导致大量的超时和锁争用的问题。实际很少使用,只有非常需要确保数据一致性时考虑。
悲观锁:
顾名思义,比较悲观,每次去拿数据都认为别人会修改,所有每次在操作前都会加锁,如:读写锁、行锁、表锁等,synchronized的原理也是悲观锁;适用于多写操作
乐观锁:
每次拿数据都认为别人不会修改,所以不会加锁,但是在更新的时候,会先判断在此期间有没有人更新该数据,如果有,返回冲突报错信息,让用户决定怎么操作;适用于多读操作
4.8
IOC(控制反转)
也叫DI(依赖注入),是一种思想,不是一种技术,IOC主张把对象的控制权交由spring,底层实现是反射+工厂方法模式,IOC容器实际上就是个Map,存放各种对象;
AOP
面向切面编程,把一些能共用、冗余、繁琐的功能提取出来,AOP能在不改变原有业务逻辑的情况下,增强横切逻辑代码,根本上解耦合,避免横切逻辑代码重复;常见使用场景有事务管理、日志、全局异常处理、用户鉴权;
4.9springboot自动装配原理?
在springboot的启动类上有个SpringBootApplication注解,这是个组合注解,这个注解里面有个注解叫EnableAutoConfiguration注解,@EnableAutoConfigration 注解会导入一个自动配置选择器去扫描每个jar包的META-INF/xxxx.factories 这个文件,这个文件是一个key-value形式的配置文件,里面存放了这个jar包依赖的具体依赖的自动配置类。这些自动配置类又通过@EnableConfigurationProperties 注解支持通过xxxxProperties 读取application.properties/application.yml属性文件中我们配置的值。如果我们没有配置值,就使用默认值,这就是所谓约定大于配置的具体落地点。
5.0 Spring MVC ⭐
处理流程
Web 容器启动时初始化 IoC 容器,加载 Bean 的定义信息并初始化所有单例 Bean,遍历容器中的 Bean,获取每个 Controller 中的所有方法访问的 URL,将 URL 和对应的 Controller 保存到一个 Map 集合中。

所有的请求会转发给 DispatcherServlet 处理,DispatcherServlet 会请求 HandlerMapping 找出容器中被 @Controler 修饰的 Bean 以及被 @RequestMapping 修饰的方法和类,生成 Handler 和 HandlerInterceptor 并以一个 HandlerExcutionChain 链的形式返回。

DispatcherServlet 使用 Handler 找到对应的 HandlerApapter,通过 HandlerApapter 调用 Handler 的方法,将请求参数绑定到方法的形参上,执行方法处理请求并得到逻辑视图 ModelAndView。

使用 ViewResolver 解析 ModelAndView 得到物理视图 View,进行视图渲染,将数据填充到视图中并返回给客户端。
5.1 注解
@SpringBootApplication:自动给程序进行必要配置,这个配置等同于:@Configuration ,@EnableAutoConfiguration 和 @ComponentScan 三个配置。
@EnableAutoConfiguration:允许 SpringBoot 自动配置注解,开启后 SpringBoot 就能根据当前类路径下的包或者类来配置 Bean。
@SpringBootConfiguration:相当于 @Configuration,只是语义不同。
5.2 SpringCloud⭐
微服务的优点
各个服务的开发、测试、部署都相互独立,用户服务可以拆分为独立服务,如果用户量很大,可以很容易对其实现负载。

当新需求出现时,使用微服务不再需要考虑各方面的问题,例如兼容性、影响度等。

使用微服务拆分项目后,各个服务之间消除了很多限制,只需要保证对外提供的接口正常可用,而不限制语言和框架等选择。

服务治理 Eureka
服务治理由三部分组成:服务提供者、服务消费者、注册中心。

服务注册:在分布式系统架构中,每个微服务在启动时,将自己的信息存储在注册中心。

服务发现:服务消费者从注册中心获取服务提供者的网络信息,通过该信息调用服务。

Spring Cloud 的服务治理使用 Eureka 实现,Eureka 是 Netflix 开源的基于 REST 的服务治理解决方案,Spring Cloud 集成了 Eureka,提供服务注册和服务发现的功能,可以和基于 Spring Boot 搭建的微服务应用轻松完成整合。

服务网关 Zuul
Spring Cloud 集成了 Zuul 组件,实现服务网关。

Zuul 是 Netflix 提供的一个开源的 API 网关服务器,是客户端和网站后端所有请求的中间层,对外开放一个 API,将所有请求导入统一的入口,屏蔽了服务端的具体实现逻辑,可以实现方向代理功能,在网关内部实现动态路由、身份认证、IP过滤、数据监控等。

负载均衡 Ribbon
Ribbon 是 Netflix 发布的均衡负载器,Spring Cloud 集成了 Ribbon,提供用于对 HTTP 请求进行控制的负载均衡客户端。

在注册中心对 Ribbon 进行注册之后,Ribbon 就可以基于某种负载均衡算法(轮循、随机、加权轮询、加权随机等)自动帮助服务消费者调用接口,开发者也可以根据具体需求自定义 Ribbon 负载均衡算法。实际开发中 Spring Clooud Ribbon 需要结合 Spring Cloud Eureka 使用,Eureka 提供所有可以调用的服务提供者列表,Ribbon 基于特定的负载均衡算法从这些服务提供者中选择要调用的实例。

声明式接口调用 Feign
Feign 是 Netflix 提供的,一个声明式、模板化的 Web Service 客户端,简化了开发者编写 Web 客户端的操作,开发者可以通过简单的接口和注解来调用 HTTP API。

相比于 Ribbon + RestTemplate 的方式,Feign 可以大大简化代码开发,支持多种注解,包括 Feign 注解、Spring MVC 注解等。

RestTemplate 是 Spring 框架提供的基于 REST 的服务组件,底层是对 HTTP 请求及响应进行了封装,提供了很多访问 REST 服务的方法,简化代码开发。

服务配置 Config
Spring Cloud Config 通过服务端可以为多个客户端提供配置服务,既可以将配置文件存储在本地,也可以将配置文件存储在远程的 Git 仓库,创建 Config Server,通过它管理所有的配置文件。

服务跟踪 Zipkin
Spring Cloud Zipkin 是一个可以采集并跟踪分布式系统中请求数据的组件,让开发者更直观地监控到请求在各个微服务耗费的时间,Zipkin 包括两部分 Zipkin Server 和 Zipkin Client。

服务熔断 Hystrix
熔断器的作用:在不改变各个微服务调用关系的前提下,针对错误情况进行预先处理。

设计原则:服务隔离、服务降级、熔断机制、提供实时监控和报警功能、提供实时配置修改功能。

Hystrix 数据监控需要结合 Spring Boot Actuator 使用,Actuator 提供了对服务的数据监控、数据统计,可以通过 hystirx-stream 节点获取监控的请求数据,同时提供了可视化监控界面。
5.2 设计模式
设计模式原则
原则 说明
开闭原则 OOP 最基础的原则,软件实体应该对扩展开放,对修改关闭。
单一职责原则 一个类、接口或方法只负责一个职责,降低代码变更风险。
依赖倒置原则 程序应该依赖于抽象类或接口,而不是实现类。
接口隔离原则 将不同功能定义在不同接口,避免类依赖它不需要的接口,减少接口冗余。
里氏替换原则 开闭原则的补充,规定任何父类可以出现的地方子类都一定可以出现,约束继承泛滥。
迪米特原则 每个模块对其他模块都要尽可能少地了解和依赖,降低耦合。
合成/聚合原则 尽量使用组合(has-a)/聚合(contains-a)而不是继承(is-a)实现复用,避免方法污染和方法爆炸。
设计模式的分类
类型 说明
创建型 创建对象时隐藏创建逻辑,不直接实例化对象,包括工厂/抽象工厂/单例/建造者/原型模式。
结构型 通过类和接口间的继承和引用创建复杂对象,包括适配器/桥接/过滤器/组合/装饰器/外观/享元/代理模式。
行为型 通过类的通信实现不同行为,包括责任链/命名/解释器/迭代器/中介者/备忘录/观察者/状态/策略/模板/访问者模式。
简单工厂模式
概念:由一个工厂对象创建实例,客户端不需要关注创建逻辑,只需提供传入工厂的参数。

场景:适用于工厂类负责创建对象较少的情况,缺点是如果要增加新产品,就需要修改工厂类的判断逻辑,违背开闭原则。

举例:

Calendar 类的 getInstance 方法,调用 createCalendar 方法根据不同的地区参数创建不同的日历对象。

Spring 中的 BeanFactory,根据传入一个唯一的标识来获得 Bean 实例。

工厂方法模式
概念:定义一个创建对象的接口,让接口的实现类决定创建哪种对象,让类的实例化推迟到子类中进行。

场景:主要解决了产品扩展的问题,在简单工厂模式中如果产品种类变多,工厂的职责会越来越多,不便于维护。

举例:

Collection 接口中定义了一个抽象的 iterator 工厂方法,返回一个 Iterator 类的抽象产品。该方法通过 ArrayList 、HashMap 等具体工厂实现,返回 Itr、KeyIterator 等具体产品。

Spring 的 FactoryBean 接口的 getObject 方法。

抽象工厂模式
概念:提供一个创建一系列相关对象的接口,无需指定它们的具体类。缺点是不方便扩展产品族,并且增加了系统的抽象性和理解难度。

场景:主要用于系统的产品有多于一个的产品族,而系统只消费其中某一个产品族产品的情况。

举例:java.sql.Connection 接口就是一个抽象工厂,其中包括很多抽象产品如 Statement、Blob、Savepoint 等。

单例模式
在任何情况下都只存在一个实例,构造方法必须是私有的、由自己创建一个静态变量存储实例,对外提供一个静态公有方法获取实例。

优点是内存中只有一个实例,减少了开销;缺点是没有抽象层,难以扩展,与单一职责原则冲突。

举例:Spring 的 ApplicationContext 创建的 Bean 实例都是单例对象,还有 ServletContext、数据库连接池等也都是单例模式。
代理模式
代理模式属于结构型模式,为其他对象提供一种代理来控制对该对象的访问。优点是可以增强目标对象的功能,降低代码耦合度;缺点是请求处理速度变慢,增加系统复杂度。

**静态代理:**代理对象持有被代理对象的引用,调用代理对象方法时会调用被代理对象的方法,但是会增加其他逻辑。需要手动完成,在程序运行前就已经存在代理类的字节码文件,代理类和被代理类的关系在运行前就已确定。 缺点是一个代理类只能为一个目标服务。

**动态代理:**动态代理在程序运行时通过反射创建具体的代理类,代理类和被代理类的关系在运行前是不确定的。动态代理的适用性更强,主要分为 JDK 动态代理和 CGLib 动态代理。

JDK 代理:
通过 Proxy 的 newProxyInstance 方法获得代理对象,需要三个参数:被代理类的接口、类加载器以及 InvocationHandler 对象,需要重写 InvocationHandler 接口的 invoke 方法指明代理逻辑。

CGLib 代理:
通过 Enhancer 对象的 create 方法获取代理对象,需要通过 setSuperclass 方法设置代理类,以及 setCallback 方法指明代理逻辑(传入一个MethodInterceptor 接口的实现类,具体代理逻辑声明在 intercept 方法)。

JDK 动态代理直接写字节码,而 CGLib 动态代理使用 ASM 框架写字节码, JDK 代理调用代理方法通过反射实现,而 GCLib 通过 FastClass 机制实现,为代理类和被代理类各生成一个类,该类为代理类和被代理类的方法分配一个 int 参数,调用方法时可以直接定位,效率更高。

装饰器模式
概念:在不改变原有对象的基础上将功能附加到对象,相比继承更加灵活。

场景:在不想增加很多子类的前提下扩展一个类的功能。

举例:java.io 包中,InputStream 通过 BufferedInputStream 增强为缓冲字节输入流。

和代理模式的区别:装饰器模式的关注点在于给对象动态添加方法,而动态代理更注重对象的访问控制。动态代理通常会在代理类中创建被代理对象的实例,而装饰器模式会将被装饰者作为构造方法的参数。

适配器模式
概念:作为两个不兼容接口之间的桥梁,使原本由于接口不兼容而不能一起工作的类可以一起工作。 缺点是过多使用适配器会让系统非常混乱,不易整体把握。

举例:

java.io 包中,InputStream 通过 InputStreamReader 转换为 Reader 字符输入流。

Spring MVC 中的 HandlerAdapter,由于 handler 有很多种形式,包括 Controller、HttpRequestHandler、Servlet 等,但调用方式又是确定的,因此需要适配器来进行处理,根据适配规则调用 handle 方法。

Arrays.asList 方法,将数组转换为对应的集合(不能使用修改集合的方法,因为返回的 ArrayList 是 Arrays 的一个内部类)。

和装饰器模式的区别:适配器模式没有层级关系,适配器和被适配者没有必然连续,满足 has-a 的关系,解决不兼容的问题,是一种后置考虑;装饰器模式具有层级关系,装饰器与被装饰者实现同一个接口,满足 is-a 的关系,注重覆盖和扩展,是一种前置考虑。

和代理模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。

策略模式
概念:定义了一系列算法并封装,之间可以互相替换。优点是算法可以自由切换,可以避免使用多重条件判断并且扩展性良好,缺点是策略类会增多并且所有策略类都需要对外暴露。

场景:主要解决在有多种算法相似的情况下,使用 if/else 所带来的难以维护。

举例:

集合框架中常用的 Comparator 就是一个抽象策略,一个类通过实现该接口并重写 compare 方法成为具体策略类。

线程池的拒绝策略。

模板模式
概念:使子类可以在不改变算法结构的情况下重新定义算法的某些步骤。优点是可以封装固定不变的部分,扩展可变的部分;缺点是每一个不同实现都需要一个子类维护,会增加类的数量。

场景:适用于抽取子类重复代码到公共父类。

举例:HttpServlet 定义了一套处理 HTTP 请求的模板,service 方法为模板方法,定义了处理HTTP请求的基本流程,doXXX 等方法为基本方法,根据请求方法的类型做相应的处理,子类可重写这些方法。

观察者模式
概念:也叫发布订阅模式,定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。缺点是如果被观察者对象有很多的直接和间接观察者的话通知很耗时, 如果存在循环依赖的话可能导致系统崩溃,另外观察者无法知道目标对象具体是怎么发生变化的。

场景:主要解决一个对象状态改变给其他对象通知的问题。

举例:ServletContextListener 能够监听 ServletContext 对象的生命周期,实际上就是监听 Web 应用。当 Servlet 容器启动 Web 应用时调用 contextInitialized 方法,终止时调用 contextDestroyed 方法。
5.3 索引⭐
作用
索引也叫键,是帮助存储引擎快速找到记录的一种数据结构。

在 MySQL 中,首先在索引中找到对应的值,然后根据匹配的索引记录找到对应的数据行。索引可以包括一个或多个列的值,如果索引包含多个列,那么列的顺序也十分重要,因为 MySQL 只能使用索引的最左前缀。

优点

大大减少服务器需要扫描的数据量、帮助服务器避免排序和临时表、将随机 IO 变成顺序 IO。
通过索引列对数据排序可以降低 CPU 开销。
缺点

实际上索引也是一张表,保存了主键与索引字段,并指向实体类的记录,也要占用空间。
降低了更新表的速度,因为更新表时 MySQL 不仅要保存数据,还要保存索引文件。
对于非常小的表,大部分情况下会采用全表扫描。对于中到大型的表,索引非常有效。
5.4 集合
ArrayList⭐
ArrayList 是容量可变列表,使用数组实现,扩容时会创建更大的数组,把原有数组复制到新数组。支持对元素的随机访问,但插入与删除速度慢。ArrayList 实现了 RandomAcess 接口,如果类实现了该接口,使用索引遍历比迭代器更快。

elementData 是 ArrayList 的数据域,被 transient 修饰,序列化时调用 writeObject 写入流,反序列化时调用 readObject 重新赋值到新对象的 elementData。原因是 elementData 容量通常大于实际存储元素的数量,所以只需发送真正有值的元素。

size 是当前实际大小,小于等于 elementData 的大小。

modCount 记录了 ArrayList 结构性变化的次数,继承自 AbstractList。expectedModCount 是迭代器初始化时记录的 modCount 值,每次访问新元素时都会检查 modCount 是否等于 expectedModCount,不等将抛出异常。这种机制叫 fail-fast,所有集合类都有。

LinkedList⭐
LinkedList 本质是双向链表,与 ArrayList 相比增删速度更快,但随机访问慢。除继承 AbstractList 外还实现了 Deque 接口,该接口具有队列和栈的性质。成员变量被 transient 修饰,原理和 ArrayList 类似。

包含三个重要的成员:size、first 和 last。size 是双向链表中节点的个数,first 和 last 分别指向首尾节点。

优点:可以将零散的内存单元通过附加引用的方式关联起来,形成按链路顺序查找的线性结构,内存利用率高。

Set
Set 元素不重复且无序,常用实现有 HashSet、LinkedHashSet 和 TreeSet。

HashSet 通过 HashMap 实现,HashMap 的 Key 即 HashSet 存储的元素,所有 Key 都使用相同的 Value ,一个 Object 类型常量。使用 Key 保证元素唯一性,但不保证有序性。HashSet 判断元素是否相同时,对于包装类型直接按值比较,对于引用类型先比较 hashCode,不同则代表不是同一个对象,相同则比较 equals,都相同才是同一个对象。

LinkedHashSet 继承自 HashSet,通过 LinkedHashMap 实现,使用双向链表维护元素插入顺序。

TreeSet 通过 TreeMap 实现的,添加元素到集合时按照比较规则将其插入合适的位置,保证插入后的集合仍然有序。

TreeMap⭐
TreeMap 基于红黑树实现,增删改查的平均和最差时间复杂度均为 O(logn) ,最大特点是 Key 有序。Key 必须实现 Comparable 接口或 Comparator 接口,所以 Key 不允许为 null。

TreeMap 依靠 Comparable 或 Comparator 排序,如果实现了 Comparator 就会优先使用 compare 方法,否则使用 Comparable 的 compareTo 方法,两者都不满足会抛出异常。

TreeMap 通过 put 和 deleteEntry 实现增加和删除树节点。插入新节点的规则有三个:① 需要调整的新节点总是红色的。② 如果插入新节点的父节点是黑色的,不需要调整。③ 如果插入新节点的父节点是红色的,由于红黑树不能出现相邻红色,进入循环判断,通过重新着色或左右旋转来调整。

HashMap ⭐
JDK8 前底层使用数组加链表,JDK8 改为数组加链表/红黑树,节点从 Entry 变为 Node。主要成员变量包括 table 数组、元素数量 size、加载因子 loadFactor。

table 数组记录 HashMap 的数据,每个下标对应一条链表,所有哈希冲突的数据都会被存放到同一条链表,Node/Entry 节点包含四个成员变量:key、value、next 和 hash。

数据以键值对的形式存在,键对应的 hash 值用来计算数组下标,如果两个元素 key 的 hash 值一样,就会发生哈希冲突,被放到同一个链表上,为使查询效率尽可能高,键的 hash 值要尽可能分散。

默认初始化容量为 16,扩容容量必须是 2 的幂次方、最大容量为 1<< 30 、默认加载因子为 0.75。

JDK8 之前

hash:计算元素 key 的散列值

① 处理 String 类型时,调用 stringHash32 方法获取 hash 值。

② 处理其他类型数据时,提供一个随机值 hashSeed 作为计算初始量,执行异或和无符号右移使 hash 值更加离散。

indexFor:计算元素下标

将 hash 值和数组长度-1 进行与操作,保证结果不超过 table 范围。

get:获取元素的 value 值

key 为 null,调用 getForNullKey 方法:

size=0 表示链表为空,返回 null。
size!=0 说明存在链表,遍历 table[0] 链表,如果找到了 key=null 的节点则返回其 value,否则返回 null。
key 不为 null,调用 getEntry 方法:

size=0 表示链表为空,返回 null 值。
size!=0,首先计算 key 的 hash 值,然后遍历该链表的所有节点,如果节点的 key 和 hash 值都和要查找的元素相同则返回其 Entry 节点。 如果找到了对应的 Entry 节点,调用 getValue 方法获取其 value 并返回,否则返回 null。
put:添加元素

key 为 null,直接存入 table[0]。

key 不为 null,计算 key 的 hash 值,调用 indexFor 计算元素下标 i,遍历 table[i] 链表:

key 已存在,更新 value 然后返回旧 value。
key 不存在,将 modCount 加 1,调用 addEntry 方法增加一个节点并返回 null。
resize:扩容数组

当前容量达到了最大容量,将阈值设置为 Integer 最大值,之后扩容不再触发。

当前容量没达到最大容量,计算新的容量,将阈值设为 newCapacity x loadFactor 和 最大容量 + 1 的较小值。创建一个容量为 newCapacity 的 Entry 数组,调用 transfer 方法将旧数组的元素转移到新数组。

transfer:转移元素

遍历旧数组的所有元素,调用 rehash 方法判断是否需要哈希重构,如果需要就重新计算元素 key 的 hash 值。

调用 indexFor 方法计算元素存放的下标 i,利用头插法将旧数组的元素转移到新数组。

JDK8

hash:计算元素 key 的散列值

如果 key 为 null 返回 0,否则就将 key 的 hashCode 方法返回值高低16位异或,让尽可能多的位参与运算,让结果的 0 和 1 分布更加均匀,降低哈希冲突概率。

put:添加元素

调用 putVal 方法添加元素:

如果 table 为空或不存在元素就进行扩容,否则计算元素下标位置,不存在就调用 newNode 创建一个节点。
如果存在元素且是链表类型,如果首节点和待插入元素相同,直接更新节点 value。
如果首节点是 TreeNode 类型,调用 putTreeVal 方法增加一个树节点,每一次都比较插入节点和当前节点的大小,待插入节点小就往左子树查找,否则往右子树查找,找到空位后执行两个方法:balanceInsert 方法,插入节点并调整平衡、moveRootToFront 方法,由于调整平衡后根节点可能变化,需要重置根节点。
如果都不满足,遍历链表,根据 hash 和 key 判断是否重复,决定更新 value 还是新增节点。如果遍历到了链表末尾则添加节点,如果达到建树阈值 7,还需要调用 treeifyBin 把链表重构为红黑树。
存放元素后将 modCount 加 1,如果 ++size > threshold ,调用 resize 扩容。
get :获取元素的 value 值

调用 getNode 方法获取 Node 节点:

如果数组不为空且存在元素,先比较第一个节点和要查找元素,如果相同则直接返回。

如果第二个节点是 TreeNode 类型则调用 getTreeNode 方法进行查找。

都不满足,遍历链表根据 hash 和 key 查找,如果没有找到就返回 null。

如果节点不是 null 就返回其 value,否则返回 null。

resize:扩容数组

重新规划长度和阈值,如果长度发生了变化,部分数据节点也要重新排列。

重新规划长度

① 如果当前容量 oldCap > 0 且达到最大容量,将阈值设为 Integer 最大值,终止扩容。

② 如果未达到最大容量,当 oldCap << 1 不超过最大容量就扩大为 2 倍。

③ 如果都不满足且当前扩容阈值 oldThr > 0,使用当前扩容阈值作为新容量。

④ 否则将新容量置为默认初始容量 16,新扩容阈值置为 12。

重新排列数据节点

① 如果节点为 null 不进行处理。

② 如果节点不为 null 且没有 next 节点,通过节点的 hash 值和 新容量-1 进行与运算计算下标存入新的 table 数组。

③ 如果节点为 TreeNode 类型,调用 split 方法处理,如果节点数 hc 达到 6 会调用 untreeify 方法转回链表。

④ 如果是链表节点,需要将链表拆分为 hash 值超出旧容量的链表和未超出容量的链表。对于hash & oldCap == 0 的部分不需要做处理,否则需要放到新的下标位置上,新下标 = 旧下标 + 旧容量。

线程不安全

JDK7 存在死循环和数据丢失问题。

数据丢失:

并发赋值被覆盖: 在 createEntry 方法中,新添加的元素放在头部,使元素可以被更快访问,但如果两个线程同时执行到此处,会导致数据覆盖。

新表被覆盖: 如果多线程同时 resize ,每个线程都会 new 一个数组,这是线程内的局部对象,线程间不可见。迁移完成后resize 的线程会赋值给 table 线程共享变量,可能会覆盖其他线程的操作,在新表中插入的对象都会被丢弃。

死循环: 扩容时 resize 调用 transfer 使用头插法迁移元素,虽然 newTable 是局部变量,但原先 table 中的 Entry 链表是共享的,问题根源是 Entry 的 next 指针并发修改,某线程还没有将 table 设为 newTable 时用完了 CPU 时间片。

JDK8 在 resize 方法中完成扩容,并改用尾插法,不会产生死循环,但并发下仍可能丢失数据。可用 ConcurrentHashMap 或 Collections.synchronizedMap 包装同步集合。
5.5 拦截器过滤器
1拦截器是基于java的反射机制的,而过滤器是基于函数回调。
  2拦截器不依赖与servlet容器,过滤器依赖与servlet容器。
  3拦截器只能对action请求起作用,而过滤器则可以对几乎所有的请求起作用。
  4拦截器可以访问action上下文、值栈里的对象,而过滤器不能访问。
  5在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。
6拦截器可以获取IOC容器中的各个bean,而过滤器就不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑。
5.6 怎么保证redis与mysql数据一致
延时双删策略
1)先删除缓存

2)再写数据库

3)休眠500毫秒

4)再次删除缓存
确保读请求结束,写请求可以删除读请求造成的缓存脏数据
5.7 声明式事务失效情况
1.@Transactional 应用在非 public 修饰的方法上
2.@Transactional 注解属性 propagation 设置错误
3.@Transactional 注解属性 rollbackFor 设置错误
4.同一个类中方法调用,导致@Transactional失效
5.异常被catch捕获导致@Transactional失效

5.8 CAS 是什么?怎么实现线程安全的?
CAS是乐观锁,线程在读取数据时不进行加锁,在准备写回数据时,先去查询原值,操作的时候比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。
5.9 如何防止CAS中ABA问题?
搞个自增的字段,操作一次就自增加一,或者搞个时间戳,比较时间戳的值。
6.0 悲观锁有哪些呢?
上面提到的CAS是乐观锁的实现,synchronized就是悲观锁了
6.1 synchronized它是如何保证同一时刻只有一个线程可以进入临界区呢?
synchronized,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。
当 Monitor 被某个线程持有后,就会处于锁定状态, Owner 部分,会指向持有 Monitor 对象的线程。
另外 Monitor 中还有两个队列分别是EntryList和WaitList,主要是用来存放进入及等待获取锁的线程。
如果线程进入,则得到当前对象锁,那么别的线程在该类所有对象上的任何操作都不能进行。
6.2 锁的级别
无锁-偏向锁-轻量级锁-重量级锁
1.偏向锁:在锁对象的对象头中记录一下当前获取到该线程的线程id 如果下一次该线程又来获取该锁就可以直接获取到
2.轻量级锁:由偏向级锁升级而来 当一个线程获取到锁后 此时这把锁是偏向锁 此时如果有第二个线程来竞争锁 偏向锁就会升级成轻量级锁 轻量级锁底层是通过自旋来实现的 不会阻塞线程
3.如果自旋次数过多任然没有获取到锁就会升级为重量级锁 重量级锁会导致线程阻塞
4.自旋锁:一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,任何时刻最多只能有一个执行单元获得锁
6.3 ReentrantLock和synchronized使用分析
ReentrantLock是Lock的实现类,是一个互斥的同步器,在多线程高竞争条件下,ReentrantLock比synchronized有更加优异的性能表现。
1 用法比较
Lock使用起来比较灵活,但是必须有释放锁的配合动作
Lock必须手动获取与释放锁,而synchronized不需要手动释放和开启锁
Lock只适用于代码块锁,而synchronized可用于修饰方法、代码块等
2 特性比较
ReentrantLock的优势体现在:
具备尝试非阻塞地获取锁的特性:当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁
能被中断地获取锁的特性:与synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
超时获取锁的特性:在指定的时间范围内获取锁;如果截止时间到了仍然无法获取锁,则返回
ReentrantLock可以实现公平锁
3 注意事项
在使用ReentrantLock类的时,一定要注意三点:
在finally中释放锁,目的是保证在获取锁之后,最终能够被释放
不要将获取锁的过程写在try块内,因为如果在获取锁时发生了异常,异常抛出的同时,也会导致锁无故被释放。
ReentrantLock提供了一个newCondition的方法,以便用户在同一锁的情况下可以根据不同的情况执行等待或唤醒的动作。

你可能感兴趣的:(笔记,java)