面试题总结(java基础)

170 阅读1小时+

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中基本数据类型对应的包装类型?

类型默认值占用空间大小字节包装类型取值范围
byte01Byte-128-127
short02Short-2^15~2^15-1
int04Interger
long0l8Long
charu00002Character0-65535
float0f4Float
double0d8Doule
booleanfalse1Booleantrue 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方法。
  • 深拷贝:基础数据类型复制值,引用类型在新的内存空间复制值,新老对象不共享内存,修改一个值,不影响另一个。

那什么是引用拷贝呢?  简单来说,引用拷贝就是两个不同的引用指向同一个对象 image.png

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, 这个接口在调用方这边提供,然后由不同的厂商实现,提供服务 image.png

SPI的实现原理?

  • SPI依赖ServiceLoader来实现的,以下是流程
    • 通过 URL 工具类从 jar 包的 /META-INF/services 目录下面找到对应的文件,
    • 读取这个文件的名称找到对应的 spi 接口,
    • 通过 InputStream 流将文件里面的具体实现类的全类名读取出来,
    • 根据获取到的全类名,先判断跟 spi 接口是否为同一类型,如果是的,那么就通过反射的机制构造对应的实例对象,
    • 将构造出来的实例对象添加到 Providers 的列表中
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中 文件目录监听服务中使用到了观察者模式, 基于 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调用请求后,会一直阻塞,直到内核把数据拷贝到用户空间。

image.png

  • 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,被称为 多路复用器,只要一个线程就可管理多个客户端连接。只有客户端数据到了,才会为其服务

image.png

image.png

  • AIO: Ayns IO, 异步IO模型,是基于事件和回调机制实现的,应用发起请求后会直接返回,不会一直阻塞,等后台处理完后,操作系统会通知线程进行后续操作

image.png

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这种属于文本类序列化的方式,虽然可读性比较好,但是性能较差,一般不会选择。
  • 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的区别? 有哪些实现类?底层实现是什么?

种类ListSetMap
元素是否有序有序无序无序
元素是否重复可重复唯一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的数组,这个HashMapEntry的数组,这个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:默认的初始容量 162)MAXIMUM_CAPACITY:最大容量  1 << 303)DEFAULT_LOAD_FACTOR:默认加载因子 0.754)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对象的个数
(9int threshold:阈值,当size达到阈值时,考虑扩容
(10double 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相同的结点,即e不为空,那么用新value替换原来的value,并返回旧value,结束put的方法。
  • 如果新增结点而不是替换,那么size++,并且还要重新判断size是否达到threshold阈值,如果达到,还要扩容。

image.png

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锁定写入数据
      1. 如果数量大于 树化临界值就会执行树化方法 ,treeifyBin会判断数组长度>=64,会将链表转化为红黑树。

image.png

为什么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状态。

image-20200521184149034.png

  • 状态转换:
    • 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();

死锁? 如何避免和预防死锁?

  • 线程死锁:多个线程同时被阻塞,它们中的一个或多个全部都在等待某个资源在被释放。由于线程被无限期地阻塞,因此线程不可能正常终止。

  • 产生死锁的四个必要条件

    • 互斥条件:该资源任意一个时刻只由一个线程占用
    • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
    • 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只能自己完毕后才释放资源
    • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源的关系。
  • 检测死锁的工具: 使用jmapjstack等命令查看 JVM 线程栈和堆内存的情况,如果有死锁,jstack的输出通常会有 Found one Java-level deadlock: 的字样,后面会跟着死锁相关的线程信息。另外,实际项目中还可以搭配使用topdffree等命令查看操作系统的基本情况,出现死锁可能会导致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)一起使用。

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, 也就是说每个线程有一个自己的ThreadLocalMap
  • ThreadLocalMapkey可以视作ThradLocal(实际上,key并不是ThreadLocal本身,而是弱引用),value为代码中放入的值。
  • 每个线程放值时,都会往自己的ThreadLocalMap中存,都也是以ThreadLocal为引用,在自己的map中找到,从而实现线程隔离。
  • ThreadLocalMap没有链表结构,是有数组, keyThreadLocal<?> 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的大小。

image.png

线程池中线程异常是销毁还是复用?

  • 使用execute()提交任务: 如果线程执行过程中发生异常,未捕获到异常时,会终止线程,并打印异常到日志中。线程池会创建新的线程,代替他,从而保证线程数保持不变。
  • 使用submit()提交任务:如果线程执行过程中发生异常,不会打印异常,而是返回给submit()的Future对象。当调用future.get()时,可以捕获到ExcutionException异常。当前线程不会终止,而是存在线程池中,等待后续处理。

如何设计一个根据任务优先度执行的线程池

  • 阻塞队列采用PriorityBlockingQueue(优先级无界阻塞队列)

    • 使用PriorityBlockingQueue,要求提交的任务必须具有排序能力
      • 提交的任务实现Comparable接口,通过重写compareTo()方法,指定任务的优先级规则。
      • 创建priorityBlockingQueue时,传入一个Comparator对象,指定任务的优先级规则。
  • 注意:

    • PriorityBlokingQueue,是无界队列,当大量任务访问时,可能会导致OOm。 可以重写PriorityBlockingQueue的offer方法,如果插入超过一定数量的,就返回false,不允许添加。
    • 饥饿问题,低优先级的任务可能一直无法执行
    • 性能问题:由于需要对元素排序以及保证线程安全(使用reetantlock来保证的),降低性能。

AQS

什么是AQS?

  • AQS的全称AbstractQueuedSynchronizer, 抽象队列同步器,在java.util.concurretn.locks包下。主要用于构建锁和同步器, 比如ReentrantReadWriteLock SynchronousQueue等都是基于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锁做了很多优化。
  • CopyOnWriteArrayList:线程安全的List, 在读多写少的场合性能非常好,远远好于 Vector
    • 核心:采用了写时复制 的策略,当需要修改(add,set,remove等操作) CopyOnWriteArrayList的内容时,不会直接修改原数组,而是会先创建底层数组的副本,对副本数组进行修改,修改完之后再将修改后的数组赋值回去,这样就可以保证写操作不会影响读操作了。
  • ConcurretnLinkedQueue: 高效的并发队列,使用链表实现。可以看作一个线程安全的 LinkedList,这是一个非阻塞队列
    • 主要使用CAS非阻塞算法来实现线程安全的。 适合在性能要求比较高的,同时对队列读写存在多个线程中同时进行的场景,即如果对队列加锁的成本较高则适合使用无锁的 concurrentLinkedQueue来替代。
  • BlockingQueue: 是个接口,JDK内部通过链表、数组等方式实现了这个接口。表示阻塞独立额,非常适合作为数据共享的通道。
    • ArrayBlockingQueue: 是BlockingQueue接口的有界队列实现类,底层采用数组类,一旦创建,容量不能改变,其并发控制采用了可重入锁 ReetrantLock, 不管是插入操作还是读取操作,都需要获取到锁才能拿操作。当队列容量满时,尝试将元素放入队列将导致操作阻塞,尝试从一个空队列中取一个元素也会同样阻塞。 默认情况下,不能保证线程访问队列的公平性,即不能按照线程等待的绝对时间顺序。 如果保证公平性,通常会降低吞吐量,可采用以下代码
    ArrayBlokcingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10, true);
    
    • LinkedBlockingQueue: 底层基于单项链表实现的阻塞队列。可以当作无界队列也可以当作有界队列来使用,同样满足 FIFO 的特性。为了防止LinkedBlockingQueue容量迅速增加,耗损大量内存,通常创建LinkedBlockingQueue对象时,会指定大小,如果未指定大小,容量等于 Integer.MAX_VALUE
    • PriorityBlockingQueue:支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现compareTo() 来指定元素排序规则, 或者 初始化通过构造器参数 Comparator 指定排序规则。 并发控制采用的时可重入锁ReetrantLock, 队列为无界队列。 它就是 PriorityQueue 线程安全的版本,不可以插入null值,同时,插入队列的对象必须时可比较大小的(comparable),否则报 ClassCastException异常。
  • ConcurrentSkipListMap: 跳表的实现。这是一个Map,使用跳表的数据结构进行快速查找。内部存储多张链表,所有的链表都是排序的,查找时,从顶级链表开始找,一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表,最底层存放的时原始链表。 跳表是一种利用空间换时间的算法。

Atomic 原子类

  • Atomic 指一个操作具有原子性,即该操作不可分割,不可中孤单。java.util.concurrent.atomic
  • Atomic 类依赖于 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: 原子更新引用类型里的字段。

[参考]

Java并发常见面试题总结(上) | JavaGuide