Skip to content
标签
java基础
字数
40322 字
阅读时间
170 分钟

基本数据类型

数据类型关键字内存占用取值范围
字节型byte1个字节-128~127
短整型short2个字节-32768~32767
整型int4个字节-2的31次方~2的31次方-1
长整型long8个字节2的63次方~2的63次方-1
单精度浮点数float4个字节1.4013E-45~3.4028E+38
双精度浮点数double8个字节4.9E-324~1.7977E+308
字符型char2个字节0-65535
布尔类型boolean1个字节true,false

编码表

字符数值
048
957
A65
Z90
a97
z122

字符集简介

ASCII英文字符集 1个字节
ISO8859-1西欧字符集 1个字节
BIG5台湾的大五码,表示繁体汉字 2个字节
GB2312大陆使用最早、最广的简体中文字符集 2个字节
GBKGB2312的扩展,可以表示繁体中文 2个字节
GB18030最新GBK的扩展,可以表示汉字、维吾尔文、藏文等中华民族字符 2个字节
Unicode国际通用字符集 2个字节

位运算符

运算符含义示例
~按位非(NOT)/取反b = ~a
&按位与(AND)c = a & b
|按位或(OR)c = a
^按位异或(相同为0相异为1)c = a ^ b
>>右移;左边空位补最高位即符号位b = a >> 2
>>>无符号右移,左边空位补0b = a >>> 2
<<左移;右边空位以补0b = a << 1

内存分析

内存类型特点
存放:局部变量 先进后出,自下而上存储 方法执行完毕,自动释放空间
存放new出来的对象 需要垃圾回收器来回收
方法区存放:类的信息(代码)、 static变量、字符串常量等.

访问控制符

修饰符含义
public公共的 可以被项目中所有的类访问。(项目可见性)
protected受保护的 可以被这个类本身访问;同一个包中的所有其他的类访问;被它的子类(同一个包以及不同包中的子类)访问
default/friendly默认的/友好的(包可见性) 被这个类本身访问;被同一个包中的类访问
private只能被这个类本身访问。(类可见性)

全部的修饰符都可以修饰成员(变量或成员方法);但只有public和default/friendly可以修饰类


继承

重写方法限制

  • 方法名、形参列表相同
  • 返回值类型和异常类型,子类小于等于父类。
  • 访问权限,子类大于等于父类

多态

多态子类转成父类:

  • 不能操作子类新增的成员变量和方法
  • 可以操作子类继承或重写的成员变量和方法
  • 子类重写父类,上转型调用时也是调用子类重写后的方法。
  • 父类转换成子类需保证该对象是子类对象。

特点:

成员方法:编译看左边,运行看右边

成员变量:编译看左边,运行看左边

静态方法:编译看左边,运行看左边


垃圾回收机制

  • 垃圾回收机制只回收JVM堆内存里的对象空间
  • 对其他物理连接,比如数据库连接、输入流输出流、Socket连接无能为力
  • 垃圾回收发生具有不可预知性,程序无法精确控制垃圾回收机制执行
  • 可以将对象的引用变量设置为null,暗示垃圾回收机制可以回收该对象。
  • 程序员可以通过System.gc()或者Runtime.getRuntime().gc()来通知系统进行垃圾回收,会有一些效果,但是系统是否进行垃圾回收依然不确定
  • 垃圾回收机制回收任何对象之前,总会先调用它的finalize方法(如果覆盖该方法,让一个新的引用变量重新引用该对象,则会重新激活对象)
  • 永远不要主动调用某个对象的finalize方法,应该交给垃圾回收机制调用。

线程

常用方法

方 法功 能
static Thread currentThread()得到当前线程
getName( )返回线程的名称
setName (String name)将线程的名称设置为由name指定的名称
int getPriority()获得线程的优先级数值
void setPriority()设置线程的优先级数值
void start( )调用run( )方法启动线程,开始线程的执行
void run( )存放线程体代码
isAlive()判断线程是否还“活”着,即线程是未终止

线程启动和创建

实现Callable接口

  • 与实行Runnable相比, Callable功能更强大些
  • 方法不同 可以有返回值,支持泛型的返回值
  • 可以抛出异常
  • 需要借助FutureTask,比如获取返回结果

Future接口

可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。

  • FutrueTask是Futrue接口的唯一的实现类
  • FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为 Future得到Callable的返回值

Lock和synchronized的区别

  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁

  2. Lock只有代码块锁,synchronized有代码块锁和方法锁

  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

  4. 优先使用顺序:

    Lock----同步代码块(已经进入了方法体,分配了相应资源)----同步方法(在方法体之外)

线程通信

方法名作用
final void wait()表示线程一直等待,直到其它线程通知
void wait(long timeout)线程等待指定毫秒参数的时间
final void wait(long timeout,int nanos)线程等待指定毫秒、微妙的时间
final void notify()唤醒一个处于等待状态的线程
final void notifyAll()唤醒同一个对象上所有调用wait()方法的线程,优先级别高
setPriority(int newPriority);设置线程的优先级从1~10,优先级低只是调用的概率低。并不按优先级来排序调用
join()阻塞指定线程等到另一个线程完成以后再继续执行
sleep ()使线程停止运行一段时间,将处于阻塞状态如果调用了sleep方法之后,没有其他等待执行的线程,这个时候当前线程不会马上恢复执行!
yield ()让当前正在执行线程暂停,不是阻塞线程,而是将线程转入就绪状态,如果调用了yield方法之后,没有其他等待执行的线程,这个时候当前线程就会马上恢复执行!
setDaemon()可以将指定的线程设置成后台线程,创建后台线程的线程结束时,后台线程也随之消亡,只能在线程启动之前把它设为后台线程
interrupt()并没有直接中断线程,而是需要被中断线程自己处理
stop()结束线程,不推荐使用

均是java.lang.Object类的方法 都只能在同步方法或者同步代码块中使用,否则会抛出异常


线程组

  • 线程组表示一个线程的集合。
  • 线程组也可以包含其他线程组。线程组构成一棵树。在树中,除了初始线程组外,每个线程组都有一个父线程组。
  • 顶级线程组名system,线程的默认线程组名称是main
  • 在创建之初,线程被限制到一个组里,而且不能改变到一个不同的组

线程组作用

  • 统一管理:便于对一组线程进行批量管理线程或线程组对象
  • 安全隔离:允许线程访问有关自己的线程组的信息,但是不允许它访问有关其线程组的父线程组或其他任何线程组的信息

线程池

线程池的好处

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 提高线程的可管理性:避免线程无限制创建、从而销耗系统资源,降低系统稳定性,甚至内存溢出或者CPU耗尽

线程池的应用场合

  • 需要大量线程,并且完成任务的时间端
  • 对性能要求苛刻
  • 接受突发性的大量请求

线程池结构

  • Executor:线程池顶级接口,只有一个方法
  • ExecutorService:真正的线程池接口
    • void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
    • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行Callable
    • void shutdown() :关闭连接池
  • AbstractExecutorService:基本实现了ExecutorService的所有方法
  • ThreadPoolExecutor:默认的线程池实现类
  • ScheduledThreadPoolExecutor:实现周期性任务调度的线程
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
    • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
    • Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
    • Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
    • Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行

线程池参数

  • corePoolSize:核心池的大小
    • 默认情况下,创建了线程池后,线程数为0,当有任务来之后,就会创建一个线程去执行任务。
    • 但是当线程池中线程数量达到corePoolSize,就会把到达的任务放到队列中等待。
  • maximumPoolSize:最大线程数。
    • corePoolSize和maximumPoolSize之间的线程数会自动释放,小于等于corePoolSize的不会释放。当大于了这个值就会将任务由一个丢弃处理机制来处理。
  • keepAliveTime:线程没有任务时最多保持多长时间后会终止
    • 默认只限于corePoolSize和maximumPoolSize之间的线程
  • TimeUnit:
    • keepAliveTime的时间单位
  • BlockingQueue:
    • 存储等待执行的任务的阻塞队列,有多中选择,可以是顺序队列、链式队列等
  • ThreadFactory
    • 线程工厂,默认是DefaultThreadFactory,Executors的静态内部类
  • RejectedExecutionHandler:
    • 拒绝处理任务时的策略。如果线程池的线程已经饱和,并且任务队列也已满,对新的任务应该采取什么策略。
    • 比如抛出异常、直接舍弃、丢弃队列中最旧任务等,默认是直接抛出异常。
      • 1、CallerRunsPolicy:如果发现线程池还在运行,就直接运行这个线程
      • 2、DiscardOldestPolicy:在线程池的等待队列中,将头取出一个抛弃,然后将当前线程放进去。
      • 3、DiscardPolicy:什么也不做
      • 4、AbortPolicy:java默认,抛出一个异常

Class类

对象获取:

  1. 对象的getClass()方法
  2. Class.forName()方法
  3. .class方法

类可获取类中的属性、方法、注解等信息,结合反射实现动态调用

反射

作用:

  • 动态加载类、动态获取类的信息(属性、方法、构造器)
  • 动态构造对象
  • 动态调用类和对象的任意方法、构造器
  • 动态调用和处理属性
  • 获取泛型信息
  • 处理注解

反射操作泛型

Java采用泛型擦除的机制来引入泛型。Java中的泛型仅仅是给编译器javac使用的,确保数据的安全性和免去强制类型转换的麻烦。但是,一旦编译完成,所有的和泛型有关的类型全部擦除

为了通过反射操作这些类型以迎合实际开发的需要,Java就新增了ParameterizedType,GenericArrayType,TypeVariable 和WildcardType几种类型来代表不能被归一到Class类中的类型但是又和原始类型齐名的类型

  • ParameterizedType: 表示一种参数化的类型,比如Collection<String>
  • GenericArrayType: 表示一种元素类型是参数化类型或者类型变量的数组类型
  • TypeVariable: 是各种类型变量的公共父接口
  • WildcardType: 代表一种通配符类型表达式,比如?, ? extends Number, ? super Integer【wildcard是一个单词:就是“通配符”】

反射操作注解

可以通过反射API:getAnnotations, getAnnotation获得相关的注解信息

java
//获得类的所有有效注解
Annotation[] annotations=clazz.getAnnotations();
for (Annotation a : annotations) {
    System.out.println(a);
}
//获得类的指定的注解
SxtTable st = (SxtTable) clazz.getAnnotation(SxtTable.class);
System.out.println(st.value());

//获得类的属性的注解
Field f = clazz.getDeclaredField("studentName");
SxtField sxtField = f.getAnnotation(SxtField.class);
System.out.println(sxtField.columnName()+"--"+sxtField.type()+"--"+sxtField.length());

反射性能问题

setAccessible

启用和禁用访问安全检查的开关,值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。值为 false 则指示反射的对象应该实施 Java 语言访问检查。并不是为true就能访问为false就不能访问。

禁止安全检查,可以提高反射的运行速度

脚本引擎

脚本引擎可以使得 Java 应用程序可以通过一套固定的接口与各种脚本引擎交互,从而达到在 Java 平台上调用各种脚本语言的目的。

Java 脚本 API 是连通 Java 平台和脚本语言的桥梁。

可以把一些复杂异变的业务逻辑交给脚本语言处理,这又大大提高了 开发效率

使用

java
//获取脚本引擎对象
ScriptEngineManager sem = new ScriptEngineManager();
ScriptEngine engine = sem.getEngineByName("javascript");

Java可以使用各种不同的实现,从而通用的调用js、groovy、python等脚本

Rhino 是一种使用 Java 语言编写的 JavaScript 的开源实现.

通过脚本引擎的运行上下文在脚本和 Java 平台间交换数据。

类加载过程

类加载机制

JVM把class文件加载到内存,并对数据进行校验、解析和初始化,最终形成JVM可以直接使用的Java类型的过程。

加载

将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区中的运行时数据结构,在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口。 这个过程需要类加载器参与。

  • 全盘负责:就是当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载
  • 器负责载入,除非显示使用另外一个类加载器来载入
  • 父类委托:就是当一个类加载器负责加载某个Class时,先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
  • 缓存机制:保证所有加载过的Class都会被缓存,当程序需要使用某个Class对象时,类加载器先从缓存区中搜索该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存储到缓存区

加载全过程

  • 链接 将Java类的二进制代码合并到JVM的运行状态之中的过程
    • 验证: – 确保加载的类信息符合JVM规范,没有安全方面的问题。
    • 准备: – 正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配
    • 解析 – 虚拟机常量池内的符号引用替换为直接引用的过程
  • 初始化
    • 初始化阶段是执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的。
    • 当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先出发其父类的初始化
    • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。
    • 成员变量也将被初始化
  • 类的主动引用(一定会发生类的初始化)
    • new一个类的对象
    • 调用类的静态成员(除了final常量)和静态方法
    • 使用java.lang.reflect包的方法对类进行反射调用
    • 当虚拟机启动,java Hello,则一定会初始化Hello类。说白了就是先启动main方法所在的类
    • 当初始化一个类,如果其父类没有被初始化,则先会初始化他的父类
  • 类的被动引用(不会发生类的初始化)
    • 当访问一个静态域时,只有真正声明这个域的类才会被初始化
    • 通过子类引用父类的静态变量,不会导致子类初始化
    • 通过数组定义类引用,不会触发此类的初始化
    • 引用常量不会触发此类的初始化(常量在编译阶段就存入调用类的常量池中了)

深入类加载器

类加载器可以分为两种:第一种是Java虚拟机自带的类加载器,分别为启动类加载器、扩展类加载器和系统类加载器。第二种是用户自定义的类加载器,是java.lang.ClassLoader的子类实例。

类加载器作用

将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区中的运行时数据结构,在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口

类缓存

标准的Java SE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过,JVM垃圾收集器可以回收这些Class对象。

作用

java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个Java 类,即 java.lang.Class类的一个实例。

除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文 件和配置文件等。

相关方法

  • getParent() 返回该类加载器的父类加载器。

  • loadClass(String name) 加载名称为 name的类,返回的结果是 java.lang.Class类的实例。不需要覆盖掉该方法,应覆盖findClass方法。

  • findClass(String name) 查找名称为 name的类,返回的结果是 java.lang.Class类的实例。ClassLoader中给findClass一个错误的实现。

  • findLoadedClass(String name) 查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。

  • defineClass(String name, byte[] b, int off, int len) 把字节数组 b中的内容转换成 Java 类,返回的结果是java.lang.Class类的实例。这个方法被声明为 final的。

  • resolveClass(Class<?> c) 链接指定的 Java 类。

  • 对于以上给出的方法,表示类名称的 name参数的值是类的二进制名称。需要注意的是内部类的表示,如com.example.Sample1com.example.SampleInner等表示方式。

类加载器的层次结构

  • 引导类加载器(bootstrap class loader)
    • 最底层的类加载器,是虚拟机的一部分,它是由C++语言实现的,且没有父加载器,也没有继承java.lang.ClassLoader类,根加载器打印出来为null
    • 它用来加载 Java 的核心库(JAVA_HOME/jre/lib/rt.jar,或sun.boot.class.path路径下的内容),是用原生代码来实现的,并不继承自 java.lang.ClassLoader。
    • 加载扩展类和应用程序类加载器。并指定他们的父类加载器。
  • 扩展类加载器(extensions class loader)
    • 用来加载 Java 的扩展库(JAVA_HOME/jre/ext/*.jar,或java.ext.dirs路径下的内容) 。
    • Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java类。开发者可以直接使用标准扩展类加载器。
    • 由sun.misc.Launcher$ExtClassLoader实现
  • 应用程序类加载器(application class loader)
    • 它根据 Java 应用的类路径(classpath,java.class.path 路径下的内容)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。
    • 由sun.misc.Launcher$AppClassLoader实现
  • 自定义类加载器
  • 开发人员可以通过继承 java.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求。

类加载器的代理模式

  • 代理模式
    • 交给其他加载器来加载指定的类
  • 双亲委托机制
    • 就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次追溯,直到最高的爷爷辈的,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
    • 双亲委托机制是为了保证 Java 核心库的类型安全。
      • 这种机制就保证不会出现用户自己能定义java.lang.Object类的情况。
    • 类加载器除了用于加载类,也是安全的最基本的屏障。
  • 双亲委托机制是代理模式的一种
    • 并不是所有的类加载器都采用双亲委托机制。
    • tomcat服务器类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的

URLClassLoader

扩展了ClassLoader,能够从本地或者网络上指定的位置加载类。我们可以使用该类作为自定义的类加载器使用。

构造方法:

public URLClassLoader(URL[] urls):指定要加载的类所在的URL地址,父类加载器默认为系统类加载器。

public URLClassLoader(URL[] urls, ClassLoader parent):指定要加载的类所在的URL地址,并指定父类加载器。

案例1:加载磁盘上的类

java
public static void main(String[] args) throws Exception{
		File file = new File("d:/");
		URI uri = file.toURI();
		URL url = uri.toURL();
        URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
        System.out.println(classLoader.getParent());
        Class aClass = classLoader.loadClass("com.itheima.Demo");
        Object obj = aClass.newInstance();
    }

案例2:加载网络上的类

java
public static void main(String[] args) throws Exception{
		URL url = new URL("http://localhost:8080/examples/");
        URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
        System.out.println(classLoader.getParent());
        Class aClass = classLoader.loadClass("com.itheima.Demo");
        aClass.newInstance();
}

自定义类加载器

  • 文件系统类加载器

  • 自定义类加载器的流程:

    • 1、首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回;否则转入步骤2
    • 2、委派类加载请求给父类加载器(更准确的说应该是双亲类加载器,真个虚拟机中各种类加载器最终会呈现树状结构),如果父类加载器能够完成,则返回父类加载器加载的Class实例;否则转入步骤3
    • 3、调用本类加载器的findClass(…)方法,试图获取对应的字节码,如果获取的到,则调用defineClass(…)导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异常给loadClass(…), loadClass(…)转抛异常,终止加载过程(注意:这里的异常种类不止一种)。
    • 注意:被两个类加载器加载的同一个类,JVM不认为是相同的类。
  • 文件类加载器

  • 网络类加载器

  • 加密解密类加载器(取反操作,DES对称加密解密)

  • 文件类加载器示例

    java
    package com.itheima.base.classloader;
    
    import sun.applet.Main;
    
    import java.io.*;
    
    public class MyFileClassLoader extends ClassLoader {
        private String directory;//被加载的类所在的目录
    
        /**
         * 指定要加载的类所在的文件目录
         * @param directory
         */
        public MyFileClassLoader(String directory,ClassLoader parent){
            super(parent);
            this.directory = directory;
        }
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                //把类名转换为目录
                String file = directory+File.separator+name.replace(".", File.separator)+".class";
                //构建输入流
                InputStream in = new FileInputStream(file);
                //存放读取到的字节数据
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                byte buf[] = new byte[1024];
                int len = -1;
                while((len=in.read(buf))!=-1){
                    baos.write(buf,0,len);
                }
                byte data[] = baos.toByteArray();
                in.close();
                baos.close();
                return defineClass(name,data,0,data.length);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    
        public static void main(String[] args) throws Exception {
            MyFileClassLoader myFileClassLoader = new MyFileClassLoader("d:/");
            Class clazz = myFileClassLoader.loadClass("com.itheima.Demo");
            clazz.newInstance();
        }
    }
  • 自定义网络类加载器示例

    java
    package com.itheima.base.classloader;
    
    import java.io.ByteArrayOutputStream;
    import java.io.File;
    import java.io.InputStream;
    import java.net.MalformedURLException;
    import java.net.URL;
    
    public class MyURLClassLoader extends ClassLoader {
        private String url;
    
        public MyURLClassLoader(String url) {
            this.url = url;
        }
    
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                String path = url+ "/"+name.replace(".","/")+".class";
                URL url = new URL(path);
                InputStream inputStream = url.openStream();
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                int len = -1;
                byte buf[] = new byte[1024];
                while((len=inputStream.read(buf))!=-1){
                    baos.write(buf,0,len);
                }
                byte[] data = baos.toByteArray();
                inputStream.close();
                baos.close();
                return defineClass(name,data,0,data.length);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
        public static void main(String[] args) throws Exception{
            MyURLClassLoader classLoader = new MyURLClassLoader("http://localhost:8080/examples");
            Class clazz = classLoader.loadClass("com.itheima.Demo");
            clazz.newInstance();
        }
    }
  • 热部署类加载器

    当我们调用loadClass方法加载类时,会采用双亲委派模式,即如果类已经被加载,就从缓存中获取,不会重新加载。如果同一个class被同一个类加载器多次加载,则会报错。因此,我们要实现热部署让同一个class文件被不同的类加载器重复加载即可。但是不能调用loadClass方法,而应该调用findClass方法,避开双亲委托模式,从而实现同一个类被多次加载,实现热部署。

    java
    MyFileClassLoader myFileClassLoader1 = new MyFileClassLoader("d:/",null);
    MyFileClassLoader myFileClassLoader2 = new MyFileClassLoader("d:/",myFileClassLoader1);
    Class clazz1 = myFileClassLoader1.loadClass("com.itheima.Demo");
    Class clazz2 = myFileClassLoader2.loadClass("com.itheima.Demo");
    System.out.println("class1:"+clazz1.hashCode());
    System.out.println("class2:"+clazz2.hashCode());
    结果:class1和class2的hashCode一致
    
    MyFileClassLoader myFileClassLoader1 = new MyFileClassLoader("d:/",null);
    MyFileClassLoader myFileClassLoader2 = new MyFileClassLoader("d:/",myFileClassLoader1);
    Class clazz3 = myFileClassLoader1.findClass("com.itheima.Demo");
    Class clazz4 = myFileClassLoader2.findClass("com.itheima.Demo");
    System.out.println("class3:"+clazz3.hashCode());
    System.out.println("class4:"+clazz4.hashCode());
    结果:class1和class2的hashCode不一致

类的显示加载和隐式加载

类的加载方式是指虚拟机将class文件加载到内存的方式。

​ 显式加载是指在java代码中通过调用ClassLoader加载class对象,比如Class.forName(String name);this.getClass().getClassLoader().loadClass()加载类。

​ 隐式加载指不需要在java代码中明确调用加载的代码,而是通过虚拟机自动加载到内存中。比如在加载某个class时,该class引用了另外一个类的对象,那么这个对象的字节码文件就会被虚拟机自动加载到内存中。

线程上下文加载器

  • 双亲委托机制以及默认类加载器的问题

    • 一般情况下, 保证同一个类中所关联的其他类都是由当前类的类加载器所加载的.。 比如,ClassA本身在Ext下找到,那么他里面new出来的一些类也就只能用Ext去查找了(不会低一个级别),所以有些明明App可以找到的,却找不到了。
    • JDBC API,他有实现的driven部分(mysql/sql server),我们的JDBC API都是由Boot或者Ext来载入的,但是JDBC driver却是由Ext或者App来载入,那么就有可能找不到driver了。在Java领域中,其实只要分成这种Api+SPI(Service Provide Interface,特定厂商提供)的,都会遇到此问题。
    • 常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers 包中。SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。
    • SPI的接口类是由根类加载器加载的,Bootstrap类加载器无法直接加载位于classpath下的具体实现类。由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载SPI的具体实现类。在这种情况下,java提供了线程上下文类加载器用于解决以上问题。
    • 线程上下文类加载器可以通过java.lang.Thread的getContextClassLoader()来获取,或者通过setContextClassLoader(ClassLoader cl)来设置线程的上下文类加载器。如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类或资源。
  • 通常当你需要动态加载资源的时候 , 你至少有三个 ClassLoader 可以选择 :

    • 1.系统类加载器或叫作应用类加载器 (system classloader or application classloader)
    • 2.当前类加载器
    • 3.当前线程类加载器

    img1574730595189

  • 当前线程类加载器是为了抛弃双亲委派加载链模式。

    • 每个线程都有一个关联的上下文类加载器。如果你使用new Thread()方式生成新的线程,新线程将继承其父线程的上下文类加载器。如果程序对线程上下文类加载器没有任何改动的话,程序中所有的线程将都使用系统类加载器作为上下文类加载器。
  • Thread.currentThread().getContextClassLoader()

TOMCAT服务器的类加载机制

  • 一切都是为了安全!
    • TOMCAT不能使用系统默认的类加载器。
      • 如果TOMCAT跑你的WEB项目使用系统的类加载器那是相当危险的,你可以直接是无忌惮是操作系统的各个目录了。
      • 对于运行在 Java EE™容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。
    • 每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式(不同于前面说的双亲委托机制),所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。但也是为了保证安全,这样核心库就不在查询范围之内。
  • 为了安全TOMCAT需要实现自己的类加载器。
    • 我可以限制你只能把类写在指定的地方,否则我不给你加载!

OSGI原理介绍

  • OSGi™是 Java 上的动态模块系统。它为开发人员提供了面向服务和基于组件的运行环境,并提供标准的方式用来管理软件的生命周期。
  • OSGi 已经被实现和部署在很多产品上,在开源社区也得到了广泛的支持。Eclipse就是基于 OSGi 技术来构建的。
  • 原理:
  • OSGi 中的每个模块(bundle)都包含 Java 包和类。模块可以声明它所依赖的需要导入(import)的其它模块的 Java 包和类(通过 Import-Package),也可以声明导出(export)自己的包和类,供其它模块使用(通过 Export-Package)。也就是说需要能 够隐藏和共享一个模块中的某些 Java 包和类。这是通过 OSGi 特有的类加载器机制来实现的。OSGi 中的每个模块都有对应的一个类加载器。它负责加载模块自己包含的Java 包和类。当它需要加载 Java 核心库的类时(以 java开头的包和类),它会代理给父类加载器(通常是启动类加载器)来完成。当它需要加载所导入的 Java 类时,它会代理给导出此 Java 类的模块来完成加载。模块也可以显式的声明某些 Java 包和类,必须由父类加载器来加载。只需要设置系统属性 org.osgi.framework.bootdelegation的值即可。

注解

注解解析图

作用

  • 不是程序本身,可以对程序作出解释。(这一点,跟注释没什么区别)
  • 可以被其他程序(比如:编译器等)读取。(注解信息处理流程,是注解和注释的重大区别。如果没有注解信息处理流程,则注解毫无意义)
  • 可以附加在package, class, method, field等上面,相当于给它们添加了额外的辅助信息,我们可以通过反射机制编程实现对这些元数据的访问

内置注解

  • @Override

    定义在java.lang.Override中,此注释只适用于修辞方法,表示一个方法声明打算重写超类中的另一个方法声明。

  • @Deprecated

    定义在java.lang.Deprecated中,此注释可用于修辞方法、属性、类表示不鼓励程序员使用这样的元素,通常是因为它很危险或存在更好的选择

  • @SuppressWarnings

    定义在java.lang.SuppressWarnings中,用来抑制编译时的警告信息与前两个注释有所不同,你需要添加一个参数才能正确使用,这些参数值都是已经定义好了的,我们选择性的使用就好了,参数如下

    参数说明
    deprecation使用了过时的类或方法的警告
    unchecked执行了未检查的转换时的警告,如使用集合时未指定泛型
    fallthrough当在switch语句使用时发生case穿透
    path在类路径、源文件路径等中有不存在的路径的警告
    serial当在可序列化的类上缺少serialVersionUID定义时的警告
    finally任何finally自居不能完成时的警告
    all关于以上所有的警告

    如:@SuppressWarnings(value={"unchecked", "deprecation"})

自定义注解

  • 使用@interface自定义注解时,自动继承了java.lang.annotation.Annotation接口

  • 要点:

    @interface用来声明一个注解

  • 格式为:

    public @interface 注解名

    其中的每一个方法实际上是声明了一个配置参数。

    方法的名称就是参数的名称

    返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)

    可以通过default来声明参数的默认值。

    如果只有一个参数成员,一般参数名为value

  • 注意:

注解元素必须要有值。我们定义注解元素时,经常使用空字符串、0作为默认值。也经常使用负数(比如:-1)表示不存在的含

元注解

元注解的作用就是负责注解其他注解。 Java定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。

  • @Target

    用于描述注解的使用范围(即:被描述的注解可以用在什么地方)

    取值ElementType修饰范围
    PACKAGEpackage包
    TYPE类、接口、枚举、Annotation类型
    CONSTRUCTOR:用于描述构造器
    FIELD:用于描述域
    METHOD:用于描述方法
    类型成员(方法、构造方法、成员变量、枚举值)
    LOCAL_VARIABLE:用于描述局部变量
    PARAMETER:用于描述参数
    方法参数和本地变量

    如:@Target(value=ElementType.TYPE)

  • @Retention

    表示需要在什么级别保存该注释信息,用于描述注解的生命周期

    取值RetentionPolicy作用
    SOURCE在源文件中有效(源文件保留)
    CLASS在class文件中有效(class保留)
    RUNTIME在运行时有效(运行时保留)
    为Runtime可以被反射机制读取

    如:

    java
    try {
                Class clazz = Class.forName("com.bjsxt.test.annotation.SxtStudent");
                 
                //获得类的所有有效注解
                Annotation[] annotations=clazz.getAnnotations();
                for (Annotation a : annotations) {
                    System.out.println(a);
                }
                //获得类的指定的注解
                SxtTable st = (SxtTable) clazz.getAnnotation(SxtTable.class);
                System.out.println(st.value());
                 
                //获得类的属性的注解
                Field f = clazz.getDeclaredField("studentName");
                SxtField sxtField = f.getAnnotation(SxtField.class);
                System.out.println(sxtField.columnName()+"--"+sxtField.type()+"--"+sxtField.length());
                 
                //根据获得的表名、字段的信息,拼出DDL语句,然后,使用JDBC执行这个SQL,在数据库中生成相关的表
                 
            } catch (Exception e) {
                e.printStackTrace();
    }
  • @Documented

    注解表明这个注解应该被 javadoc工具记录. 默认情况下,javadoc是不包括注解的. 但如果声明注解时指定了 @Documented,则它会被 javadoc 之类的工具处理, 所以注解类型信息也会被包括在生成的文档中,是一个标记注解,没有成员。

  • @Inherited

    类继承关系中@Inherited的作用

    类继承关系中,子类会继承父类使用的注解中被@Inherited修饰的注解

    接口继承关系中@Inherited的作用

    接口继承关系中,子接口不会继承父接口中的任何注解,不管父接口中使用的注解有没有被@Inherited修饰

    类实现接口关系中@Inherited的作用

    类实现接口时不会继承任何接口中定义的注解

Stream流

主要方法

stream.distinct()  去重
stream.filter((p)->p.getAge()>=15) 比较过滤
stream.limit(2)   限制个数
stream.sorted((o1,o2)->o1.getAge()-o2.getAge())  排序比较
stream.min((o1,o2)->o1.getAge()-o2.getAge()).get()  获取最小值
stream.max((o1,o2)->o1.getAge()-o2.getAge()).get();   获取最大值
stream.parallel并行流().mapToInt对流中元素抽取(p->p.getAge()).sum()  对流中的特定元素进行抽取并求和

汇总流
IntStream is = stream.parallel().mapToInt((p)->p.getAge());
IntSummaryStatistics sum = is.summaryStatistics();
sum.getCount()   获取数量
sum.getAverage()  获取平均值
sum.getMax()  获取最大值
sum.getMin()  获取最小值
sum.getSum()  求和

接口

接口,是Java语言中一种引用类型,是方法的集合,如果说类的内部封装了成员变量、构造方法和成员方法,那么

接口的内部主要就是封装了方法,包含抽象方法(JDK 7及以前),默认方法和静态方法(JDK 8),私有方法 (JDK 9)。

接口中,无法定义成员变量,但是可以定义常量,其值不可以改变,默认使用public static fifinal修饰。

接口中,没有构造方法,不能创建对象。

接口中,没有静态代码块。

抽象方法

多实现存在重名抽象方法是,只需重写一次。

默认方法

默认方法可以被集成和重写。

多实现接口中,有多个默认方法时,实现类都可继承使用。如果默认方法有重名的,必须重写一次。

静态方法

静态与.class 文件相关,只能使用接口名调用,不可以通过实现类的类名或者实现类的对象调用

私有方法

私有方法:只有默认方法可以调用。

私有静态方法:默认方法和静态方法可以调用。

多实现优先级

当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的默认方法重名,子类就近选择执

行父类的成员方法。

多继承

一个接口能继承另一个或者多个接口,这和类之间的继承比较相似。接口的继承使用 extends 关键字,子接口继

承父接口的方法。如果父接口中的默认方法有重名的,那么子接口需要重写一次

集合

ArrayList

扩容规则**

  1. ArrayList() 会使用长度为零的数组

  2. ArrayList(int initialCapacity) 会使用指定容量的数组

  3. public ArrayList(Collection<? extends E> c) 会使用 c 的大小作为数组容量

  4. add(Object o) 首次扩容为 10,再次扩容为上次容量的 1.5 倍

  5. addAll(Collection c) 没有元素时,扩容为 Math.max(10, 实际元素个数),有元素时为 Math.max(原容量 1.5 倍, 实际元素个数)

其中第 4 点必须知道,其它几点视个人情况而定

提示

  • 测试代码见 TestArrayList ,这里不再列出

  • java
    
    import java.lang.reflect.Field;
    import java.util.ArrayList;
    import java.util.List;
    
    // --add-opens java.base/java.util=ALL-UNNAMED
    public class TestArrayList {
        public static void main(String[] args) {
            System.out.println(arrayListGrowRule(30));
    //        testAddAllGrowEmpty();
            testAddAllGrowNotEmpty();
        }
    
        private static List<Integer> arrayListGrowRule(int n) {
            List<Integer> list = new ArrayList<>();
            int init = 0;
            list.add(init);
            if (n >= 1) {
                init = 10;
                list.add(init);
            }
            for (int i = 1; i < n; i++) {
                init += (init) >> 1;
                list.add(init);
            }
            return list;
        }
    
        private static void testAddAllGrowEmpty() {
            ArrayList<Integer> list = new ArrayList<>();
    //        list.addAll(List.of(1, 2, 3));
    //        list.addAll(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11));
            System.out.println(length(list));
        }
    
        private static void testAddAllGrowNotEmpty() {
            ArrayList<Integer> list = new ArrayList<>();
            for (int i = 0; i < 10; i++) {
                list.add(i);
            }
    //        list.addAll(List.of(1, 2, 3));
            list.addAll(List.of(1, 2, 3, 4, 5, 6));
            System.out.println(length(list));
        }
    
        public static int length(ArrayList<Integer> list) {
            try {
                Field field = ArrayList.class.getDeclaredField("elementData");
                field.setAccessible(true);
                return ((Object[]) field.get(list)).length;
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
    
    
    }
  • 注意的是,示例中用反射方式来更直观地反映 ArrayList 的扩容特征,但从 JDK 9 由于模块化的影响,对反射做了较多限制,需要在运行测试代码时添加 VM 参数 --add-opens java.base/java.util=ALL-UNNAMED 方能运行通过,后面的例子都有相同问题

代码说明

  • add(Object) 方法的扩容规则,输入参数 n 代表打印多少次扩容后的数组长度
java

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

// --add-opens java.base/java.util=ALL-UNNAMED
public class TestArrayList {
    public static void main(String[] args) {
        System.out.println(arrayListGrowRule(30));
//        testAddAllGrowEmpty();
        testAddAllGrowNotEmpty();
    }

    private static List<Integer> arrayListGrowRule(int n) {
        List<Integer> list = new ArrayList<>();
        int init = 0;
        list.add(init);
        if (n >= 1) {
            init = 10;
            list.add(init);
        }
        for (int i = 1; i < n; i++) {
            init += (init) >> 1;
            list.add(init);
        }
        return list;
    }

    private static void testAddAllGrowEmpty() {
        ArrayList<Integer> list = new ArrayList<>();
//        list.addAll(List.of(1, 2, 3));
//        list.addAll(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11));
        System.out.println(length(list));
    }

    private static void testAddAllGrowNotEmpty() {
        ArrayList<Integer> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }
//        list.addAll(List.of(1, 2, 3));
        list.addAll(List.of(1, 2, 3, 4, 5, 6));
        System.out.println(length(list));
    }

    public static int length(ArrayList<Integer> list) {
        try {
            Field field = ArrayList.class.getDeclaredField("elementData");
            field.setAccessible(true);
            return ((Object[]) field.get(list)).length;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


}

Iterator

Fail-Fast 与 Fail-Safe

  • ArrayList 是 fail-fast 的典型代表,遍历的同时不能修改,尽快失败

  • CopyOnWriteArrayList 是 fail-safe 的典型代表,遍历的同时可以修改,原理是读写分离

java

import java.util.ArrayList;
import java.util.concurrent.CopyOnWriteArrayList;

public class FailFastVsFailSafe {
    // fail-fast 一旦发现遍历的同时其它人来修改,则立刻抛异常
    // fail-safe 发现遍历的同时其它人来修改,应当能有应对策略,例如牺牲一致性来让整个遍历运行完成

    private static void failFast() {
        ArrayList<Student> list = new ArrayList<>();
        list.add(new Student("A"));
        list.add(new Student("B"));
        list.add(new Student("C"));
        list.add(new Student("D"));
        for (Student student : list) {
            System.out.println(student);
        }
        System.out.println(list);
    }

    private static void failSafe() {
        CopyOnWriteArrayList<Student> list = new CopyOnWriteArrayList<>();
        list.add(new Student("A"));
        list.add(new Student("B"));
        list.add(new Student("C"));
        list.add(new Student("D"));
        for (Student student : list) {
            System.out.println(student);
        }
        System.out.println(list);
    }

    static class Student {
        String name;

        public Student(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }

    public static void main(String[] args) {
        failSafe();
    }
}

LinkedList

LinkedList

  1. 基于双向链表,无需连续内存
  2. 随机访问慢(要沿着链表遍历)
  3. 头尾插入删除性能高
  4. 占用内存多

ArrayList

  1. 基于数组,需要连续内存
  2. 随机访问快(指根据下标访问)
  3. 尾部插入、删除性能可以,其它部分插入、删除都会移动数据,因此性能会低
  4. 可以利用 cpu 缓存,局部性原理

代码说明

  • randomAccess 对比随机访问性能
  • addMiddle 对比向中间插入性能
  • addFirst 对比头部插入性能
  • addLast 对比尾部插入性能
  • linkedListSize 打印一个 LinkedList 占用内存
  • arrayListSize 打印一个 ArrayList 占用内存
java

import org.openjdk.jol.info.ClassLayout;
import org.springframework.util.StopWatch;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

import static day01.list.TestArrayList.length;
import static day01.sort.Utils.randomArray;

@SuppressWarnings("all")
public class ArrayListVsLinkedList {
    public static void main(String[] args) {
        int n = 1000;
        int insertIndex = n;
        for (int i = 0; i < 1; i++) {
            int[] array = randomArray(n);
            List<Integer> list1 = Arrays.stream(array).boxed().collect(Collectors.toList());
            LinkedList<Integer> list2 = new LinkedList<>(list1);

//            randomAccess(list1, list2, n / 2);
//            addFirst(list1,list2);
//            addMiddle(list1, list2, n / 2);
//            addLast(list1,list2);
            arrayListSize((ArrayList<Integer>) list1);
            linkedListSize(list2);
        }
    }

    static void linkedListSize(LinkedList<Integer> list) {
        try {
            long size = 0;
            ClassLayout layout = ClassLayout.parseInstance(list);
//            System.out.println(layout.toPrintable());
            size += layout.instanceSize();
            Field firstField = LinkedList.class.getDeclaredField("first");
            firstField.setAccessible(true);
            Object first = firstField.get(list);
//            System.out.println(ClassLayout.parseInstance(first).toPrintable());
            long nodeSize = ClassLayout.parseInstance(first).instanceSize();
            size += (nodeSize * (list.size() + 2));
            long elementSize = ClassLayout.parseInstance(list.getFirst()).instanceSize();
            System.out.println("LinkedList size:[" + size + "],per Node size:[" + nodeSize + "],per Element size:[" + elementSize * list.size() + "]");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static void arrayListSize(ArrayList<Integer> list) {
        try {
            long size = 0;
            ClassLayout layout = ClassLayout.parseInstance(list);
//            System.out.println(layout.toPrintable());
            size += layout.instanceSize();
            Field elementDataField = ArrayList.class.getDeclaredField("elementData");
            elementDataField.setAccessible(true);
            Object elementData = elementDataField.get(list);
//            System.out.println(ClassLayout.parseInstance(elementData).toPrintable());
            size += ClassLayout.parseInstance(elementData).instanceSize();
            long elementSize = ClassLayout.parseInstance(list.get(0)).instanceSize();
            System.out.println("ArrayList size:[" + size + "],array length:[" + length(list) + "],per Element size:[" + elementSize * list.size() + "]");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static void randomAccess(List<Integer> list1, LinkedList<Integer> list2, int mid) {
        StopWatch sw = new StopWatch();
        sw.start("ArrayList");
        list1.get(mid);
        sw.stop();

        sw.start("LinkedList");
        list2.get(mid);
        sw.stop();

        System.out.println(sw.prettyPrint());
    }

    private static void addMiddle(List<Integer> list1, LinkedList<Integer> list2, int mid) {
        StopWatch sw = new StopWatch();
        sw.start("ArrayList");
        list1.add(mid, 100);
        sw.stop();

        sw.start("LinkedList");
        list2.add(mid, 100);
        sw.stop();

        System.out.println(sw.prettyPrint());
    }

    private static void addFirst(List<Integer> list1, LinkedList<Integer> list2) {
        StopWatch sw = new StopWatch();
        sw.start("ArrayList");
        list1.add(0, 100);
        sw.stop();

        sw.start("LinkedList");
        list2.addFirst(100);
        sw.stop();

        System.out.println(sw.prettyPrint());
    }

    private static void addLast(List<Integer> list1, LinkedList<Integer> list2) {
        StopWatch sw = new StopWatch();
        sw.start("ArrayList");
        list1.add(100);
        sw.stop();

        sw.start("LinkedList");
        list2.add(100);
        sw.stop();

        System.out.println(sw.prettyPrint());
    }
}

HashMap

1)基本数据结构

  • 1.7 数组 + 链表
  • 1.8 数组 + (链表 | 红黑树)

特点:

1.存取无序的

2.键和值位置都可以是null,但是键位置只能是一个null

3.键位置是唯一的,底层的数据结构控制键的

4.jdk1.8前数据结构是:链表 + 数组 jdk1.8之后是 : 链表 + 数组 + 红黑树

5.阈值(边界值) > 8 并且数组长度大于64,才将链表转换为红黑树,变为红黑树的目的是为了高效的查询。

2)树化与退化

树化意义

  • 红黑树用来避免 DoS 攻击,防止链表超长时性能下降,树化应当是偶然情况,是保底策略
  • hash 表的查找,更新的时间复杂度是 O(1),而红黑树的查找,更新的时间复杂度是 O(log2n),TreeNode 占用空间也比普通 Node 的大,如非必要,尽量还是使用链表
  • hash 值如果足够随机,则在 hash 表内按泊松分布,在负载因子 0.75 的情况下,长度超过 8 的链表出现概率是 0.00000006,树化阈值选择 8 就是为了让树化几率足够小

树化规则

  • 当链表长度超过树化阈值 8 时,先尝试扩容来减少链表长度,如果数组容量已经 >=64,才会进行树化

退化规则

  • 情况1:在扩容时如果拆分树时,树元素个数 <= 6 则会退化链表
  • 情况2:remove 树节点时,若 root、root.left、root.right、root.left.left 有一个为 null ,也会退化为链表

3)索引计算

索引计算方法

  • 首先,计算对象的 hashCode()

    对于key的hashCode做hash操作,无符号右移16位然后做异或运算。
    还有平方取中法,伪随机数法和取余数法。这三种效率都比较低。而无符号右移16位异或运算效率是最高的。
  • 再进行调用 HashMap 的 hash() 方法进行二次哈希

    • 二次 hash() 是为了综合高位数据,让哈希分布更为均匀
  • 最后 & (capacity – 1) 得到索引

数组容量为何是 2 的 n 次幂

  1. 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
  2. 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap

注意

  • 二次 hash 是为了配合 容量是 2 的 n 次幂 这一设计前提,如果 hash 表的容量不是 2 的 n 次幂,则不必二次 hash
  • 容量是 2 的 n 次幂 这一设计计算索引效率更好,但 hash 的分散性就不好,需要二次 hash 来作为补偿,没有采用这一设计的典型例子是 Hashtable

4)put 与扩容

说明:

1.size表示 HashMap中K-V的实时数量 , 注意这个不等于数组的长度 。

2.threshold( 临界值) =capacity(容量) * loadFactor( 加载因子 )。这个值是当前已占用数组长度的最大值。size超过这个临界值就重新resize(扩容),扩容后的 HashMap 容量是之前容量的两倍 。

put 流程

  1. HashMap 是懒惰创建数组的,首次使用才创建数组
  2. 计算索引(桶下标)
  3. 如果桶下标还没人占用,创建 Node 占位返回
  4. 如果桶下标已经有人占用
    1. 已经是 TreeNode 走红黑树的添加或更新逻辑
    2. 是普通 Node,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑
  5. 返回前检查容量是否超过阈值,一旦超过进行扩容

1.7 与 1.8 的区别

  1. 链表插入节点时,1.7 是头插法,1.8 是尾插法

  2. 1.7 是大于等于阈值且没有空位时才扩容,而 1.8 是大于阈值就扩容

  3. 1.8 在扩容计算 Node 索引时,会优化

扩容(加载)因子为何默认是 0.75f

  1. 在空间占用与查询时间之间取得较好的权衡
  2. 大于这个值,空间节省了,但链表就会比较长影响性能
  3. 小于这个值,冲突减少了,但扩容就会更频繁,空间占用也更多

5)并发问题

扩容死链(1.7 会存在)

1.7 源码如下:

java
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}
  • e 和 next 都是局部变量,用来指向当前节点和下一个节点
  • 线程1(绿色)的临时变量 e 和 next 刚引用了这俩节点,还未来得及移动节点,发生了线程切换,由线程2(蓝色)完成扩容和迁移

image-20210831084325075

  • 线程2 扩容完成,由于头插法,链表顺序颠倒。但线程1 的临时变量 e 和 next 还引用了这俩节点,还要再来一遍迁移

image-20210831084723383

  • 第一次循环
    • 循环接着线程切换前运行,注意此时 e 指向的是节点 a,next 指向的是节点 b
    • e 头插 a 节点,注意图中画了两份 a 节点,但事实上只有一个(为了不让箭头特别乱画了两份)
    • 当循环结束是 e 会指向 next 也就是 b 节点

image-20210831084855348

  • 第二次循环
    • next 指向了节点 a
    • e 头插节点 b
    • 当循环结束时,e 指向 next 也就是节点 a

image-20210831085329449

  • 第三次循环
    • next 指向了 null
    • e 头插节点 a,a 的 next 指向了 b(之前 a.next 一直是 null),b 的 next 指向 a,死链已成
    • 当循环结束时,e 指向 next 也就是 null,因此第四次循环时会正常退出

image-20210831085543224

数据错乱(1.7,1.8 都会存在)

java

import java.util.HashMap;

public class HashMapMissData {
    public static void main(String[] args) throws InterruptedException {

        HashMap<String, Object> map = new HashMap<>();
        Thread t1 = new Thread(() -> {
            map.put("a", new Object()); // 97  => 1
        }, "t1");

        Thread t2 = new Thread(() -> {
            map.put("1", new Object()); // 49 => 1
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(map);
    }
}

HashMapDistribution 演示 map 中链表长度符合泊松分布

java

import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

// --add-opens java.base/java.util=ALL-UNNAMED
public class HashMapDistribution {

    public static void main(String[] args) throws IOException {
        Object value = new Object();
        Map<String, Object> words = Files.readAllLines(Path.of("words")).stream()
                .collect(Collectors.toMap(w -> w, w -> value));
        System.out.println(words.getClass());
        showDistribution(words);
    }

    private static void showDistribution(Map<String, Object> map) {
        try {
            Field tableField = HashMap.class.getDeclaredField("table");
            Field nextField = Class.forName("java.util.HashMap$Node").getDeclaredField("next");

            tableField.setAccessible(true);
            nextField.setAccessible(true);
            Object array = tableField.get(map);
            int length = Array.getLength(array);
            System.out.println("总的桶个数[" + length + "]");
            Map<Integer, AtomicInteger> result = new HashMap<>();
            for (int i = 0; i < length; i++) {
                Object node = Array.get(array, i);
                AtomicInteger c = result.computeIfAbsent(i, key -> new AtomicInteger());
                while (node != null) {
                    c.incrementAndGet();
                    node = nextField.get(node);
                }
            }
            Map.Entry maxEntry = null;
            int max = -1;
            HashMap<Integer, AtomicInteger> counting = new HashMap<>();
            for (Map.Entry<Integer, AtomicInteger> entry : result.entrySet()) {
                int value = entry.getValue().get();
                AtomicInteger c = counting.computeIfAbsent(value, k -> new AtomicInteger());
                c.incrementAndGet();
            }
            counting.forEach((k, v) -> {
                System.out.println(k + "个元素的桶个数[" + v + "]");
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

DistributionAffectedByCapacity 演示容量及 hashCode 取值对分布的影响

#hashtableGrowRule 演示了 Hashtable 的扩容规律

java

import day01.sort.Utils;

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

// --add-opens java.base/java.util=ALL-UNNAMED
public class DistributionAffectedByCapacity {
    // .net 是原始容量 * 2 开始找下一个质数作为新容量
    public static void main(String[] args) {
        System.out.println(hashtableGrowRule(10));
//        int[] array = Utils.randomArray(1000); // 足够随机
        int[] array = Utils.lowSameArray(1000);
//        int[] array = Utils.evenArray(1000);
        System.out.println(Arrays.toString(array));
        int[] sizes = {11, 16, 23};
        printHashResult(array, sizes);
    }

    public static void printHashResult(int[] array, int[] sizes) {
        List<Map<Integer, AtomicInteger>> maps = new ArrayList<>();
        for (int size : sizes) {
            maps.add(getMap(size));
        }
        for (int hash : array) {
            for (int j = 0; j < sizes.length; j++) {
                maps.get(j).get(hash % sizes[j]).incrementAndGet();
            }
        }
        for (Map<Integer, AtomicInteger> map : maps) {
            System.out.printf("size:[%d] %s%n", map.size(), map);
        }
    }

    private static HashMap<Integer, AtomicInteger> getMap(int size) {
        HashMap<Integer, AtomicInteger> result = new HashMap<>();
        for (int i = 0; i < size; i++) {
            result.put(i, new AtomicInteger());
        }
        return result;
    }

    private static List<Integer> hashtableGrowRule(int n) {
        List<Integer> list = new ArrayList<>();
        int init = 0;
        list.add(init);
        if (n >= 1) {
            init = 11;
            list.add(init);
        }
        for (int i = 1; i < n; i++) {
            init = (init << 1) + 1;
            list.add(init);
        }
        return list;
    }

}

#randomArray 如果 hashCode 足够随机,容量是否是 2 的 n 次幂影响不大

#lowSameArray 如果 hashCode 低位一样的多,容量是 2 的 n 次幂会导致分布不均匀

#evenArray 如果 hashCode 偶数的多,容量是 2 的 n 次幂会导致分布不均匀

由此得出对于容量是 2 的 n 次幂的设计来讲,二次 hash 非常重要

java

import java.util.Random;

public class Utils {
    public static void swap(int[] array, int i, int j) {
        int t = array[i];
        array[i] = array[j];
        array[j] = t;
    }

    public static void shuffle(int[] array) {
        Random rnd = new Random();
        int size = array.length;
        for (int i = size; i > 1; i--) {
            swap(array, i - 1, rnd.nextInt(i));
        }
    }

    public static int[] randomArray(int n) {
        int lastVal = 1;
        Random r = new Random();
        int[] array = new int[n];
        for (int i = 0; i < n; i++) {
            int v = lastVal + Math.max(r.nextInt(10), 1);
            array[i] = v;
            lastVal = v;
        }
        shuffle(array);
        return array;
    }

    public static int[] evenArray(int n) {
        int[] array = new int[n];
        for (int i = 0; i < n; i++) {
            array[i] = i * 2;
        }
        return array;
    }

    public static int[] sixteenArray(int n) {
        int[] array = new int[n];
        for (int i = 0; i < n; i++) {
            array[i] = i * 16;
        }
        return array;
    }

    public static int[] lowSameArray(int n) {
        int[] array = new int[n];
        Random r = new Random();
        for (int i = 0; i < n; i++) {
            array[i] = r.nextInt() & 0x7FFF0002;
        }
        return array;
    }
}

HashMapVsHashtable 演示了对于同样数量的单词字符串放入 HashMap 和 Hashtable 分布上的区别

java

import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;

// --add-opens java.base/java.util=ALL-UNNAMED
@SuppressWarnings("all")
public class HashMapVsHashtable {
    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        List<String> words = Files.readAllLines(Path.of("words"));
        int size = (int) Math.pow(2, 19);
        int pShift = 5;
        Object value = new Object();
        HashMap<String, Object> map = new HashMap<>(size);
        Hashtable<String, Object> hashtable = new Hashtable<>(393215);
        for (String word : words) {
            map.put(word, value);
            hashtable.put(word, value);
        }
        hashMap(pShift, map);
        hashtable(pShift, hashtable);
    }


    private static void hashtable(int pShift, Hashtable<String, Object> hashtable) throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException {
        Field tableField = Hashtable.class.getDeclaredField("table");
        Field nextField = Class.forName("java.util.Hashtable$Entry").getDeclaredField("next");
        tableField.setAccessible(true);
        nextField.setAccessible(true);
        Object array = tableField.get(hashtable);
        int length = Array.getLength(array);
        System.out.printf("(%d)------------------------------------------->%n", length);
        Map<Integer, AtomicInteger> result = new HashMap<>();
        for (int i = 0; i < length; i++) {
            Object node = Array.get(array, i);
            AtomicInteger c = result.computeIfAbsent(i >>> 19 - pShift, key -> new AtomicInteger());
            while (node != null) {
                c.incrementAndGet();
                node = nextField.get(node);
            }
        }
        LongAdder sum = new LongAdder();
        result.forEach((k, v) -> {
            int star = (int) Math.round(v.get() / 1000.0);
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < star; i++) {
                sb.append("*");
            }
            System.out.println(String.format("%02d", k) + " " + sb.toString());
            sum.add(v.get());
        });
        System.out.println("------------------------------------------->");
        System.out.println(sum);
    }

    private static void hashMap(int pShift, HashMap<String, Object> map) {
        try {
            Field tableField = HashMap.class.getDeclaredField("table");
            Field nextField = Class.forName("java.util.HashMap$Node").getDeclaredField("next");

            tableField.setAccessible(true);
            nextField.setAccessible(true);
            Object array = tableField.get(map);
            int length = Array.getLength(array);
            System.out.printf("(%d)------------------------------------------->%n", length);
            Map<Integer, AtomicInteger> result = new HashMap<>();
            for (int i = 0; i < length; i++) {
                Object node = Array.get(array, i);
                AtomicInteger c = result.computeIfAbsent(i >>> 19 - pShift, key -> new AtomicInteger());
                while (node != null) {
                    c.incrementAndGet();
                    node = nextField.get(node);
                }
            }
            LongAdder sum = new LongAdder();
            result.forEach((k, v) -> {
                int star = (int) Math.round(v.get() / 1000.0);
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < star; i++) {
                    sb.append("*");
                }
                System.out.println(String.format("%02d", k) + " " + sb.toString());
                sum.add(v.get());
            });
            System.out.println("------------------------------------------->");
            System.out.println(sum);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

6)key 的设计

key 的设计要求

  1. HashMap 的 key 可以为 null,但 Map 的其他实现则不然
  2. 作为 key 的对象,必须实现 hashCode 和 equals,并且 key 的内容不能修改(不可变)
  3. key 的 hashCode 应该有良好的散列性

如果 key 可变,例如修改了 age 会导致再次查询时查询不到

java
public class HashMapMutableKey {
    public static void main(String[] args) {
        HashMap<Student, Object> map = new HashMap<>();
        Student stu = new Student("张三", 18);
        map.put(stu, new Object());

        System.out.println(map.get(stu));

        stu.age = 19;
        System.out.println(map.get(stu));
    }

    static class Student {
        String name;
        int age;

        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Student student = (Student) o;
            return age == student.age && Objects.equals(name, student.name);
        }

        @Override
        public int hashCode() {
            return Objects.hash(name, age);
        }
    }
}

String 对象的 hashCode() 设计

  • 目标是达到较为均匀的散列效果,每个字符串的 hashCode 足够独特
  • 字符串中的每个字符都可以表现为一个数字,称为 Si,其中 i 的范围是 0 ~ n - 1
  • 散列公式为: S031(n1)+S131(n2)+Si31(n1i)+S(n1)310
  • 31 代入公式有较好的散列特性,并且 31 * h 可以被优化为
    • 即 $32 ∗h -h $
    • 25hh
    • h5h

单例模式

饿汉式

java
public class Singleton1 implements Serializable {
    private Singleton1() {
        if (INSTANCE != null) {
            throw new RuntimeException("单例对象不能重复创建");
        }
        System.out.println("private Singleton1()");
    }

    private static final Singleton1 INSTANCE = new Singleton1();

    public static Singleton1 getInstance() {
        return INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }

    public Object readResolve() {
        return INSTANCE;
    }
}
  • 构造方法抛出异常是防止反射破坏单例
  • readResolve() 是防止反序列化破坏单例

枚举饿汉式

java
public enum Singleton2 {
    INSTANCE;

    private Singleton2() {
        System.out.println("private Singleton2()");
    }

    @Override
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }

    public static Singleton2 getInstance() {
        return INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}
  • 枚举饿汉式能天然防止反射、反序列化破坏单例

懒汉式

java
public class Singleton3 implements Serializable {
    private Singleton3() {
        System.out.println("private Singleton3()");
    }

    private static Singleton3 INSTANCE = null;

    // Singleton3.class
    public static synchronized Singleton3 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }

}
  • 其实只有首次创建单例对象时才需要同步,但该代码实际上每次调用都会同步
  • 因此有了下面的双检锁改进

双检锁懒汉式

java
public class Singleton4 implements Serializable {
    private Singleton4() {
        System.out.println("private Singleton4()");
    }

    private static volatile Singleton4 INSTANCE = null; // 可见性,有序性

    public static Singleton4 getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton4.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton4();
                }
            }
        }
        return INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

为何必须加 volatile:

  • INSTANCE = new Singleton4() 不是原子的,分成 3 步:创建对象、调用构造、给静态变量赋值,其中后两步可能被指令重排序优化,变成先赋值、再调用构造
  • 如果线程1 先执行了赋值,线程2 执行到第一个 INSTANCE == null 时发现 INSTANCE 已经不为 null,此时就会返回一个未完全构造的对象

内部类懒汉式

java
public class Singleton5 implements Serializable {
    private Singleton5() {
        System.out.println("private Singleton5()");
    }

    private static class Holder {
        static Singleton5 INSTANCE = new Singleton5();
    }

    public static Singleton5 getInstance() {
        return Holder.INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}
  • 避免了双检锁的缺点

JDK 中单例的体现

  • Runtime 体现了饿汉式单例
  • Console 体现了双检锁懒汉式单例
  • Collections 中的 EmptyNavigableSet 内部类懒汉式单例
  • ReverseComparator.REVERSE_ORDER 内部类懒汉式单例
  • Comparators.NaturalOrderComparator.INSTANCE 枚举饿汉式单例

并发

1. 线程状态

六种状态及转换

image-20210831090722658

分别是

  • 新建
    • 当一个线程对象被创建,但还未调用 start 方法时处于新建状态
    • 此时未与操作系统底层线程关联
  • 可运行
    • 调用了 start 方法,就会由新建进入可运行
    • 此时与底层线程关联,由操作系统调度执行
  • 终结
    • 线程内代码已经执行完毕,由可运行进入终结
    • 此时会取消与底层线程关联
  • 阻塞
    • 当获取锁失败后,由可运行进入 Monitor 的阻塞队列阻塞,此时不占用 cpu 时间
    • 当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的阻塞线程,唤醒后的线程进入可运行状态
  • 等待
    • 当获取锁成功后,但由于条件不满足,调用了 wait() 方法,此时从可运行状态释放锁进入 Monitor 等待集合等待,同样不占用 cpu 时间
    • 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的等待线程,恢复为可运行状态
  • 有时限等待
    • 当获取锁成功后,但由于条件不满足,调用了 wait(long) 方法,此时从可运行状态释放锁进入 Monitor 等待集合进行有时限等待,同样不占用 cpu 时间
    • 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的有时限等待线程,恢复为可运行状态,并重新去竞争锁
    • 如果等待超时,也会从有时限等待状态恢复为可运行状态,并重新去竞争锁
    • 还有一种情况是调用 sleep(long) 方法也会从可运行状态进入有时限等待状态,但与 Monitor 无关,不需要主动唤醒,超时时间到自然恢复为可运行状态

其它情况(只需了解)

  • 可以用 interrupt() 方法打断等待有时限等待的线程,让它们恢复为可运行状态
  • park,unpark 等方法也可以让线程等待和唤醒

五种状态

五种状态的说法来自于操作系统层面的划分

image-20210831092652602

  • 运行态:分到 cpu 时间,能真正执行线程内代码的
  • 就绪态:有资格分到 cpu 时间,但还未轮到它的
  • 阻塞态:没资格分到 cpu 时间的
    • 涵盖了 java 状态中提到的阻塞等待有时限等待
    • 多出了阻塞 I/O,指线程在调用阻塞 I/O 时,实际活由 I/O 设备完成,此时线程无事可做,只能干等
  • 新建与终结态:与 java 中同名状态类似,不再啰嗦

2. 线程池

七大参数

  1. corePoolSize 核心线程数目 - 池中会保留的最多线程数
  2. maximumPoolSize 最大线程数目 - 核心线程+救急线程的最大数目
  3. keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
  4. unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
  5. workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
  6. threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
  7. handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略
    1. 抛异常 java.util.concurrent.ThreadPoolExecutor.AbortPolicy
    2. 由调用者执行任务 java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy
    3. 丢弃任务 java.util.concurrent.ThreadPoolExecutor.DiscardPolicy
    4. 丢弃最早排队任务 java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy

image-20210831093204388

代码说明

TestThreadPoolExecutor 以较为形象的方式演示了线程池的核心组成

java

import org.slf4j.Logger;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static day02.LoggerUtils.*;

// --add-opens java.base/java.util.concurrent=ALL-UNNAMED
public class TestThreadPoolExecutor {

    public static void main(String[] args) throws InterruptedException {
        AtomicInteger c = new AtomicInteger(1);
        ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(2);
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                2,
                3,
                0,
                TimeUnit.MILLISECONDS,
                queue,
                r -> new Thread(r, "myThread" + c.getAndIncrement()),
                new ThreadPoolExecutor.DiscardOldestPolicy());
        showState(queue, threadPool);
        threadPool.submit(new MyTask("1", 3600000));
        showState(queue, threadPool);
        threadPool.submit(new MyTask("2", 3600000));
        showState(queue, threadPool);
        threadPool.submit(new MyTask("3"));
        showState(queue, threadPool);
        threadPool.submit(new MyTask("4"));
        showState(queue, threadPool);
        threadPool.submit(new MyTask("5", 3600000));
        showState(queue, threadPool);
        threadPool.submit(new MyTask("6"));
        showState(queue, threadPool);
    }

    private static void showState(ArrayBlockingQueue<Runnable> queue, ThreadPoolExecutor threadPool) {
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        List<Object> tasks = new ArrayList<>();
        for (Runnable runnable : queue) {
            try {
                Field callable = FutureTask.class.getDeclaredField("callable");
                callable.setAccessible(true);
                Object adapter = callable.get(runnable);
                Class<?> clazz = Class.forName("java.util.concurrent.Executors$RunnableAdapter");
                Field task = clazz.getDeclaredField("task");
                task.setAccessible(true);
                Object o = task.get(adapter);
                tasks.add(o);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        main.debug("pool size: {}, queue: {}", threadPool.getPoolSize(), tasks);
    }

    static class MyTask implements Runnable {
        private final String name;
        private final long duration;

        public MyTask(String name) {
            this(name, 0);
        }

        public MyTask(String name, long duration) {
            this.name = name;
            this.duration = duration;
        }

        @Override
        public void run() {
            try {
                LoggerUtils.get("myThread").debug("running..." + this);
                Thread.sleep(duration);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        @Override
        public String toString() {
            return "MyTask(" + name + ")";
        }
    }
}

3. wait vs sleep

共同点

  • wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态

不同点

  • 方法归属不同

    • sleep(long) 是 Thread 的静态方法
    • 而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有
  • 醒来时机不同

    • 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
    • wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去
    • 它们都可以被打断唤醒
  • 锁特性不同(重点)

    • wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
    • wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)
    • 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)

4. lock vs synchronized

三个层面

不同点

  • 语法层面
    • synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现
    • Lock 是接口,源码由 jdk 提供,用 java 语言实现
    • 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁
  • 功能层面
    • 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
    • Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量
    • Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock
  • 性能层面
    • 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
    • 在竞争激烈时,Lock 的实现通常会提供更好的性能

公平锁

  • 公平锁的公平体现
    • 已经处在阻塞队列中的线程(不考虑超时)始终都是公平的,先进先出
    • 公平锁是指未处于阻塞队列中的线程来争抢锁,如果队列不为空,则老实到队尾等待
    • 非公平锁是指未处于阻塞队列中的线程来争抢锁,与队列头唤醒的线程去竞争,谁抢到算谁的
  • 公平锁会降低吞吐量,一般不用

条件变量

  • ReentrantLock 中的条件变量功能类似于普通 synchronized 的 wait,notify,用在当线程获得锁后,发现条件不满足时,临时等待的链表结构
  • 与 synchronized 的等待集合不同之处在于,ReentrantLock 中的条件变量可以有多个,可以实现更精细的等待、唤醒控制

代码说明

  • TestReentrantLock 用较为形象的方式演示 ReentrantLock 的内部结构
java

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static day02.LoggerUtils.*;

// --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util.concurrent.locks=ALL-UNNAMED
public class TestReentrantLock {
    static final MyReentrantLock LOCK = new MyReentrantLock(true);

    static Condition c1 = LOCK.newCondition("c1");
    static Condition c2 = LOCK.newCondition("c2");

    static volatile boolean stop = false;

    public static void main(String[] args) throws InterruptedException, IOException {
        learnLock();
    }

    private static void learnLock() throws InterruptedException {
        System.out.println(LOCK);
        new MyThread(() -> {
            LOCK.lock();
            get("t").debug("acquire lock...");
        }, "t1").start();

        Thread.sleep(100);
        new MyThread(() -> {
            LOCK.lock();
            get("t").debug("acquire lock...");
        }, "t2").start();

        Thread.sleep(100);
        new MyThread(() -> {
            LOCK.lock();
            get("t").debug("acquire lock...");
        }, "t3").start();

        Thread.sleep(100);
        new MyThread(() -> {
            LOCK.lock();
            get("t").debug("acquire lock...");
        }, "t4").start();
    }

    private static void fairVsUnfair() throws InterruptedException {
        new MyThread(() -> {
            LOCK.lock();
            get("t").debug("acquire lock...");
            sleep1s();
            LOCK.unlock();
        }, "t1").start();

        Thread.sleep(100);
        new MyThread(() -> {
            LOCK.lock();
            get("t").debug("acquire lock...");
            sleep1s();
            LOCK.unlock();
        }, "t2").start();

        Thread.sleep(100);
        new MyThread(() -> {
            LOCK.lock();
            get("t").debug("acquire lock...");
            sleep1s();
            LOCK.unlock();
        }, "t3").start();

        Thread.sleep(100);
        new MyThread(() -> {
            LOCK.lock();
            get("t").debug("acquire lock...");
            sleep1s();
            LOCK.unlock();
        }, "t4").start();

        get("t").debug("{}", LOCK);

        while (!stop) {
            new Thread(() -> {
                try {
                    boolean b = LOCK.tryLock(10, TimeUnit.MILLISECONDS);
                    if (b) {
                        System.out.println(Thread.currentThread().getName() + " acquire lock...");
                        stop = true;
                        sleep1s();
                        LOCK.unlock();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }

    private static void sleep1s() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static class MyReentrantLock extends ReentrantLock {
        private final Map<String, Condition> conditions = new HashMap<>();

        public MyReentrantLock(boolean fair) {
            super(fair);
        }

        public Condition newCondition(String name) {
            Condition condition = super.newCondition();
            conditions.put(name, condition);
            return condition;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder(512);
            String queuedInfo = getQueuedInfo();
            List<String> all = new ArrayList<>();
            all.add(String.format("| owner[%s] state[%s]", this.getOwner(), this.getState()));
            all.add(String.format("| blocked queue %s", queuedInfo));
            for (Map.Entry<String, Condition> entry : this.conditions.entrySet()) {
                String waitingInfo = getWaitingInfo(entry.getValue());
                all.add(String.format("| waiting queue [%s] %s", entry.getKey(), waitingInfo));
            }
            int maxLength = all.stream().map(String::length).max(Comparator.naturalOrder()).orElse(100);
            for (String s : all) {
                sb.append(s);
                String space = IntStream.range(0, maxLength - s.length() + 7).mapToObj(i -> " ").collect(Collectors.joining(""));
                sb.append(space).append("|\n");
            }
            sb.deleteCharAt(sb.length() - 1);
            String line1 = IntStream.range(0, maxLength ).mapToObj(i -> "-").collect(Collectors.joining(""));
            sb.insert(0, String.format("%n| Lock %s|%n", line1));
            maxLength += 6;
            String line3 = IntStream.range(0, maxLength).mapToObj(i -> "-").collect(Collectors.joining(""));
            sb.append(String.format("%n|%s|", line3));
            return sb.toString();
        }

        private Object getState() {
            try {
                Field syncField = ReentrantLock.class.getDeclaredField("sync");
                Class<?> aqsClass = Class.forName("java.util.concurrent.locks.AbstractQueuedSynchronizer");
                Field stateField = aqsClass.getDeclaredField("state");
                syncField.setAccessible(true);
                AbstractQueuedSynchronizer sync = (AbstractQueuedSynchronizer) syncField.get(this);
                stateField.setAccessible(true);
                return stateField.get(sync);
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
        }

        private String getWaitingInfo(Condition condition) {
            List<String> result = new ArrayList<>();
            try {
                Field firstWaiterField = AbstractQueuedSynchronizer.ConditionObject.class.getDeclaredField("firstWaiter");
                Class<?> conditionNodeClass = Class.forName("java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode");
                Class<?> nodeClass = Class.forName("java.util.concurrent.locks.AbstractQueuedSynchronizer$Node");
                Field waiterField = nodeClass.getDeclaredField("waiter");
                Field statusField = nodeClass.getDeclaredField("status");
                Field nextWaiterField = conditionNodeClass.getDeclaredField("nextWaiter");
                firstWaiterField.setAccessible(true);
                waiterField.setAccessible(true);
                statusField.setAccessible(true);
                nextWaiterField.setAccessible(true);
                Object fistWaiter = firstWaiterField.get(condition);
                while (fistWaiter != null) {
                    Object waiter = waiterField.get(fistWaiter);
                    Object status = statusField.get(fistWaiter);
                    result.add(String.format("([%s]%s)", status, waiter));
                    fistWaiter = nextWaiterField.get(fistWaiter);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return String.join("->", result);
        }

        private String getQueuedInfo() {
            List<String> result = new ArrayList<>();
            try {
                Field syncField = ReentrantLock.class.getDeclaredField("sync");
                Field headField = AbstractQueuedSynchronizer.class.getDeclaredField("head");
                Class<?> nodeClass = Class.forName("java.util.concurrent.locks.AbstractQueuedSynchronizer$Node");
                Field waiterField = nodeClass.getDeclaredField("waiter");
                Field statusField = nodeClass.getDeclaredField("status");
                Field nextField = nodeClass.getDeclaredField("next");
                syncField.setAccessible(true);
                AbstractQueuedSynchronizer sync = (AbstractQueuedSynchronizer) syncField.get(this);
                waiterField.setAccessible(true);
                statusField.setAccessible(true);
                nextField.setAccessible(true);
                headField.setAccessible(true);
                Object head = headField.get(sync);
                while (head != null) {
                    Object waiter = waiterField.get(head);
                    Object status = statusField.get(head);
                    result.add(String.format("({%s}%s)", status, waiter));
                    head = nextField.get(head);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return String.join("->", result);
        }
    }

    static class MyThread extends Thread {
        public MyThread(Runnable target, String name) {
            super(target, name);
        }

        @Override
        public String toString() {
            return this.getName();
        }
    }
}

5. volatile

原子性

  • 起因:多线程下,不同线程的指令发生了交错导致的共享变量的读写混乱
  • 解决:用悲观锁或乐观锁解决,volatile 并不能解决原子性

可见性

  • 起因:由于编译器优化、或缓存优化、或 CPU 指令重排序优化导致的对共享变量所做的修改另外的线程看不到
  • 解决:用 volatile 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见

有序性

  • 起因:由于编译器优化、或缓存优化、或 CPU 指令重排序优化导致指令的实际执行顺序与编写顺序不一致
  • 解决:用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果
  • 注意:
    • volatile 变量写加的屏障是阻止上方其它写操作越过屏障排到 volatile 变量写之下
    • volatile 变量读加的屏障是阻止下方其它读操作越过屏障排到 volatile 变量读之上
    • volatile 读写加入的屏障只能防止同一线程内的指令重排

AddAndSubtract 演示原子性

java

import day02.LoggerUtils;

import java.util.concurrent.CountDownLatch;

// 原子性例子

/**

 t1 10
 0: getstatic
                 t2
                 0: getstatic 10
                 3: iconst_5
                 4: isub
                 5: putstatic
                    5
 3: iconst_5
 4: iadd
 5: putstatic
 15



 */
public class AddAndSubtract {

    static volatile int balance = 10;

    public static void subtract() {
        int b = balance;
        b -= 5;
        balance = b;
    }

    public static void add() {
        int b = balance;
        b += 5;
        balance = b;
    }

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(2);
        new Thread(() -> {
            subtract();
            latch.countDown();
        }).start();
        new Thread(() -> {
            add();
            latch.countDown();
        }).start();
        latch.await();
        LoggerUtils.get().debug("{}", balance);
    }
}

threadsafe.ForeverLoop 演示可见性

注意:本例经实践检验是编译器优化导致的可见性问题

java

import static day02.LoggerUtils.get;

// 可见性例子
// -Xint
public class ForeverLoop {
    static volatile boolean stop = false;

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stop = true;
            get().debug("modify stop to true...");
        }).start();

        new Thread(() -> {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            get().debug("{}", stop);
        }).start();

        new Thread(() -> {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            get().debug("{}", stop);
        }).start();

        foo();
    }

    static void foo() {
        int i = 0;
        while (!stop) {
            i++;
        }
        get().debug("stopped... c:{}", i);
    }
}

Reordering 演示有序性

需要打成 jar 包后测试

java

import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.II_Result;

// 有序性例子
// java -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -jar jcstress.jar -t day02.threadsafe.Reordering.Case1
// java -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -jar jcstress.jar -t day02.threadsafe.Reordering.Case2
// java -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -jar jcstress.jar -t day02.threadsafe.Reordering.Case3
public class Reordering {
    @JCStressTest
    @Outcome(id = {"0, 0", "1, 1", "0, 1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
    @Outcome(id = "1, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "INTERESTING")
    @State
    public static class Case1 {
        int x;
        int y;

        @Actor
        public void actor1() {
            x = 1;
            y = 1;
        }

        @Actor
        public void actor2(II_Result r) {
            r.r1 = y;
            r.r2 = x;
        }
    }

    @JCStressTest
    @Outcome(id = {"0, 0", "1, 1", "0, 1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
    @Outcome(id = "1, 0", expect = Expect.FORBIDDEN, desc = "FORBIDDEN")
    @State
    public static class Case2 {
        int x;
        volatile int y;

        @Actor
        public void actor1() {
            x = 1;
            y = 1;
        }

        @Actor
        public void actor2(II_Result r) {
            r.r1 = y;
            r.r2 = x;
        }
    }

    @JCStressTest
    @Outcome(id = {"0, 0", "1, 1", "0, 1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
    @Outcome(id = "1, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "ACCEPTABLE_INTERESTING")
    @State
    public static class Case3 {
        volatile int x;
        int y;

        @Actor
        public void actor1() {
            x = 1;
            y = 1;
        }

        @Actor
        public void actor2(II_Result r) {
            r.r1 = y;
            r.r2 = x;
        }
    }
}

6. 悲观锁 vs 乐观锁

对比悲观锁与乐观锁

  • 悲观锁的代表是 synchronized 和 Lock 锁

    • 其核心思想是【线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程,都得停下来等待】
    • 线程从运行到阻塞、再从阻塞到唤醒,涉及线程上下文切换,如果频繁发生,影响性能
    • 实际上,线程在获取 synchronized 和 Lock 锁时,如果锁已被占用,都会做几次重试操作,减少阻塞的机会
  • 乐观锁的代表是 AtomicInteger,使用 cas 来保证原子性

    • 其核心思想是【无需加锁,每次只有一个线程能成功修改共享变量,其它失败的线程不需要停止,不断重试直至成功】
    • 由于线程一直运行,不需要阻塞,因此不涉及线程上下文切换
    • 它需要多核 cpu 支持,且线程数不应超过 cpu 核数

代码说明

  • SyncVsCas 演示了分别使用乐观锁和悲观锁解决原子赋值
java

import jdk.internal.misc.Unsafe;

// --add-opens java.base/jdk.internal.misc=ALL-UNNAMED
public class SyncVsCas {
    static final Unsafe U = Unsafe.getUnsafe();
    static final long BALANCE = U.objectFieldOffset(Account.class, "balance");

    static class Account {
        volatile int balance = 10;
    }

    private static void showResult(Account account, Thread t1, Thread t2) {
        try {
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            LoggerUtils.get().debug("{}", account.balance);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void sync(Account account) {
        Thread t1 = new Thread(() -> {
            synchronized (account) {
                int old = account.balance;
                int n = old - 5;
                account.balance = n;
            }
        },"t1");

        Thread t2 = new Thread(() -> {
            synchronized (account) {
                int o = account.balance;
                int n = o + 5;
                account.balance = n;
            }
        },"t2");

        showResult(account, t1, t2);
    }

    public static void cas(Account account) {
        Thread t1 = new Thread(() -> {
            while (true) {
                int o = account.balance;
                int n = o - 5;
                if (U.compareAndSetInt(account, BALANCE, o, n)) {
                    break;
                }
            }
        },"t1");

        Thread t2 = new Thread(() -> {
            while (true) {
                int o = account.balance;
                int n = o + 5;
                if (U.compareAndSetInt(account, BALANCE, o, n)) {
                    break;
                }
            }
        },"t2");

        showResult(account, t1, t2);
    }

    private static void basicCas(Account account) {
        while (true) {
            int o = account.balance;
            int n = o + 5;
            if(U.compareAndSetInt(account, BALANCE, o, n)){
                break;
            }
        }
        System.out.println(account.balance);
    }

    public static void main(String[] args) {
        Account account = new Account();
        cas(account);
    }


}

7. Hashtable vs ConcurrentHashMap

Hashtable 对比 ConcurrentHashMap

  • Hashtable 与 ConcurrentHashMap 都是线程安全的 Map 集合
  • Hashtable 并发度低,整个 Hashtable 对应一把锁,同一时刻,只能有一个线程操作它
  • ConcurrentHashMap 并发度高,整个 ConcurrentHashMap 对应多把锁,只要线程访问的是不同锁,那么不会冲突

ConcurrentHashMap 1.7

  • 数据结构:Segment(大数组) + HashEntry(小数组) + 链表,每个 Segment 对应一把锁,如果多个线程访问不同的 Segment,则不会冲突
  • 并发度:Segment 数组大小即并发度,决定了同一时刻最多能有多少个线程并发访问。Segment 数组不能扩容,意味着并发度在 ConcurrentHashMap 创建时就固定了
  • 索引计算
    • 假设大数组长度是 2m,key 在大数组内的索引是 key 的二次 hash 值的高 m 位
    • 假设小数组长度是 2n,key 在小数组内的索引是 key 的二次 hash 值的低 n 位
  • 扩容:每个小数组的扩容相对独立,小数组在超过扩容因子时会触发扩容,每次扩容翻倍
  • Segment[0] 原型:首次创建其它小数组时,会以此原型为依据,数组长度,扩容因子都会以原型为准

ConcurrentHashMap 1.8

  • 数据结构:Node 数组 + 链表或红黑树,数组的每个头节点作为锁,如果多个线程访问的头节点不同,则不会冲突。首次生成头节点时如果发生竞争,利用 cas 而非 syncronized,进一步提升性能
  • 并发度:Node 数组有多大,并发度就有多大,与 1.7 不同,Node 数组可以扩容
  • 扩容条件:Node 数组满 3/4 时就会扩容
  • 扩容单位:以链表为单位从后向前迁移链表,迁移完成的将旧数组头节点替换为 ForwardingNode
  • 扩容时并发 get
    • 根据是否为 ForwardingNode 来决定是在新数组查找还是在旧数组查找,不会阻塞
    • 如果链表长度超过 1,则需要对节点进行复制(创建新节点),怕的是节点迁移后 next 指针改变
    • 如果链表最后几个元素扩容后索引不变,则节点无需复制
  • 扩容时并发 put
    • 如果 put 的线程与扩容线程操作的链表是同一个,put 线程会阻塞
    • 如果 put 的线程操作的链表还未迁移完成,即头节点不是 ForwardingNode,则可以并发执行
    • 如果 put 的线程操作的链表已经迁移完成,即头结点是 ForwardingNode,则可以协助扩容
  • 与 1.7 相比是懒惰初始化
  • capacity 代表预估的元素个数,capacity / factory 来计算出初始数组大小,需要贴近 2n
  • loadFactor 只在计算初始数组大小时被使用,之后扩容固定为 3/4
  • 超过树化阈值时的扩容问题,如果容量已经是 64,直接树化,否则在原来容量基础上做 3 轮扩容

8. ThreadLocal

作用

  • ThreadLocal 可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】,避免争用引发的线程安全问题
  • ThreadLocal 同时实现了线程内的资源共享

常用方法

方法声明描述
ThreadLocal()创建ThreadLocal对象
public void set( T value)设置当前线程绑定的局部变量
public T get()获取当前线程绑定的局部变量
public void remove()移除当前线程绑定的局部变量

原理

每个线程内有一个 ThreadLocalMap 类型的成员变量,用来存储资源对象

  • 调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中
  • 调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值
  • 调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值

ThreadLocalMap 的一些特点

  • key 的 hash 值统一分配
  • 初始容量 16,扩容因子 2/3,扩容容量翻倍
  • key 索引冲突后用开放寻址法解决冲突

ThreadLocal与synchronized的区别

​ 虽然ThreadLocal模式与synchronized关键字都用于处理多线程并发访问变量的问题, 不过两者处理问题的角度和思路不同。

synchronizedThreadLocal
原理同步机制采用'以时间换空间'的方式, 只提供了一份变量,让不同的线程排队访问ThreadLocal采用'以空间换时间'的方式, 为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰
侧重点多个线程之间访问资源的同步多线程中让每个线程之间的数据相互隔离

弱引用 key

ThreadLocalMap 中的 key 被设计为弱引用,原因如下

  • Thread 可能需要长时间运行(如线程池中的线程),如果 key 不再使用,需要在内存不足(GC)时释放其占用的内存

内存泄露

内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。

内存泄漏的发生跟ThreadLocalMap中的key是否使用弱引用是没有关系的。

内存泄漏的情况中,都有两个前提:

1. 没有手动删除这个Entry
2. CurrentThread依然运行

​ 第一点很好理解,只要在使用完ThreadLocal,调用其remove方法删除对应的Entry,就能避免内存泄漏。

​ 第二点稍微复杂一点, 由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以它的生命周期跟Thread一样长。那么在使用完ThreadLocal的使用,如果当前Thread也随之执行结束,ThreadLocalMap自然也会被gc回收,从根源上避免了内存泄漏。

​ 综上,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏。

内存释放时机

  • 被动 GC 释放 key
    • 仅是让 key 的内存释放,关联 value 的内存并不会释放
  • 懒惰被动释放 value
    • get key 时,发现是 null key,则释放其 value 内存
    • set key 时,会使用启发式扫描,清除临近的 null key 的 value 内存,启发次数与元素个数,是否发现 null key 有关
  • 主动 remove 释放 key,value
    • 会同时释放 key,value 的内存,也会清除临近的 null key 的 value 内存
    • 推荐使用它,因为一般使用 ThreadLocal 时都把它作为静态变量(即强引用),因此无法被动依靠 GC 回收

ThreadLocal的核心方法源码

基于ThreadLocal的内部结构,我们继续分析它的核心方法源码,更深入的了解其操作原理。

除了构造方法之外, ThreadLocal对外暴露的方法有以下4个:

方法声明描述
protected T initialValue()返回当前线程局部变量的初始值
public void set( T value)设置当前线程绑定的局部变量
public T get()获取当前线程绑定的局部变量
public void remove()移除当前线程绑定的局部变量

​ 以下是这4个方法的详细源码分析(为了保证思路清晰, ThreadLocalMap部分暂时不展开,下一个知识点详解)

set方法

(1 ) 源码和对应的中文注释

java
  /**
     * 设置当前线程对应的ThreadLocal的值
     *
     * @param value 将要保存在当前线程对应的ThreadLocal的值
     */
    public void set(T value) {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 判断map是否存在
        if (map != null)
            // 存在则调用map.set设置此实体entry
            map.set(this, value);
        else
            // 1)当前线程Thread 不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
    }

 /**
     * 获取当前线程Thread对应维护的ThreadLocalMap 
     * 
     * @param  t the current thread 当前线程
     * @return the map 对应维护的ThreadLocalMap 
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
	/**
     *创建当前线程Thread对应维护的ThreadLocalMap 
     *
     * @param t 当前线程
     * @param firstValue 存放到map中第一个entry的值
     */
	void createMap(Thread t, T firstValue) {
        //这里的this是调用此方法的threadLocal
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

(2 ) 代码执行流程

​ A. 首先获取当前线程,并根据当前线程获取一个Map

​ B. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)

​ C. 如果Map为空,则给该线程创建 Map,并设置初始值

get方法

(1 ) 源码和对应的中文注释

java
    /**
     * 返回当前线程中保存ThreadLocal的值
     * 如果当前线程没有此ThreadLocal变量,
     * 则它会通过调用{@link #initialValue} 方法进行初始化值
     *
     * @return 返回当前线程对应此ThreadLocal的值
     */
    public T get() {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null) {
            // 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 对e进行判空 
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 获取存储实体 e 对应的 value值
                // 即为我们想要的当前线程对应此ThreadLocal的值
                T result = (T)e.value;
                return result;
            }
        }
        /*
        	初始化 : 有两种情况有执行当前代码
        	第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象
        	第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry
         */
        return setInitialValue();
    }

    /**
     * 初始化
     *
     * @return the initial value 初始化后的值
     */
    private T setInitialValue() {
        // 调用initialValue获取初始化的值
        // 此方法可以被子类重写, 如果不重写默认返回null
        T value = initialValue();
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 判断map是否存在
        if (map != null)
            // 存在则调用map.set设置此实体entry
            map.set(this, value);
        else
            // 1)当前线程Thread 不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
        // 返回设置的值value
        return value;
    }

(2 ) 代码执行流程

​ A. 首先获取当前线程, 根据当前线程获取一个Map

​ B. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,否则转到D

​ C. 如果e不为null,则返回e.value,否则转到D

​ D. Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map

总结: 先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值。

remove方法

(1 ) 源码和对应的中文注释

java
 /**
     * 删除当前线程中保存的ThreadLocal对应的实体entry
     */
     public void remove() {
        // 获取当前线程对象中维护的ThreadLocalMap对象
         ThreadLocalMap m = getMap(Thread.currentThread());
        // 如果此map存在
         if (m != null)
            // 存在则调用map.remove
            // 以当前ThreadLocal为key删除对应的实体entry
             m.remove(this);
     }

(2 ) 代码执行流程

​ A. 首先获取当前线程,并根据当前线程获取一个Map

​ B. 如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry

initialValue方法
java
/**
  * 返回当前线程对应的ThreadLocal的初始值
  
  * 此方法的第一次调用发生在,当线程通过get方法访问此线程的ThreadLocal值时
  * 除非线程先调用了set方法,在这种情况下,initialValue 才不会被这个线程调用。
  * 通常情况下,每个线程最多调用一次这个方法。
  *
  * <p>这个方法仅仅简单的返回null {@code null};
  * 如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值,
  * 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法
  * 通常, 可以通过匿名内部类的方式实现
  *
  * @return 当前ThreadLocal的初始值
  */
protected T initialValue() {
    return null;
}

​ 此方法的作用是 返回该线程局部变量的初始值。

(1) 这个方法是一个延迟调用方法,从上面的代码我们得知,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次。

(2)这个方法缺省实现直接返回一个null

(3)如果想要一个除null之外的初始值,可以重写此方法。(备注: 该方法是一个protected的方法,显然是为了让子类覆盖而设计的)

ThreadLocalMap源码分析

ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现。

(1) 成员变量

java
    /**
     * 初始容量 —— 必须是2的整次幂
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * 存放数据的table,Entry类的定义在下面分析
     * 同样,数组长度必须是2的整次幂。
     */
    private Entry[] table;

    /**
     * 数组里面entrys的个数,可以用于判断table当前使用量是否超过阈值。
     */
    private int size = 0;

    /**
     * 进行扩容的阈值,表使用量大于它的时候进行扩容。
     */
    private int threshold; // Default to 0

​ 跟HashMap类似,INITIAL_CAPACITY代表这个Map的初始容量;table 是一个Entry 类型的数组,用于存储数据;size 代表表中的存储数目; threshold 代表需要扩容时对应 size 的阈值。

链接:https://www.jianshu.com/p/acfd2239c9f4

来源:简书

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

(2) 存储结构 - Entry

java
/*
 * Entry继承WeakReference,并且用ThreadLocal作为key.
 * 如果key为null(entry.get() == null),意味着key不再被引用,
 * 因此这时候entry也可以从table中清除。
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

​ 在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。不过Entry中的key只能是ThreadLocal对象,这点在构造方法中已经限定死了。

​ 另外,Entry继承WeakReference,也就是key(ThreadLocal)是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。

hash冲突的解决

​ hash冲突的解决是Map中的一个重要内容。我们以hash冲突的解决为线索,来研究一下ThreadLocalMap的核心源码。

(1) 首先从ThreadLocal的set() 方法入手

java
  public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocal.ThreadLocalMap map = getMap(t);
        if (map != null)
            //调用了ThreadLocalMap的set方法
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    ThreadLocal.ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        	//调用了ThreadLocalMap的构造方法
        t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
    }

这个方法我们刚才分析过, 其作用是设置当前线程绑定的局部变量 :

​ A. 首先获取当前线程,并根据当前线程获取一个Map

​ B. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)

(这里调用了ThreadLocalMap的set方法)

​ C. 如果Map为空,则给该线程创建 Map,并设置初始值

(这里调用了ThreadLocalMap的构造方法)

这段代码有两个地方分别涉及到ThreadLocalMap的两个方法, 我们接着分析这两个方法。

(2)构造方法ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)

java
 /*
  * firstKey : 本ThreadLocal实例(this)
  * firstValue : 要保存的线程本地变量
  */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //初始化table
        table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
        //计算索引(重点代码)
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        //设置值
        table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
        size = 1;
        //设置阈值
        setThreshold(INITIAL_CAPACITY);
    }

​ 构造函数首先创建一个长度为16的Entry数组,然后计算出firstKey对应的索引,然后存储到table中,并设置size和threshold。

重点分析int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)

a. 关于firstKey.threadLocalHashCode

java
 	private final int threadLocalHashCode = nextHashCode();
    
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
//AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减,适合高并发情况下的使用
    private static AtomicInteger nextHashCode =  new AtomicInteger();
     //特殊的hash值
    private static final int HASH_INCREMENT = 0x61c88647;

​ 这里定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENT,HASH_INCREMENT = 0x61c88647,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 也就是Entry[] table中,这样做可以尽量避免hash冲突。

b. 关于& (INITIAL_CAPACITY - 1)

​ 计算hash的时候里面采用了hashCode & (size - 1)的算法,这相当于取模运算hashCode % size的一个更高效的实现。正是因为这种算法,我们要求size必须是2的整次幂,这也能保证在索引不越界的前提下,使得hash发生冲突的次数减小。

(3) ThreadLocalMap中的set方法

java
private void set(ThreadLocal<?> key, Object value) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        //计算索引(重点代码,刚才分析过了)
        int i = key.threadLocalHashCode & (len-1);
        /**
         * 使用线性探测法查找元素(重点代码)
         */
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
            //ThreadLocal 对应的 key 存在,直接覆盖之前的值
            if (k == key) {
                e.value = value;
                return;
            }
            // key为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,
           // 当前数组中的 Entry 是一个陈旧(stale)的元素
            if (k == null) {
                //用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏
                replaceStaleEntry(key, value, i);
                return;
            }
        }
    
    	//ThreadLocal对应的key不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的Entry。
            tab[i] = new Entry(key, value);
            int sz = ++size;
            /**
             * cleanSomeSlots用于清除那些e.get()==null的元素,
             * 这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。
             * 如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行				 * rehash(执行一次全表的扫描清理工作)
             */
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
}

 /**
     * 获取环形数组的下一个索引
     */
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }

​ 代码执行流程:

A. 首先还是根据key计算出索引 i,然后查找i位置上的Entry,

B. 若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值,

C. 若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry,

D. 不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1。

​ 最后调用cleanSomeSlots,清理key为null的Entry,最后返回是否清理了Entry,接下来再判断sz 是否>= thresgold达到了rehash的条件,达到的话就会调用rehash函数执行一次全表的扫描清理。

重点分析 : ThreadLocalMap使用线性探测法来解决哈希冲突的。

​ 该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。

​ 举个例子,假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。

​ 按照上面的描述,可以把Entry[] table看成一个环形数组。

JVM虚拟机

1. JVM 内存结构

结合一段 java 代码的执行理解内存划分

image-20210831165728217

  • 执行 javac 命令编译源代码为字节码
  • 执行 java 命令
    1. 创建 JVM,调用类加载子系统加载 class,将类的信息存入方法区
    2. 创建 main 线程,使用的内存区域是 JVM 虚拟机栈,开始执行 main 方法代码
    3. 如果遇到了未见过的类,会继续触发类加载过程,同样会存入方法区
    4. 需要创建对象,会使用内存来存储对象
    5. 不再使用的对象,会由垃圾回收器在内存不足时回收其内存
    6. 调用方法时,方法内的局部变量、方法参数所使用的是 JVM 虚拟机栈中的栈帧内存
    7. 调用方法时,先要到方法区获得到该方法的字节码指令,由解释器将字节码指令解释为机器码执行
    8. 调用方法时,会将要执行的指令行号读到程序计数器,这样当发生了线程切换,恢复时就可以从中断的位置继续
    9. 对于非 java 实现的方法调用,使用内存称为本地方法栈(见说明)
    10. 对于热点方法调用,或者频繁的循环代码,由 JIT 即时编译器将这些代码编译成机器码缓存,提高执行性能

说明

  • 加粗字体代表了 JVM 虚拟机组件
  • 对于 Oracle 的 Hotspot 虚拟机实现,不区分虚拟机栈和本地方法栈

会发生内存溢出的区域

  • 不会出现内存溢出的区域 – 程序计数器
  • 出现 OutOfMemoryError 的情况
    • 堆内存耗尽 – 对象越来越多,又一直在使用,不能被垃圾回收
    • 方法区内存耗尽 – 加载的类越来越多,很多框架都会在运行期间动态产生新的类
    • 虚拟机栈累积 – 每个线程最多会占用 1 M 内存,线程个数越来越多,而又长时间运行不销毁时
  • 出现 StackOverflowError 的区域
    • JVM 虚拟机栈,原因有方法递归调用未正确结束、反序列化 json 时循环引用

方法区、永久代、元空间

  • 方法区是 JVM 规范中定义的一块内存区域,用来存储类元数据、方法字节码、即时编译器需要的信息等
  • 永久代是 Hotspot 虚拟机对 JVM 规范的实现(1.8 之前)
  • 元空间是 Hotspot 虚拟机对 JVM 规范的另一种实现(1.8 以后),使用本地内存作为这些信息的存储空间

image-20210831170457337

从这张图学到三点

  • 当第一次用到某个类是,由类加载器将 class 文件的类元信息读入,并存储于元空间
  • X,Y 的类元信息是存储于元空间中,无法直接访问
  • 可以用 X.class,Y.class 间接访问类元信息,它们俩属于 java 对象,我们的代码中可以使用

image-20210831170512418

从这张图可以学到

  • 堆内存中:当一个类加载器对象,这个类加载器对象加载的所有类对象,这些类对象对应的所有实例对象都没人引用时,GC 时就会对它们占用的对内存进行释放
  • 元空间中:内存释放以类加载器为单位,当堆中类加载器内存释放时,对应的元空间中的类元信息也会释放

2. JVM 内存参数

堆内存,按大小设置

image-20210831173130717

解释:

  • -Xms 最小堆内存(包括新生代和老年代)
  • -Xmx 最大对内存(包括新生代和老年代)
  • 通常建议将 -Xms 与 -Xmx 设置为大小相等,即不需要保留内存,不需要从小到大增长,这样性能较好
  • -XX:NewSize 与 -XX:MaxNewSize 设置新生代的最小与最大值,但一般不建议设置,由 JVM 自己控制
  • -Xmn 设置新生代大小,相当于同时设置了 -XX:NewSize 与 -XX:MaxNewSize 并且取值相等
  • 保留是指,一开始不会占用那么多内存,随着使用内存越来越多,会逐步使用这部分保留内存。下同

堆内存,按比例设置

image-20210831173045700

解释:

  • -XX:NewRatio=2:1 表示老年代占两份,新生代占一份
  • -XX:SurvivorRatio=4:1 表示新生代分成六份,伊甸园占四份,from 和 to 各占一份

元空间内存设置

image-20210831173118634

解释:

  • class space 存储类的基本信息,最大值受 -XX:CompressedClassSpaceSize 控制
  • non-class space 存储除类的基本信息以外的其它信息(如方法字节码、注解等)
  • class space 和 non-class space 总大小受 -XX:MaxMetaspaceSize 控制

注意:

  • 这里 -XX:CompressedClassSpaceSize 这段空间还与是否开启了指针压缩有关,这里暂不深入展开,可以简单认为指针压缩默认开启

代码缓存内存设置

image-20210831173148816

解释:

  • 如果 -XX:ReservedCodeCacheSize < 240m,所有优化机器代码不加区分存在一起
  • 否则,分成三个区域(图中笔误 mthod 拼写错误,少一个 e)
    • non-nmethods - JVM 自己用的代码
    • profiled nmethods - 部分优化的机器码
    • non-profiled nmethods - 完全优化的机器码

线程内存设置

image-20210831173155481

官方参考文档

3. JVM 垃圾回收

三种垃圾回收算法

标记清除法

image-20210831211008162

解释:

  1. 找到 GC Root 对象,即那些一定不会被回收的对象,如正执行方法内局部变量引用的对象、静态变量引用的对象
  2. 标记阶段:沿着 GC Root 对象的引用链找,直接或间接引用到的对象加上标记
  3. 清除阶段:释放未加标记的对象占用的内存

要点:

  • 标记速度与存活对象线性关系
  • 清除速度与内存大小线性关系
  • 缺点是会产生内存碎片

标记整理法

image-20210831211641241

解释:

  1. 前面的标记阶段、清理阶段与标记清除法类似
  2. 多了一步整理的动作,将存活对象向一端移动,可以避免内存碎片产生

特点:

  • 标记速度与存活对象线性关系

  • 清除与整理速度与内存大小成线性关系

  • 缺点是性能上较慢

标记复制法

image-20210831212125813

解释:

  1. 将整个内存分成两个大小相等的区域,from 和 to,其中 to 总是处于空闲,from 存储新创建的对象
  2. 标记阶段与前面的算法类似
  3. 在找出存活对象后,会将它们从 from 复制到 to 区域,复制的过程中自然完成了碎片整理
  4. 复制完成后,交换 from 和 to 的位置即可

特点:

  • 标记与复制速度与存活对象成线性关系
  • 缺点是会占用成倍的空间

GC 与分代回收算法

GC 的目的在于实现无用对象内存自动释放,减少内存碎片、加快分配速度

GC 要点:

  • 回收区域是堆内存,不包括虚拟机栈
  • 判断无用对象,使用可达性分析算法三色标记法标记存活对象,回收未标记对象
  • GC 具体的实现称为垃圾回收器
  • GC 大都采用了分代回收思想
    • 理论依据是大部分对象朝生夕灭,用完立刻就可以回收,另有少部分对象会长时间存活,每次很难回收
    • 根据这两类对象的特性将回收区域分为新生代老年代,新生代采用标记复制法、老年代一般采用标记整理法
  • 根据 GC 的规模可以分成 Minor GCMixed GCFull GC

分代回收

  1. 伊甸园 eden,最初对象都分配到这里,与幸存区 survivor(分成 from 和 to)合称新生代,

image-20210831213622704

  1. 当伊甸园内存不足,标记伊甸园与 from(现阶段没有)的存活对象

image-20210831213640110

  1. 将存活对象采用复制算法复制到 to 中,复制完毕后,伊甸园和 from 内存都得到释放

image-20210831213657861

  1. 将 from 和 to 交换位置

image-20210831213708776

  1. 经过一段时间后伊甸园的内存又出现不足

image-20210831213724858

  1. 标记伊甸园与 from(现阶段没有)的存活对象

image-20210831213737669

  1. 将存活对象采用复制算法复制到 to 中

image-20210831213804315

  1. 复制完毕后,伊甸园和 from 内存都得到释放

image-20210831213815371

  1. 将 from 和 to 交换位置

image-20210831213826017

  1. 老年代 old,当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)

GC 规模

  • Minor GC 发生在新生代的垃圾回收,暂停时间短

  • Mixed GC 新生代 + 老年代部分区域的垃圾回收,G1 收集器特有

  • Full GC 新生代 + 老年代完整垃圾回收,暂停时间长,应尽力避免

三色标记

即用三种颜色记录对象的标记状态

  • 黑色 – 已标记
  • 灰色 – 标记中
  • 白色 – 还未标记
  1. 起始的三个对象还未处理完成,用灰色表示
image-20210831215016566
  1. 该对象的引用已经处理完成,用黑色表示,黑色引用的对象变为灰色
image-20210831215033510
  1. 依次类推
image-20210831215105280
  1. 沿着引用链都标记了一遍
image-20210831215146276
  1. 最后为标记的白色对象,即为垃圾
image-20210831215158311

并发漏标问题

比较先进的垃圾回收器都支持并发标记,即在标记过程中,用户线程仍然能工作。但这样带来一个新的问题,如果用户线程修改了对象引用,那么就存在漏标问题。例如:

  1. 如图所示标记工作尚未完成
image-20210831215846876
  1. 用户线程同时在工作,断开了第一层 3、4 两个对象之间的引用,这时对于正在处理 3 号对象的垃圾回收线程来讲,它会将 4 号对象当做是白色垃圾
image-20210831215904073
  1. 但如果其他用户线程又建立了 2、4 两个对象的引用,这时因为 2 号对象是黑色已处理对象了,因此垃圾回收线程不会察觉到这个引用关系的变化,从而产生了漏标
image-20210831215919493
  1. 如果用户线程让黑色对象引用了一个新增对象,一样会存在漏标问题
image-20210831220004062

因此对于并发标记而言,必须解决漏标问题,也就是要记录标记过程中的变化。有两种解决方法:

  1. Incremental Update 增量更新法,CMS 垃圾回收器采用
    • 思路是拦截每次赋值动作,只要赋值发生,被赋值的对象就会被记录下来,在重新标记阶段再确认一遍
  2. Snapshot At The Beginning,SATB 原始快照法,G1 垃圾回收器采用
    • 思路也是拦截每次赋值动作,不过记录的对象不同,也需要在重新标记阶段对这些对象二次处理
    • 新加对象会被记录
    • 被删除引用关系的对象也被记录

垃圾回收器 - Parallel GC

  • eden 内存不足发生 Minor GC,采用标记复制算法,需要暂停用户线程

  • old 内存不足发生 Full GC,采用标记整理算法,需要暂停用户线程

  • 注重吞吐量

垃圾回收器 - ConcurrentMarkSweep GC

  • 它是工作在 old 老年代,支持并发标记的一款回收器,采用并发清除算法

    • 并发标记时不需暂停用户线程
    • 重新标记时仍需暂停用户线程
  • 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC

  • 注重响应时间

垃圾回收器 - G1 GC

  • 响应时间与吞吐量兼顾
  • 划分成多个区域,每个区域都可以充当 eden,survivor,old, humongous,其中 humongous 专为大对象准备
  • 分成三个阶段:新生代回收、并发标记、混合收集
  • 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC

G1 回收阶段 - 新生代回收

  1. 初始时,所有区域都处于空闲状态
image-20210831222639754
  1. 创建了一些对象,挑出一些空闲区域作为伊甸园区存储这些对象
image-20210831222653802
  1. 当伊甸园需要垃圾回收时,挑出一个空闲区域作为幸存区,用复制算法复制存活对象,需要暂停用户线程
image-20210831222705814
  1. 复制完成,将之前的伊甸园内存释放
image-20210831222724999
  1. 随着时间流逝,伊甸园的内存又有不足
image-20210831222737928
  1. 将伊甸园以及之前幸存区中的存活对象,采用复制算法,复制到新的幸存区,其中较老对象晋升至老年代
image-20210831222752787
  1. 释放伊甸园以及之前幸存区的内存
image-20210831222803281

G1 回收阶段 - 并发标记与混合收集

  1. 当老年代占用内存超过阈值后,触发并发标记,这时无需暂停用户线程
image-20210831222813959
  1. 并发标记之后,会有重新标记阶段解决漏标问题,此时需要暂停用户线程。这些都完成后就知道了老年代有哪些存活对象,随后进入混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间目标优先回收价值高(存活对象少)的区域(这也是 Gabage First 名称的由来)。
image-20210831222828104
  1. 混合收集阶段中,参与复制的有 eden、survivor、old,下图显示了伊甸园和幸存区的存活对象复制
image-20210831222841096
  1. 下图显示了老年代和幸存区晋升的存活对象的复制
image-20210831222859760
  1. 复制完成,内存得到释放。进入下一轮的新生代回收、并发标记、混合收集
image-20210831222919182

4. 内存溢出

典型情况

  • 误用线程池导致的内存溢出

    java
    
    import day02.LoggerUtils;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.TimeUnit;
    
    // -Xmx64m
    // 模拟短信发送超时,但这时仍有大量的任务进入队列
    public class TestOomThreadPool {
        public static void main(String[] args) {
            ExecutorService executor = Executors.newFixedThreadPool(2);
            LoggerUtils.get().debug("begin...");
            while (true) {
                executor.submit(()->{
                    try {
                        LoggerUtils.get().debug("send sms...");
                        TimeUnit.SECONDS.sleep(30);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
            }
        }
    }
  • 查询数据量太大导致的内存溢出

    java
    
    import org.openjdk.jol.info.ClassLayout;
    
    import java.nio.charset.StandardCharsets;
    
    // 演示对象的内存估算
    public class TestOomTooManyObject {
        public static void main(String[] args) {
            // 对象本身内存
            long a = ClassLayout.parseInstance(new Product()).instanceSize();
            System.out.println(a);
            // 一个字符串占用内存
            String name = "联想小新Air14轻薄本 英特尔酷睿i5 14英寸全面屏学生笔记本电脑(i5-1135G7 16G 512G MX450独显 高色域)银";
            long b = ClassLayout.parseInstance(name).instanceSize();
            System.out.println(b);
            String desc = "【全金属全面屏】学生商务办公,全新11代处理器,MX450独显,100%sRGB高色域,指纹识别,快充(更多好货)";
            long c = ClassLayout.parseInstance(desc).instanceSize();
            System.out.println(c);
            System.out.println(16 + name.getBytes(StandardCharsets.UTF_8).length);
            System.out.println(16 + desc.getBytes(StandardCharsets.UTF_8).length);
            // 一个对象估算的内存
            long avg = a + b + c + 16 + name.getBytes(StandardCharsets.UTF_8).length + 16 + desc.getBytes(StandardCharsets.UTF_8).length;
            System.out.println(avg);
            // ArrayList 24, Object[] 16 共 40
            System.out.println((1_000_000 * avg + 40) / 1024 / 1024 + "Mb");
        }
    
        static public class Product {
            private int id;
            private String name;
            private int price;
            private String desc;
    
            public int getId() {
                return id;
            }
    
            public void setId(int id) {
                this.id = id;
            }
    
            public String getName() {
                return name;
            }
    
            public void setName(String name) {
                this.name = name;
            }
    
            public int getPrice() {
                return price;
            }
    
            public void setPrice(int price) {
                this.price = price;
            }
    
            public String getDesc() {
                return desc;
            }
    
            public void setDesc(String desc) {
                this.desc = desc;
            }
        }
    }
  • 动态生成类导致的内存溢出

    java
    
    import groovy.lang.GroovyShell;
    
    import java.io.FileReader;
    import java.io.IOException;
    import java.util.concurrent.atomic.AtomicInteger;
    
    // -XX:MaxMetaspaceSize=24m
    // 模拟不断生成类, 但类无法卸载的情况
    public class TestOomTooManyClass {
    
    //    static GroovyShell shell = new GroovyShell();
    
        public static void main(String[] args) {
            AtomicInteger c = new AtomicInteger();
            while (true) {
                try (FileReader reader = new FileReader("script")) {
                    GroovyShell shell = new GroovyShell();
                    shell.evaluate(reader);
                    System.out.println(c.incrementAndGet());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

5. 类加载

类加载过程的三个阶段

  1. 加载

    1. 将类的字节码载入方法区,并创建类.class 对象

    2. 如果此类的父类没有加载,先加载父类

    3. 加载是懒惰执行

  2. 链接

    1. 验证 – 验证类是否符合 Class 规范,合法性、安全性检查
    2. 准备 – 为 static 变量分配空间,设置默认值
    3. 解析 – 将常量池的符号引用解析为直接引用
  3. 初始化

    1. 静态代码块、static 修饰的变量赋值、static final 修饰的引用类型变量赋值,会被合并成一个 <cinit> 方法,在初始化时被调用
    2. static final 修饰的基本类型变量赋值,在链接阶段就已完成
    3. 初始化是懒惰执行

验证手段

  • 使用 jps 查看进程号
  • 使用 jhsdb 调试,执行命令 jhsdb.exe hsdb 打开它的图形界面
    • Class Browser 可以查看当前 jvm 中加载了哪些类
    • 控制台的 universe 命令查看堆内存范围
    • 控制台的 g1regiondetails 命令查看 region 详情
    • scanoops 起始地址 结束地址 对象类型 可以根据类型查找某个区间内的对象地址
    • 控制台的 inspect 地址 指令能够查看这个地址对应的对象详情
  • 使用 javap 命令可以查看 class 字节码

代码说明

  • day03.loader.TestLazy - 验证类的加载是懒惰的,用到时才触发类加载

    java
    
    import java.io.IOException;
    import day03.loader.Student;
    
    /**
     * 此案例说明
     <ul>
        <li>类加载是懒惰的, 首次用到时才加载(下面的初始化条件满足也会导致类加载)
          <ol>
             <li>使用了类.class</li>
             <li>用类加载器的 loadClass 方法加载类</li>
          </ol>
        </li>
        <li>类初始化是懒惰的, 满足条件有
          <ol>
             <li>main 方法所在类</li>
             <li>首次访问静态方法或静态变量(非 final, 或 final的 引用类型)</li>
             <li>子类初始化, 导致的父类初始化</li>
             <li>Class.forName(类名, true, loader) 或 Class.forName(类名)</li>
             <li>new, clone, 反序列化时</li>
          </ol>
        </li>
     </ul>
    
     */
    public class TestLazy {
        private Class<?> studentClass;
        public static void main(String[] args) throws IOException {
            System.out.println("未用到 Student");
            System.in.read();
    
            System.out.println(Student.class);   // 关键代码1,会触发类加载
            System.out.println("已加载 Student");
            TestLazy testLazy = new TestLazy();
            testLazy.studentClass = Student.class;
            System.in.read();
    
            Student stu = new Student();        // 关键代码2,会触发类初始化
            System.out.println("已初始化 Student");
            System.in.read();
        }
    }
  • day03.loader.TestFinal - 验证使用 final 修饰的变量不会触发类加载

java

import java.io.IOException;

public class TestFinal {
    public static void main(String[] args) throws IOException {
        System.out.println(Student.c); // c 是 final static 基本类型
        System.in.read();

        System.out.println(Student.m); // m 是 final static 基本类型
        System.in.read();

        System.out.println(Student.n); // n 是 final static 引用类型
        System.in.read();
    }
}

jdk 8 的类加载器

名称加载哪的类说明
Bootstrap ClassLoaderJAVA_HOME/jre/lib无法直接访问
Extension ClassLoaderJAVA_HOME/jre/lib/ext上级为 Bootstrap,显示为 null
Application ClassLoaderclasspath上级为 Extension
自定义类加载器自定义上级为 Application

双亲委派机制

所谓的双亲委派,就是指优先委派上级类加载器进行加载,如果上级类加载器

  • 能找到这个类,由上级加载,加载后该类也对下级加载器可见
  • 找不到这个类,则下级类加载器才有资格执行加载

双亲委派的目的有两点

  1. 让上级类加载器中的类对下级共享(反之不行),即能让你的类能依赖到 jdk 提供的核心类

  2. 让类的加载有优先次序,保证核心类优先加载

对双亲委派的误解

下面面试题的回答是错误的

image-20210901110910016

错在哪了?

  • 自己编写类加载器就能加载一个假冒的 java.lang.System 吗? 答案是不行。

  • 假设你自己的类加载器用双亲委派,那么优先由启动类加载器加载真正的 java.lang.System,自然不会加载假冒的

  • 假设你自己的类加载器不用双亲委派,那么你的类加载器加载假冒的 java.lang.System 时,它需要先加载父类 java.lang.Object,而你没有用委派,找不到 java.lang.Object 所以加载会失败

  • 以上也仅仅是假设。事实上操作你就会发现,自定义类加载器加载以 java. 打头的类时,会抛安全异常,在 jdk9 以上版本这些特殊包名都与模块进行了绑定,更连编译都过不了

代码说明

  • day03.loader.TestJdk9ClassLoader - 演示类加载器与模块的绑定关系
java

import jdk.internal.loader.*;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

// --add-opens java.base/jdk.internal.loader=ALL-UNNAMED
public class TestJdk9ClassLoader {
    public static void main(String[] args) {
        ClassLoader appLoader = TestJdk9ClassLoader.class.getClassLoader();
        System.out.println(appLoader + "============>");
        showPackages(appLoader);

        ClassLoader platformLoader = appLoader.getParent();
        System.out.println(platformLoader + "============>");
        showPackages(platformLoader);

        ClassLoader bootLoader = getBootLoader(platformLoader);
        System.out.println(bootLoader + "============>");
        showPackages(bootLoader);
    }

    private static ClassLoader getBootLoader(ClassLoader platformLoader) {
        try {
            Field parent = BuiltinClassLoader.class.getDeclaredField("parent");
            parent.setAccessible(true);
            return (ClassLoader) parent.get(platformLoader);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static void showPackages(ClassLoader loader) {
        try {
            Field nameToModule = BuiltinClassLoader.class.getDeclaredField("nameToModule");
            nameToModule.setAccessible(true);

            Map<String, Object> map = (Map<String, Object>) nameToModule.get(loader);
            List<String> list = new ArrayList<>(map.keySet());
            list.sort(Comparator.naturalOrder());
            for (int i = 0; i < list.size(); i++) {
                System.out.print(list.get(i));
                System.out.print("\t");
                if ((i + 1) % 6 == 0 || i == list.size() - 1) {
                    System.out.println();
                }
            }
            System.out.println();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

6. 四种引用

强引用

  1. 普通变量赋值即为强引用,如 A a = new A();

  2. 通过 GC Root 的引用链,如果强引用不到该对象,该对象才能被回收

image-20210901111903574

软引用(SoftReference)

  1. 例如:SoftReference a = new SoftReference(new A());

  2. 如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存仍不足,再次回收时才会释放对象

  3. 软引用自身需要配合引用队列来释放

  4. 典型例子是反射数据

image-20210901111957328

弱引用(WeakReference)

  1. 例如:WeakReference a = new WeakReference(new A());

  2. 如果仅有弱引用引用该对象时,只要发生垃圾回收,就会释放该对象

  3. 弱引用自身需要配合引用队列来释放

  4. 典型例子是 ThreadLocalMap 中的 Entry 对象

image-20210901112107707

虚引用(PhantomReference)

  1. 例如: PhantomReference a = new PhantomReference(new A(), referenceQueue);

  2. 必须配合引用队列一起使用,当虚引用所引用的对象被回收时,由 Reference Handler 线程将虚引用对象入队,这样就可以知道哪些对象被回收,从而对它们关联的资源做进一步处理

  3. 典型例子是 Cleaner 释放 DirectByteBuffer 关联的直接内存

image-20210901112157901

代码说明

  • day03.reference.TestPhantomReference - 演示虚引用的基本用法

    java
    
    import day02.LoggerUtils;
    
    import java.io.IOException;
    import java.lang.ref.*;
    import java.util.ArrayList;
    import java.util.List;
    
    public class TestPhantomReference {
        public static void main(String[] args) throws IOException, InterruptedException {
            ReferenceQueue<String> queue = new ReferenceQueue<>();// 引用队列
            List<MyResource> list = new ArrayList<>();
            list.add(new MyResource(new String("a"), queue));
            list.add(new MyResource("b", queue));
            list.add(new MyResource(new String("c"), queue));
    
            System.gc(); // 垃圾回收
            Thread.sleep(100);
            Object ref;
            while ((ref = queue.poll()) != null) {
                if (ref instanceof MyResource resource) {
                    resource.clean();
                }
            }
        }
    
        static class MyResource extends PhantomReference<String> {
            public MyResource(String referent, ReferenceQueue<? super String> q) {
                super(referent, q);
            }
            // 释放外部资源的方法
            public void clean() {
                LoggerUtils.get().debug("clean");
            }
        }
    }
  • day03.reference.TestWeakReference - 模拟 ThreadLocalMap, 采用引用队列释放 entry 内存

java

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;

public class TestWeakReference {

    public static void main(String[] args) {
        MyWeakMap map = new MyWeakMap();
        map.put(0, new String("a"), "1");
        map.put(1, "b", "2");
        map.put(2, new String("c"), "3");
        map.put(3, new String("d"), "4");
        System.out.println(map);

        System.gc();
        System.out.println(map.get("a"));
        System.out.println(map.get("b"));
        System.out.println(map.get("c"));
        System.out.println(map.get("d"));
        System.out.println(map);
        map.clean();
        System.out.println(map);
    }

    // 模拟 ThreadLocalMap 的内存泄漏问题以及一种解决方法
    static class MyWeakMap {
        static ReferenceQueue<Object> queue = new ReferenceQueue<>();
        static class Entry extends WeakReference<String> {
            String value;

            public Entry(String key, String value) {
                super(key, queue);
                this.value = value;
            }
        }
        public void clean() {
            Object ref;
            while ((ref = queue.poll()) != null) {
                System.out.println(ref);
                for (int i = 0; i < table.length; i++) {
                    if(table[i] == ref) {
                        table[i] = null;
                    }
                }
            }
        }

        Entry[] table = new Entry[4];

        public void put(int index, String key, String value) {
            table[index] = new Entry(key, value);
        }

        public String get(String key) {
            for (Entry entry : table) {
                if (entry != null) {
                    String k = entry.get();
                    if (k != null && k.equals(key)) {
                        return entry.value;
                    }
                }
            }
            return null;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("[");
            for (Entry entry : table) {
                if (entry != null) {
                    String k = entry.get();
                    sb.append(k).append(":").append(entry.value).append(",");
                }
            }
            if (sb.length() > 1) {
                sb.deleteCharAt(sb.length() - 1);
            }
            sb.append("]");
            return sb.toString();
        }
    }
}

7. finalize

finalize

  • 它是 Object 中的一个方法,如果子类重写它,垃圾回收时此方法会被调用,可以在其中进行资源释放和清理工作
  • 将资源释放和清理放在 finalize 方法中非常不好,非常影响性能,严重时甚至会引起 OOM,从 Java9 开始就被标注为 @Deprecated,不建议被使用了

finalize 原理

  1. 对 finalize 方法进行处理的核心逻辑位于 java.lang.ref.Finalizer 类中,它包含了名为 unfinalized 的静态变量(双向链表结构),Finalizer 也可被视为另一种引用对象(地位与软、弱、虚相当,只是不对外,无法直接使用)
  2. 当重写了 finalize 方法的对象,在构造方法调用之时,JVM 都会将其包装成一个 Finalizer 对象,并加入 unfinalized 链表中

image-20210901121032813

  1. Finalizer 类中还有另一个重要的静态变量,即 ReferenceQueue 引用队列,刚开始它是空的。当狗对象可以被当作垃圾回收时,就会把这些狗对象对应的 Finalizer 对象加入此引用队列
  2. 但此时 Dog 对象还没法被立刻回收,因为 unfinalized -> Finalizer 这一引用链还在引用它嘛,为的是【先别着急回收啊,等我调完 finalize 方法,再回收】
  3. FinalizerThread 线程会从 ReferenceQueue 中逐一取出每个 Finalizer 对象,把它们从链表断开并真正调用 finallize 方法

image-20210901122228916

  1. 由于整个 Finalizer 对象已经从 unfinalized 链表中断开,这样没谁能引用到它和狗对象,所以下次 gc 时就被回收了

finalize 缺点

  • 无法保证资源释放:FinalizerThread 是守护线程,代码很有可能没来得及执行完,线程就结束了
  • 无法判断是否发生错误:执行 finalize 方法时,会吞掉任意异常(Throwable)
  • 内存释放不及时:重写了 finalize 方法的对象在第一次被 gc 时,并不能及时释放它占用的内存,因为要等着 FinalizerThread 调用完 finalize,把它从 unfinalized 队列移除后,第二次 gc 时才能真正释放内存
  • 有的文章提到【Finalizer 线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的CPU时间较少,因此它永远也赶不上主线程的步伐】这个显然是错误的,FinalizerThread 的优先级较普通线程更高,原因应该是 finalize 串行执行慢等原因综合导致

代码说明

  • day03.reference.TestFinalize - finalize 的测试代码
java

import day02.LoggerUtils;

import java.io.IOException;

public class TestFinalize {
    static class Dog {
        private String name;

        public Dog(String name) {
            this.name = name;
        }

        @Override
        protected void finalize() throws Throwable {
            LoggerUtils.get().debug("{}被干掉了?", this.name);
            int i = 1 / 0;
        }
    }

    public static void main(String[] args) throws IOException {
        new Dog("大傻");
        new Dog("二哈");
        new Dog("三笨");
        System.gc();
        System.in.read();
    }
    /*
    第一,从表面上我们能看出来 finalize 方法的调用次序并不能保证
    第二,日志中的 Finalizer 表示输出日志的线程名称,从这我们看出是这个叫做 Finalizer 的线程调用的 finalize 方法
    第三,你不能注释掉 `System.in.read()`,否则会发现(绝大概率)并不会有任何输出结果了,从这我们看出 finalize 中的代码并不能保证被执行
    第四,如果将 finalize 中的代码出现异常,会发现根本没有异常输出
    第五,还有个疑问,垃圾回收时就会立刻调用  finalize 方法吗?
     */
}

Jdk8新日期api

原日期类的毫秒值与日期直接转换比较繁琐,其次通过毫秒值来计算时间的差额步骤较多.

同时日期格式化的SimpleDateFormat类是线程不安全的,在多线程的情况下,全局共享一个SimpleDateFormat类中的Calendar对象有可能会出现异常

在java.util.Date和java.util.Calendar类之前,枚举类型(ENUM)还没有出现,所以在字段中使用整数常量导致整数常量都是可变的,而不是线程安全的.为了处理实际开发中遇到的问题,标准库随后引入了java.sql.Date作为java.util.Date的子类,但是还是没能彻底解决问题

Date-Time API中的基本类使用

常用类概述与功能介绍

  • Instant类 Instant类对时间轴上的单一瞬时点建模,可以用于记录应用程序中的事件时间戳,在之后学习的类型转换中,均可以使用Instant类作为中间类完成转换.

    Instant封装的时间为祖鲁时间并非当前时间.祖鲁时间也是格林尼治时间,也就是国际标准时间.

  • Duration类 Duration类表示秒或纳秒时间间隔,适合处理较短的时间,需要更高的精确性.

  • Period类 Period类表示一段时间的年、月、日.

  • LocalDate类 LocalDate是一个不可变的日期时间对象,表示日期,通常被视为年月日.LocalDate封装的只有年月日,没有时分秒,格式为yyyy-MM-dd.

  • LocalTime类 LocalTime是一个不可变的日期时间对象,代表一个时间,通常被看作是小时-秒,时间表示为纳秒精度.LocalTime封装的只有时分秒,没有年月日,格式为hh:mm:ss.sss,最后的sss是纳秒

  • LocalDateTime类 LocalDateTime是一个不可变的日期时间对象,代表日期时间,通常被视为年-月-日- 时-分-秒.LocalDateTime将LocalDate和LocalTime合二为一,在年月日与时分秒中间使用T作为分隔.

  • ZonedDateTime类 ZonedDateTime是具有时区的日期时间的不可变表示,此类存储所有日期和时间字段,精度为纳秒,时区为区域偏移量,用于处理模糊的本地日期时间。ZonedDateTime中封装了年月日时分秒,以及UTC(祖鲁时间)偏移量,并且还有一个地区名。+8:00代表中国是东八区,时间比国际标准时间快八小时.

  • Year类

    表示年

  • YearMonth类

    表示年月

  • MonthDay类

    表示月日

now方法在日期/时间类的使用

Date-Time API中的所有类均生成不可变实例,它们是线程安全的,并且这些类不提供公共构造函数,也就是说没办法通过new的方式直接创建,需要采用工厂方法加以实例化

java
//使用now方法创建Instant的实例对象.
Instant instantNow = Instant.now();
//使用now方法创建LocalDate的实例对象.
LocalDate localDateNow = LocalDate.now();
//使用now方法创建LocalTime的实例对象.
LocalTime localTimeNow = LocalTime.now();
//使用now方法创建LocalDateTime的实例对象.
LocalDateTime localDateTimeNow = LocalDateTime.now();
//使用now方法创建ZonedDateTime的实例对象.
ZonedDateTime zonedDateTimeNow = ZonedDateTime.now();
//初始化Year的实例化对象.
Year year = Year.now();
//初始化YearMonth的实例化对象
YearMonth yearMonth = YearMonth.now();
//初始化MonthDay的实例化对象.
MonthDay monthDay = MonthDay.now();

//将实例对象打印到控制台.
System.out.println("Instant:"+instantNow);
System.out.println("LocalDate:"+localDateNow);
System.out.println("LocalTime:"+localTimeNow);
System.out.println("LocalDateTime:"+localDateTimeNow);
System.out.println("ZonedDateTime:"+zonedDateTimeNow);

of方法在日期/时间类的应用

of方法可以根据给定的参数生成对应的日期/时间对象,基本上每个基本类都有of方法用于生成的对应的对象,而且重载形式多变,可以根据不同的参数生成对应的数据

java
//初始化2018年8月8日的LocalDate对象.
LocalDate date = LocalDate.of(2018, 8, 8);
System.out.println("LocalDate:" + date);

/*
         初始化晚上7点0分0秒的LocalTime对象.
         LocalTime.of方法的重载形式有以下几种,可以根据实际情况自行使用.
         LocalTime of(int hour, int minute) -> 根据小时/分钟生成对象.
         LocalTime of(int hour, int minute, int second) -> 根据小时/分钟/秒生成对象.
         LocalTime of(int hour, int minute, int second, int nanoOfSecond) -> 根据小时/分钟/毫秒/纳秒生成对象.

         注意:如果秒和纳秒为0的话,那么默认不会封装这些数据,只显示小时和分钟.
         */

LocalTime time = LocalTime.of(19, 0, 0, 0);
System.out.println("LocalTime:" + time);

/*
        初始化2018年8月8日下午7点0分的LocalDateTime对象.
        LocalDateTime.of方法的重载形式有以下几种,可以根据事情自行使用.
        LocalDateTime of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond) -> 根据年/月/日/时/分/秒生成对象.
        LocalDateTime of(int year, int month, int dayOfMonth, int hour, int minute) -> 根据年/月/日/时/分生成对象.
        注意:LocalDateTime of(LocalDate date, LocalTime time)方法可以将一个LocalDate对象和一个LocalTime对象合并封装为一个LocalDateTime对象.
         */
LocalDateTime.of(2018, 8, 8, 19, 0, 0, 0);
LocalDateTime localDateTime = LocalDateTime.of(date, time);
System.out.println("LocalDateTime:" + localDateTime);

为LocalDateTime添加时区信息

可以通过给LocalDateTime添加时区信息来查看到不同时区的时间

java
//获取所有的时区信息
Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
for (String zoneId : availableZoneIds) {
    System.out.println(zoneId);
}

//获取当前系统默认的时区信息
/*ZoneId zoneId = ZoneId.systemDefault();
System.out.println(zoneId);*/

 //1.封装LocalDateTime对象,参数自定义 -> 2018年11月11日 8点54分38秒
LocalDateTime time = LocalDateTime.of(2018, 11, 11, 8, 54, 38);
//2.封装完成后的time对象只是封装的是一个时间,并没有时区相关的数据,所以添加时区到对象中,使用atZone方法.
ZonedDateTime zonedDateTime = time.atZone(ZoneId.of("Asia/Shanghai"));
System.out.println("Asia/Shanghai的时间是:" + zonedDateTime);
//3.更改时区查看其它时区的当前时间,通过withZoneSameInstant方法即可更改.
ZonedDateTime otherZonedTime = zonedDateTime.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
System.out.println("在同一时刻,Asia/Tokyo的时间是:" + otherZonedTime);

Month枚举类的使用

java.time包中引入了Month的枚举,Month中包含标准日历中的12个月份的常量(从JANURAY到DECEMEBER)也提供了一些方便的方法供我们使用. 推荐在初始化LocalDate和LocalDateTime对象的时候,月份的参数使用枚举的方式传入,这样更简单易懂而且不易出错,因为如果是老的思维,Calendar传入0的话,那么会出现异常

java
//在初始化LocalDate和LocalDateTime的时候,月份的参数传入枚举类(2011年5月15日11时11分11秒)
LocalDateTime.of(2011, Month.JUNE,15,11,11,11);

//of方法可以根据传入的数字返回对应的月份.
Month month = Month.of(12);
System.out.println(month);

修改日期/时间实例

plus和minus增加和减去时间

修改某个日期/时间对象的现有实例时,可以使用plus和minus方法来完成操作. Java8中日期时间相关的API中的所有实例都是不可改变的,一旦创建LocalDate,LocalTime,LocalDateTime就无法修改他们(类似于String),这对于 线程安全非常有利.

minus 同plus 调用minus 其实还是调用的plus,值为负数

java
//封装LocalDate对象参数为2016年2月13日.
LocalDate date = LocalDate.of(2016, Month.FEBRUARY, 13);
//计算当前时间的4天后的时间.
LocalDate plusDaysTime = date.plusDays(4);
//计算当前时间的周后的时间.
LocalDate plusWeeksTime = date.plusWeeks(3);
//计算当前时间的5个月后的时间.
LocalDate plusMonthsTime = date.plusMonths(5);
//计算当前时间的2年后的时间.
LocalDate plusYearsTime = date.plusYears(2);

//封装LocalTime对象参数为8时14分39秒218纳秒.
LocalTime time = LocalTime.of(8, 14, 39, 218);
//计算当前时间500纳秒后的时间.
LocalTime plusNanosTime = time.plusNanos(500);
//计算当前时间45秒后的时间.
LocalTime plusSecondsTime = time.plusSeconds(45);
//计算当前时间19分钟后的时间.
LocalTime plusMinutesTime = time.plusMinutes(19);
//计算当前时间3小时后的时间.
LocalTime plusHoursTime = time.plusHours(3);

// plus(TemporaAmount amountToAdd)
// TemporaAmount是一个接口,当接口作为方法的参数的时候,实际上传入的是接口的实现类对象,接口有一个实现类,名字叫做Period
// Period.of(1,2,3)返回的对象表示的即为1年2个月3天
 LocalDate date = LocalDate.now(); //date表示当前时间.
//固然可以使用对于年月日依次+2,+3,+8的方式来操作,但是有些繁琐,首先我们先将2年3月8天封装为一段时间,也就是封装为一个Period对象.
Period time = Period.of(2, 3, 8);
//使用plus方法对于date对象直接进行增加的操作.
LocalDate endDate = date.plus(time);

// plus(long l,TemporaUnit unit) 
// TemporaUnit是一个接口 ,子类,ChronoUnit封装了很多时间段
LocalDateTime marryTime = LocalDateTime.of(2020, Month.FEBRUARY, 2, 11, 11, 11);
//使用plus方法进行计算,添加1个,ChronoUnit.DECADES(十年).
LocalDateTime time = marryTime.plus(1, ChronoUnit.DECADES);

with方法修改时间

对日期进行直接修改日期的话,可以使用with方法

java
LocalDateTime time = LocalDateTime.now();

// 修改纳秒
LocalDateTime endTime = time.withNano(1);
// 修改秒
LocalDateTime endTime = time.withSecond(1);
// 修改分钟
LocalDateTime endTime = time.withMinute(1);
// 修改小时
LocalDateTime endTime = time.withHour(1);
//经过使用发现time中的时间有错误,应该是1日,在不知道原有时间的基础上,无法进行增减操作,所以可以直接使用with方法进行修改.
// 修改日
LocalDateTime endTime = time.withDayOfMonth(1);
// 修改月
LocalDateTime endTime = time.withMonth(1);
// 修改年
LocalDateTime endTime = time.withYear(1);

// with(TemporalField field, long newValue)
// TemporalField是一个接口 ,子类,ChronoField封装了一些日期时间中的组成部分
//经过使用发现time中的时间有错误,应该是1日,在不知道原有时间的基础上,无法进行增减操作,所以可以直接使用with方法进行修改.
// 将日期中的月份中的天数改为1
LocalDateTime endTime = time.with(ChronoField.DAY_OF_MONTH,1);
//  将日期中的年份改为2021.
LocalDateTime endTime = time.with(ChronoField.YEAR,2021);

调节器TemporalAdjuster与查询TemporalQuery

TemporalAdjuster

with方法有一个重载形式,需要传入一个TemporalAdjuster对象,

java
//封装日期时间对象为当前时间,LocalDate.
LocalDate time = LocalDate.now();
/*
        with方法可以修改time对象中封装的数据,需要传入一个TemporalAdjuster对象,
        通过查看发现TemporalAdjuster是一个接口,方法的参数是一个接口,那么实际上传入的是这个接口的实现类对象.
        TemporalAdjusters的类可以给我们提供一些常用的方法.
         */

//with方法传入了TemporalAdjuster类的实现对象,是由TemporalAdjusters类的方法实现了adjustInto方法,当前的逻辑是:将时间修改为当月的第一天.
LocalDate firstDayOfMonth = time.with(TemporalAdjusters.firstDayOfMonth());
//将时间修改为下个月的第一天.
LocalDate firstDayOfNextMonth = time.with(TemporalAdjusters.firstDayOfNextMonth());
//将时间修改为下一年的第一天.
LocalDate firstDayOfNextYear = time.with(TemporalAdjusters.firstDayOfNextYear());
//将时间修改为本年的第一天.
LocalDate firstDayOfYear = time.with(TemporalAdjusters.firstDayOfYear());
//将时间修改为本月的最后一天.
LocalDate lastDayOfMonth = time.with(TemporalAdjusters.lastDayOfMonth());
//将时间修改为本年的最后一天.
LocalDate lastDayOfYear = time.with(TemporalAdjusters.lastDayOfYear());

DayOfWeek的使用

DayOfWeek是一周中星期几的枚举类,其中封装了从周一到周日.

java
/封装日期时间对象为当前时间,LocalDate.
    LocalDate time = LocalDate.now();
/*
        DayOfWeek是一周中星期几的枚举类,其中封装了从周一到周日.
         */
//将当前时间修改为下一个周日
LocalDate nextSunday = time.with(TemporalAdjusters.next(DayOfWeek.SUNDAY));
//将当前时间修改为上一个周三
LocalDate previousWednesday = time.with(TemporalAdjusters.previous(DayOfWeek.WEDNESDAY));
// 将当前时间修改为当月的第一个周日
LocalDate nextSunday = time.with(TemporalAdjusters.firstInMonth(DayOfWeek.SUNDAY));
// 将当前时间修改为当月的最后一个周日
LocalDate nextSunday = time.with(TemporalAdjusters.lastInMonth(DayOfWeek.SUNDAY));

自定义TemporalAdjuster调节器

创建类实现TemporalAdjuster接口

实现TemporalAdjuster中的adjustInto方法,传入一个日期时间对象,完成逻辑之后返回日期时间对象.

通过with方法传入自定义调节器对象完成更改

java

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.*;

/**
 * 假如员工一个月中领取工资,发薪日是每个月的15号,如果发薪日是周末,则调整为周五.
 */
public class PayDayAdjuster implements TemporalAdjuster {
    @Override
    public Temporal adjustInto(Temporal temporal) {
        //1.将temporal转换为子类对象LocalDate,from方法可以将任何时态对象转换为LocalDate.
        LocalDate payDay = LocalDate.from(temporal);
        //2.判断当前封装的时间中的日期是不是当月15日,如果不是,则更改为15日.
        int day;
        if (payDay.getDayOfMonth() != 15) {
            day = 15;
        } else {
            day = payDay.getDayOfMonth();
        }
        LocalDate realPayDay = payDay.withDayOfMonth(day);
        //3.判断realPayDay对象中封装的星期数是不是周六或者是周日,如果是周末或者是周日则更改为周五.
        if (realPayDay.getDayOfWeek() == DayOfWeek.SUNDAY || realPayDay.getDayOfWeek() == DayOfWeek.SATURDAY) {
            //说明发薪日是周末,则更改为周五.
            realPayDay =  realPayDay.with(TemporalAdjusters.previous(DayOfWeek.FRIDAY));
        }
        return realPayDay;
    }
}

//封装LocalDate对象为2018年12月1日.
LocalDate payDay = LocalDate.of(2019, 12, 1);
//2018年12月15日为周末,所以要提前到周五发放工资,通过自定义调节器完成对时间的修改.
LocalDate realPayDay = LocalDate.from(new PayDayAdjuster().adjustInto(payDay));

TemporalQuery

针对日期进行查询.

java

import java.time.LocalDate;
import java.time.Month;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalQuery;

/**
 * 获取某一天距离下一个劳动节的相隔天数的实现类.
 */
public class UntilDayQueryImpl implements TemporalQuery<Long> {
    @Override
    public Long queryFrom(TemporalAccessor temporal) {
        //获取当前的年/月/日信息.
        int year = temporal.get(ChronoField.YEAR);
        int month = temporal.get(ChronoField.MONTH_OF_YEAR);
        int day = temporal.get(ChronoField.DAY_OF_MONTH);
        //将获取到的数据封装为一个LocalDate对象.
        LocalDate time = LocalDate.of(year, month, day);
        //封装劳动节的时间,年参数传递year,month和day是5和1.
        LocalDate laborDay = LocalDate.of(year, Month.MAY,1);
        //判断当前时间是否已经超过了当年的劳动节,如果超过了,则laborDay+1年.
        if (time.isAfter(laborDay)){
            laborDay = laborDay.plusYears(1);
        }
        //通过ChronoUnit的between方法计算两个时间点的差额.
        long l = ChronoUnit.DAYS.between(time, laborDay);
        return l;
    }
}

 //封装LocalDate对象为当前时间.
LocalDate time = LocalDate.now();
//调用time对象的query方法查询距离下一个五一劳动节还有多少天.
Long l = time.query(new UntilDayQueryImpl());

转换为LocalDate

java.util.Date转换

java
//初始化Date对象.
Date d = new Date();
//将Date类对象转换为Instant类对象.
Instant i = d.toInstant();
//Date类包含日期和时间信息,但是并不提供时区信息,和Instant类一样,可以通过Instant类的atZone方法添加时区信息之后进行转换.
ZonedDateTime zonedDateTime = i.atZone(ZoneId.systemDefault());
//将ZonedDateTime通过toLocalDate方法转换为LocalDate对象.
LocalDate localDate = zonedDateTime.toLocalDate();

java.sql.Date转换

java
//初始化java.sql.Date对象.
Date d = new Date(System.currentTimeMillis());
//将java.sql.Date对象通过toLocalDate方法转换为LocalDate对象.
LocalDate localDate = d.toLocalDate();

java.sql.Timestamp转换

java
//初始化java.sql.Timestamp对象. 
Timestamp t = new Timestamp(System.currentTimeMillis()); 
//将java.sql.Timestamp对象通过toLocalDateTime方法转换为LocalDateTime对 象. 
LocalDateTime localDateTime = t.toLocalDateTime();

将java.util包中的类转换为java.time包中的相应类

java

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;

/**
 * 编写工具类传入不同的对象可以转换为对应的对象.
 */
public class Java8TimeConvertTool {
    /**
     * 将java.sql.Date转换为LocalDate
     *
     * @param date
     * @return
     */
    public static LocalDate convertFromSqlDateToLocalDate(java.sql.Date date) {
        return date.toLocalDate();
    }

    /**
     * 将LocalDate转换为java.sql.Date
     * @param date
     * @return
     */
    public static java.sql.Date convertFromLocalDateToSqlDate(LocalDate date) {
        return java.sql.Date.valueOf(date);
    }

    /**
     * 将java.util.Date转换为LocalDate
     * @param date
     * @return
     */
    public static LocalDate convertFromUtilDateToLocalDate(java.util.Date date) {
        return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
    }

    /**
     * 将java.sql.Timestamp转换为LocalDateTime
     * @param timestamp
     * @return
     */
    public static LocalDateTime convertFromTimestampToLocalDateTime(java.sql.Timestamp timestamp) {
        return timestamp.toLocalDateTime();
    }

    /**
     * 将LocalDateTime转换为java.sql.Timestamp
     * @param localDateTime
     * @return
     */
    public static java.sql.Timestamp convertFromLocalDateTimeToTimestamp(LocalDateTime localDateTime) {
        return java.sql.Timestamp.valueOf(localDateTime);
    }

    /**
     * 将LocalDate转换为java.util.Date
     * @param date
     * @return
     */
    public static java.util.Date convertFromLocalDateToUtilDate(LocalDate date){
        ZonedDateTime zonedDateTime = date.atStartOfDay(ZoneId.systemDefault());
        return Date.from(zonedDateTime.toInstant());
    }
}

java.util.Date转换2

java
//初始化Date对象.
Date d = new Date();
/*
        java.sql.Date类提供了转换为LocalDate的方法,那么可以将java.util.Date先转换为java.sql.Date.
        通过java.sql.Date的构造方法直接传入一个毫秒值可以构造一个java.sql.Date对象,毫秒值可以通过java.util.Date对象的getTime方法获取到.
         */
java.sql.Date date = new java.sql.Date(d.getTime());
//将java.sql.Date转化为LocalDate.
LocalDate localDate = date.toLocalDate();

Calendar转换为ZonedDateTime

java
 //初始化Canlendar对象.
Calendar cal = Calendar.getInstance();
//Calendar对象自Java1.1开始提供了一个方法获取时区对象的方法,getTimeZone,要将Calendar对象转换为ZonedDateTime需要先获取到时区对象.
TimeZone timeZone = cal.getTimeZone();
//从Java1.8开始TimeZone类提供了一个方法可以获取到ZonedId.
ZoneId zoneId = timeZone.toZoneId();
//获取到zoneId之后就可以初始化ZonedDateTime对象了,ZonedDateTime类有一个ofInstant方法,可以将一个Instant对象和ZonedId对象作为参数传入构造一个ZonedDateTime对象.
ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(cal.toInstant(), zoneId);

Calendar转换为LocalDateTime

java
//初始化Canlendar对象.
Calendar cal = Calendar.getInstance();
//通过Getter方法获取到Calendar对象中封装的数据.
int year = cal.get(Calendar.YEAR);
int month = cal.get(Calendar.MONTH);
int day = cal.get(Calendar.DAY_OF_MONTH);
int hour = cal.get(Calendar.HOUR);
int minute = cal.get(Calendar.MINUTE);
int second = cal.get(Calendar.SECOND);
//将以上获取到的数据作为LocalDateTime的of方法的参数进行对象的封装.
LocalDateTime localDateTime = LocalDateTime.of(year, month, day, hour, minute, second);

日期格式解析及格式化DateTimeFormatter

SimpleDateFormat类是线程不安全的

DateTimeFormatter类提供了大量预定义格式化器,包括常量(如ISO_LOCAL_DATE),模式字母(如yyyy-MM-dd)以及本地化样式.并且 ,通过时间日期对象的parse/format方法可以直接进行转换.format方法需要传入一个DateTimeFormatter对象

java
//对LocalDateTime进行格式化与解析,初始化LocalDateTime对象.
LocalDateTime time = LocalDateTime.now();

//DateTimeFormatter类中定义了很多方式,通过常量名可以指定格式化方式.
String result = time.format(DateTimeFormatter.ISO_DATE_TIME);
System.out.println("ISO_DATE_TIME格式化之后的String是:" + result);

String result1 = time.format(DateTimeFormatter.ISO_DATE);
System.out.println("ISO_DATE格式化之后的String是:" + result1);

//解析字符串的方式通过LocalDateTime类的静态方法parse方法传入需要解析的字符串即可.
LocalDateTime localDateTime = LocalDateTime.parse(result);
System.out.println("解析了字符串之后的LocalDateTime是:" + localDateTime);

对日期进行格式化

通过DateTimeFormatter的ofLocalizedDate的方法也可以调整格式化的方式,此方法需要传入一个FormatStyle类对象,为枚举对象,值为:

Full:全显示(年月日+星期) Long:全显示(年月日) Medium:缩略显示(没有年月日汉字) SHORT:精简显示(精简年+月日)

此种方式在不同时区的显示方式不一样,在其他时区不会显示中文,会根据当前系统的默认时区来进行区别显示.

java
//对LocalDateTime进行格式化与解析,初始化LocalDateTime对象.
LocalDateTime time = LocalDateTime.now();
//通过DateTimeFormatter的ofLocalizedDate指定解析格式也可以格式化日期
String r1 = time.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL));
String r2 = time.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG));
String r3 = time.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM));
String r4 = time.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT));

自定义格式

通过DateTimeFormatter类提供的ofPattern方式创建自定时格式化器,格式化的写法与之前使用的SimpleDateFormat相同.

java
 //对LocalDateTime进行格式化与解析,初始化LocalDateTime对象.
LocalDateTime time = LocalDateTime.now();
//通过通过DateTimeFormatter的ofPattern方法可以自定义格式化模式.
String result = time.format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss:SSS"));

编码与解码

电脑是由电路板组成,电路板里面集成了无数的电阻和电容, 交流电经过电容的时候,电压比较低 记为低电平 ,用0表示,交流电流过电阻的时候,电压比较高,记为高电平,用1来表示; 所以每一个1 和0 在计算机中被称为 位,也就是bit位。然而,如果使用一个位来表示计算机中的最小存储单元, 那么这个存储单元只能存储0或者1,存储的范围太小了,所以我们规定用用8个bit位为一组 来表示 计算机的最小存储单元。 8个位 每个位上能存储0或者1,则byte的存储范围则是 00000000-11111111(换算成整数即0-255)。 这个最小存储单元 就是byte 字节。

计算机的底层只能存储0和1,如果是日常生活中遇到的数字 比如 127 ,这个可以通过10进制和二进制的转换从而让计算机存储01111111

如果计算机存储类似于汉字、英文字符、符号字符等内容时,时计算机提供了很多的编码表记录了字符和数字的一一对应关系,编码就是把字符对应编码表中的码值存储在电脑中,而解码则是把码值在编码表中的对应的字符展现出来。

java
注意:计算机中存储一个数 是用二进制来表示的,比如 存储127,那么计算机的底层是 0111 1111,人看这些二进制的数通常都是眼花缭乱的,如何方便而规整的表示这些二进制数呢,不妨引入十六进制。二进制换算成十六进制,则是每四位为一组转换为16进制数即可, 比如0111 1111 这个数前4位 0111 转换为 7 , 后4位转换为F, 则最终的16进制数是 7F,一般我繁琐的二进制数使用十六进制数来表示会比较方便规整,所以人们习惯用十六进制数来表示码值。

常见的编码见 #字符集简介

Ascii码是基础,一个字节表示,最高位设为0,其他7位表示128个字符。其他编码都是兼容Ascii的,最高位使用1来进行区分。西欧主要使用Windows-1252,使用一个字节,增加了额外128个字符。中文大陆地区的三个主要编码GB2312,GBK,GB18030,有时间先后关系,表示的字符数越来越多,且后面的兼容前面的,GB2312和GBK都是用两个字节表示,而GB18030则使用两个或四个字节表示。香港台湾地区的主要编码是Big5。

如果文本里的字符都是Ascii码字符,那么采用以上所说的任一编码方式都是一样的,不会乱码。但如果有高位为1的字符,除了GB2312/GBK/GB18030外,其他编码都是不兼容的,比如,Windows-1252和中文的各种编码是不兼容的,即使Big5和GB18030都能表示繁体字,其表示方式也是不一样的,而这就会出现所谓的乱码。

乱码和兼容

兼容:GB2312/GBK/GB18030 ASCII是兼容的 比如我们文本里面 a字符,使用这四种码表任何一种都是可以正常显示的。

​ windows-1252和ISO-8859-1 和ASCII是兼容的

​ Big5和ASCII是兼容的

​ 但是 西欧编码 和 Big5 以及 GB系列的编码 他们相互之间是不兼容的,也就是 同样的码值在三种编码表中显示的内容是不一样的。

乱码:如果编码的时候同一种编码表,而解码的时候通过的却是一种不兼容的编码表,则就就会出现乱码现象。

乱码的原因和可逆性

乱码原因

乱码产生的根源一般情况下可以归结为三方面即:编码引起的乱码、解码引起的乱码以及缺少某种字体库引起的乱码(这种情况需要用户安装对应的字体库),其中大部分乱码问题是由不合适的解码方式造成的

乱码可逆情况

其中缺少字体,只需要安装对应的字体库即可解决乱码,比如 Windows 系统在 C:\Windows\Fonts 目录下会有安装好的字体库列表。安装字体库比较简单,下载后解压,然后复制到对应系统的 Fonts 目录下。

解码方式和编码方式不一致的情况,只需要让解码方式和编码方式一致即可让乱码恢复。

乱码不可逆情况

GBK编码不支持这几个字符 "𠮷" "♠" "♥" , 如果再一个 GBK编码的文件中,写入 "𠮷" "♠" "♥" 这些字符, 那么他们就会变成??, ?对应的码值是3F,这样的情况就没有办法恢复。 因为 "𠮷"的本来的码值 变成了 两个 3F (即两个问号),无论如何也不能恢复过来了。

Java的char字符

在 Java内部进行字符处理时,采用的都是Unicode,具体编码格式是UTF-16BE。简单回顾一下,UTF-16使用两个或四个字节表示一个字 符,Unicode编号范围在65536以内的占两个字节,超出范围的占四个字节,BE (Big Endian)就是先输出高位字节,再输出低位字节,这与整数的内存表示是一致的。

char本质上是一个固定占用两个字节的无符号正整数,这个正整数对应于Unicode编号,用于表示那个Unicode编号对应的字符。

由于固定占用两个字节,char只能表示Unicode编号在65536以内的字符,而不能表示超出范围的字符。

那超出范围的字符怎么表示呢?只能使用String类来表示,例如汉字"𠮷"的 Unicode 码点为 0x20BB7,该码点显然超出了65535,所只能用String表,而当粘贴到代码中时,自动转换为了两个字符"\uD842\uDFB7"

java
char c ='味';
System.out.println(c);
//char c1 = '\uD842\uDFB7';
String str = "\uD842\uDFB7";
System.out.println(str);

char有多种赋值方式:

java
char c = 'A';
char c = '马';
char c = 39532;
char c = 0x9a6c;
char c = '\u9a6c';

第1种赋值方式是最常见的,将一个能用Ascii码表示的字符赋给一个字符变量。

第 2种也很常见,但这里是个中文字符,需要注意的是,直接写字符常量的时候应该注意文件的编码,比如说,GBK编码的代码文件按UTF-8打开,字符会变成乱码,所以有的时候为了避免代码中出现的汉字常量乱码 可以使用第5中方式赋值,至于汉字和Unicode的码值转换有很多网站可以做到。比如 百度上搜索 汉字 转换Unicode第一条链接http://www.atool9.com/chinese2unicode.php

第3种是直接将十进制的常量赋给字符,第4种是将16进制常量赋给字符,第5种是按Unicode字符形式。

以上,2,3,4,5都是一样的,本质都是将Unicode编号39532赋给了字符。

char 的加减运算就是按其Unicode编号进行运算,一般对字符做加减运算没什么意义,但Ascii码字符是有意义的。比如大小写转换,大写A-Z的编号是 65-90,小写a-z的编号是97-122,正好相差32,所以大写转小写只需加32,而小写转大写只需减32。加减运算的另一个应用是加密和解密,将 字符进行某种可逆的数学运算可以做加解密。

String类

编码的方法

getBytes()方法

public byte[] getBytes(); 此方法根据java命令运行时参数 file.encoding设置的编码表进行编码的。

java
String s = "黑马";
byte[] bytes = s.getBytes();
System.out.println(Arrays.toString(bytes));//[-23, -69, -111, -23, -87, -84]

打印结果是[-23, -69, -111, -23, -87, -84],很明显2个中文6个字节,应该是采用的UTF-8编码,查看getBytes方法的底层发现

java
class String{
    //省略部分源码
    public byte[] getBytes() {
        return StringCoding.encode(value, 0, value.length);
    }
     //省略部分源码
}
class StringCoding{
     //省略部分源码
    static byte[] encode(char[] ca, int off, int len) {
        String csn = Charset.defaultCharset().name();
        try {
            // use charset name encode() variant which provides caching.
            return encode(csn, ca, off, len);
        } catch (UnsupportedEncodingException x) {
            warnUnsupportedCharset(csn);
        }
        try {
            return encode("ISO-8859-1", ca, off, len);
        } catch (UnsupportedEncodingException x) {
            // If this code is hit during VM initialization, MessageUtils is
            // the only way we will be able to get any kind of error message.
            MessageUtils.err("ISO-8859-1 charset not available: "
                             + x.toString());
            // If we can not find ISO-8859-1 (a required encoding) then things
            // are seriously wrong with the installation.
            System.exit(1);
            return null;
        }
    }
     //省略部分源码
}
class Charset{
     //省略部分源码
    public static Charset defaultCharset() {
        if (defaultCharset == null) {
            synchronized (Charset.class) {
                String csn = AccessController.doPrivileged(
                    new GetPropertyAction("file.encoding"));
                Charset cs = lookup(csn);
                if (cs != null)
                    defaultCharset = cs;
                else
                    defaultCharset = forName("UTF-8");
            }
        }
        return defaultCharset;
    }
     //省略部分源码
}

经过查看源码,我们发现底层循环默认编码defaultCharset 是根据的 file.encoding,file.encodig 是System类里面的的一次参数,可以通过System类来获取, 通过 java命令运行java程序的时候 -Dfile.encoding=编码表 来设置。

java
System.out.println(System.getProperty("file.encoding"));//UTF-8
String s = "黑马";
byte[] bytes = s.getBytes();
System.out.println(Arrays.toString(bytes));//[-23, -69, -111, -23, -87, -84]

getBytes(String charsetName)方法

public byte[] getBytes(String charsetName); 此方法 根据指定的编码名称charsetName进行编码

java
String s = "黑马";
byte[] bytes = s.getBytes("GBK");
System.out.println(Arrays.toString(bytes));//[-70, -38, -62, -19]

byte[] bytes1 = s.getBytes("UTF-8");
System.out.println(Arrays.toString(bytes1));//[-23, -69, -111, -23, -87, -84]

解码的方法

String(byte[] code)

public String(byte[] code); 此方法根据file.encoding 进行解码

java
String s = "黑马";
byte[] bytes = s.getBytes();
System.out.println(Arrays.toString(bytes)); //[-23, -69, -111, -23, -87, -84]

String str = new String(bytes);
System.out.println(str); //黑马

String(byte[] code,String charsetName)

public String(byte[] code,String charsetName); 此方法根据执行的码表名称 charsetName进行解码

java
String s = "黑马";
byte[] bytes = s.getBytes("GBK");
System.out.println(Arrays.toString(bytes)); //[-70, -38, -62, -19]

String str = new String(bytes,"GBK");
System.out.println(str); //黑马

乱码的情况

可逆的情况
java
String s = "黑马";
byte[] bytes = s.getBytes("GBK");
System.out.println(Arrays.toString(bytes)); //[-70, -38, -62, -19]

String str = new String(bytes,"UTF-8");
System.out.println(str); //����

String str1 = new String(bytes, "GBK");
System.out.println(str1); //黑马
不可逆的情况
java
String s = "黑马";
byte[] bytes = s.getBytes("ISO-8859-1");
System.out.println(Arrays.toString(bytes)); //[63, 63]

String str = new String(bytes,"ISO-8859-1");
System.out.println(str); //??
String str1 = new String(bytes,"GBK");
System.out.println(str1); //??
String str2 = new String(bytes,"UTF-8");
System.out.println(str2); //??
java
String s = "\uD842\uDFB7"; //𠮷 的Unicode码值
byte[] bytes = s.getBytes("GBK");
System.out.println(Arrays.toString(bytes)); //[63]

String str = new String(bytes,"GBK");
System.out.println(str); //?
java
String s = "♠"; 
byte[] bytes = s.getBytes("GBK");
System.out.println(Arrays.toString(bytes)); //[63]

String str = new String(bytes,"GBK");
System.out.println(str); //?
ISO-8895-1编码的妙用

这也是为什么tomcat使用ISO-8859-1编码的原因。

java
String s = "黑马";
byte[] bytes = s.getBytes("GBK");
System.out.println(Arrays.toString(bytes)); //[-70, -38, -62, -19]

String str = new String(bytes,"UTF-8");
System.out.println(str); //����

byte[] bytes1 = str.getBytes("UTF-8");
System.out.println(Arrays.toString(bytes1));//[-17, -65, -67, -17, -65, -67, -17, -65, -67, -17, -65, -67]

String str1 = new String(bytes1, "GBK");
System.out.println(str1);//锟斤拷锟斤拷
java
String s = "黑马";
byte[] bytes = s.getBytes("GBK");
System.out.println(Arrays.toString(bytes)); //[-70, -38, -62, -19]

String str = new String(bytes,"ISO-8859-1");
System.out.println(str); //ºÚÂí

byte[] bytes1 = str.getBytes("ISO-8859-1");
System.out.println(Arrays.toString(bytes1));//[-70, -38, -62, -19]

String str1 = new String(bytes1, "GBK");
System.out.println(str1);//黑马

IO流-字符流

InputStreamReader

正常
java
InputStreamReader isr = new InputStreamReader(new FileInputStream("myString\\a.txt"),"UTF-8"); // 使用UTF-8编码读取a.txt文件  // a.txt 文件的编码格式是 UTF-8格式, 里面的内容是"中国";
int ch;
while ((ch=isr.read())!=-1) {
    System.out.print((char)ch);//中国
}
isr.close();
乱码
java
InputStreamReader isr = new InputStreamReader(new FileInputStream("myString\\a.txt"),"GBK"); // 使用GBK编码读取a.txt文件
//a.txt 文件的编码格式是 UTF-8格式, 里面的内容是"中国"
int ch;
while ((ch=isr.read())!=-1) {
    System.out.print((char)ch);//涓浗
}
isr.close();

OutputStreamWriter

正常
java
OutputStreamWriter osw2 = new OutputStreamWriter(new FileOutputStream("myString\\a.txt"),"UTF-8"); //打开a.txt 不乱码
osw2.write("中国");
osw2.close();
乱码
java
OutputStreamWriter osw2 = new OutputStreamWriter(new FileOutputStream("myString\\a.txt"),"GBK");
osw2.write("中国");
osw2.close();

复制文件

字符流复制文本乱码因素

4个因素 源文件编码 Reader缓冲区编码 Writer缓冲区编码 目标文件编码,其中 源文件编码 和Reader缓冲区编码需要一致, Writer缓冲区编码 和 目标文件编码 需要一致。

java
InputStreamReader isr = new InputStreamReader(new FileInputStream("d:\\b.txt"),"UTF-8"); // b.txt的GBK编码格式的  b.txt里面的内容是“中国”
OutputStreamWriter osw2 = new OutputStreamWriter(new FileOutputStream("a.txt"),"UTF-8"); // a.txt 使用Idea 打开,乱码
int ch;
while ((ch=isr.read())!=-1) {
    osw2.write(ch);
}
isr.close();
osw2.close();
字符流UTF-8 编码复制图片

复制完成后, 新的图片存储大小会变大,并且无法正常打开。

java
InputStreamReader isr = new InputStreamReader(new FileInputStream("d:\\1.jpg"),"UTF-8"); //1.jpg 是15.3kb  能正常打开 
OutputStreamWriter osw2 = new OutputStreamWriter(new FileOutputStream("d:\\2.jpg"),"UTF-8"); //2.jpg 是26.5k 不能正常打开
int ch;
while ((ch=isr.read())!=-1) {
    osw2.write(ch);
}
isr.close();
osw2.close();
ISO-8859-1的妙用

使用ISO-8859-1编码的字符流复制文件,可以原样复制成功,并且可以正常打开。

java
InputStreamReader isr = new InputStreamReader(new FileInputStream("d:\\1.jpg"),"ISO-8859-1");//1.jpg 是15.3kb  能正常打开 
OutputStreamWriter osw2 = new OutputStreamWriter(new FileOutputStream("d:\\2.jpg"),"ISO-8859-1");//2.jpg 是15.3kb  能正常打开 
int ch;
while ((ch=isr.read())!=-1) {
    osw2.write(ch);
}
isr.close();
osw2.close();