1、哪些情况下的对象会被垃圾回收机制处理掉?
利用可达性分析算法, 虚拟机 会将一些对象定义为 GCRoots,从 GCRoots 出发沿着引用链向下寻找,如果某个对象不能通过 GCRoots 寻找到,虚拟机就认为该对象可以被回收掉。
* 哪些对象可以被看做是 GCRoots 呢?
1)虚拟机栈(栈帧中的本地变量表)中引用的对象;
2)方法区中的类静态属性引用的对象,常量引用的对象;
3)本地方法栈中 JNI (Native 方法)引用的对象;
* 对象不可达,一定会被垃圾收集器回收么?
即使不可达,对象也不一定会被垃圾收集器回收,
1)先判断对象是否有必要执行 finalize ()方法,对象必须重写 finalize()方法且没有被运行过。
2)若有必要执行,会把对象放到一个队列中, JVM 会开一个 线程 去回收它们,这是对象最后一次可以逃逸清理的机会。
2、讲一下常见 编码 方式?
编码的意义:计算机中存储的最小单元是一个字节即 8bit,所能表示的字符范围是 255 个, 而人类要表示的符号太多,无法用一个字节来完全表示,固需要将符号编码,将各种语言翻 译成计算机能懂的语言。
* ASCII 码 :总共 128 个,用一个字节的低 7 位表示,0〜31 控制字符如换回车删除等;
32~126 是打印字符,可通过键盘输入并显示出来;
* ISO-8859-1 ,用来扩展 ASCII 编码,256 个字符,涵盖了大多数西欧语言字符。
* GB2312:双字节编码,总编码范围是 A1-A7,A1-A9 是符号区,包含 682 个字符, B0-B7 是 汉字区,包含 6763 个汉字;
* GBK 为了扩展 GB2312,加入了更多的汉字,编码范围是 8140~FEFE,有 23940 个 码位,能 表示 21003 个汉字。
* UTF -16: ISO 试图想创建一个全新的超语言字典,世界上所有语言都可通过这本字典 Unicode 来相互翻译,而 UTF-16 定义了 Unicode 字符在计算机中存取方法,用两个字节来表 示 Unicode 转化格式。不论什么字符都可用两字节表示,即 16bit,固叫UTF-16。
* utf-8 :UTF-16 统一采用两字节表示一个字符,但有些字符只用一个字节就可表示,浪费存储空间,而 UTF-8 采用一种变长技术,每个编码区域有不同的字码长度。 不同类型的 字 符 可 以 由 1~6 个字节组成。
3、utf-8 编码中的中文占几个字节;int 型几个字节?
utf-8 是一种变长编码技术,utf-8 编码中的中文占用的字节不确定,可能 2 个、3 个、4个,int 型占 4 个字节。
4、静态代理和动态代理的区别,什么场景使用?
代理是一种常用的设计模式,目的是:为其他对象提供一个代理以控制对某个对象的访问,将两个类的关系 解耦 。代理类和委托类都要实现相同的接口,因为代理真正调用的是委托类的方法。
区别:
* 静态代理 :由程序员创建或是由特定工具生成,在代码编译时就确定了被代理的类是哪一个是静态代理。静态代理通常只代理一个类;
* 动态代理 :在代码运行期间,运用 反射机制 动态创建生成。动态代理代理的是一个接口下的多个实现类;
实现步骤:
a.实现 InvocationHandler 接口创建自己的调用处理器;
b.给 Proxy 类提供 Classloader 和代理接口类型数组创建动态代理类;
c.利用反射机制得到动态代理类的构造函数;
d.利用动态代理类的构造函数创建动态代理类对象;
使用场景:Retrofit 中直接调用接口的方法; Spring 的 AOP 机制;
5、简述下 Java 的异常体系。
java 中 Throwable 是所有异常和错误的超类,两个直接子类是 Error(错误)和 Exception (异常):
* Error 是程序无法处理的错误,由 JVM 产生和抛出,如 OOM、ThreadDeath 等。这些异常 发生时,JVM 一般会选择终止程序。
* Exception 是程序本身可以处理的异常
1).运行时异常(RuntimeException)也称作未检测的异常(unchecked exception),这表示这种异常不需要编译器来检测。RuntimeException是所有可以在运行时抛出的异常的父类。一个方法除要捕获异常外,如果它执行的时候可能会抛出RuntimeException的子类,那么它就不需要用throw语句来声明抛出的异常。 运行时异常有NullPointerException、IndexOutOfBoundsException 等。
2).非 运 行 时 异 常又叫 受检查异常(checked exception)是编译器在编译时进行校验的,通过throws语句或者try{}cathch{} 语句块来处理检测异常。编译器会分析哪些异常会在执行一个方法或者构造函数的时候抛出 。,这些异常一般是由程序逻辑错误引起 的,应尽可能避免。非运行时异常有IOExceptionSQlException File NotFoundException 以及 由用户自定义的Exception 异常等。
6、谈谈你对解析与分派的认识。
解析指方法在运行前,即编译期间就可知的,有一个确定的版本,运行期间也不会改变。解析是静态的,在类加载的解析阶段就可将符号引用转变成直接引用。
分派可分为静态分派和动态分派,重载属于静态分派,覆盖属于动态分派。静态分派是指在重载时通过参数的静态类型而非实际类型作为判断依据,在编译阶段,编译器可根据参数的静态类型决定使用哪一个重载版本。动态分派则需要根据实际类型来调用相应的方法。
7、修改对象 A 的 equals 方法的签名,那么使用 HashMap 存放这个对象实例的时候,会用哪个 equals 方法?
会调用对象对象的 equals 方法。
“==”如果是基本类型的话就是看他们的数据值是否相等就可以。 如果是引用类型的话,比较的是栈内存局部变量表中指向堆内存中的指针的值是否相等。 “equals”如果对象的equals 方法没有重写的话,equals 方法和“==”是同一种。hashcod 是返回对象实例内存地址的 hash 映射。 理论上所有对象的 hash 映射都是不相同的。
8、Java 中实现 多态 的机制是什么?
多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编译时不确定,在运行期间才确定,一个引用变量到底会指向哪个类的实例。这样就可以不用 修改源程序,就可以让引用变量绑定到各种不同的类实现上。Java 实现多态有三个必要条件:
继承、重定、向上转型,在多态中需要将子类的引用赋值给父类对象,只有这样该引用才能够具备调用父类方法和子类的方法。
9、如何将一个 Java 对象序列化到文件里?
ObjectOutputStream.writeObject()负责将指定的流写入,ObjectInputStream.readObject()
从指 定流读取序列化数据。
//写 入
try {
ObjectOutputStream os = new ObjectOutputStream(new
FileOutputStream ("D:/student.txt"));
os.writeObject(studentlist);
os.close();
} catch (FileNotFoundExceptione) {
e.printStackTrace();
} catch (IOExceptione) {
e.printStackTrace();
}
10、说说你对 Java 反射的理解。
在运行状态中,对任意一个类,都能知道这个类的所有属性和方法,对任意一个对象,都能调用它的任意一个方法和属性。这种能动态获取信息及动态调用对象方法的功能称为 java 语言的反射机制。
反射的作用:开发过程中,经常会遇到某个类的某个成员变量、方法或属性是私有的,或只对系统应用开放,这里就可以利用 java 的反射机制通过反射来获取所需的私有成员或是方法。
* 获取类的 Class 对象实例 Class clz=Class.forName(“com.zhenai.api.Apple”);
* 根据 Class 对象实例获取 Constructor 对 象 Constructor appConstructor = clz.getConstructor();
* 使用 Constructor 对 象 的 newInstance 方 法 获 取 反 射 类 对 象 Object appleObj = appConstructor.newInstance();
* 获取方法的 Method 对象
MethodsetPriceMethod=clz.getMethod(“setPrice”,int.class);
* 利用 invoke 方法调用方法 setPriceMethod.invoke(appleObj,14);
* 通过 getFields()可以获取 Class 类的属性,但无法获取私有属性,而getDeclaredFields()可 以获取到包括私有属性在内的所有属性。带有 Declared 修饰的方法可以反射到私有的方法, 没有 Declared 修饰的只能用来反射公有的方法,其他如 Annotation FieldConstructor 也是如此。
11、说说你对 Java 注解 的理解。
注解是通过@interface 关键字来进行定义的,形式和接口差不多,只是前面多了一个@
public@interfaceTestAnnotation{
}
使用时@TestAnnotation 来引用,要使 注解 能正常工作,还需要使用元注解,它是可以注解到注解上的注解。
元标签有@Retention、@Documented、@Target、@Inherited 和 @Repeatable 五种。
@Retention 说明注解的存活时间,取值有 RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时被丢弃;RetentionPolicy.ClASS 注解只保留到编译进行的时候,并不会被加载到 JVM 中。RetentionPolicy.RUNTIME 可以留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。
@Documented 注解中的元素包含到 javadoc 中去。
@Target 限定注解的应用场景,ElementType.FIELD 给属性进行注解;
ElementType.lOCAl_VARIABlE 可以给局部变量进行注解;ElementType.METHOD 可以给方法进行注解;ElementType.PACKAGE 可以给一个包进行注解 ElementType.TYPE
可以给一个类型进行注解,如类、接口、枚举。
@Inherited 若一个超类被@Inherited 注解过的注解进行注解,它的子类没有被任何注解应用的话,该子类就可继承超类的注解;
注解的作用:
* 提供信息给编译器:编译器可利用注解来探测错误和警告信息
* 编译阶段:软件工具可以利用注解信息来生成代码、 html 文档或做其它相应处理;
* 运行阶段:程序运行时可利用注解提取代码
注解是通过反射获取的,可以通过 Class 对象的 isAnnotationPresent()方法判断它是否应用了某个注解,再通过 getAnnotation()方法获取 Annotation 对象
12、说一下 泛型 原理,并举例说明。
泛型就是将类型变成参数传入,使得可以使用的类型多样化,从而实现解耦。 Java 泛型 是在Java1.5 以后出现的,为保持对以前版本的兼容,使用了擦除的方法实现泛型。擦除是指在一定程度无视类型参数 T,直接从 T 所在的类开始向上 T 的父类去擦除,如调用泛型方法,传入类型参数 T 进入方法内部,若没在声明时做类似publicTmethodName(TextendsFathert){},Java 就进行了向上类型的擦除,直接把参数 t 当做 Object 类来处理,而不是传进去的 T。 即在有泛型的任何类和方法内部,它都无法知道自己的泛型参数,擦除和转型都是在边界上发生,即传进去的参在进入类或方法时被擦除掉,但传出来的时候又被转成了我们设置的 T。在泛型类或方法内,任何涉及到具体类型(即擦除 后的类型的子类)操作都不能进行,如 newT(),或者 T.play()(play 为某子类的方法而不是擦除后的类的方法)。
13、谈谈你对 Java 中 String 的了解。
* String 类 是 final 型,因此String 类不能被继承,它的成员方法也都默认为 final 方法。
String 对象一旦创建就固定不变了,对 String 对象的任何改变都不影响到原对象,相关的任何改变 操作都会生成新的 String 对象。
* String 类是通过 char 数组来保存 字符串 的,String 对 equals 方法进行了重定,比较的是值相等。
String a=”test”;String b=”test”;String c=new String(“test”);
a、b 和字面上的 test 都是指向 JVM 字符串常量池中的”test”对象,他们指向同一个对象。而 new 关键字一定会产生一个对象 test,该 对象存储 在堆中。所以 new String(“test”)产生了两个对象,保存在栈中的 c 和保存在堆中的 test。而在 java 中根本就不存在两个完全一模一样的字符串对象,故在堆中的 test 应该是引用 字符串常量 池中的 test。
例:String str1=”abc”;//栈中开辟一块空间存放引用 str1,str1 指向池中 String 常量”abc”
String str2=”def”;//栈中开辟一块空间存放引用 str2,str2 指向池中 String 常量”def”
String str3=str1+str2;//栈中开辟一块空间存放引用 str3//str1+str2 通过 StringBuilder 的最后一步
toString ()方法返回一个新的 String 对象”abcdef”
//会在堆中开辟一块空间存放此对象,引用 str3 指向堆中的(str1+str2)所返回的新 String 对象。
System.out.println(str3==”abcdef”);//返回 false 因为 str3 指向堆中的”abcdef”对象,而”abcdef”是字符池中的对象,所以结果为 false。JVM 对
String str=”abc”对象放在常量池是在编译时做的 , 而 String str3=str1+str2 是在运行时才知道的,new 对象也是在运行时才做的。
14、String 为什么要设计成不可变的?
* 字符串常量池需要 String 不可变。因为 String 设计成不可变,当创建一个 String 对象
时, 若此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的
对象。 如果字符串变量允许必变,会导致各种逻辑错误,如改变一个对象会影响到另一个
独立对象。
* String 对象可以缓存 hashCode 。字符串的不可变性保证了 hash 码的唯一性,因此可
以缓 存 String 的 hash Code,这样不用每次去重新计算哈希码。在进行字符串比较时,
可以直接比较 hashCode,提高了比较性能;
* 安全性。String 被许多 java 类用来当作参数,如 url 地址,文件 path 路径,反射机
制所 需的 Strign 参数等,若 String 可变,将会引起各种安全隐患。
15、 Redis 常见的几种数据结构说一下?各自的使用场景?
string
介绍:string 数据结构是简单的 key-value 类型。
使用场景: 一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等。
list
介绍:list 即是 链表
使用场景:发布与订阅或者说消息队列、慢查询。
hash
介绍:hash 类似于 JDK 1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。
使用场景:系统中对象数据的存储。
set
介绍:set 类似于 Java 中的 HashSet 。Redis 中的 set 类型是一种无序集合,集合中的元素没有先后顺序。当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 *ist 所不能提供的。可以基于 set 轻易实现交集、并集、 差集 的操作使用场景: 需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景。
sorted set
介绍:和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按
score 进行有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中HashMap 和 TreeSet 的结合体。
使用场景:需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息。
bitmap
介绍:bitmap 存储的是连续的二进制数字(0 和 1),通过 bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte ,所以 bitmap 本身会极大的节省储存空间。
使用场景:适合需要保存状态信息(比如是否签到、是否登录…)并需要进一步对这些信息进行分析的场景。比如用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)。
16、谈一谈 缓存 穿透、缓存击穿和缓存雪崩,以及各自的解决方案?
缓存穿透
* 问题:大量并发查询不存在的 KEY,在缓存和数据库中都不存在,同时给缓存和数据库带来压力。
* 原因:一般而言,缓存穿透有 2 种可能性:业务数据被误删,导致缓存和数据库中都没有数据。恶意进行 ddos 攻击。
* 分析:为什么会多次 透传 呢?不存在 一直为空,需要注意让缓存能够区分 KEY 不存在和查询到一个空值。
* 解决办法:缓存空值的 KEY,这样第一次不存在也会被加载会记录,下次拿到有这个KEY。Bloom 过滤或 RoaingBitmap 判断 KEY 是否存在,如果 布隆过滤器 中没有查到这个数据,就不去数据库中查。在处理请求前增加恶意请求检查,如果检测到是恶意攻击,则拒绝进行服务。完全以缓存为准,使用延迟异步加载的策略(异步线程负责维护缓存的数据,定期或根据条件触发更新),这样就不会触发更新。
缓存击穿
* 问题:某个 KEY 失效的时候,正好有大量并发请求访问这个 KEY。
* 分析:跟穿透其实很像,属于比较偶然的。
* 解决办法:KEY 的更新操作添加全局互斥锁。完全以缓存为准,使用延迟异步加载的策略(异步线程负责维护缓存的数据,定期或根据条件触发更新),这样就不会触发更新。
缓存雪崩
* 问题:当某一时刻发生大规模的缓存失效的情况,导致大量的请求无法获取数据,从而将流量压力传导到数据库上,导致数据库压力过大甚至宕机。
* 原因:一般而言,缓存雪崩有 2 种可能性:大量的数据同一个时间失效:比如业务关系强相关的数据要求同时失效 Redis 宕机
* 分析:一般来说,由于更新策略、或者数据热点、缓存服务宕机等原因,可能会导致缓存数据同一个时间点大规模不可用,或者都更新。所以,需要我们的更新策略要在时间上合适,数据要均匀分享,缓存服务器要多台高可用。
* 解决办法:更新策略在时间上做到比较平均。如果数据需要同一时间失效,可以给这批数据加上一些随机值,使得这批数据不要在同一个时间过期,降低数据库的压力。使用的热 数据尽量分散到不同的机器上。多台机器做主从复制或者多副本,实现高可用。做好主从的部署,当主节点挂掉后,能快速的使用从结点顶上。实现熔断限流机制,对系统进行负载能力控制。对于非核心功能的业务,拒绝其请求,只允许核心功能业务访问数据库获取数据。
服务降价:提供默认返回值,或简单的提示信息。
17、讲下 Kafka 、 RabbitMQ 、RocketMQ 之间的区别是什么?
性能
消息中间件的性能主要衡量吞吐量,Kafka 的吞吐量比 RabbitMQ 要高出 1~2 个数量级
RabbitMQ 的单机 QPS 在万级别,Kafka 的单机 QPS 能够达到百万级别。RocketMQ
单机写入 TPS 单实例约 7 万条/秒,单机部署 3 个 Broker,可以跑到最高 12 万条/秒,
消息大小 10 个字节,Kafka 如果开启幂等、事务等功能,性能也会有所降低。
数据可靠性
Kafka 与 RabbitMQ 都具备多副本机制,数据可靠性较高。RocketMQ 支持异步实时刷盘,
同步刷盘,同步 Replication,异步 Replication。
服务可用性
Kafka 采用集群部署,分区与多副本的设计,使得单节点 宕机 对服务无影响,且支持消息容
量的线性提升。RabbitMQ 支持集群部署,集群节点数量有多种规格。RocketMQ 是分布式
架构,可用性高。
功能
Kafka 与 RabbitMQ 都是比较主流的两款消息中间件,具备消息传递的基本功能,但在一些特殊的功能方面存在差异,RocketMQ 在 阿里集团 内部有大量的应用在使用。
18、Kafka 的架构说一下?
整个架构中包括三个角色。
* 生产者(Producer):消息和数据生产者。
* 代理(Broker):缓存代理,Kafka 的核心功能。
* 消费者(Consumer):消息和数据消费者。
Kafka 给 Producer 和 Consumer 提供注册的接口,数据从 Producer 发送到 Broker,Broker 承担一个中间缓存和分发的作用,负责分发注册到系统中的 Consumer。
19、Kafka 怎么保证消息是有序的?
消息在被追加到 Partition(分区)的时候都会分配一个特定的偏移量(offset)。Kafka 通过偏
移量(offset)来保证消息在分区内的顺序性。发送消息的时候指定 key/Partition。
20、Kafka 怎么保证消息不丢失?
生产者丢失消息的情况
生产者(Producer) 调用 send 方法发送消息之后,消息可能因为网络问题并没有发送过去。
为了确定消息是发送成功,我们要判断消息发送的结果,Kafka 生产者(Producer) 使用
send 方法发送消息实际上是异步的操作,我们可以通过 get()方法获取调用结果,但是这样
也让它变为了同步操作,可以采用为其添加 回调函数 的形式,示例代码如下:
ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send(topic, o);
future.addCallback(result -> logger.info(“生产者成功发送消息到 topic:{} partition:{}的消息”, result.getRecordMetadata().topic(), result.getRecordMetadata().partition()), ex -> logger.error(“生产者发送消失败,原因:{}”, ex.getMessage()));Producer 的 retries(重试次数)设置一个比较合理的值,一般是 3 ,但是为了保证消息不 丢失的话一般会设置比较大一点。设置完成之后,当出现网络问题之后能够自动重试消息发送, 避免消息丢失。另外,建议还要设置重试间隔,因为间隔太小的话重试的效果就不明显了,网络波动一次你 3 次一下子就重试完了,消费者丢失消息的情况当消费者拉取到了分区的某个消息之后,消费者会自动提交了 offset。自动提交的话会有一个问题,试想一下,当消费者刚拿到这个消息准备进行真正消费的时候,突然挂掉了,消息实际上并没有被消费,但是 offset 却被自动提交了。
解决办法也比较粗暴,我们手动关闭自动提交 offset,每次在真正消费完消息之后再自己手动提交 offset 。 但是,细心的朋友一定会发现,这样会带来消息被重新消费的问题。比如你刚刚消费完消息之后,还没提交 offset,结果自己挂掉了,那么这个消息理论上就会被消费两次。
Kafka 弄丢了消息试想一种情况:假如 leader 副本所在的 broker 突然挂掉,那么就要从 follower 副本重新选出一个 leader ,但是 leader 的数据还有一些没有被 follower 副本的同步的话,就会造成消息丢失。
当我们配置了 unclean.leader.election.enable = false 的话,当 leader 副本发生故障时就不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader ,这样降低了消息丢失的可能性。
21、Kafka 怎么解决重复消费?
* 生产者发送每条数据的时候,里面加一个全局唯一的 id,消费到了之后,先根据这个 id 去比如 Redis 里查一下,之前消费过吗,如果没有消费过,就处理,然后这个 id 写Redis。如果消费过就别处理了。
* 基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据。
22、介绍下 MySQL 聚簇索引与非聚簇索引的区别(InnoDB 与 Myisam 引擎)?
聚集索引是指数据库表行中数据的物理顺序与键值的逻辑(索引)顺序相同。一个表只能有一个聚簇索引,因为一个表的物理顺序只有一种情况,所以,对应的聚簇索引只能有一个。 聚簇索引的叶子节点就是数据节点,既存储索引值,又在叶子节点存储行数据。
Innodb 创建表后生成的文件有:
frm:创建表的语句
idb:表里面的数据+索引文件
非聚集索引(MyISAM 引擎的底层实现)的逻辑顺序与磁盘上行的物理存储顺序不同。非聚簇 索引的叶子节点仍然是索引节点,只不过有指向对应数据块的指针。索引命中后,需要回表查询。
Myisam 创建表后生成的文件有:
frm:创建表的语句 MYD:表里面的数据文件(myisam data)
MYI:表里面的索引文件(myisam index)
innodb 的次索引指向对主键的引用 (聚簇索引)
myisam 的次索引和主索引都指向物理行 (非聚簇索引)
23、然后给一个联合索引(a,b)和一个语句,select * from table where b = ‘xxx’,
判断是否能命中索引?为什么?
不能命中。
对于查询 SElECT * FROM TABLE WHERE a=xxx and b=xxx,显然是可以使用(a,b)这个联合索引的。
对于单个的 a 列查询 SELECT * FROM TABLEWHERE a=xxx,也可以使用这个(a,b)
索引。
但对于 b 列的查询 SELECT * FROM TABLE WHERE b=xxx,则不可以使用这棵 B+树索引。
在 innoDb 数据引擎中,可以发现叶子节点上的 b 值为 1、2、1、4、1、2,显然不是排序的,因此对于 b 列的查询使用不到(a,b)的索引
24、Java 多线程有哪几种实现方式?
* 通过继承 Thread 类创建线程类
* 实现 Runnable 接口创建线程类
* 通过 Callable 和 Future 接口创建线程
25、用过 ConcurrentHashMap,讲一下他和 HashTable 的不同之处?
* HashTable 就是实现了 HashMap 加上了 synchronized,而 ConcurrentHashMap
底层采用分段的数组+链表实现,线程安全
* ConcurrentHashMap 通过把整个 Map 分为 N 个 Segment,可以提供相同的线程安
全,但是效率提升 N 倍,默认提升 16 倍。
* 并且读操作不加锁,由于 HashEntry 的 value 变量是 volatile 的,也能保证读取到最新的值。
* Hashtable 的 synchronized 是针对整张 Hash 表的,即每次锁住整张表让线程独占,ConcurrentHashMap 允许多个修改操作并发进行,其关键在于使用了锁分离技术
* 扩容:段内扩容(段内元素超过该段对应 Entry 数组长度的 75%触发扩容,不会对整个Map 进行扩容),插入前检测需不需要扩容,有效避免无效扩容26、Java 怎么实现线程安全?
* 使用同步代码块
* 使用同步方法
* 使用 lock 锁机制, 通过创建 lock 对象,采用 lock()加锁,unlock()解锁,来保护指定的代码块
27、描述 Threadlocal(线程本地变量)的底层实现原理及常用场景。
实现原理:
* 每个 Thread 线程内部都有一个 ThreadlocalMap;以线程作为 key,泛型作为 value,可以理解为线程级别的缓存。每一个线程都会获得一个单独的 map。
* 提供了 set 和 get 等访问方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此 get 方法总是返回由当前执行线程在调用 set 时设置的最新值。
应用场景:
* JDBC 连接
* Session 管理
* Spring 事务管理
* 调用链,参数传递
* AOP
Threadlocal 是一个解决线程并发问题的一个类,用于创建线程的本地变量,我们知道一个对象的所有线程会共享它的全局变量,所以这些变量不是线程安全的,我们可以使用同步技术。但是当我们不想使用同步的时候,我们可以选择 Threadlocal 变量。例如,由于 JDBC 的连接对象不是线程安全的,因此,当多线程应用程序在没有协同的情况下,使用全局变量时,就不是线程安全的。通过将 JDBC 的连接对象保存到 Threadlocal 中,每 个线程都会拥有属于自己的连接对象副本。
28、介绍下 Spring Bean 都有哪些作用域 ?
* 单例 singleton : bean 在每个 Spring IOC 容器中只有一个实例。
* 原型 prototype:一个 bean 的定义可以有多个实例。
* request:每次 http 请求都会创建一个 bean。
* session:在一个 HTTP Session 中,一个 bean 定义对应一个实例。
* globalsession
* application
29、注解 @Autowired 和 @Resource 有什么区别?
* Resource 是 JDK 提供的,而 Autowired 是 Spring 提供的
* Resource 不允许找不到 bean 的情况,而 Autowired 允许(@Autowired(required =false))
* 指定 name 的方式不一样,@Resource(name =”baseDao”),@Autowired()@Qualifier(“baseDao”)
Resource 默认通过 name 查找,而 Autowired 默认通过 type 查找
(1)@Autowired 与@Resource 都可以用来装配 bean,都可以写在字段或 setter 方法上
(2)@Autowired 默认按类型装配,默认情况下必须要求依赖对象存在,如果要允许 null 值,可以设置它的 required 属性为 false。如果想使用名称装配可以结合@Qualifier 注解进行使用。
(3)@Resource,默认按照名称进行装配,名称可以通过 name 属性进行指定,如果没有指定 name 属性,当注解写在字段上时,默认取字段名进行名称查找。如果注解写在 setter 方法上默认取属性名进行装配。当找不到与名称匹配的 bean 时才按照类型进行装配。但是需要注意的是,如果 name 属性一旦指定,就只会按照名称进行装配。
30、RPC 的实现基础?
* 需要有非常高效的网络通信,比如一般选择 Netty 作为网络通信框架;
* 需要有比较高效的序列化框架,比如谷歌的 Protobuf 序列化框架;
* 可靠的寻址方式(主要是提供服务的发现),比如可以使用 Zookeeper 来注册服务等等;
* 如果是带会话(状态)的 RPC 调用,还需要有会话和状态保持的功能;
31、CMS,G1 垃圾回收器中的三色标记了解吗?
三色标记算法思想
三色标记法是一种垃圾回收法,它可以让 JVM 不发生或仅短时间发生 STW(Stop The World),从而达到清除 JVM 内存垃圾的目的。
三色标记法将对象的颜色分为了黑、灰、白,三种颜色。
黑色:该对象已经被标记过了,且该对象下的属性也全部都被标记过了。(程序所需要的对象);
灰色:对象已经被垃圾收集器扫描过了,但是对象中还存在没有扫描的引用(GC 需要从此对象中去寻找垃圾);
白色:表示对象没有被垃圾收集器访问过,即表示不可达。
CMS 解决办法:增量更新
在应对漏标问题时,CMS 使用了增量更新(Increment Update)方法来做,在一个未被标记的对象(白色对象)被重新引用后,引用它的对象若为黑色则要变成灰色,在下次二次标记时让 GC 线程继续标记它的属性对象(但还是存在漏标的问题)。
CMS 另两个致命缺陷
CMS 采用了 Mark-Sweep 算法,最后会产生许多内存碎片,当到一定数量时,CMS 无法清理这些碎片了,CMS 会让 Serial Old 垃圾处理器来清理这些垃圾碎片,而 Serial Old 垃圾处理器是单线程操作进行清理垃圾的,效率很低。所以使用 CMS 就会出现一种情况,硬件升级了,却越来越卡顿,其原因就是因为进行Serial Old GC 时,效率过低。
解决方案:使用 Mark-Sweep-Compact 算法,减少垃圾碎片
调优参数(配套使用):
-XX:+UseCMSCompactAtFullCollection 开启 CMS 的压缩
-XX:CMSFullGCsBeforeCompaction 默认为 0,指经过多少次 CMS FullGC 才进行压缩
当 JVM 认为内存不够,再使用 CMS 进行并发清理内存可能会发生 OOM 的问题,而不得不进行 Serial Old GC,Serial Old 是单线程垃圾回收,效率低
解决方案:降低触发 CMS GC 的阈值,让浮动垃圾不那么容易占满老年代
调优参数:
-XX:CMSInitiatingOccupancyFraction 92% 可以降低这个值,让老年代占用率达到该值就进行 CMS GC
G1 解决办法:SATB
SATB(Snapshot At The Beginning), 在应对漏标问题时,G1 使用了 SATB 方法来做,具体
流程:
* 在开始标记的时候生成一个快照图标记存活对象
* 在一个引用断开后,要将此引用推到 GC 的堆栈里,保证白色对象(垃圾)还能被 GC线程扫描到(在**write barrier(写屏障)**里把所有旧的引用所指向的对象都变成非白的)
* 配合 Rset,去扫描哪些 Region 引用到当前的白色对象,若没有引用到当前对象,则回收
G1 会不会进行 Full GC?
会,当内存满了的时候就会进行 Full GC;且 JDK10 之前的 Full GC,为单线程的,所以使用 G1 需要避免 Full GC 的产生。
解决方案:
* 加大内存;
* 提高 CPU 性能,加快 GC 回收速度,而对象增加速度赶不上回收速度,则 Full GC 可以避免;
* 降低进行 Mixed GC 触发的阈值,让 Mixed GC 提早发生(默认 45%)