JAVA基础
JDK 和 JRE 有什么区别?
- JDK:Java Development Kit 的简称,java 开发工具包,提供了 java 的开发环境和运行环境。
- JRE:Java Runtime Environment 的简称,java 运行环境,为 java 的运行提供了所需环境。
- JDK=JRE+JVM
- 具体来说 JDK 其实包含了 JRE,同时还包含了编译 java 源码的编译器 javac,还包含了很多 java 程序调试和分析的工具。简单来说:如果你需要运行 java 程序,只需安装 JRE 就可以了,如果你需要编写 java 程序,需要安装 JDK。
Java中有哪8种基本类型? 他们的默认值和占用空间大小知道吗?说说这8中基本数据类型对应的包装类型?
| 类型 | 默认值 | 占用空间大小字节 | 包装类型 | 取值范围 | |
|---|---|---|---|---|---|
| byte | 0 | 1 | Byte | -128-127 | |
| short | 0 | 2 | Short | -2^15~2^15-1 | |
| int | 0 | 4 | Interger | ||
| long | 0l | 8 | Long | ||
| char | u0000 | 2 | Character | 0-65535 | |
| float | 0f | 4 | Float | ||
| double | 0d | 8 | Doule | ||
| boolean | false | 1 | Boolean | true false |
包装类的常量池技术?或者缓存?
Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 TRUE or FALSE。
对于 Integer,可以通过 JVM 参数 -XX:AutoBoxCacheMax=<size> 修改缓存上限,但不能修改下限 -128。实际使用时,并不建议设置过大的值,避免浪费内存,甚至是 OOM。
什么是自动拆箱装箱?
- 装箱:将基本数据类型用他们的包装类包装起来
- 原理:用了包装类的valueOf()
Integer i =1 等价于 Integer i = Integer.valueOf(1);
- 拆箱:将包装类中的值变为基本类型。
- 原理:用了包装类的xxxValue()
int n =10 等价于 int n = i.intValue()
如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。
- 自动拆装箱可能引发的NPE问题:
- 数据库查询结果可能为null,因为自动拆箱,用基本数据类型接收有 NPE 风险;
- 三目运算符使用不当会导致诡异的NPE异常
== 和 equals 的区别是什么?
== 对于基本类型来说是值比较,对于引用类型来说是比较的是引用; equals 默认情况下是引用比较,只是很多类重写了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等。 equals的java.lang.Object里的方法,默认没有被重写,作用与==相当。
hashCode?
public int hashCode():返回每个对象的hash值。
如果重写equals,那么通常会一起重写hashCode()方法,hashCode()方法主要是为了当对象存储到哈希表(后面集合章节学习)等容器中时提高存储和查询性能用的,这是因为关于hashCode有两个常规协定:
- ①如果两个对象的hash值是不同的,那么这两个对象一定不相等;
- ②如果两个对象的hash值是相同的,那么这两个对象不一定相等。
重写equals和hashCode方法时,要保证满足如下要求:
- ①如果两个对象调用equals返回true,那么要求这两个对象的hashCode值一定是相等的;
- ②如果两个对象的hashCode值不同的,那么要求这个两个对象调用equals方法一定是false;
- ③如果两个对象的hashCode值相同的,那么这个两个对象调用equals可能是true,也可能是false
public static void main(String[] args) {
System.out.println("Aa".hashCode());//2112
System.out.println("BB".hashCode());//2112
}
final 在 java 中有什么作用?
- final 修饰的类叫最终类,该类不能被继承。
- final 修饰的方法不能被重写。
- final 修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改。
String、StringBuffer、StringBuilder的区别, String不可变的原因?
- String是final修饰的,不可变,每次操作都会产生新的String对象
- StringBuffer和StringBuilder都是在原对象上操作
- StringBuffer是线程安全的, StringBuilder线程不安全, 所以StringBuilder的速度要比StringBuffer快
- StringBuffer方法都是synchronized修饰的
- 性能: StringBuilder > StringBuffer > String
- 场景: 如果要操作少量的数据用 String, 多线程操作字符串缓冲区下操作大量数据用 StringBuffer; 单线程操作字符串缓冲区大量数据 StringBuilder.
- String中的数组被final修饰且为私有,并且没有暴漏修改的方法
- String类被final修饰,避免了被子类修改
重载和重写的区别
- 重载:发生在同一个类中, 方法名必须相同,参数类型、个数、顺序不同,方法返回值和方法修饰符可以不同, 发生在编译时。
- 重写: 发生在父子类中。 方法名、参数列表必须相同。返回值范围小于等于父类, 抛出的异常范围小于等于父类,访问修饰符大于等于父类;如果父类方法访问修饰符为private则子类就不能重写该方法了。
- 重载:方法名相同,参数列表不同,其它不要求
- 重写:两同两小一大 - 两同:方法名相同、形参列表相同 - 两小: - 子类方法返回值类型小于等于父类方法返回值类型 - 子类方法throws异常类型小于等于父类方法返回值类型 - 一大:子类方法权限修饰符大于等于父类方法权限修饰符
内部类? 匿名内部类?
将一个类A定义在另一个类B里面,里面的那个类A就称为内部类(InnerClass) ,类B则称为外部类(OuterClass) 。
(1)成员内部类:
- 静态成员内部类
- 非静态成员内部类
(2)局部内部类
- 有名字的局部内部类
- 匿名的内部类
接口和抽象类的区别:
接口(Interface)
- 接口是一种特殊的抽象类,只包含抽象方法和常量
- 不能实例化: 接口不能创建对象实例
- 没有构造方法: 接口中不能包含构造方法
- 方法默认为公开抽象: 接口中的默认为
public abstract, 不需要显示申明- 变量默认为
public static final, 必须初始化且不能更改。- 一个类可以实现多个接口,接口支持多重继承
抽象类(abstract class)
- 抽象类是使用
abstract关键字定义的类,可以包含抽象方法和非抽象方法。- 抽象类不能直接创建对象实例,但可以通过子类实例化。
- 抽象类中可以有构造方法,主要用与子类调用
- 抽象类总不仅可以包含抽象方法,还可以包含普通方法。
- 一个类只能继承一个抽象类。
区别
- 抽象类可以存在普通成员函数,而接口在java7之前所有方法都是抽象的,java8之后可以包含非抽象方法
- 抽象类中的方法可以是任意修饰符,接口中java8之前都是public, java9 支持private
- 接口中的变量默认为
public static final的,抽象类的不是,可以是各种类型的。- 抽象类只能继承一个,接口可以实现多个。
背记:
-
关键字
- 抽象类:使用abstract关键字定义
- 接口:使用interface关键字定义
-
设计
- 抽象类:代表对事物通用特性的抽象,可以包含属性和方法
- 接口:主要关注事物的行为,所以它是方法的集合
-
构造器和成员变量
- 抽象类:有构造器,有普通成员变量
- 接口:没有构造器和普通成员变量,其内部变量默认为public static final类型
-
方法实现
- 抽象类:可以包含抽象方法和非抽象方法
- 接口:只能声明抽象方法,但在Java 8起可以有默认方法和静态方法
-
继承与实现
- 一个类只能继承一个抽象类,但可以实现多个接口,这是Java支持多态性的重要方式
-
访问修饰符
- 抽象类:抽象方法可以有修饰符
- 接口:抽象方法只能是public类型
-
实例化
- 抽象类和接口因为包含抽象方法,所以都不能直接用于创建对象,实例化之前都必须给出抽象方法的具体实现
- 通过匿名内部类方式创建对象时,其实抽象方法在匿名内部类中实现了
浅拷贝和深拷贝的区别
- 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点), 基础数据类型复制值,引用类型复制引用地址,修改一个对象的值,另一个对象的值也随之变化。实现
Cloneable接口,并重写clone方法。- 深拷贝:基础数据类型复制值,引用类型在新的内存空间复制值,新老对象不共享内存,修改一个值,不影响另一个。
那什么是引用拷贝呢? 简单来说,引用拷贝就是两个不同的引用指向同一个对象
String#itern方法的作用
itern()方法的主要作用确保字符串引用在常量池的唯一性。- 当调用
itern()时,如果常量池中已经存在相同内容的字符串,则返回常量池中已有对象的引用;否则,将该字符串添加到常量池并返回其引用。
Java异常体系
- Java中所有异常都来自顶级父类Throwable.
- Throwable下有两个子类Exception 和 Error
- Error是程序无法处理的错误,一旦出现这个错误,则程序将被迫停止运行。比如Java虚拟机运行错误(
Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等。- Exception不会导致程序停止,又分为两个部分。RuntimeException运行时异常和CheckedException受检异常
- RuntimeException发生在程序运行过程时,会导致程序当前线程执行失败。CheckedException发生在程序编译过程中,会导致程序编译不通过。
RuntimeException,及其子类都统称为非受检查异常:
- ①
NullPointerException(空指针异常)- ②
IllegalArgumentException(参数错误比如方法入参类型错误)- ③
NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)- ④
ArrayIndexOutOfBoundsException(数组越界异常)- ⑤
ClassCastException(类型转换错误)- ⑥
ArithmeticException(算术异常)- ⑦
SecurityException(安全异常比如权限不够)- ⑧
UnsupportedOperationException(不支持的操作异常比如重新创建同一个用户)
什么是反射
- 反射主要时指 可以获取任意一个类的属性和方法,并调用或者修改这些属性和方法。
- 优缺点:使代码变得更加灵活,为框架的开箱即用提供便利; 但是可以操作任意一个类,会有安全问题,并且性能较差,不过对于框架来说,不是很重要。
- 对反射的理解: 日常开发中应用的比较少,但是框架中例如spring\springboot\mybatis中经常用到反射,比如说AOP, 大量框架应用了动态代理,动态代理也用了反射。
java反射
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时判断任意一个类所具有的成员变量和方法
- 在运行时获取泛型信息
- 在运行时调用任意一个对象的成员变量和方法
- 在运行时处理注解
- 生成动态代理
获取Class类的实例的方法
方式1:要求编译期间已知类型, 若已知具体的类,通过类的class属性获取,该防范最为安全可靠,程序性能最高。
Class clazz = String.class
方式2:可以获取对象运行时类型, 已知某个类的实例,调用该实例的getClass()方法获取Class对象。
Class clazz = "xxx".getClass();
方式3:可以获取编译期间未知的类型,已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法forName()获取,可能抛出ClassNotFoundException
Class clazz = Class.forName("java.lang.String");
方式4:可以用系统类加载对象或自定义加载器对象加载指定路径下的类型
ClassLoader c1 = this.getClass().getClassLoader();
class clazz = c1.loadClass("类的全类名");
什么是Java中的动态代理?JDK 动态代理和 CGLIB 动态代理有什么区别?
- 动态代理是Java中的一种设计模式,它允许在运行时创建代理对象,并在不修改原始类的情况下增强其功能。动态代理主要用于拦截方法调用,进行日志记录、性能监控、事务管理等操作
- 动态代理的两种实现方法: 基于接口的动态代理(JDK动态代理)、基于类的动态代理(CGLIB动态代理)
基于接口的动态代理
- JDK自带的动态代理主要通过
java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口实现。它只能代理实现接口的类。
基于类的动态代理
- cgLIB(Code Generation Library) 是一个强大的字节码生成库,允许创建基于类的代理,而无需实现接口。 CGLIB通过继承目标类并重写其方法来实现代理。
动态代理的实际应用
- AOP(面向切面编程)
- 远程方法调用(RMI)
- Mock单元测试。
什么是注解?以及解决了什么问题
- Annotation注解,用于修饰类、方法、变量,提供某些信息在程序编译或运行时使用。
- 常见的解析方法:
- 编译器直接扫描: 在编译期间,扫描到注解并进行处理,比如@Override, 编译器期间扫描到,编译器就会检测是否重写了父类方法。
- 运行期间反射处理: 像Spring框架中的@value @Component,都是运行期间扫描到进行反射处理的。
Java泛型? 有什么作用? 有哪些限制? 什么是类型擦除? 常用通配符?
- 在定义类或接口时,通过一个标识,来指定类中方法的参数类型或返回值类型或者属性类型;
- 一种标识,使用泛型,可以增强代码稳定性和可读性;
- 比如:集合方面:使用泛型,增强了安全性,获取元素时,可以不用强转类型
- 泛型擦除机制:Java的泛型是伪泛型,在编译期间,会将泛型信息都擦除。 为了增加泛型机制不引入新类型,不增加虚机运行的负担,所以编译器通过擦除,将泛型类变为一般类。
- 泛型的限制:由泛型擦除引起的,擦除为Object之后无法判断类型
- 只能声明,不能实例化
- 泛型类型不能为基础类型,因为基础类型的父类不是Object, 可以使用其包装类
- 不能使用static修饰泛型
- 泛型无法通过instanceof和getClass进行类型判断
- 无法实现两个不同泛型的同一接口,擦除之后多个父类的桥方法会冲突
- 通配符:提供类型参数的变化,解决泛型无法协调的问题
- 上界通配符:
List<? extends person>传入的实参必须是指定类型的子类 - 下界通配符:
List<? super persion>传入的实参必须是指定类型的父类
- 上界通配符:
SPI是什么? 有什么用? SPI和API有什么区别?
-
SPI: Service Provider Interface, 服务提供者结构,专门提供给服务提供者调用的接口
-
SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。
-
很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。
-
通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如:
- 需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。
- 当多个
ServiceLoader同时load时,会有并发问题
-
API: 当实现方提供接口和实现,就可以调用实现方接口从而拥有实现方提供的能力,接口和实现都放在实现方,调用方调用实现方的接口,不需要关注具体实现细节
-
SPI:当接口在调用方这边就是SPI, 这个接口在调用方这边提供,然后由不同的厂商实现,提供服务
SPI的实现原理?
- SPI依赖ServiceLoader来实现的,以下是流程
- 通过 URL 工具类从 jar 包的
/META-INF/services目录下面找到对应的文件, - 读取这个文件的名称找到对应的 spi 接口,
- 通过
InputStream流将文件里面的具体实现类的全类名读取出来, - 根据获取到的全类名,先判断跟 spi 接口是否为同一类型,如果是的,那么就通过反射的机制构造对应的实例对象,
- 将构造出来的实例对象添加到
Providers的列表中
- 通过 URL 工具类从 jar 包的
package edu.jiangxuan.up.service;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
public class MyServiceLoader<S> {
// 对应的接口 Class 模板
private final Class<S> service;
// 对应实现类的 可以有多个,用 List 进行封装
private final List<S> providers = new ArrayList<>();
// 类加载器
private final ClassLoader classLoader;
// 暴露给外部使用的方法,通过调用这个方法可以开始加载自己定制的实现流程。
public static <S> MyServiceLoader<S> load(Class<S> service) {
return new MyServiceLoader<>(service);
}
// 构造方法私有化
private MyServiceLoader(Class<S> service) {
this.service = service;
this.classLoader = Thread.currentThread().getContextClassLoader();
doLoad();
}
// 关键方法,加载具体实现类的逻辑
private void doLoad() {
try {
// 读取所有 jar 包里面 META-INF/services 包下面的文件,这个文件名就是接口名,然后文件里面的内容就是具体的实现类的路径加全类名
Enumeration<URL> urls = classLoader.getResources("META-INF/services/" + service.getName());
// 挨个遍历取到的文件
while (urls.hasMoreElements()) {
// 取出当前的文件
URL url = urls.nextElement();
System.out.println("File = " + url.getPath());
// 建立链接
URLConnection urlConnection = url.openConnection();
urlConnection.setUseCaches(false);
// 获取文件输入流
InputStream inputStream = urlConnection.getInputStream();
// 从文件输入流获取缓存
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
// 从文件内容里面得到实现类的全类名
String className = bufferedReader.readLine();
while (className != null) {
// 通过反射拿到实现类的实例
Class<?> clazz = Class.forName(className, false, classLoader);
// 如果声明的接口跟这个具体的实现类是属于同一类型,(可以理解为Java的一种多态,接口跟实现类、父类和子类等等这种关系。)则构造实例
if (service.isAssignableFrom(clazz)) {
Constructor<? extends S> constructor = (Constructor<? extends S>) clazz.getConstructor();
S instance = constructor.newInstance();
// 把当前构造的实例对象添加到 Provider的列表里面
providers.add(instance);
}
// 继续读取下一行的实现类,可以有多个实现类,只需要换行就可以了。
className = bufferedReader.readLine();
}
}
} catch (Exception e) {
System.out.println("读取文件异常。。。");
}
}
// 返回spi接口对应的具体实现类列表
public List<S> getProviders() {
return providers;
}
}
IO流为什么要使用字节流和字符流?
- 不管是网络发送还是文件读写,信息最小存储单元都是字节,可能的两个原因:
- java字符流是虚机运行将字节转换出来的,还是比较耗时的;
- 如果不知道编码类型,在使用字节流过程中会导致乱码问题
IO流使用到的设计模式?
-
装饰器模式:Decorator,可以在不改变原有对象的基础上扩展原功能。 通过组合替代继承来扩展原始类的功能,在一些继承比较复杂的场景用比较适合。 装饰器模式很重要的一个特征,那就是可以对原始类嵌套使用多个装饰器。装饰器类需要跟原始类继承相同的抽象类或者实现相同的接口。
- 对于字节流来说,FilterInputStream(输入流)和FilterOutputStream(输出流)是装饰器模式的核心,分别用于增强InputStream和OutputStream子类对象的功能。
- 常见的
BufferedInputStream(字节缓冲输入流)、DataInputStream等等都是FilterInputStream的子类,BufferedOutputStream(字节缓冲输出流)、DataOutputStream等等都是FilterOutputStream的子类。
-
适配器模式:Adapter Pattern, 主要用于协调接口互不兼容的情况,类似于生活的电源适配器。被适配的对象称为适配者,而作用于适配者的对象或类称为适配器。适配器分为对象适配器和类适配器,类适配器基于继承实现,而对象适配器基于组合关系来实现。
- InputStreamReader 和 OutputStreamWriter 是字节流和字符流之间的桥梁和对象适配器,可以将字节流适配为字符流对象。
- InputStreamReader 使用 StreamDeCoder(流解码器) 对字节解码; OutputStreamWirter 使用 StreamEncoder(流编码器) 对字符编码, 可以通过字节流读取或写入字符数据了
- 其中InputStream和OutputStrema是适配者,而InputStreamReader 和 outputStreamWriter是适配器。
- 装饰器模式和适配器模式的区别:
- 装饰器默认:动态的增强原始类的功能,装饰器和原始类继承相同的抽象类或者实现相同的接口,装饰器支持对原始类嵌套多个装饰器类
- 适配器模式:更侧重协调不兼容的接口或不能交互的类一起工作。内部通过调用适配者方法和适配类方法
- 其中FutureTask中就是使用了适配器,Excutors 可以调用Runnable返回Callable,就是其内部类RunnableAdapter适配器对Callable进行了适配
// 实际调用的是 Executors 的内部类 RunnableAdapter 的构造方法
public static <T> Callable<T> callable(Runnable task, T result) {
if (task == null)
throw new NullPointerException();
return new RunnableAdapter<T>(task, result);
}
// 适配器
static final class RunnableAdapter<T> implements Callable<T> {
final Runnable task;
final T result;
RunnableAdapter(Runnable task, T result) {
this.task = task;
this.result = result;
}
public T call() {
task.run();
return result;
}
}
-
工厂模式
- 工厂模式主要是用于创建对象,其中NIO使用了大量的工厂模式,
- new InputStream 创建 inputStream 对象,属于静态工厂
- Paths类通过get方法创建Path对象,属于静态工厂
- 工厂模式主要是用于创建对象,其中NIO使用了大量的工厂模式,
-
观察者模式
- NIO中 文件目录监听服务中使用到了观察者模式, 基于 WatchService是观察者, Watchable 是被观察者。
- Watchable的resgiter(): 将对象注册到WatchService,并绑定监听事件
- WatchService 是监控服务,用于监听事件变化,一个监听服务可以监听多个文件目录
- WatchService 内部通过daemon Thread(守护线程),定时轮询查询文件变化
class PollingWatchService
extends AbstractWatchService
{
// 定义一个 daemon thread(守护线程)轮询检测文件变化
private final ScheduledExecutorService scheduledExecutor;
PollingWatchService() {
scheduledExecutor = Executors
.newSingleThreadScheduledExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
}});
}
void enable(Set<? extends WatchEvent.Kind<?>> events, long period) {
synchronized (this) {
// 更新监听事件
this.events = events;
// 开启定期轮询
Runnable thunk = new Runnable() { public void run() { poll(); }};
this.poller = scheduledExecutor
.scheduleAtFixedRate(thunk, period, period, TimeUnit.SECONDS);
}
}
}
BIO NIO AIO 之间有什么区别?
- 一个地址空间分为:用户空间 和 内核空间; 当应用程序发起IO请求时,由操作系统执行内核的具体IO操作。也是就应用程序只是IO的发起者,执行者是操作系统内核。应用程序发送IO操作请求后,会有两个步骤 ① 内核等待IO设备准备好 ②将数据从内核空间拷贝到用户空间。
- BIO: Blocking IO,同步阻塞IO, 应用发起read调用请求后,会一直阻塞,直到内核把数据拷贝到用户空间。
- NIO: Non-Blocking IO, 可以看作IO多路复用,分为三个主要模块 Buffer Channel Selector。
- 不是同步非阻塞I/O, 因为同步非阻塞I/O会一直轮询发起read请求,直到内核将数据拷贝到用户空间,内核将数据拷贝的用户空间还是阻塞的,虽然不会一直阻塞线程,但是一直轮询会消耗大量的CPU资源。
- 而I/O多路复用模型就不同了。线程首先向内核发送select/poll/epoll,查询内核是否准备好数据。等待内核准备好数据,线程再发起read请求,由内核将数据拷贝到用户空间,这个过程是阻塞的。
- I/O多路复用通过减少无用的系统调用,减少了对CPU资源的消耗。
- NIO中有个概念为Selector,被称为 多路复用器,只要一个线程就可管理多个客户端连接。只有客户端数据到了,才会为其服务
- AIO: Ayns IO, 异步IO模型,是基于事件和回调机制实现的,应用发起请求后会直接返回,不会一直阻塞,等后台处理完后,操作系统会通知线程进行后续操作
Java魔法类Unsafe
- Unsafe类是
sun.misc中, 提供执行低级别、不安全操作的方法,如直接访问操作系统的内存资源、和管理内存资源等。为提升Java运行效率和增强Java底层操作资源能力。
Unsafe类提供的功能
- 内存操作:Java不允许直接对内存进行操作,对象的内存和分配都是由JVM自己实现
//分配新的本地空间
public native long allocateMemory(long bytes);
//重新调整内存空间的大小
public native long reallocateMemory(long address, long bytes);
//将内存设置为指定值
public native void setMemory(Object o, long offset, long bytes, byte value);
//内存拷贝
public native void copyMemory(Object srcBase, long srcOffset,Object destBase, long destOffset,long bytes);
//清除内存
public native void freeMemory(long address);
- 内存屏障:通过屏障两边的指令重排序从而避免编译器和硬件的不正确优化。
//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
public native void loadFence();
//内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
public native void storeFence();
//内存屏障,禁止load、store操作重排序
public native void fullFence();
- 对象操作:
- 数据操作
- CAS操作
- 线程操作
//取消阻塞线程
public native void unpark(Object thread);
//阻塞线程
public native void park(boolean isAbsolute, long time);
//获得对象锁(可重入锁)
@Deprecated
public native void monitorEnter(Object o);
//释放对象锁
@Deprecated
public native void monitorExit(Object o);
//尝试获取对象锁
@Deprecated
public native boolean tryMonitorEnter(Object o);
-Class操作 -系统信息:
什么是多态?
- 多态性:在Java中的体现,父类的引用指向子类的实现
- 格式: (父类类型: 指子类继承父类类型,或者实现的接口类型) 父类类型 变量名 = 子类对象;
- Java引用变量有两个类型:编译时类型和运行时类型。编译时类型由 声明 该变量时使用的类型决定,运行时类型由 实际赋给该变量的对象决定。 简称编译时看左边,运行时看右边
- 多态的使用前提:①类的继承关系 ②方法的重写。
好处和弊端
- 好处:变量引用子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大,可维护性和扩展性更好
- 弊端:一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么变量就不能再访问子类中独有的属性和方法。
序列化和反序列化
-
序列化:将数据结构或对象转换为可以存储或传输的形式,通常是二进制字节流,可以是JSON\XML等文本格式
-
反序列化:将序列化过程中生成的数据转换为原始数据结构或对象的过程。
-
常见应用场景:主要目的是网络传输或将文件存储到数据库、内存、文件系统中。
- 对象进行网络传输(比如远程方法调用rpc的时候),需要先被序列化,接收到序列化对象之后,在进行反序列化。
- 将对象存储到文件之前,需要进行序列化,将对象文件读取出来需要进行反序列化
- 将对象存储到数据库之前(如redis),需要用户到序列化,将对象从缓存数据库中读取出来需要反序列化。
- 将对象存储到内存之前,需要进行序列化,从内存读取出来之后需要进行反序列化。
-
如果有些字段不想进行序列化,可以使用
transient关键字修饰transient只能修饰变量,不能修饰类和方法transient修饰的变量,在反序列化后变量值将会被置为类型的默认值。static变量因为不属于任何对象(Object),无论有没有transient修饰,都不会被序列化。
-
常见的序列化协议:
- JDK自带的序列化方式一般不会用,因为序列化效率低并且存在安全问题
- 不支持跨语言调用:如果是其他语言开发的服务就不支持了
- 性能差:相比其他框架的序列化性能更低,主要是序列化后的字节数组体积较大,导致传输成本增加。
- 存在安全问题:序列化和反序列化本身没有问题,但是序列化之后的数据被用户控制,攻击者可以通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。
- 使用
ObjectStreamClass.writeObject()序列化,实现Serializable; 使用ObjectStreamClass.readObject()反序列化。
- 常用的有Hessian、kryo、Protobuf、ProtoStuff,这些都是二进制的序列化协议
- dubbo默认的序列化协议是Hessian,但是面向生产时,推荐使用kryo这种成熟且高效的序列化方式。
- 像JSON\XML这种属于文本类序列化的方式,虽然可读性比较好,但是性能较差,一般不会选择。
- JDK自带的序列化方式一般不会用,因为序列化效率低并且存在安全问题
-
kryo的序列化和反序列化代码
/**
* Kryo serialization class, Kryo serialization efficiency is very high, but only compatible with Java language
*
* @author shuang.kou
* @createTime 2020年05月13日 19:29:00
*/
@Slf4j
public class KryoSerializer implements Serializer {
/**
* Because Kryo is not thread safe. So, use ThreadLocal to store Kryo objects
*/
private final ThreadLocal<Kryo> kryoThreadLocal = ThreadLocal.withInitial(() -> {
Kryo kryo = new Kryo();
kryo.register(RpcResponse.class);
kryo.register(RpcRequest.class);
return kryo;
});
@Override
public byte[] serialize(Object obj) {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Output output = new Output(byteArrayOutputStream)) {
Kryo kryo = kryoThreadLocal.get();
// Object->byte:将对象序列化为byte数组
kryo.writeObject(output, obj);
kryoThreadLocal.remove();
return output.toBytes();
} catch (Exception e) {
throw new SerializeException("Serialization failed");
}
}
@Override
public <T> T deserialize(byte[] bytes, Class<T> clazz) {
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
Input input = new Input(byteArrayInputStream)) {
Kryo kryo = kryoThreadLocal.get();
// byte->Object:从byte数组中反序列化出对象
Object o = kryo.readObject(input, clazz);
kryoThreadLocal.remove();
return clazz.cast(o);
} catch (Exception e) {
throw new SerializeException("Deserialization failed");
}
}
}
List Set Map的区别? 有哪些实现类?底层实现是什么?
| 种类 | List | Set | Map |
|---|---|---|---|
| 元素是否有序 | 有序 | 无序 | 无序 |
| 元素是否重复 | 可重复 | 唯一 | key唯一 |
| 存储的元素类型 | key-value键值对 |
-
List:
- ArrayList: Object[] 数组
- LinkedList: 双向循环链表
- Vector: Object[] 数组
-
Set:
- HashSet: 基于HashMap实现的
- LinkedHashSet: 基于链表和哈希表, 元素的插入和取出的顺序满足FIFO
- TreeSet: 基于红黑树实现的
-
Map:
- HashMap: 数组+链表+红黑树
- Hashtable:
ArrayList和LinkedList的区别?
- 底层实现不同: ArrayList是基于动态数组的数据结构,而LinkedList是基于链表的数据结构
- 随机访问性能不同: ArrayList 优于 LinkedList, 因为 ArrayList 可以根据下标以 O(1) 时间复杂度对元素进程随机访问。 而LinkedList的访问时间复杂都为O(n), 因为它需要变量整个链表才能找到指定的元素。
- 插入和删除性能不同: LinkedList 优于 ArrayList ,因为LinkedList的底层物理结构是链表,因此根据索引曾访问效率低,但是插入和删除不需要移动元素,只需要修改前后元素的指向关系即可,而且链表的添加不会升级到扩容问题。 数据索引访问的效率高,但是非末尾位置的插入和删除效率不高,因为涉及到移动元素,另外添加操作时涉及到扩容问题,会增加时空消耗。
ArrayList和Vector的区别?
- 底层物理结构都是动态数组。
- ArrayList时新版的动态数组,线程不安全,效率高, Vector是旧版的动态数组,线程安全,效率低。
- 动态数组的扩容机制不同,ArrayList扩容为原来的1.5倍, Vector扩容增加为原来的2倍。
- 数组的初始化容量,如果再构建ArrayList和Vector的集合对象时,没有显示指定初始化容量,那么Vector的内部数组的初始容量默认为10,而ArrayList在JDK1.6之前也是10, JDK1.7之后ArrayList初始化长度为0的空数组,之后在添加第一个元素时,再创建长度为10的数组。(用的时候再创建数组,避免浪费。)
- Vector因为版本古老,支持Enumeration迭代器。但是该迭代器不支持快速失败。而Iterator和ListIterator迭代器支持快速失败。如果再迭代器创建后任意时间从结构上修改了向量(通过迭代器自身的remove和add之外的任何其他方式),则迭代器将抛出 ConcurrentModifiactionException. 因此,面对并发的修改,迭代器很快就完全失败,而不是冒着再将来不确定的时间任意发生不确定性的风险。
ArrayList的扩容机制
- 快速失败的思想:对可能发生的异常提前表示故障并停止运行,通过提前的发现和停止错误,可以降低故障的级联风险。
- 初始化ArrayList的长度为0的空数组,当插入第一个元素时,再创建长度为10的数组
- 插入数据时,判断插入后的最小需要容量是否大于当前数组长度,
- 如果大于,并且扩容后的新数组容量大于最小需要容量,就创建一个当前大小的1.5倍的空数组,将原来的数据复制过来,并进行插入,
- 如果大于,并且扩容后新数组的容量小于等于最小需要容量,就创建一个等于插入后的大小的数据,将原始数据复制过来,并进行插入。
Queue和Deque的区别
- Queue: 单端队列,只能从一端插入,一端删除,遵循FIFO原则。 扩展了Colletions,会因为容量问题导致操作失败而处理的方式不同:一种是操作失败后抛异常,一种是返回特殊值
| Queue接口 | 抛出异常 | 返回特殊值 |
|---|---|---|
| 插入末尾 | add(E e) | offer(E e) |
| 删除首位 | remove() | poll() |
| 查看首位 | element() | peek() |
- Deque: 双端队列,首尾两端都可以插入和删除,并且也因为容量问题导致操作失败而处理的方式不同
| Deque接口 | 抛出异常 | 返回特殊值 |
|---|---|---|
| 插入首位 | addFirst(E e) | offerFirst(E e) |
| 插入末尾 | addLast(E e) | offerLast(E e) |
| 删除首位 | removeFirst() | pollFirst() |
| 删除末尾 | removeLast() | pollLast() |
| 查看首位 | getFirst() | peekFirst() |
| 查看末尾 | getLast() | peekLast() |
ArrayDeque 和 LinkedList的区别:
- ArrayDeque 和 LinkedList都是实现了 Deque接口,都具有队列的功能。
- 从结构上:ArrayDeque通过可变长的数组+双指针实现; ListedList通过链表来实现
- 从扩容角度上说: ArrayDeque基于数组,可以根据插入的数据个数进行动态扩容,扩容之后,插入仍是O(1); LinkedList是每插入一个都会申请新的堆空间。
- 存储NUll值: ArrayDeque不支持, LinkedList支持
PriorityQueue 的特点
- 元素出队的顺序与优先级相关,优先级越高的越先出队
- 底层使用二叉堆结构实现的,并用可变长的数组来进行存储数据的。
- 非线程安全的。
- 默认是小顶堆,可以通过Comparator构建函数,来修改优先级的先后。
HashMap和Hashtable的区别?
- 都是实现了Map接口,用于存储键值对的数据结构,底层数据结构都是数组加链表形式。
- 线程安全:Hashtable是线程安全的,而HashMap是非线程安全的。
- 性能:因为Hashtable使用synchronized给整个方法加锁,所以相比HashMap来说,它的性能不如HashMap。
- 存储:HashMap允许key和value为null,而Hashtable不允许存储null键和null值。
- Hashtable不能存储的原因:key值进行哈希计算,如果为null的话,无法调用该方法,还会抛出空指针异常。而value为null也会主动抛出空指针异常。
- HashMap允许key和value为null的原因:hash()对null的值进行了特殊处理,如果为null,会把值赋值为0.
- Hashtable是线程安全,但性能差,不推荐使用。推荐使用ConcurrentHashMap在多线程场景下使用。
HashMap和HashSet的区别?
- HashSet实现了Set接口,只存储对象; HashMap实现了Map接口,用于存储键值对。
- HashSet底层用HashMap存储,HashSet封装了一系列HashMap的方法,HashSet将值保存到HashMap的key里。
- HashSet不允许有重复的值。 而HashMap的键不能重复,值可以重复。
HashMap
解决【index】冲突问题
- 虽然使用hashCode(),来尽量减少冲突,但仍然存在两个不同对象返回hashCode值相同的情况。
- JDK1.8之前使用:数组+链表
- JDK1.8之后使用:数组+链表/红黑树
- 即hash值相同的元素,存储在同一个桶table[index]中,使用链表或红黑树连接起来。
- 哈希冲突的键值对以链表的形式存储在同一索引位置,插入新节点时,采用尾插法(JDK8改进,JDK7是头插法(多线程并发扩容时,头头插法导致链表反转,产生循环引用))
为什么1.8会出现红黑树和链表共存
- 当hash冲突比较严重时,链表长度会很长,导致查询效率降低,而选择二叉树可以大大提高查询效率。但由于二叉树的结构太复杂,节点个数小时,选择链表反而更加简单。所以会出现红黑树与链表共存。
什么时候树化?什么时候反树化?
static final int TREEIFY_THRESHOLD = 8;//树化阈值
static final int UNTREEIFY_THRESHOLD = 6;//反树化阈值
static final int MIN_TREEIFY_CAPACITY = 64;//最小树化容量
- 当table[index]下的链表节点个数达到8,并且 table.length >= 64, 那么新Entry对象还添加到该table[index]中,那么就会将table[index]的链表进行树化。
- 当某table[index]下的红黑树节点个数少于6个,此时
- 当继续删除table[index下]的树结点,最后这个根节点的左右结点有null,或者根节点的左结点为null,会反树化。
- 当重新添加新的映射关系到map中,导致了map重新扩容了,这个时候如果table[index]下面还是小于等于6个,会反树化。
负载因子为什么是0.75
//初始化容量:
int DEFAULT_INITIAL_CAPACITY = 1 << 4;//16 目的是体现2的n次方
//①默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//②阈值:扩容的临界值
int threshold;
threshold = table.length * loadFactor;
//③负载因子
final float loadFactor;
- 如果太大,threshold就会很大,那么如果冲突比较严重的话,就会导致table[index]下面的结点个数很多,影响效率。
- 如果太小,threshold就会很小,那么数据扩容的频率就会提高,数组的使用率也会降低,那么会造成空间浪费。
1.7 put 源码分析
- 当第一次添加映射关系时,数组初始化为一个长度为16的HashMapEntry类型是实现了java.util.Map.Entry接口。
- 特殊考虑:如果key为null,index直接是[0],hash也是0.
- 如果key不为null,在计算index之前,会对key的hashCode()值,做一个hash(key)再次哈希的运算,这样可以使得Entry对象更加散列的存储到table中
- 计算index=table.length-1 & hash
- 如果table[index]下面,已经有映射关系的key与要添加的映射关系的key相同了,会用新的value替换旧的value
- 如果没有相同的,会把新映射关系添加到链表的头,原来table[index]下面的Entry对象连接到新的映射关系的next中。
- 添加之前先判断if(size >= thresold && table[index] != null) 如果该条件为true,①会扩容 ②会重新计算key的hash ③会重新计算index。
- size++
1.8put源码分析
(1)DEFAULT_INITIAL_CAPACITY:默认的初始容量 16
(2)MAXIMUM_CAPACITY:最大容量 1 << 30
(3)DEFAULT_LOAD_FACTOR:默认加载因子 0.75
(4)TREEIFY_THRESHOLD:默认树化阈值8,当链表的长度达到这个值后,要考虑树化
(5)UNTREEIFY_THRESHOLD:默认反树化阈值6,当树中的结点的个数达到这个阈值后,要考虑变为链表
(6)MIN_TREEIFY_CAPACITY:最小树化容量64
当单个的链表的结点个数达到8,并且table的长度达到64,才会树化。
当单个的链表的结点个数达到8,但是table的长度未达到64,会先扩容
(7)Node<K,V>[] table:数组
(8)size:记录有效映射关系的对数,也是Entry对象的个数
(9)int threshold:阈值,当size达到阈值时,考虑扩容
(10)double loadFactor:加载因子,影响扩容的频率
- 先计算key的hash值,如果key是null,hash值就是0,如果不为null,使用((h=key.hashCode)^(h >> 16)) 得到hash值。
- 若table是空的,先初始化table数组。
- 通过hash值计算存储的索引位置index = hash & (table.length - 1)
- 如果table[index]==null, 那么直接创建一个Node结点存储到table[index]中即可
- 如果table[index] != null
- 判断table[index]的根节点的key是否与新的key
相同(hash值相同并且满足key地址相同或key的equals返回true),如果是那么用一个Node结点变量e记录这个根结点 - 如果table[index]的根结点的key与新key
不相同,而且table[index]是一个TreeNode结点,说明table[index]下是一棵红黑树,如果该树的某个结点的key与新的key相同(hash值相同并且满足key的地址相同或者key的equals返回true),那么用一个Node结点变量e记录这个相同的结点,否则将(key,value)封装为一个TreeNode结点,连接到红黑树中 - 如果table[index]的根结点的key与新key
不相同,并且table[index]不是一个TreeNode结点,说明table[index]下是一个链表,如果该链表中的某个结点的key与新的key相同,那么用一个Node结点变量e来记录这个相同的结点,否则将新的映射关系封装为一个Node的结点直接链接到链表尾部,并且判断table[index]下结点个数达到 TREEIFY_THRESHOLD(8) 个,如果table[index]下的结点个数达到了8个, 那么判断talbe.length 是否达到 MIN_TREEIFY_CAPACITY(64) 个,如果没有达到,那么先扩容,扩容会导致所有元素重新计算index,并且调整位置,如果table[index]下结点个达到8个,并且table.length的个数达到64,那么会将该链表转为一颗自平衡的红黑树。
- 判断table[index]的根节点的key是否与新的key
- 如果table[index]下找到了新key
相同的结点,即e不为空,那么用新value替换原来的value,并返回旧value,结束put的方法。 - 如果新增结点而不是替换,那么size++,并且还要重新判断size是否达到threshold阈值,如果达到,还要扩容。
ConcurrentHashMap和Hashtable的区别?
- 底层结构:
- ConcurrentHashMap的1.7版本采用 分段的数组+链表; 1.8后采用数组+链表/红黑树的结构
- Hashtable 采用数组+链表的结构
- 实现线程安全的方式:
-
ConcurrentHashMap:
- 1.7 采用segment分段锁,将整个桶数组分割分段(分段锁),每把锁只锁定一部分数据,不同线程访问不同段的数据,就不会存在锁竞争,提高并发访问效率
- 1.8 摒弃分段锁,采用Node数组+链表+红黑树,采用synchronized 和 CAS 来实现线程安全的。
-
Hashtable(同一把锁): 使用synchronized进行修饰, 当一个线程进行访问同步方法,其他线程也进行访问,就会导致进入阻塞或轮询状态。效率低下,一个线程使用put,其他线程就不能访问put或get.
-
ConcurrentHashMap的线程安全实现方案和具体底层实现方案?
- 1.8底层采用Node数组+链表/红黑树,当Hash冲突到一定地步时,就会将链表转换为红黑树。
- put的流程:
- 1.通过key计算出哈希值
- 2.判断是否需要初始化
- 3.通过key的哈希值,定位Node数组的位置,如果为空,通过CAS写入,失败则自旋保证成功。
- 4.如果 hashCode == MOVED == -1 的值,则需要扩容。
- 5.如果不满足,通过synchronized锁定写入数据
-
- 如果数量大于 树化临界值就会执行树化方法 ,treeifyBin会判断数组长度>=64,会将链表转化为红黑树。
为什么ConcurrentHashMap不能添加null?
- 在HashMap中,Key和value值都可以为null
- 在ConcurrentHashMap中,key或者value值都不能为null。由于底层实现的时候,如果key和value若为null,会抛出NullPointerException的异常。更深层次的原因:如果允许ConcurrentHashMap的key或者value为null的情况下,就会存在经典的“二义性问题”
- 对应以上null的二义性:①这个值null表示一种具体的“null”值状态 ② null还表示“没有”的意思,因为没有设置。hashMap没有这个问题,因为单线程。 而ConcurrentHashMap是在多线程下,多个进程会改动数据,有这样的问题。
程序、进程、线程、管程
- 程序(program): 为完成特定任务,用某种语言编写的一组指令的集合。即一段静态的代码,静态的对象
- 进程(process): 程序一次执行过程,或正在内存中运行的应用程序。每个进程都有一个独立的空间,系统运行一个程序即是一个进程即是一个进程从创建、运行到消亡的过程。==生命周期。 进程是动态的,进程作为
操作系统调度和分配资源的最小单位,系统运行时会为每个进程分配不同的内存区域。 - 线程(thread): 进程可进一步细分为线程,是一个程序内部的一条执行路径。一个进程至少有一个线程。
- 若一个进程同时间
并行执行多个线程,就是支持多线程的。 - 线程作为CPU调度和执行的最小单位,每个线程拥有独立的运行栈和程序计数器,线程切换的开销小。
- 一个进程中的多个线程共享相同的内存单元/内存地址空间---> 它们从同一堆中分配对象,可以访问相同的变量和对象。这使得线程间通信更简便,搞笑。但多个线程操作共享的系统资源可能会带来安全隐患。
- 若一个进程同时间
- 管程:Monitor(监视器),就是我们平时说的锁。
线程状态
- 在java.lang.Thread.State的枚举类中这样定义:
public enum State{ NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WATINIG, TERMINATED; } BLOCKED: 指互有竞争关系的几个线程,其中一个线程占有锁对象,其他线程只能等待锁,只有获取锁对象的线程才有执行机会TIMED_WAITING: 当前线程执行过程中遇到Treahd类的 sleep或 jion, Object类的wait,LockSupport类的park方法,并且在调用这些方法时,设置了时间,那么当前线程会进入 TIMED_WAITING, 知道时间到,或被中断。WAITING: 当前线程执行过程中遇到Object类的wait,Thread类的jion,LockSupport类的park方法,并调用这些方法,没有指定时间,那么当前线程会进入WAITING状态,直到被唤醒。- 通过Object的wait- Object.notify/notifyAll 唤醒
- Condition.await - Condition.signal 唤醒
- LockSupport.park - LockSupport.unpark 唤醒
- Thread.join, 只有调用join() 的线程对象结束才能让当前线程恢复。
- 当WAITING或TIMED_WAITING恢复到RANNABLE状态时,如果发现当前线程没有得到监视器锁,那么就会立刻装入BLOCKED状态。
- 状态转换:
- start -> RUNNABLE
- synchronized 竞争锁失败 -> BLOCKED
- wait -> WAITING, notify()唤醒 -> BLOCED
- sleep(ms) -> TIMED_WAITING
线程等待和唤醒有几种实现手段
- Object类下的wait() notify() 和 notifyAll() 方法
- wait(): 让当前线程处于等待状态、并释放当前拥有的锁
- notify(): 随机唤醒等待该锁的其他线程,重新获取锁,并执行后续的流程,只能唤醒一个线程
- notifyAll(): 唤醒所有等待该锁的线程(锁只有一把,虽然所有线程被唤醒,但所有线程需要排队执行)
- Condition 类下的 await() singal() 和 signalAll()
- await(): 对应Object的wait(),线程等待
- singal(): 对象Object的notify(), 随机唤醒一个线程
- singalAll(): 对应Object的notifiAll(), 唤醒所有线程
- LockSupport类下park() 和 unpark()
- LockSupport.park():休眠当前线程
- LockSupport.unpark(线程对象): 唤醒某一个指定的线程
- LockSupport无需配锁(synchronized或lock)一起使用。
线程的创建方式
- 继承Thread类: 重写run(),但单继承限制
- 实现Runnable: 解耦任务与线程,推荐方式
- 实现Callable: 支持返回值和异常,需配合FutureTask
- 线程池:推荐使用ExecutorService.submit();
死锁? 如何避免和预防死锁?
-
线程死锁:多个线程同时被阻塞,它们中的一个或多个全部都在等待某个资源在被释放。由于线程被无限期地阻塞,因此线程不可能正常终止。
-
产生死锁的四个必要条件
- 互斥条件:该资源任意一个时刻只由一个线程占用
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只能自己完毕后才释放资源
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源的关系。
-
检测死锁的工具: 使用
jmap、jstack等命令查看 JVM 线程栈和堆内存的情况,如果有死锁,jstack的输出通常会有Found one Java-level deadlock:的字样,后面会跟着死锁相关的线程信息。另外,实际项目中还可以搭配使用top、df、free等命令查看操作系统的基本情况,出现死锁可能会导致CPU、内存等小号过高。 -
避免和预防死锁的方法,破环产生死锁的必要条件即可:
- 一次性申请所有需要的资源
- 如果已经占有资源的线程,要申请其他资源,申请不到时,便直接释放已占有的资源
- 靠按序申请资源。按某一顺序申请资源,再反序释放资源,破环循环等待条件即可。
乐观锁和悲观锁
- 悲观锁:总是假设最坏的情况,认为共享资源每次被访问时,就会出现问题(比如共享数据被修改),所以每次在获取资源操作时都会上锁,这样其他线程想拿到这个资源就会阻塞知道锁被上一个持有者释放。也就是说共享资源每次只个一个线程使用,其他线程阻塞,用完后再把资源转让给其他线程。 高并发场景下,激烈的锁竞争会照成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。 有可能会导致死锁问题,影响代码正常运行。
- 通常用于写比较多的情况(多写场景,竞争激烈),可以避免频繁失败和重试影响性能,悲观锁的开销时固定的。
- 乐观锁:总是假设最好的情况,认为共享资源每次访问时不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是提交修改时去验证对应资源是否被其他线程修改了。 高并发场景下,乐观锁相比悲观锁,不存在锁竞争造成线程阻塞,也不会有死锁问题,在性能上更胜一筹,但如果冲突频繁发生,会频繁失败并重试,这样同样会影响性能。
- 通常用于读比较多,写比较少的场景,可以避免频繁加锁影响性能。乐观锁主要针对对象是单个共享变量(atomic的原子变量)
如何实现乐观锁?
- 一般会采用版本号机制或者CAS算法来实现。CAS算法相对更多一些。
- 版本号机制: 数据表中存放版本号version字段,每次修改version的值都会加1。当修改数据的时候,会先读取数据表中的数据,同时也读到了version的值,在更新值时,会将之前读到的version值与数据库的中version进行比较,如果相等才进行修改,否则进行重试操作,直到更新成功。
CAS
- compare and swap,比较并交换,是一条CPU并发原语:判断内存某个位置的值是否为预期值,如果是更新否则什么都不做,这个过程是原子的。原语的执行必须是连续的,执行过程不允许中断,不会造成数据不一致问题。
- 包含三个操作数---内存位置、预期原值及更新值
- 执行CAS操作时,将内存位置与预期原值比较:
- 匹配,将该位置值更新为新值
- 不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功
- 缺点:
- 循环时间长,开销大
- 只能保证一个共享变量的原子操作
- 引出来ABA的问题(一个线程A正在写入时,另一个线程B对资源修改后,再改回原状,期间其他线程C读取到,B修改的值,而A使用CAS判断与预期值相同,替换更新值。)
- ABA解决办法:使用带版本号的CAS,也称CAS(Double CAS)或版本号CAS:每次进行CAS操作时,不仅需要比较要修改的内存地址的值与期望的值是否相等,还需要比较这个内存地址的版本号是否与期望的版本号相等。如果相等才能修改。 比如可以使用AtomicStampedRefrence来解决ABA问题。
synchronized 锁升级
-
synchronized的底层: 本质上两者都是对象监视器mintor的获取
-
synchronized修饰同步代码块,底层是monitorenter代表同步代码块开始, monitorexit代表同步代码块结束
- 执行monitorenter时,会尝试获取对象锁,当锁的计数器为0时,表示可以获取到锁,获取到锁后,锁计数器变为1
- 拥有对象锁的线程可以执行monitorexit时,会进行释放锁操作,将锁的计数器变为0,表示锁已释放,可以被其他线程获取。
-
synchronized修饰同步方法,底层会在同步方法处增加表示ACC_SYNCHRONIZED表示,表明此方法是个同步方法。
-
-
状态
- 无锁:对于共享资源,不涉及多线程的竞争访问
- 偏向锁: 共享资源首次被访问,JVM会对该共享资源对象做一些设置,比如将对象头中是否偏向锁标志位设置为1,对象头中的线程id设置为当前线程ID,后续当前线程再次访问这个共享资源,会根据偏向锁标识和线程ID进行对比,比对成功则直接获取到锁,进入临界区域(被锁保护的线程只能串行的代码),这也是synchronized锁的可重入功能。
- 轻量级锁: 当多个线程同时申请共享资源的访问时,就产生了竞争,JVM会先尝试使用轻量级锁,以CAS方式获取锁(一般就是自旋加锁,不阻塞线程采用循环等待的方式), 成功获取到锁,状态为轻量级锁,失败(达到一定的自旋次数)则锁升级为重量级锁。
- 重量级锁:如果共享资源锁已经被某个线程持有,此时是偏向锁,未释放锁前,再由其他线程来竞争,则会升级到重量级锁,另外轻量级锁状态多线程竞争锁时,也会升级到重量级锁。重量级锁由操作系统来实现,所以性能消耗相对较高。
-
另外需要注意的是,由于硬件资源的不断升级,获取锁的成本随之下降,jdk15版本后默认关闭了偏向锁。也就是从无锁直接到了轻量级锁的状态。
-
无锁 -> 偏向锁 -> 轻量级锁(CAS自旋) -> 重量级锁(内核态切换)
-
使用场景:修饰方法(实例方法锁this, 静态方法锁Class对象)或代码块(显式指定锁对象)
ReentrantLock
- 特点:API级别可重入锁,支持公平/非公平模式、可中断、超时等待
- AQS原理: 通过stat变量和CLH队列实现锁竞争
- 对比synchronized:
- 灵活控制锁粒度(tryLock)
- 需手动释放锁,避免死锁
Synchronzied 和 ReentrantLock的区别?
- 两者都是可重入锁,可重入锁就是线程可以再次获取自己的内部锁。
- synchronized依赖于JVM 而 ReentrantLock依赖于API
- ReetrantLock,API层面的,通过lock和unlock再搭配try-catch-finally来完成
- ReentrantLock比synchronized多了一些高级功能
- 等待可中断:ReentrantLock提供了一种中断等待锁线程的机制,通过
lock.interruptibly()来实现,就是说当前线程在等待获取锁,其他线程中断此线程(interrupt()),当前线程会抛出 InterrputedException异常,捕获并进行处理。 - 支持公平锁:synchronized是非公平锁。 ReentrantLock默认也是非公平锁。公平锁是先等待的线程获取到锁。 ReentrantLock(boolean fair)使用此构造方法来指定公平锁。
- 支持超时:使用tryLock来获取等待获取锁,等待最大时间后,超时还没有获取到锁,就会获取锁失败,不再等待。
- 等待唤醒机制: ReentrantLock实现Condition接口。
- Object类下的wait() notify() 和 notifyAll() 方法
- wait(): 让当前线程处于等待状态、并释放当前拥有的锁
- notify(): 随机唤醒等待该锁的其他线程,重新获取锁,并执行后续的流程,只能唤醒一个线程
- notifyAll(): 唤醒所有等待该锁的线程(锁只有一把,虽然所有线程被唤醒,但所有线程需要排队执行)
- Condition 类下的 await() singal() 和 signalAll()
- await(): 对应Object的wait(),线程等待
- singal(): 对象Object的notify(), 随机唤醒一个线程
- singalAll(): 对应Object的notifiAll(), 唤醒所有线程
- LockSupport类下park() 和 unpark()
- LockSupport.park():休眠当前线程
- LockSupport.unpark(线程对象): 唤醒某一个指定的线程
- LockSupport无需配锁(synchronized或lock)一起使用。
- Object类下的wait() notify() 和 notifyAll() 方法
- 等待可中断:ReentrantLock提供了一种中断等待锁线程的机制,通过
volatile
- 可见性:强制写操作刷主内存,读操作从主内存读取(内存屏障实现)
- 有序性:禁止指令重排(happens-before规则:告诉程序在什么情况下一个线程的操作结果对另一个线程可见)
- 局限性:不保证原子性(如i++需结合AtomicInteger)
双重校验锁实现单例对象
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
Synchronized 和 volatile的区别?
- volatile是线程同步的轻量级实现,所以性能要比synchronzied高。 而且volatile是修饰变量的,而synchronzied是修饰方法和代码块的。
- volatile只能保证可见性,但不能保证原子性。synchronzied是两者都能保证。
- volatile主要解决的是变量在多个线程之间的可见性。 synchronzied解决多个线程访问资源的同步性。
ThreadLocal
-
ThreadLocal有什么用?
- 为每个线程提供独立的变量副本,解决多线程数据竞争和线程安全问题。每个线程都拥有自己的本地变量盒子,确保数据不会互相影响。可以通过get() set()方法获取和修改自己的副本,从而避免线程安全问题。
-
ThreadLocal提供线程局部变量。这些与正常变量不同,因为每个线程在访问ThreadLocal实例时(通过其get或set方法)都有自己的、独立初始化变量副本。 ThreadLocal实例通常是类中私有静态字段,使用它的目的是希望将状态(例如:用户ID或事务ID)与线程关联起来。
-
必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄漏等问题,尽量在代理中使用try-finally中回收
-
ThreadLocal中的
ThreadLocalMap中使用的key为ThreadLocal的弱引用,而value是强应用,所以ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候,key会被清理掉,而value不会被清理掉。这样ThreadLocalMap中就会出现key为null的Entry。加入我们不做任何措施,value将永远无法被GC回收,可能会产生内存泄漏。最好在最后手动调用remove()方法。
objectThreadLocal.set(userInfo);
try{
//..
}finally{
objectThreadLocal.remove();
}
初始化
ThreadLocal.withInitial(Supplier<? extends S> supplier) //创建一个threadLocal变量
核心总结
- 每个Thread内有自己的
实例副本且该副本只由当前线程自己使用 - 既然其他Thread不可访问,那就不存在多线程共享的问题
- 统一设置初始值,但是每个线程对这个值的修改都是各自线程相互独立的
- 如何才能不争抢:加入synchronized或者Lock控制资源的访问顺序
ThreadLocal类
- ThreadLocal(自己线程共享): set/get 只能设置和获取自己所在的信息,获取不到别的线程的。
- InheritableThreadLocal: ① 全局数据,父子线程之间传递,子线程可以获取到父线程的set ②最好是新建线程,一旦遇到线程池,主数据变更、线程池内不会同步更新
- TransmittableThreadLocal: 使用前先通过pom,引入transmittable-thread-local,可以实现线程池之间的共享
TransmittableThreadLocal<String> transmittableThreadLocal = new TransmittableThreadLocal<>();
ExecutorService threadPool = Executors.newSingleThreadExecutor();
threadPool = TtlExecutors.getTtlExecutorService(threadPool);
底层结构ThreadLocalMap
Thread类有个类型为ThreadLocal.ThreadLocalMap的实例变量 threadLocals, 也就是说每个线程有一个自己的ThreadLocalMapThreadLocalMap的key可以视作ThradLocal(实际上,key并不是ThreadLocal本身,而是弱引用),value为代码中放入的值。- 每个线程放值时,都会往自己的ThreadLocalMap中存,都也是以
ThreadLocal为引用,在自己的map中找到,从而实现线程隔离。 - ThreadLocalMap没有链表结构,是有数组,
key是ThreadLocal<?> k,继承自WeakReference,是弱引用类型。
29.Java四种引用类型
- 强引用:new出来的对象一般为强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
- 软引用:使用SoftReference 修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
- 弱引用:使用WeakReference 修饰的对象被称为弱引用,只要发生垃圾税后,若这个对象只被弱引用指向,那么就会被回收
- 虚引用:虚引用是最弱的引用,在Java中使用哦个 PhantomReference进行定义。虚引用中唯一的作用就是用队列接受对象即将死亡的通知。
为什么要使用线程池?
- 降低资源消耗:可以重复利用已经创建的线程,降低线程创建和销毁的消耗
- 提高响应效率:不需要等待线程创建
- 管理线程资源:线程是稀缺资源,如果无限制的创建,会浪费大量资源,降低系统稳定性。使用线程池统一分配,调度和监控。
为什么不用自带的线程池?
- Executors的自带方法:
- FixedThreadPool 和 SingleThreadExcutor使用的阻塞队列是 LinkedBlockingQueue, 长度为 Integer.MAX, 任务过多可能会导致OOM
- CacheTreadPool 使用 SynchronousQueue,长度也是 Integer.MAX, 任务过多时,可能导致OOM
- ScheduledThreadPool 和 SingleScheduledExcutor的队列是 DelayedWorkQueue, 任务长度为Integer.MAX, 任务过多可能会导致oom
线程池参数的含义
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
{ //... }
-
corePoolSize: 核心线程数, 空闲状态,如果未设置超时关闭,就会处于Waiting状态。
-
maxmumPoolSize: 最大线程数,线程池允许创建的最大线程数
-
KeepAliveTime: 空闲线程存活时间,没有任务,会清理非核心线程(存活时间)
-
TimeUnit: 时间单位
-
BlockingQueue: 线程池任务队列,阻塞队列,用来存储所有待执行任务
- ArraryBlockingQueue: 一个有数组结构组成的有界阻塞队列
- LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列
- SynchronusQueu: 一个不存储元素的阻塞队列,即直接提交给线程不保持它们
- PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列
- DelayQueue: 一个优先级队列的无界阻塞队列,只有在延迟期满时才能提取元素
- LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列,与SynchronusQueue类似,还有非阻塞方法
- LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。
-
ThreadFactory: 创建线程的工厂
-
RejectedExecutionHandler: 拒绝策略
- AbortPolicy: 抛出异常拒绝新任务
- CallRunsPolicy: 把任务交给添加此任务的主线程来执行
- DiscardPolicy: 直接丢弃新任务
- DiscardOldesPolicy: 丢弃最早未处理的任务
-
清理核心线程的方法:使用 allowCoreThreadTimeOut(Boolean value) 方法设置true,允许回收核心线程,超时时间还是keepAliveTime
线程池底层的执行原理
- 在创建了线程池之后,开始等待请求
- 当调用execute() 方法添加一个请求任务后,线程池会做出如下判断:
- 如果正在运行的线程数量小于corePoolSize, 那么马上会创建线程运行这个任务
- 如果正在运行的线程数量大于或等于corePoolSize, 那么将这个任务放入队列
- 如果这个时候队列满了且正在运行的线程数量还小于maxmumPoolSize, 那么还要创建非核心线程来立刻运行这个任务
- 如果队列满了且正在运行的线程数量大于或等于maxmumPoolSize, 那么线程池会启动饱和拒绝策略来执行
- 当一个线程完成任务时,会从队列中取下一个任务来执行
- 当一个线程无事可做超过一定时间(keepAliveTime)时,线程会判断:
- 如果当前运行的线程数大于corePoolSize, 那么这个线程就会被停掉
- 所有线程池的所有任务完成后,最终会收缩到corePoolSize的大小。
线程池中线程异常是销毁还是复用?
- 使用execute()提交任务: 如果线程执行过程中发生异常,未捕获到异常时,会终止线程,并打印异常到日志中。线程池会创建新的线程,代替他,从而保证线程数保持不变。
- 使用submit()提交任务:如果线程执行过程中发生异常,不会打印异常,而是返回给submit()的Future对象。当调用future.get()时,可以捕获到ExcutionException异常。当前线程不会终止,而是存在线程池中,等待后续处理。
如何设计一个根据任务优先度执行的线程池
-
阻塞队列采用PriorityBlockingQueue(优先级无界阻塞队列)
- 使用PriorityBlockingQueue,要求提交的任务必须具有排序能力
- 提交的任务实现Comparable接口,通过重写compareTo()方法,指定任务的优先级规则。
- 创建priorityBlockingQueue时,传入一个Comparator对象,指定任务的优先级规则。
- 使用PriorityBlockingQueue,要求提交的任务必须具有排序能力
-
注意:
- PriorityBlokingQueue,是无界队列,当大量任务访问时,可能会导致OOm。 可以重写PriorityBlockingQueue的offer方法,如果插入超过一定数量的,就返回false,不允许添加。
- 饥饿问题,低优先级的任务可能一直无法执行
- 性能问题:由于需要对元素排序以及保证线程安全(使用reetantlock来保证的),降低性能。
AQS
什么是AQS?
-
AQS的全称
AbstractQueuedSynchronizer, 抽象队列同步器,在java.util.concurretn.locks包下。主要用于构建锁和同步器, 比如ReentrantReadWriteLockSynchronousQueue等都是基于AQS的。 -
核心: 利用一个双向队列来保存等待锁的的线程,同时利用一个变量
state变量来表示锁的状态 -
AQS的同步器可以分为独占模式和共享模式两种。独占模式是指同一时刻只允许一个线程获取锁,常见实现类有 ReentrantLock, 共享模式是指同一个时刻允许多个线程同时获取锁,常见的实现类有:Semaphore,CountDownLatch,CyclicBarrier等。
-
CountDownLatch: 秦灭六国,一统华夏
- CountDownLatch 主要有两个方法,当一个或多个线程调用await() 方法时,这些线程会阻塞
- 其他线程调用 countDown() 会将计数器减1(调用countDown() 方法的线程不会阻塞)
- 当计数器的值变为0,因await()阻塞的线程会被唤醒,继续执行。
-
CyclicBarrier: 集齐七颗龙珠,召唤神龙
- CyclicBarrierd的字面意思是可循环(Cyclic)使用的屏障(Barrier)。要做的事情让一组线程到达一个屏障(也叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活
- 线程进入屏障通过 CyclicBarrier的 await()
-
Semaphore: 停车位:
- acquire(获取)当一个线程调用acquire操作,要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量或超时
- release(释放)实际上会将信号量的值加1,然后唤醒等待的线程
- 信号量主要用于两个目的;一个用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
AQS的原理
- 核心思想: 如果当前被请求的共享资源空闲,则将当前空闲线程设置为工作线程,并锁定共享资源。如果请求的共享资源已被占用,那么就需要一套线程阻塞等待以及被唤醒锁分配的机制,这个机制是AQS底层 CLH队列实现。即 将暂时获取不到锁的线程放入队列。
- CLH队列是对自旋锁进行改进,是基于单链表的自旋锁。在多线程情况下,会将所有的线程编织成一个队列,等待的线程会通过自旋检查上一个线程的状态,上一个线程释放锁,当前节点才能抢到锁。
Java常见并发容器java.util.concurrent
ConcurrentHashMap:线程安全的HashMap- JDK1.7:
ConcurrentHashMap对整个桶数组进行了分割分段(Segment,分段锁),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 - JDK1.8:
ConcurrentHashMap摒弃了Segment, 直接用 Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和CAS来操作,因为1.6后,Synchronized锁做了很多优化。
- JDK1.7:
CopyOnWriteArrayList:线程安全的List, 在读多写少的场合性能非常好,远远好于 Vector- 核心:采用了写时复制 的策略,当需要修改(add,set,remove等操作) CopyOnWriteArrayList的内容时,不会直接修改原数组,而是会先创建底层数组的副本,对副本数组进行修改,修改完之后再将修改后的数组赋值回去,这样就可以保证写操作不会影响读操作了。
ConcurretnLinkedQueue: 高效的并发队列,使用链表实现。可以看作一个线程安全的 LinkedList,这是一个非阻塞队列- 主要使用CAS非阻塞算法来实现线程安全的。 适合在性能要求比较高的,同时对队列读写存在多个线程中同时进行的场景,即如果对队列加锁的成本较高则适合使用无锁的
concurrentLinkedQueue来替代。
- 主要使用CAS非阻塞算法来实现线程安全的。 适合在性能要求比较高的,同时对队列读写存在多个线程中同时进行的场景,即如果对队列加锁的成本较高则适合使用无锁的
BlockingQueue: 是个接口,JDK内部通过链表、数组等方式实现了这个接口。表示阻塞独立额,非常适合作为数据共享的通道。ArrayBlockingQueue: 是BlockingQueue接口的有界队列实现类,底层采用数组类,一旦创建,容量不能改变,其并发控制采用了可重入锁ReetrantLock, 不管是插入操作还是读取操作,都需要获取到锁才能拿操作。当队列容量满时,尝试将元素放入队列将导致操作阻塞,尝试从一个空队列中取一个元素也会同样阻塞。 默认情况下,不能保证线程访问队列的公平性,即不能按照线程等待的绝对时间顺序。 如果保证公平性,通常会降低吞吐量,可采用以下代码
ArrayBlokcingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10, true);LinkedBlockingQueue: 底层基于单项链表实现的阻塞队列。可以当作无界队列也可以当作有界队列来使用,同样满足 FIFO 的特性。为了防止LinkedBlockingQueue容量迅速增加,耗损大量内存,通常创建LinkedBlockingQueue对象时,会指定大小,如果未指定大小,容量等于Integer.MAX_VALUEPriorityBlockingQueue:支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现compareTo()来指定元素排序规则, 或者 初始化通过构造器参数Comparator指定排序规则。 并发控制采用的时可重入锁ReetrantLock, 队列为无界队列。 它就是PriorityQueue线程安全的版本,不可以插入null值,同时,插入队列的对象必须时可比较大小的(comparable),否则报ClassCastException异常。
ConcurrentSkipListMap: 跳表的实现。这是一个Map,使用跳表的数据结构进行快速查找。内部存储多张链表,所有的链表都是排序的,查找时,从顶级链表开始找,一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表,最底层存放的时原始链表。 跳表是一种利用空间换时间的算法。
Atomic 原子类
Atomic指一个操作具有原子性,即该操作不可分割,不可中孤单。java.util.concurrent.atomicAtomic类依赖于 CAS 乐观锁来保证其方法的原子性,而不需要使用传统锁机制(如 synchronized 块或 ReetrantLock)
基本类型
AtomicInteger:整型原子类AtomicLong: 长整型原子类AtomicBoolean: 布尔型原子类- 常用方法
- get(): 获取当前值
- getAndSet(int newValue):获取当前的指,并设置新的值
- getAndIncrement():获取当前的值,并自增
- getAndDecrement(): 获取当前的值,并自减
- getAndAdd(int delta) 获取当前的值,并加上预期的值
- compareAndSet(int expect, int update): 如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
- lazySet(int newValue): 最终设置为newValue, 比set还弱的语义,可能之后一段时间还有线程可以读到旧值,但可能更高效
数组类型
AtomicIntegerArray: 整型数组原子类AtomicLongArray: 长整型数组原子类AtomicReferenceArray: 引用类型数组原子类- 常用方法
get(int i): 获取 index=1 位置元素的值 getAndSet(int i, int newValue) 返回index=1的值,并设置为新值newValue getAndIncrement(int i): 获取index=i 位置元素的值,并让该位置的元素自增 getAndDecrement(int i): 获取index=i 位置元素的值,并让该位置的元素自减 getAndAdd(int i,int expect,int update): 如果输入的数值等于预期值,则以原子的方式将i位置的元素更新为update lazySet(int i,int newVlaue) 最终,将index=1位置的元素设置为newValue
引用类型
AtomicReference: 引用类型原子类AtomicMarkableReference原子更新带有标记的引用类型。AtomicStampedReference: 原子更新带有版本号的引用类型。可以解决CAS进行原子更新时出现的ABA问题。
对象的属性修改类型
AtomicIntegerFieldUpdater: 原子更新整型字段的更新器AtomicLongFieldUpdater: 原子更新长整型字段的更新器AtomicReferenceFieldUpdater: 原子更新引用类型里的字段。