Home Basics of Java
Post
Cancel

Basics of Java

基本知识

三大特点:封装继承多态。

语法糖:switch支持String、泛型、自动拆装箱、变长参数、枚举、内部类、条件编译、断言、数值下划线、for-each、try-with-resources、Lambda表达式

装箱:Integer i = Integer.valueOf(10), 拆箱:int n = i.intValue()

内存结构

运行时数据区域包含线程私有的程序计数器、虚拟机栈、本地方法栈,线程共享的堆(包含字符串常量池)和非运行时数据区的元空间(包含类常量池和运行时常量池)、直接内存。

  • 程序计数器:与操作系统中的程序计数器类似,为了线程切换后能恢复到正确的执行位置,是唯一一个不会出现 OutOfMemoryError 的内存区域。
  • 虚拟机栈:以帧为单位,帧由局部变量表、操作数栈、动态链接、方法返回地址组成。每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。
    • 局部变量表:主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)
    • 操作数栈:用于存放方法执行过程中产生的中间计算结果,也存放计算过程中产生的临时变量。
    • 动态链接:将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。与类加载中的解析类似。
    • 方法返回地址:顾名思义。
  • 本地方法栈:和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法(机器码)服务
  • 堆:唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
  • 字符串常量池:简单理解为用C++实现的默认固定大小为1009的HashTable。在每个VM中只有一份,存放的是字符串常量的引用值 。关于常量池中的String类型的数据,String#intern 的用法
  • 类常量池:每个java文件被编译成class文件后会有一项常量池,用于存放编译器生成的字面量符号引用。在编译阶段,存放的是常量的符号引用
  • 运行时常量池:是在类加载完成后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析阶段,将符号引用替换成直接引用,与字符串常量池中的引用值保持一致。

class常量池、字符串常量池和运行时常量池的区别

内存模型(JMM)

JMM 旨在提供一个统一的可参考的规范,屏蔽平台内存访问差异性。这个规范为读写共享变量时如何与内存交互提供了规则和保证。并发编程中,程序会因为 CPU 多级缓存或指令重排序等出现问题,因此需要一些规范要保证并发编程的可靠性。

关键概念包括:

  • 主内存:表示所有线程都可以访问的共享内存。线程不能直接读写主内存中的变量。
  • 工作内存:每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存共享变量的副本拷贝,线程对变量的操作都在工作内存中进行。当一个线程修改了自己工作内存中的变量时,它必须把这个变量的最新值写回到主内存中,以便其他线程可以看到这个最新的值。
  • 共享变量:这些变量可以被多个线程访问。它们可以是实例变量或静态变量。必须存储在主内存中。

JMM 为处理共享变量定义了三个特征(多线程中的概念):

  • 可见性:当一个线程修改共享变量的值,其他线程能够立即知道被修改了。当变量被 volatile 修饰时,这个变量被修改后会立刻刷新到主内存,当其它线程需要读取该变量时,会去主内存中读取新值。但普通变量读取的仍是旧值。
  • 原子性:一个操作是不可分割,不可中断的,一个线程在执行时不会被其他线程干扰。Synchronized 块之间的操作具有原子性
  • 顺序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。

volatile

通过内存屏障来保证可见性的

  • 保证可见性,但不保证原子性!只是确保将变量的更新操作通知到其他线程。不能一定能保证线程安全。
  • 禁止指令重排,背景:为了提高性能,编译器和处理器常常会对指令重排。禁止指令重排避免了多线程环境下程序出现乱序执行的现象。

happens-before

happens-before原则定义如下:

  1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  2. 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。重排序之后的执行结果与按照happens-before关系来执行的结果一致即可。

as-if-serial

As-if-serial的意思是所有的语句都可以为了优化而被重排序,但是必须保证它们重排序后的结果和程序代码本身的应有结果是一致的。为保证as-if-serial语义,Java异常处理机制也会为重排序做一些特殊处理。

八种内存交互操作

内存屏障

  1. LoadLoad 屏障:对于这样的语句Load1,LoadLoad,Load2。在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  2. StoreStore屏障:对于这样的语句Store1, StoreStore, Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
  3. LoadStore 屏障:对于这样的语句Load1, LoadStore,Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  4. StoreLoad 屏障:对于这样的语句Store1, StoreLoad,Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

在每个volatile读操作后插入LoadLoad屏障,在读操作后插入LoadStore屏障。

在每个volatile写操作的前面插入一个StoreStore屏障,后面插入一个SotreLoad屏障。

  

类的生命周期

  • 加载:通过类的全限定名(包名 + 类名)获取class文件的二进制字节流(通过类加载器来完成,其加载过程使用双亲委派模型),将其转化为方法区运行时的数据结构,最后在堆中实例化一个java.lang.Class对象,作为方法区中这个类的信息的入口。
  • 连接
    • 验证:确保被加载的类的正确性
    • 准备:为类的静态变量分配内存并设为jvm的默认值(不同于下文的初值,基本类型为零,引用类型为null,final修饰的常量为设定的值),对于非静态的变量,则不会为它们分配内存。
    • 解析:虚拟机将常量池中的符号引用替换为直接引用,主要针对类或接口,字段,类方法,方法类型等。举例:使用内存地址(直接引用)指向方法名(符号引用)代替方法名。
  • 初始化:按照顺序自上而下运行类中的变量赋值语句和静态语句,如果有父类,则首先按照顺序运行父类中的变量赋值语句和静态语句在类的初始化阶段,只会初始化与类相关的静态赋值语句和静态语句。类变量(静态变量)在方法区分配内存,并设置初值
  • 使用:包括主动引用和被动引用。直接引用就会触发类的初始化,其中包括以下四种情况:
    1. 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法。
    2. 初始化子类的时候,会触发父类的初始化。
    3. 作为程序入口直接运行时(也就是直接调用main方法)。
    4. 通过反射方式执行以上三种行为。
  • 卸载:需要同时满足以下三个条件:该类所有的实例都已经被回收,即Java堆中不存在该类的任何实例;加载该类的ClassLoader已经被回收;该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。单例对象不会被JVM垃圾回收,因为无法满足卸载的第一个条件,Java堆中会始终存在该单例的实例。

类加载器

线程上下文类加载器:破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器,例如SPI (Service Provider Interface)。SPI接口中的代码经常需要加载具体的实现类。SPI接口是Java核心库的一部分,由 启动类加载器(Bootstrap Classloader) 来加载,而实现类由 系统类加载器(AppClassLoader) 来加载。

双亲委派机制概念:双亲委派机制是指当一个类加载器收到某个类加载请求时,该类加载器首先会把请求委派给父类加载器。每个类加载器都是如此,它会先委托父类加载器在自己的搜索范围内找不到对应的类时,该类加载器才会尝试自己去加载。

Tomcat中的类加载器:

  • Tomcat自身所使用的类加载器,会加载jre的lib包及tomcat的lib包的类,遵循双亲委派机制。加载顺序:(1).先从缓存中加载;(2).如果没有,则从JVM的Bootstrap类加载器加载;(3).如果没有,则从父类加载器加载,加载顺序是AppClassLoader、Common、Shared。(4).如果没有,则从当前类加载器加载(按照WEB-INF/classes、WEB-INF/lib的顺序);
  • 每个Web应用程序用的,每个web应用程序都有自己专用的WebappClassLoader,优先加载/web-inf/lib下的jar中的class文件,这样就隔离了每个web应用程序的影响,不遵循双亲委派机制。加载顺序:(1).先从缓存中加载;(2).如果没有,则从JVM的Bootstrap类加载器加载;(3).如果没有,则从当前类加载器加载(按照WEB-INF/classes、WEB-INF/lib的顺序);(4).如果没有,则从父类加载器加载,由于父类加载器采用默认的委派模式,所以加载顺序是AppClassLoader、Common、Shared。

Java的SPI:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。

1.6 -> 1.7 -> 1.8:

  • 1.6 -> 1.7: 字符串常量池从方法区(永久代)中移到堆中。原因: GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收。
  • 1.7 -> 1.8: 将运行时数据区方法区(永久代)移动到直接内存中,字符串常量池仍然在堆中。

对象的创建过程:

  1. 类加载检查
  2. 分配内存:指针碰撞或空闲列表
  3. 初始化零值
  4. 设置对象头
  5. 执行构造方法

对象的内存布局:

  1. 对象头,两部分组成:存储自身运行时数据如哈希码,GC分代年龄;指向类的类型指针
  2. 实例数据,真正存储有效信息的部分
  3. 对齐填充,起占位作用

对象的定位访问(针对JVM虚拟机栈中的局部变量表)

  1. 句柄,Java 堆中将会划分出一块内存来作为句柄池,局部变量表 reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。
  2. 直接指针,局部变量表里 reference 中存储的直接就是对象的地址。

垃圾回收

JVM触发GC时,首先会让所有的用户线程到达安全点SafePoint时阻塞,也就是STW,然后枚举根节点,即找到所有的GC Roots,通过可达性算法向下搜寻活跃对象,可达的对象就保留,不可达的对象就回收

可达性算法

  • 引用计数算法
  • 可达性分析

哪些对象可以作为GC Roots?

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈(Native 方法)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁持有的对象

内存分配和回收原则:

  • 对象优先在Eden区分配
  • 大对象直接进入老年代
  • 长期存活的进入老年代

GC 分类

  • Partial GC
    • Minor GC:只对新生代进行垃圾收集
    • Major GC:只对老年代进行垃圾收集
    • Mixed GC:整个新生代和部分老年代,只有G1收集器有
  • Full GC:整个Java堆和方法区

垃圾收集算法

  • 标记——清除算法:顾名思义,标记可回收的对象并清除。
  • 标记——复制算法:将内存分成大小相同的两份,需要垃圾收集时,将存活的对象复制到另一份内存中,缺点:内存缩小为原来的一半。
  • 标记——整理算法:标记可回收的对象,将存活的对象向一端移动,适合老年代这种垃圾回收频率不高的场景。
  • 分代收集算法:在新生代和老年代不同的代用不同的垃圾收集算法。

垃圾收集器

  1. Serial 收集器,单线程、复制算法的新生代收集器
  2. ParNew 收集器,多线程、复制算法的新生代收集器,老年代采用Serial Old收集器
  3. Parallel Scavenge 收集器,多线程、复制算法的新生代收集器,高吞吐量。
  4. Serial Old 收集器,单线程、标记-整理算法的老年代收集器。
  5. Parallel Old 收集器,多线程、标记-整理算法的老年代收集器。
  6. CMS(Concurrent Mark Sweep) 收集器,标记-清除算法,以获取最短回收停顿时间为目标的收集器。JDK14正式移除。
  7. G1(Garbage-First) 收集器,标记-整理 + 复制算法,内存碎片的产生率大大降低。JDK9-JDK17的默认垃圾收集器。

G1收集器内存模型

垃圾收集器发展历程:

  • JDK8 默认 Parallel Scavenge + Parallel Old
  • JDK9 默认 G1
  • JDK11 提出ZGC
  • JDK14 CMS 被移除

CMS(Concurrent Mark Sweep)收集器: 基于标记-清除算法,在Minor GC时会暂停所有的应用线程,并以多线程的方式进行垃圾回收。在Full GC时不再暂停应用线程,而是使用若干个后台线程定期的对老年代空间进行扫描。

  • 步骤:
    1. 初始标记(CMS initial mark):有STW,但速度很快
    2. 并发标记(CMS concurrent mark):从GC Roots的直接关联对象开始遍历整个对象图
    3. 重新标记(CMS remark):STW,为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录;采用三色标记算法和增量更新避免漏标
    4. 并发清除(CMS concurrent sweep):清理删除掉标记阶段判断的已经死亡的对象
  • 缺点:
    1. 工作时会占用一部分CPU资源而导致用户程序变慢,降低总吞吐量
    2. CMS无法清除浮动垃圾
    3. 基于标记-清除算法会导致内存碎片不断增多,在分配大对象时有可能会提前触发一次Full GC。

Garbage First(G1)收集器:

  • 特点:引入分区的思路,弱化了分代的概念,并合理利用垃圾收集各个周期的资源。
  • 内存结构:堆内存被切分为多个固定大小的区域,最小为1M,最大为32M,默认2048份。
  • 内存分配:每个区域被标记为E、S、O和H,分别表示Eden,Survivor,Old,Humongous。Humongous区域是为了那些存储超过50%标准region大小的对象而设计的,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
  • 执行特点:
    • 并行与并发:使用多个CPU核缩短Stop The World停顿时间。
    • 空间整合:从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
    • 可观测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。
    • G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。
  • 步骤:
    1. 初始标记(Initial Marking),STW,标记一下 GC Roots 能直接关联到的对象。
    2. 并发标记(Concurrent Marking),从 GC Root 开始对堆中对象进行可达性分析,找到存活对象。
    3. 最终标记(Final Marking),为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,STW,但是可并行执行。
    4. 筛选回收(Live Data Counting and Evacuation),对各个Region中的回收价值和成本进行排序并制定回收计划。

JVM调优

  1. 选择合适的垃圾收集器:CPU单核,只能选择Serial;CPU多核,关注吞吐量 ,那么选择Parallel Scavenge(标记复制) + Paralle Old(标记整理)组合;CPU多核,关注用户停顿时间,JDK版本1.6或者1.7,内存小,那么选择CMS。CPU多核,关注用户停顿时间,JDK1.8及以上,JVM可用内存6G以上,那么选择G1。
  2. 调整内存大小,现象:垃圾收集频率非常频繁。
  3. 设置符合预期的停顿时间,现象:程序间接性的卡顿。参数:-XX:MaxGCPauseMillis
  4. 调整内存区域大小比率,现象:某一个区域的GC频繁,其他都正常。参数:-XX:SurvivorRatio=6, -XX:NewRatio=4
  5. 提升老年代年龄标准,现象:老年代频繁GC,每次回收的对象很多。参数:-XX:InitialTenuringThreshol=7
  6. 调整大对象的标准,现象:老年代频繁GC,每次回收的对象很多,而且单个对象的体积都比较大。参数:-XX:PretenureSizeThreshold=1000000//新生代可容纳的最大对象,大于则直接会分配到老年代,0代表没有限制。
  7. 调整GC的触发时机,现象:CMS收集器的情况下,G1 经常 Full GC,程序卡顿严重。
  8. 调整JVM本地内存(直接内存)大小,现象:堆内存空间充足,但是报OOM

调优的一条经验总结:

将新对象预留在新生代,由于 Full GC 的成本远高于 Minor GC,因此尽可能将对象分配在新生代是明智的做法,实际项目中根据 GC 日志分析新生代空间大小分配是否合理,适当通过“-Xmn”命令调节新生代大小,最大限度降低新对象直接进入老年代的情况。

Java IO:

  • 阻塞/非阻塞,如果方法不能立即返回,需要等待,线程会阻塞
  • 同步/异步,如果能开启新的线程,则叫异步,多线程是实现异步的一种方式。

NIO vs IO:

  • IO是面向流的,NIO是面向缓冲区的
  • IO流是阻塞的,NIO流是不阻塞的
  • 选择器:Java NIO的选择器允许一个单独的线程来监视多个输入通道

IO多路复用(事件驱动):一个线程不断轮询多个socket的状态,只有当socket真正有读写状态时,借用当前线程或者使用线程池额外启动线程,调用实际的IO读写操作。

Java NIO:

  • 实际上也是一种多路复用的IO。
  • 三大核心部分:Channel(通道) ,Buffer(缓冲区), Selector(选择器),Channel 负责传输, Buffer 负责存取数据
    • Channel是双向的,数据总是从通道读到缓冲区或者从缓冲区中写入通道内。
  • 额外一个Selector线程,用于监听多个通道(Channel)的事件(比如:连接打开,数据到达),如果由事件发生,则获取事件并对每个事件进行相应的响应处理。

并发编程

一个 Java 程序的运行是 main 线程和多个其他线程同时运行。

多线程

同一时刻只能有一个线程运行 synchronized(lock) 内的代码块,其他线程会否则阻塞。PS:获取锁(运行代码块),释放锁(阻塞代码块)

  1. wait(): 获取锁并使线程进入等待状态
  2. notify(): 随机唤醒一个在等待锁释放(wait())的线程
  3. notifyAll(): 唤醒所有正在等待锁释放(wait())的线程,

注意:notify() 或 notifyAll() 必须等到退出 synchronized() 或 wait() 后才释放锁!

1
2
3
4
5
6
7
8
synchronized (obj) {
    // 条件不满足
    while (condition does not hold) {
        obj.wait();
    }
    // 执行满足条件的代码
    obj.notifyAll();
}

or

1
2
3
4
5
6
7
8
9
10
11
synchronized (obj) {
  while (true){
    // 条件满足
    if (condition holds){
      // 执行满足条件的代码
      obj.notifyAll();
    }
    obj.wait();
  }
}

run() 和 start() 的区别:

  • run(),调用普通方法,并不开启新线程。
  • start(),启动新线程,由JVM调用线程的run()方法。

守护线程(Daemon Thread)

Java 中的线程分为两种:

  1. 用户线程。
  2. 守护线程,其主要作用是为用户线程服务,比如垃圾回收线程,就是最典型的守护线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。也就是守护线程拥有自动结束自己生命周期的特性,而非守护线程不具备这个特点。

集合框架:

  • Collection
    • Set
    • List
    • Queue
  • Map

线程安全的list:

  • vector
  • CopyOnWriteArrayList 读多写少的情况
  • Collections.synchronizedList() 读少写多的情况

HashMap 知识点

  • HashMap: 乱序,数组+链表+红黑树,链表长度大于8转红黑树,红黑树节点个数小于6转链表。
  • LinkedHashMap: 按插入顺序排序
  • TreeMap: 按字典序排序,因为是按字典序排序的,所以键肯定不能为null,值可以为null
  • IdentityHashMap:利用哈希表实现Map接口,不同的是,其比较键(或值)时,使用引用相等性代替对象相等性。
  • ConcurrentSkipListMap:基于跳表的线程安全的,实现快速查找的链表结构。

HashMap面试题

为什么计算哈希值采用低十六位和高十六位异或操作:

计算数组下标是与操作,只有低 n 位进行与操作,高位不参与任何操作 -> 为了增大散列程度减小哈希碰撞,因此将高十六位参与进哈希值的计算。

put() 的流程:

  1. hashcode的高十六位和低十六位进行异或运算
  2. (n - 1) & hash 计算数组下标,当 n 为二次幂时,等价于取余操作((n - 1)& hash = hash % n)。
  3. 判断当前下标是否有元素,若有元素,使用尾插法。再根据链表长度判断是否需要转换成红黑树。

Set:

  • HashSet: 乱序,基于HashMap实现
  • LinkedHashSet: 按插入的顺序排序,基于LinkedHashMap实现
  • TreeSet: 按字典序排序,基于红黑树

NULL key AND NULL value:

  • key
    • HashMap、LinkedHashMap 能使用 null key
    • ConcurrentHashMap、TreeMap、HashTable 不能使用 null key。
  • value
    • HashMap、LinkedHashMap、TreeMap 能使用 null value
    • Hashtable、ConcurrentHashMap 不能使用 null value。

为什么ConcureentHashMap的key和value都不能为null:

  • value不能为null:多线程情况下需要杜绝二义性。二义性是指当返回null时,无法判断是存在value为null的key还是不存在key从而返回null。因为单线程中可以使用 containsKey() 解决,但是多线程下无法使用同样的方法,因为可能会有其他线程进行其他操作影响返回值

ConcurrentHashMap JDK7 vs JDK8

  • JDK7: 数组 + 链表。先定位 Segment,再定位桶。底层结构是继承了ReentrantLock的Segment数组。可以看成是由线程安全的HashMap组成的一个map数组,数组的长度决定了支持的最大的并发量。
  • JDK8: 数组 + 链表 + 红黑树。可以直接定位到桶。链表中的元素超过8后,将链表结构转换成红黑树。通过对Node数组以CAS方式实现扩容和对Node数组的每个元素的synchronized保证ConcurrentHashMap整体的线程安全。

HashTable速度慢:使用synchronized对整个对象加锁。

JDK7:对整个数组进行分段(每段都是由若干个 hashEntry 对象组成的链表),每个分段都有一个 Segment 分段锁(继承 ReentrantLock 分段锁)。与hashtable相比,加锁粒度更细,但是初始化Segment数组长度后就无法扩容。ConcurrentHashMap 是一个二级哈希表。在一个总的哈希表下面,有若干个子哈希表。

JDK8:对table数组的头节点加锁(哈希桶为空时,使用CAS将新的Node写入哈希桶的首节点;哈希桶不为空时,使用synchronized对首节点加锁接着添加节点)

  • put:分两步,计算哈希值和一个死循环,循环步骤,
    1. first节点还没有初始化,所以初始化first节点,然后进入下次循环;
    2. first节点初始化了,但是为空,采用CAS方式把当前要put的值设置进这处,设置失败则进入下次循环,成功则保存成功,退出循环;
    3. 如果判断有其他线程正在对ConcurrentHashMap扩容(hash==MOVED),获取要去获取新的tab,进入下次循环;
    4. 找到了对应哈希桶的首节点f,直接对f加synchronized同步,然后判断f节点是链表结构还是红黑树结构,链表结构则遍历链表进行设置,红黑树则采用红黑树设置进去。设置成功后判断是否需要把链表结构转红黑树;

ThreadLocal: 提供线程内的局部变量,在多线程的环境中保证各个线程内的变量不同。将数据封闭在线程中而避免使用同步,即线程封闭。一个ThreadLocal对象即是一个线程局部变量。jdbc连接池就是用ThreadLocal,典型例子。以下使四种方法:

  • Object get():获取该线程局部变量的值。
  • void set(Object value):给该线程局部变量赋值。
  • protected Object initialValue():返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。
  • public void remove():将当前线程局部变量的值删除。

正确使用方法:

  • 每次使用完ThreadLocal都调用它的remove()方法清除数据
  • 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。

线程池

线程池的七个参数:

  1. 核心线程数(corePoolSize): 核心线程数是线程池中保持活动状态的线程数。即使没有任务需要执行,核心线程也不会被回收。当有新任务提交时,如果核心线程都在忙碌,则会创建新的线程来处理任务。

  2. 最大线程数(maximumPoolSize): 最大线程数是线程池中允许的最大线程数。当工作队列满了并且活动线程数达到最大线程数时,如果还有新任务提交,线程池将创建新的线程来处理任务。但是,超过最大线程数的线程可能会导致资源消耗过大。

  3. 空闲线程存活时间(keepAliveTime): 空闲线程存活时间指的是非核心线程在没有任务执行时的最长存活时间。当线程池中的线程数超过核心线程数且空闲时间达到设定值时,多余的线程将被终止,直到线程池中的线程数不超过核心线程数。

  4. 时间单位(unit): 时间单位是用于表示核心线程数和空闲线程存活时间的单位。常见的时间单位包括秒、毫秒、分钟等。

  5. 工作队列(workQueue): 工作队列用于存储待执行的任务。当线程池中的线程都在忙碌时,新提交的任务将被添加到工作队列中等待执行。常见的工作队列类型有有界队列(如 ArrayBlockingQueue)和无界队列(如 LinkedBlockingQueue)等。

  6. 线程工厂(threadFactory): 线程工厂用于创建新线程。线程工厂提供了创建线程的方法,可以自定义线程的名称、优先级等属性。

  7. 拒绝策略(rejectedExecutionHandler): 拒绝策略定义了当线程池无法接受新任务时的处理策略。当工作队列已满且线程池中的线程数已达到最大线程数时,新任务将被拒绝执行。常见的拒绝策略有丢弃、丢弃最旧的任务、抛出异常等。

JDK四种线程池:

  • newCachedThreadPool,可根据需要创建新线程的线程池
  • newSingleThreadExecutor,单线程池
  • newFixedThreadPool,创建固定大小的线程池
  • newScheduledThreadPool,创建一个大小无限的线程池

线程池执行顺序:

  1. 首先判断 corePoolSize 是否已满,如果没有满,那么就去创建一个线程去执行该任务;否则请看下一步
  2. 如果线程池的核心线程数已满,那么就继续判断 BlockingQueue 是否已满,如果没满,那么就将任务放到任务队列中;否则请看下一步
  3. 如果任务队列已满,那么就判断线程池中的线程数量是否达到了maxumunPoolSize,如果没达到,那么就创建线程去执行该任务;否则请看下一步;
  4. 如果线程池已满,那么就根据拒绝策略来做出相应的处理;

简而言之:corePool->workQueue->maxPool

四种拒绝策略:

  1. AbortPolicy 拒绝任务并抛出一个异常 RejectedExecutionException
  2. DiscardPolicy 拒绝任务,不抛出异常。
  3. DiscardOldestPolicy 把老的任务丢掉,执行新任务。
  4. CallerRunsPolicy 直接调用线程处理该任务,

线程池被回收:线程池也是在堆中也是一个对象,一定要调用shutdown

线程池何时回收线程:getTask()的返回值为null时

  1. 未调用shutdown(),并且当前工作线程数过多
  2. 调用shutdown(),缓冲队列中的线程为空

线程数设置

  1. CPU密集型任务:CPU核心数 + 1
  2. IO密集型任务:CPU核心数 * 2
  3. 混合型任务:CPU核心数 * 目标CPU利用率 * (1 + 线程等待时间/线程运行时间)

  1. 乐观锁,悲观锁:
    • 乐观锁,修改数据前比较数据是否被修改过。CAS,原子类的递增操作,适合频繁读
    • 悲观锁,加锁使其他线程无法修改。synchronized和lock的实现类,适合频繁写
  2. 自旋锁,非自旋锁:获取同步资源的锁失败,资源被占用(上下文切换,也就是线程的唤醒和阻塞是十分耗时的)
    • 自旋锁,不放弃CPU时间片,通过自旋等待锁的释放,但自旋超过一定次数(默认10次)仍没有获得锁,那么线程被挂起。线程竞争不激烈并且锁持有的时间不长时,可以使用自旋锁。
    • 非自旋锁,线程会进入阻塞状态
  3. 无锁,偏向锁,轻量级锁,重量级锁:指针对synchronized同步锁的状态,锁可以升级但不能降级。
    • 偏向锁,通过对比Mark Word中是否存储着指向当前线程的偏向锁以解决加锁问题,避免执行CAS操作来加锁和解锁,Java15放弃偏向锁。使用背景:锁不仅不存在多线程竞争,而且总是由同一个线程多次获取,那么在同一个线程反复获取所释放锁中,其中并还没有锁的竞争。
    • 轻量级锁,通过用CAS修改Mark Word操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。
    • 重量级锁,将除了拥有锁的线程以外的线程都阻塞。
  4. 公平锁,非公平锁:
    • 公平锁,每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的。
    • 非公平锁,每个线程获取锁的顺序是随机的,并不会遵循先来先得的规则,所有线程会竞争获取锁。
  5. 可重入锁(递归锁),非可重入锁:ReentrantLock和synchronized都是可重入锁,NonReentrantLock是非可重入锁
    • 可重入锁,指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。好处是一定程度避免死锁。
    • 非可重入锁,如果一个方法中获取锁并调用另外方法,那么在调用另外方法前需要释放锁。
  6. 独享锁(排它锁),共享锁
    • 独享锁,ReentrantLock、synchronized、ReentrantReadWriteLock的写锁
    • 共享锁,ReentrantReadWriteLock的读锁,可以再加共享锁但不可以加排他锁!

Synchronized(同步锁):属于独占锁、悲观锁、可重入锁、非公平锁。

ReentrantLock:继承了Lock类,两者都是可重入锁、悲观锁、独占锁、默认非公平锁。

AbstractQueuedSynchronizer(AQS)

该类是一个抽象类,采用模板方法的设计模式,规定了独占共享模式需要实现的方法。

简单解释:CAS修改volatile修饰的int值state(该值代表竞争资源标识) + 一个存放等待锁的线程队列。其定义了两种资源共享模式:

  1. 独占式。ReentrantLock 是独占式的锁资源。初始化 state = 0,表示资源未被锁定,调用 lock() 方法时state的值加一,并且当 state = 0 才表明其他线程有机会获取锁。

  2. 共享式。ReentrantWriteLock 和 CountDownLatch 是共享锁模式。CountDownLatch 会将任务分成 N 个子任务,初始化 state = N,每个子线程完成任务后会减一,直到为零。

Contract接口模式,结合feign实现

  • contract 用于暴露接口
  • service 用于实现接口

接口和抽象类的区别:

  • 相同点:
    • 都不能被实例化
    • 接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能实例化
  • 不同点:
    • 接口只有定义,不能有方法的实现,java 1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。
    • 实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。
    • 接口强调特定功能的实现,而抽象类强调所属关系。
    • 接口成员变量默认为public static final,必须赋初值,不能被修改;其所有的成员方法都是public、abstract的。抽象类中成员变量默认default,可在子类中被重新定义,也可被重新赋值;抽象方法被abstract修饰,不能被private、static、synchronized和native等修饰,必须以分号结尾,不带花括号。

static修饰词

java中静态属性和静态方法可以被继承,但是不能被重写,因此不能实现多态。

静态常量/静态变量/静态方法是用static修饰的常量/变量/方法,其从属于类。另外,static是不允许用来修饰局部变量的。

  • 静态方法可以调用静态变量,但不能调用非静态变量,因为静态方法在类加载时就分配了内存,而非静态变量是在对象实例化时才分配内存。

  • 非静态方法可以调用静态变量,也可以调用非静态变量。

静态初始化块、初始化块和构造方法的区别

执行顺序:静态初始化块 > 初始化块 > 构造方法

非静态初始化块(构造代码块):

作用:给对象进行初始化。对象一建立就运行,且优先于构造函数的运行。

与构造函数的区别:

非静态初始化块给所有对象进行统一初始化,构造函数只给对应对象初始化。

应用:将所有构造函数共性的东西定义在构造代码块中。

静态初始化块:

作用:给类进行初始化。随着类的加载而执行,且只执行一次

与构造代码块的区别:

  • 构造代码块用于初始化对象,每创建一个对象就会被执行一次;静态代码块用于初始化类,随着类的加载而执行,不管创建几个对象,都只执行一次。
  • 静态代码块优先于构造代码块的执行
  • 都定义在类中,一个带static关键字,一个不带static
This post is licensed under CC BY 4.0 by the author.

Java知识点记录博客

MySQL知识点汇总