JavaSE

JVM 虚拟机

JVM是可运行Java代码的假想计算机,包括一套字节码指令集,一组寄存器,一个栈,一个垃圾回收,堆,和一个存储方法区。

### 内存分布

image-20240909082136850

栈桢

Clip_2024-09-09_08-26-15

垃圾回收

运行时内存分布

从GC角度,Java内存可分为:

Clip_2024-09-09_08-29-24

  • Eden区伊甸区,新对象诞生的地方。新对象体积过大直接放入老年区。
  • ServivoFrom上一次GC的幸存者,本次GC的被扫描者。
  • ServivoTo保留幸存者的地区,

#### JVM中的垃圾回收算法

  1. 引用计数法:通过给对象添加引用计数器,当引用计数器为0时,说明对象不再被使用,可以被回收。但该方法存在循环引用的问题,所以Java中未采用此方法。
  2. 可达性分析:通过一系列称为GC Roots的点作为起点,向下搜索,当一个对象到任何GC Roots没有引用链相连时,该对象被视为不可达,可以被回收。
  3. 标记清除算法:分为标记和清除两个阶段,首先标记出所有需要回收的对象,然后统一清除。该算法会产生内存碎片。
  4. 复制算法:将内存分为大小相等的两块,每次只使用其中一块。当需要进行垃圾回收时,将存活的对象复制到另一块内存中,然后清空原来的内存。该算法适用于存活对象较少的情况,缺点是浪费内存。
  5. 标记整理算法:结合了标记清除和复制算法的优点,在标记完存活对象后,将它们移动到一端,然后清理掉端边界外的对象。
  6. 分代收集算法:根据对象的生命周期将内存分为新生代和老年代,针对不同代的特点采用不同的垃圾回收算法。
    • 新生代:通常采用复制算法,因为新生代中的对象大多生命周期较短,复制少量存活对象的成本较低。
    • 老年代:对象存活率高,一般采用标记清除或标记整理算法。 此外,Java 8中引入了G1(Garbage First)收集器,它是一种并行与并发的收集器,采用了分区的思路,能更好地管理大内存堆,实现可预测的停顿时间。

MinorGC-新生代垃圾回收

Eden区域满时,进行MinorGC,采用复制算法

  1. 在一次GC中,检索 ServivoFrom区域中所有存活的对象,并将其和Eden中的对象复制到 ServivoTo区。若有对象年龄已经达到15,则放入老年区。
  2. 清空 EdenServivoFrom
  3. ServivoTo作为下一次GC的 ServivoFrom,与Eden一起进行下一次垃圾回收。

MajorGC-老年代垃圾回收

老年代空间不足等情况下,进行MajorGC,采用标记清除算法

扫描标记出老年代区域中所有存活的对象,然后回收没有标记的对象。

MajorGC耗时较长,会产生内存碎片。我们一般需要合并或者标记,以便进行下一次分配。

GC不会对永久代中的对象回收。主要存放 Class 和 Meta(元数据)的信息。

注意:

在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间
的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用
本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native
memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由
MaxPermSize 控制, 而由系统的实际可用空间来控制。

垃圾收集器

Serial 收集器

  • 单线程收集器:它在进行垃圾收集时,会暂停所有用户线程(Stop - The - World),直到垃圾收集过程结束。例如,在一个简单的 Java 应用程序中,如果使用 Serial 收集器,当垃圾收集触发时,整个程序的执行会暂停,直到 Serial 收集器完成新生代或者老年代的垃圾回收工作。
  • 适用场景:适用于单 CPU 环境下的 Client 模式,因为它的实现简单,没有线程交互的开销。

ParNew 收集器

  • 并行收集器:ParNew 收集器是 Serial 收集器的多线程版本。它在新生代垃圾收集时,可以使用多个线程同时进行垃圾回收操作。例如,在多核 CPU 环境下,ParNew 收集器能够充分利用多核的优势,加快垃圾回收的速度。
  • 与 CMS 收集器配合使用:ParNew 收集器常常与 Concurrent Mark Sweep(CMS)收集器配合使用,其中 ParNew 负责新生代的垃圾回收,CMS 负责老年代的垃圾回收。

Parallel Scavenge 收集器

  • 吞吐量优先收集器:Parallel Scavenge 收集器的目标是达到一个可控制的吞吐量(Throughput)。吞吐量是指 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值。例如,在一些对响应时间要求不高,但需要高吞吐量的场景,如后台批量处理任务,Parallel Scavenge 收集器可以通过调整参数来优化系统的吞吐量。
  • 自适应调节策略:它具有自适应调节的功能,能够根据系统的运行情况自动调整一些参数,如新生代的大小、晋升老年代的对象大小等,以达到设定的吞吐量目标。

Serial Old 收集器

  • Serial 收集器的老年代版本:Serial Old 收集器是 Serial 收集器的老年代版本,同样是单线程收集器,在进行老年代垃圾收集时会暂停所有用户线程。例如,在 Client 模式下或者一些特定的环境中,如果需要对老年代进行垃圾回收,并且系统资源有限或者对简单性有要求时,可以使用 Serial Old 收集器。
  • 与 CMS 收集器配合使用:在 CMS 收集器出现 Concurrent Mode Failure 的情况下,Serial Old 收集器可以作为备用的老年代垃圾收集器。

Parallel Old 收集器

  • Parallel Scavenge 收集器的老年代版本:Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,它在老年代垃圾收集时采用多线程并行的方式。例如,当需要在整个堆内存(包括新生代和老年代)都实现高吞吐量的垃圾回收时,可以使用 Parallel Scavenge 收集器和 Parallel Old 收集器组合。
  • 注重吞吐量:它与 Parallel Scavenge 收集器一样,注重系统的吞吐量,适用于对吞吐量有较高要求的场景。

CMS 收集器(Concurrent Mark Sweep)

  • 低停顿收集器:CMS 收集器的目标是尽可能减少垃圾收集时造成的停顿时间,以提高用户程序的响应性。它在垃圾收集过程中,大部分工作是与用户线程并发执行的。例如,在一个 Web 应用程序中,如果对响应时间要求较高,不希望因为垃圾收集而导致长时间的停顿,CMS 收集器可以在一定程度上满足这种需求。
  • 分阶段垃圾收集:CMS 收集器的垃圾收集过程分为初始标记、并发标记、重新标记和并发清除四个阶段。其中初始标记和重新标记阶段需要暂停用户线程,但是这两个阶段的停顿时间相对较短;并发标记和并发清除阶段与用户线程并发执行。

G1 收集器(Garbage - First)

  • 分区收集器:G1 收集器将堆内存划分为多个大小相等的独立区域(Region),每个区域可以根据需要扮演新生代或老年代的角色。例如,在一个大内存的 Java 应用程序中,G1 收集器可以更灵活地管理内存,避免了传统分代收集器在内存分配上的一些限制。
  • 可预测的停顿时间:G1 收集器的一个重要特点是可以预测垃圾收集的停顿时间。通过设置一个期望的最大停顿时间,G1 收集器会尽量在这个时间范围内完成垃圾收集工作。这对于对响应时间有严格要求的实时系统或者大型企业级应用程序非常有用。
  • 并发与并行结合:G1 收集器在垃圾收集过程中,既采用了并发的方式与用户线程一起工作,又在某些阶段使用了多线程并行执行,以提高垃圾回收的效率。

引用

以下是 Java 中强、弱、软、虚引用的代码示例:

强引用(Strong Reference)

强引用是最常见的引用类型,只要强引用存在,对象就不会被垃圾回收。

public class StrongReferenceExample {
    public static void main(String[] args) {
        // 创建一个强引用对象
        Object strongReference = new Object();
        // 即使不进行任何操作,对象也不会被回收,因为有强引用指向它
    }
}

弱引用(Weak Reference)

当垃圾回收器运行时,如果一个对象仅被弱引用关联,那么该对象将被回收。

import java.lang.ref.WeakReference;

public class WeakReferenceExample {
    public static void main(String[] args) {
        // 创建一个弱引用对象
        WeakReference<Object> weakReference = new WeakReference<>(new Object());

        // 获取弱引用指向的对象
        Object object = weakReference.get();
        if (object!= null) {
            System.out.println("Object is still alive");
        } else {
            System.out.println("Object has been garbage collected");
        }
    }
}

在运行上述代码时,由于没有强引用指向新创建的对象,并且垃圾回收器可能会在某个时刻运行,所以弱引用指向的对象可能会被回收。

软引用(Soft Reference)

软引用在内存充足时,与强引用类似,不会被回收;但当内存不足时,垃圾回收器会回收软引用指向的对象。

import java.lang.ref.SoftReference;

public class SoftReferenceExample {
    public static void main(String[] args) {
        // 创建一个软引用对象
        SoftReference<Object> softReference = new SoftReference<>(new Object());

        // 获取软引用指向的对象
        Object object = softReference.get();
        if (object!= null) {
            System.out.println("Object is still alive");
        } else {
            System.out.println("Object has been garbage collected");
        }
    }
}

虚引用(Phantom Reference)

虚引用主要用于在对象被回收时接收一个系统通知。虚引用必须与引用队列(ReferenceQueue)一起使用。

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public class PhantomReferenceExample {
    public static void main(String[] args) {
        ReferenceQueue<Object> queue = new ReferenceQueue<>();
        // 创建一个虚引用对象,并关联引用队列
        PhantomReference<Object> phantomReference = new PhantomReference<>(new Object(), queue);

        // 获取虚引用指向的对象,总是返回 null
        Object object = phantomReference.get();
        if (object == null) {
            System.out.println("Phantom reference's referent is always null");
        }

        // 可以通过引用队列检查对象是否已被回收
        if (queue.poll()!= null) {
            System.out.println("The object has been garbage collected");
        }
    }
}

## IO

IO模型

  • 阻塞IO模型

    传统模型,在读写时候会阻塞

  • 非阻塞IO模型

    持续读写,如果返回error就继续读写,直到读到数据为止

  • 多路复用IO模型(JavaNIO)

    设置数个channel,每个channel分管几个socket,通过一个县城进行轮询,有数据则读,无数据则继续轮询

  • 信号驱动IO模型

    需要读取数据时,向socket注册一个信号函数,然后继续执行业务代码。内核准备好数据后,用户线程接收到信号,可在信号函数中调用IO操作。

  • 异步IO模型

    用户线程发起read操作,内核收到read后立即返回,线程执行业务代码。内核在数据准备完成后将数据拷贝给用户线程,并发送信号,完成read操作。

JavaNIO

NIO 主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。传统 IO 基于字节流和字符流进行操作,而 NIO 基于 Channel 和buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,
数据到达)。因此,单个线程可以监听多个数据通道。

JVM类加载机制

加载流程

加载(Loading)

  • 获取字节流:通过类的全限定名,从文件系统、网络、JAR 包等来源获取定义此类的二进制字节流。例如,从本地文件系统中的.class 文件读取字节流,或者从网络上的远程服务器下载字节流。
  • 转换为方法区数据结构:将获取到的字节流所代表的静态存储结构转化为方法区的运行时数据结构。在这个过程中,JVM 会在方法区为类分配内存空间,并将类的相关信息(如类的版本、字段、方法、接口等)存储在这个内存区域。
  • 生成 Class 对象:在堆内存中创建一个 java.lang.Class 类的对象,作为方法区中这个类的各种数据的访问入口。这个 Class 对象就像是一面镜子,通过它可以反射出类的所有信息,比如可以使用反射机制来获取类的方法、字段等信息。

验证(Verification)

  • 文件格式验证:检查字节流是否符合 Class 文件格式的规范,例如是否以魔数(0xCAFEBABE)开头、主次版本号是否在当前 JVM 所能处理的范围内等。如果字节流的格式不符合规范,JVM 将拒绝加载这个类。
  • 元数据验证:对类的元数据信息进行语义分析,比如检查这个类是否有父类(除了 java.lang.Object 类),父类是否是被允许继承的类(final 修饰的类不能被继承)等。确保类的继承关系、实现的接口等信息是正确的。
  • 字节码验证:通过分析字节码流,确保字节码的执行不会导致违反 Java 语言的语义和 JVM 的安全模型。例如,检查类型转换是否合法、操作数栈是否不会出现溢出或下溢等情况。
  • 符号引用验证:在解析阶段将符号引用转换为直接引用的时候进行。主要验证符号引用中通过字符串描述的全限定名是否能找到对应的类、字段、方法等,以及类、字段、方法的访问性(public、private、protected 等)是否可被当前类访问。

准备(Preparation)

  • 分配内存空间:在方法区为类的静态变量(被 static 修饰的变量)分配内存空间。例如,对于一个静态的 int 变量,会分配 4 个字节的内存空间;对于一个静态的对象引用变量,会分配一个引用类型的内存空间。
  • 设置初始值:将静态变量初始化为默认值,而不是代码中显式赋予的值。比如,静态的 int 变量初始化为 0,静态的 boolean 变量初始化为 false,静态的对象引用变量初始化为 null。

解析(Resolution)

  • 符号引用转换为直接引用:在 Java 中,当一个类使用了其他类、接口、字段或者方法时,在编译阶段并不知道这些被引用的元素的实际内存地址,而是使用符号引用来表示。在解析阶段,JVM 会把这些符号引用转换为直接引用。直接引用是可以直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。
  • 类或接口的解析:假设在类 A 中引用了类 B,当解析类 B 时,如果类 B 还没有被加载,那么会先触发类 B 的加载过程。然后根据类 B 在方法区中的内存地址,生成类 A 对类 B 的直接引用。
  • 字段解析:对于类中的字段引用,会根据字段所属的类或接口的符号引用进行解析。如果字段是静态的,直接在方法区中该类的静态变量区域查找;如果是实例字段,则在对象实例的数据区域查找。
  • 方法解析:方法解析相对复杂一些。首先会解析出方法所属的类或接口,然后在类或接口的方法表中查找对应的方法。如果是接口方法,会检查调用类是否实现了该接口以及是否重写了该方法等。

初始化(Initialization)

  • 执行初始化代码:这是类加载的最后一步,也是真正执行类中定义的 Java 程序代码的阶段。在这个阶段,会执行类的构造器方法 <clinit>,这个方法是由编译器自动收集类中的所有静态变量的赋值动作和静态代码块中的语句合并产生的。例如,如果类中有一个静态变量在定义时被赋予了一个表达式的值,或者有一个静态代码块执行了一些初始化操作,这些都会在 <clinit>方法中执行。

  • 类初始化的触发时机

    :当遇到以下几种情况时,会触发类的初始化:

    • 创建类的实例,比如使用 new关键字创建一个对象。
    • 访问类的静态变量或者对静态变量赋值,除了被 final 修饰且已经在编译期把常量值放入常量池的静态变量。
    • 调用类的静态方法。
    • 反射调用类的时候,比如使用 java.lang.reflect包中的方法来操作类。
    • 当初始化一个类的时候,如果发现其父类还没有被初始化,则需要先触发父类的初始化。

类加载器

启动类加载器

负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar)的类

扩展类加载器

负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。

应用程序类加载器

负责加载用户路径(classpath)上的类库

双亲委派机制

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成。

每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。

Clip_2024-09-11_11-02-42

Java集合框架

集合类存放于 Java.util 包中,主要有 3 种:set(集)、list(列表包含 Queue)和 map(映射)。

List

有序

Java 的 List 是非常常用的数据类型。List 是有序的 Collection。Java List 一共三个实现类:
分别是 ArrayList、Vector 和 LinkedList。

ArrayList(数组)

有序|扩展|随机访问|增删慢|线程不安全

ArrayList 是最常用的 List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数
组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数
组的数据复制到新的存储空间中。当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进
行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。

Vector(数组|线程同步)

有序|扩展|随机访问较慢|增删慢|线程不安全

Vector 与 ArrayList 一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一
个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,
访问它比访问 ArrayList 慢。

链表|增删块|访问慢|堆.栈.双向队列

LinkedList 是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较
慢。另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆
栈、队列和双向队列使用

Set

无序

Set 注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素,值不能重
复。对象的相等性本质是对象 hashCode 值(java 是依据对象的内存地址计算出的此序号)判断
的,如果想要让两个不同的对象视为相等的,就必须覆盖 Object 的 hashCode 方法和 equals 方
法。

HashSet

哈希表边存放的是哈希值。HashSet 存储元素的顺序并不是按照存入时的顺序 而是按照哈希值来存的所以取数据也是按照哈希值取得。元素的哈希值是通过元素的hashcode 方法来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals 方法 如果 equls 结果为 true ,HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。

TreeSet

使用二叉树原理对新增加的对象按照指定的顺序排序(插入树的指定位置)。

自定义类必须实现Comparable接口,并且重写CompareTo方法,才能进行比较。

LinkHashSet

增删性能略低|访问快|有序

HashSet的子类,有序。

Map

HashMap

红黑树|数组|链表|

Clip_2024-09-09_11-21-05

ConcurrentHashMap

线程安全的 HashMap,有 segment概念。

Clip_2024-09-09_11-23-33

ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。

HashTable

Hashtable 是遗留类,很多映射的常用功能与 HashMap 类似,不同的是它承自 Dictionary 类,并且是线程安全的,任一时间只有一个线程能写 Hashtable**,并发性不如 ConcurrentHashMap,因为 ConcurrentHashMap 引入了分段锁**。Hashtable 不建议在新代码中使用,不需要线程安全的场合可以用 HashMap 替换,需要线程安全的场合可以用 ConcurrentHashMap 替换。

TreeMap

TreeMap 实现 SortedMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。如果使用排序的映射,建议使用 TreeMap。在使用 TreeMap 时,key 必须实现 Comparable 接口或者在构造 TreeMap 传入自定义的Comparator,否则会在运行时抛出 java.lang.ClassCastException 类型的异常。

LinkedHashMap

保留插入顺序|可排序

LinkedHashMap 是 HashMap 的一个子类,保存了记录的插入顺序,在用 Iterator 遍历
LinkedHashMap 时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

Java多线程并发

Java线程实现|创建方式

继承Thread类

示例:

创建并启动线程

// 自定义线程类,继承自 Thread 类
class MyThread extends Thread {
    private String threadName;

    public MyThread(String name) {
        this.threadName = name;
    }

    // 重写 run 方法,定义线程执行的逻辑
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(threadName + " - " + i);
            try {
                // 线程休眠一段时间,模拟耗时操作
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        // 创建两个线程对象
        MyThread thread1 = new MyThread("Thread 1");
        MyThread thread2 = new MyThread("Thread 2");

        // 启动线程
        thread1.start();
        thread2.start();

        // 主线程继续执行其他操作
        for (int i = 0; i < 5; i++) {
            System.out.println("Main Thread - " + i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

线程优先级设置

class MyThread extends Thread {
    // 构造方法和 run 方法省略,与上一个示例类似

    // 可以添加设置优先级的方法
    public void setMyPriority(int priority) {
        if (priority >= Thread.MIN_PRIORITY && priority <= Thread.MAX_PRIORITY) {
            setPriority(priority);
        }
    }
}

public class ThreadPriorityExample {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread("High Priority Thread");
        MyThread thread2 = new MyThread("Low Priority Thread");

        // 设置不同的优先级
        thread1.setMyPriority(Thread.MAX_PRIORITY);
        thread2.setMyPriority(Thread.MIN_PRIORITY);

        thread1.start();
        thread2.start();
    }
}

线程的等待与唤醒

class WaitNotifyThread extends Thread {
    private Object lock;

    public WaitNotifyThread(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock) {
            try {
                System.out.println(Thread.currentThread().getName() + " is waiting.");
                // 调用 wait 方法使线程进入等待状态
                lock.wait();
                System.out.println(Thread.currentThread().getName() + " is awakened.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class WaitNotifyExample {
    public static void main(String[] args) {
        Object lock = new Object();

        WaitNotifyThread thread1 = new WaitNotifyThread(lock);
        WaitNotifyThread thread2 = new WaitNotifyThread(lock);

        thread1.start();
        thread2.start();

        try {
            // 主线程等待一段时间后通知其他线程
            Thread.sleep(1000);
            synchronized (lock) {
                // 调用 notify 或者 notifyAll 方法唤醒等待的线程
                lock.notifyAll();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

实现Runable接口

示例:

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " - " + i);
        }
    }
}

public class RunnableExample {
    public static void main(String[] args) {
        // 创建 Runnable 对象
        MyRunnable myRunnable = new MyRunnable();
        // 创建线程对象并将 Runnable 对象传递给它
        Thread thread1 = new Thread(myRunnable);
        Thread thread2 = new Thread(myRunnable);

        // 启动线程
        thread1.start();
        thread2.start();
    }
}

实现Callable接口

示例:

基本实现:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable<Integer> {
    private int number;

    public MyCallable(int number) {
        this.number = number;
    }

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= number; i++) {
            sum += i;
        }
        return sum;
    }
}

public class CallableExample {
    public static void main(String[] args) {
        // 创建 Callable 对象
        MyCallable callable = new MyCallable(10);
        // 将 Callable 对象包装成 FutureTask 对象
        FutureTask<Integer> futureTask = new FutureTask<>(callable);

        // 创建线程并启动
        Thread thread = new Thread(futureTask);
        thread.start();

        try {
            // 获取线程执行结果
            Integer result = futureTask.get();
            System.out.println("计算结果: " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

线程池

示例:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class MyTask implements Runnable {
    private final int taskId;

    public MyTask(int taskId) {
        this.taskId = taskId;
    }

    @Override
    public void run() {
        System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName());
    }
}

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 创建一个固定大小为 3 的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        // 提交 5 个任务到线程池
        for (int i = 1; i <= 5; i++) {
            MyTask myTask = new MyTask(i);
            executorService.execute(myTask);
        }

        // 关闭线程池
        executorService.shutdown();
    }
}
线程池的种类
  1. CachedThreadPool缓存线程池:

    特点:

    • 线程数量动态变化。如果有新的任务提交,而当前没有空闲线程,就会创建新的线程来处理任务。如果线程在一段时间(默认 60 秒)内没有执行任务,就会被回收。
    • 任务队列同步移交。不使用额外的任务队列,直接将任务交给线程执行。

    适用场景:

    • 适用于执行大量短期异步任务的场景。

    创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行
    很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造
    的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并
    从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资
    源。

  2. FixedThreadPool固定大小线程池:

    固定大小线程池

    特点:

    • 线程数量固定
    • 任务队列无界

    使用场景:

    比较稳定的高并发场景。

    创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大
    多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,
    则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何
    线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之
    前,池中的线程将一直存在。

  3. ScheduledThreadPool定时线程池

    特点:

    • 可以执行定时任务和周期性任务。除了具备普通线程池的功能外,还可以按照预定的时间执行任务或者周期性地执行任务。
    • 支持多种定时策略。可以指定任务在固定延迟后执行、在固定速率下周期性执行等。

    适用场景:

    • 适用于需要定时执行任务或者周期性执行任务的场景。

    创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

  4. SingleThreadExecutor单线程线程池

    特点:

    • 只有一个工作线程。所有任务按照提交的顺序依次执行。
    • 任务队列无界。如果这个唯一的线程在执行任务时发生异常而终止,线程池会创建一个新的线程来替代它继续执行后续任务。

    使用场景:

    • 保证任务按顺序执行的场景。

    Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程
    池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去

  5. WorkStealingPool工作窃取线程池

    特点:

    • 基于工作窃取算法。内部会创建多个线程,每个线程都有自己的任务队列。当某个线程完成了自己队列中的任务后,它会从其他线程的队列尾部窃取任务来执行。
    • 动态调整线程数量。根据系统的 CPU 核心数来动态调整线程的数量,通常是创建与 CPU 核心数相同数量的线程。

    适用场景:

    • 适用于在多核环境下执行计算密集型任务的场景。例如,在一个图像处理程序中,需要对大量的图像数据进行复杂的数学运算(如图像的傅里叶变换等),使用工作窃取线程池可以充分利用多核 CPU 的优势,提高计算效率。

线程的生命周期

在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞
(Blocked)和死亡(Dead)5 种状态。

  1. 新建状态:使用 new关键字创建线程对象后,线程处于新建状态

  2. 就绪状态start()方法被调用后,线程进入就绪状态

  3. 运行状态:就绪状态的线程获得CPU后进入运行状态

  4. 阻塞状态:线程因为某种原因放弃CPU,进入阻塞状态,阻塞状态分为3种:

    1. *等待阻塞:*执行 wait()方法后进入等待队列
    2. 同步阻塞:运行状态的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线
      程放入锁池(lock pool)中。
    3. 其他阻塞:运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。
  5. 线程死亡

    正常结束:run()或 call()方法执行完成,线程正常结束。

    异常结束:线程抛出一个未捕获的 Exception 或 Error。
    调用stop:直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。

线程的生命周期

线程池

线程池参数

  1. corePoolSize:指定了线程池中的线程数量。
  2. maximumPoolSize:指定了线程池中的最大线程数量。
  3. keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多
    次时间内会被销毁。
  4. unit:keepAliveTime 的单位。
  5. workQueue:任务队列,被提交但尚未被执行的任务。
  6. threadFactory:线程工厂,用于创建线程,一般用默认的即可。
  7. handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。

线程池阻塞队列类型

  1. ArrayBlockingQueue :
    • 由数组结构组成的有界阻塞队列
    • 先进先出
    • 不保证公平访问
  2. LinkedBlockingQueue :
    • 由链表结构组成的有界阻塞队列
    • 先进先出
    • 并发效率高-生产者消费者独立锁
  3. PriorityBlockingQueue :
    • 支持优先级排序的无界阻塞队列
    • 自定义排序规则
  4. DelayQueue:
    • 使用优先级队列实现的无界阻塞队列
    • 支持延迟获取元素 (可用于订单超时取消)
  5. SynchronousQueue:
    • 不存储元素的阻塞队列
    • 可用于线程间的消息传递
  6. LinkedTransferQueue:
    • 由链表结构组成的无界阻塞队列
    • 增加了 transfer方法,若没有消费者承担任务,则放到队列尾部,反之传递给消费者,消费者完成任务后,方法返回。
    • 增加了 TayTransfer方法,没有消费者承担任务时,返回 false
  7. LinkedBlockingDeque:
    • 由链表结构组成的双向阻塞队列
    • 两端都可以插入移除,比较高效。

线程池原理

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后
启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,
再从队列中取出任务来执行。他的主要特点为:线程复用;控制最大并发数,管理线程。

每一个 Thread 的类都有一个 start 方法。 当调用 start 启动线程时 Java 虚拟机会调用该类的 run
方法。 那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。 我们可以继承重写
Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。 这就是线程池的实
现原理。循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以
是阻塞的。

终止线程

  1. 正常运行结束

  2. 使用退出标志

    设置一个 Boolean变量,当其值变为 false时退出线程。

  3. Interrupt

    1. **线程处于阻塞状态:**如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,
      会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。
      阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让
      我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实
      际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正
      常结束 run 方法。
    2. **线程未处于阻塞状态:**使用 isInterrupted()判断线程的中断标志来退出循环。
  4. stop方法

    thread.stop()调用之后,线程结束,父线程抛出 ThreadDeatherror 的错误,会释放子
    线程所持有的所有锁,不安全

sleep 与 wait区别

  1. 对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于
    Object 类中的。
  2. sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然
    保持,当指定的时间到了又会自动恢复运行状态。
  3. 在调用 sleep()方法的过程中,线程不会释放对象锁。
  4. 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此
    对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

start 与 run 区别

  1. start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,
    可以直接继续执行下面的代码。
  2. 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运
    行。
  3. 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运
    行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。

守护线程

守护线程--也称“服务线程”,他是后台线程,它有一个特性,即为用户线程 提供 公
共服务,在没有用户线程可服务时会自动离开。

  1. 优先级:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。
  2. 设置:通过 setDaemon(true)来设置线程为“守护线程”;将一个用户线程设置为守护线程
    的方式是在 线程对象创建 之前 用线程对象的 setDaemon 方法。
  3. 在 Daemon 线程中产生的新线程也是 Daemon 的。
  4. 线程则是 JVM 级别的,以 Tomcat 为例,如果你在 Web 应用中启动一个线程,这个线程的
    生命周期并不会和 Web 应用程序保持同步。也就是说,即使你停止了 Web 应用,这个线程
    依旧是活跃的。
  5. example: 垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,
    程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是 JVM 上仅剩的线
    程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统
    中的可回收资源。
  6. 生命周期:守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周
    期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依
    赖于系统,与系统“同生共死”。当 JVM 中所有的线程都是守护线程的时候,JVM 就可以退
    出了;如果还有一个或以上的非守护线程则 JVM 不会退出。

线程基本方法

线程等待-wait

调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。

线程睡眠-sleep

sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致
线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态

线程让步-yield

yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,
优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对
线程优先级并不敏感。

线程中断-interrupt

中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这
个线程本身并不会因此而改变状态(如阻塞,终止等)。

  1. 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线
    程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。
  2. 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出
    InterruptedException,从而使线程提前结束 TIMED-WATING 状态
  3. 许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异
    常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。
  4. 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止
    一个线程 thread 的时候,可以调用 thread.interrupt()方法,在线程的 run 方法内部可以
    根据 thread.isInterrupted()的值来优雅的终止线程。

等待线程-join

join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞
状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。

线程唤醒

Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象
上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调
用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继
续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞
争。类似的方法还有 notifyAll(),唤醒再次监视器上等待的所有线程。

其他方法

其他方法:

  1. sleep():强迫一个线程睡眠N毫秒。
  2. isAlive(): 判断一个线程是否存活。
  3. join(): 等待线程终止。
  4. activeCount(): 程序中活跃的线程数。
  5. enumerate(): 枚举程序中的线程。
  6. currentThread(): 得到当前线程。
  7. isDaemon(): 一个线程是否为守护线程。
  8. setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线
    程依赖于主线程结束而结束)
  9. setName(): 为线程设置一个名称。
  10. wait(): 强迫一个线程等待。
  11. notify(): 通知一个线程继续运行。
  12. setPriority(): 设置一个线程的优先级。
  13. getPriority():获得一个线程的优先

线程数据同步

  1. 将数据抽象成一个类,并将数据的操作作为这个类的方法,天假 synchronized修饰
  2. 将Runable对象设计为内部类,共享数据为外部类,每个线程都可以访问外部类的成员。

ThreadLocal

每一个线程都有的本地变量,可用于存放同一个线程内不同方法共用的变量。

P93线程调度未完待续

Java锁

乐观锁

基本概念
  • 乐观锁假设在数据处理过程中并发冲突的概率比较小,所以在操作数据时并不加锁,而是在更新数据的时候去判断在此期间是否有其他线程修改了数据。
  • 比如在多人协作编辑文档的场景中,每个人都可以自由地编辑文档,在保存时才检查是否有其他人修改了自己正在编辑的部分。
实现方式
  • 常见的实现方式是使用版本号机制或者 CAS(Compare - And - Swap,比较并交换)操作。
  • 例如,使用 AtomicInteger类(内部使用 CAS操作)来实现乐观锁:
import java.util.concurrent.atomic.AtomicInteger;

public class OptimisticLockExample {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        int oldValue, newValue;
        do {
            oldValue = count.get();
            newValue = oldValue + 1;
        } while (!count.compareAndSet(oldValue, newValue));
    }

    public int getCount() {
        return count.get();
    }
}

悲观锁

基本概念
  • 悲观锁认为在数据处理过程中极有可能存在并发冲突,所以在数据被操作之前就先对其加锁,以确保在整个操作过程中数据不会被其他线程修改。
  • 就像在一个房间里,当一个人进入后就立刻把门锁上,不允许其他人进入,直到他完成任务离开房间并解锁。
实现方式
  • 在 Java 中,synchronized关键字和 ReentrantLock是常见的悲观锁实现方式。
  • 例如,使用 synchronized关键字来保证同步:
public class PessimisticLockExample {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

在上述代码中,incrementgetCount方法都使用了 synchronized关键字,在同一时刻只允许一个线程执行这些方法。

自旋锁

如果需要的资源被其他线程占用,则持续占据CPU等待,知道资源被释放。

等待过程中,线程占据CPU进行死循环,不适用于需要长时间等待的线程。

Synchronized同步锁

它是一个对象锁,当一个对象被 synchronized传入时,该对象仅能被一个线程操作。

当一个方法被 synchronized 修饰时,该方法同时只能被当前线程执行。

具有可重入性,假设某线程即将实行某个类的两个方法,执行完第一个方法后,该线程可以直接执行第二个方法,无需等在锁释放。

P66Java锁未完待续

异常

异常分类

Error

内部错误或资源耗尽错误。应用程序不会抛出。

Exception

RuntimeException

如:NullPointerExceptionClassCastException;一个是检查异常 CheckedException,如 I/O 错误导致的 IOExceptionSQLExceptionRuntimeException 是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。

CheckException

一般是外部错误,这种异常都发生在编译阶段,Java 编译器会强制程序去捕获此类异常,即会出现要求你把这段可能出现异常的程序进行 try catch,该类异常一般包括几个方面:

  1. 试图在文件尾部读取数据
  2. 试图打开一个错误格式的 URL
  3. 试图根据给定的字符串查找 class 对象,而这个字符串表示的类并不存在

异常处理

Throw

不进行处理,抛给调用者

Try-catch

指定出现异常后执行逻辑

反射

定义

运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制。

反射API

  1. Class类:反射的核心类,可以获取类的属性,方法等信息。
  2. Field类:Java.lang.reflec 包中的类,表示类的成员变量,可以用来获取和设置类之中的属性值。
  3. Method类:Java.lang.reflec 包中的类,表示类的方法,它可以用来获取类中的方法信息或者执行方法。
  4. Constructor类: Java.lang.reflec包中的类,表示类的构造方法。

获取Class

  1. 调用某个对象的 getClass()方法

    Person p=new Person();Class clazz=p.getClass();
    
  2. 调用某个类的 class 属性来获取该类对应的 Class 对象
    Class clazz=Person.class;

  3. 使用 Class 类中的 forName()静态方法(最安全/性能最好)
    Class clazz=Class.forName("类的全路径"); (最常用)

Java注解

Annotation(注解)是 Java 提供的一种对元程序中元素关联信息和元数据(meta data)的途径和方法。Annatation(注解)是一个接口,程序可以通过反射来获取指定程序中元素的 Annotation对象,然后通过该 Annotation对象来获取注解中的元数据信息。

元注解

  1. @Target注解类型,类级注解、方法注解、字段注解等。
  2. @Retention注解保留阶段,使用 ElementType的如下静态字段
    • SOURCE:注解只在源代码阶段保留,编译器在编译时会丢弃该注解。
    • CLASS:注解在编译后的字节码文件中保留,但在运行时无法通过反射获取。
    • RUNTIME:注解在运行时仍然可用,可以通过反射获取和处理注解信息
  3. @Docunmented描述其它类型的 annotation 应该被作为被标注的程序成员的公共 API,因此可以被例如 javadoc 此类的工具文档化。
  4. @Inherited阐述了某个被标注的类型是被继承的

注解处理

需要用反射来获取对象的注解值,然后操作。示例:

/** 定义注解*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitProvider {
    /**供应商编号*/
    public int id() default -1;
    /*** 供应商名称*/
    public String name() default "";
        13/01/2022 Page 108 of 283
        /** * 供应商地址*/
        public String address() default "";
}
//2:注解使用
public class Apple {
    @FruitProvider(id = 1, name = "陕西红富士集团", address = "陕西省西安市延安路")
    private String appleProvider;
    public void setAppleProvider(String appleProvider) {
        this.appleProvider = appleProvider;
    }
    public String getAppleProvider() {
        return appleProvider;
    }
}
/*********** 注解处理器 ***************/
public class FruitInfoUtil {
    public static void getFruitInfo(Class<?> clazz) {
        String strFruitProvicer = "供应商信息:";
        Field[] fields = clazz.getDeclaredFields();//通过反射获取处理注解
        for (Field field : fields) {
            if (field.isAnnotationPresent(FruitProvider.class)) {
                FruitProvider fruitProvider = (FruitProvider) field.getAnnotation(FruitProvider.class);
                //注解信息的处理地方 
                strFruitProvicer = " 供应商编号:" + fruitProvider.id() + " 供应商名称:"
                    + fruitProvider.name() + " 供应商地址:"+ fruitProvider.address();
                System.out.println(strFruitProvicer);
            }
        }
    }
}
13/01/2022 Page 109 of 283
    public class FruitRun {
        public static void main(String[] args) {
            FruitInfoUtil.getFruitInfo(Apple.class);
            /***********输出结果***************/
            // 供应商编号:1 供应商名称:陕西红富士集团 供应商地址:陕西省西安市延
        }
    }

Java对象序列化

可以把对象写入磁盘,下次启动复用。1

Java 平台允许我们在内存中创建可复用的 Java 对象,但一般情况下,只有当 JVM 处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比 JVM 的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。Java 对象序列化就能够帮助我们实现该功能。

序列化对象以字节数组保持-静态成员不保存
使用 Java 对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量

Serializable 实现序列化

在 Java 中,只要一个类实现了 java.io.Serializable 接口,那么它就可以被序列化。

ObjectOutputStream 和 ObjectInputStream

通过 ObjectOutputStream 和 ObjectInputStream 对对象进行序列化及反序列化。

writeObject 和 readObject 自定义序列化策略

在类中增加 writeObject 和 readObject 方法可以实现自定义序列化策略。

序列化 ID

虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个
类的序列化 ID 是否一致(就是 private static final long serialVersionUID)

序列化并不保存静态变量

序列化子父类说明

要想将父类对象也序列化,就需要让父类也实现 Serializable 接口。

对象拷贝

直接赋值

复制的是引用,不是对象本身。

浅拷贝

class Resume implements Cloneable{ 
    public Object clone() { 
        try { 
            return (Resume)super.clone(); 
        } catch (Exception e) { 
            e.printStackTrace(); 
            return null; 
        } 
    } 
}

创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其副本引用同一个对象。

深拷贝

class Student implements Cloneable {

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

    public Object clone() {

        Student o = null;
        try {
            o = (Student) super.clone();
        } catch (CloneNotSupportedException e) {
            System.out.println(e.toString());
        }
        o.p = (Professor) p.clone();
        return o;
    }
}

深拷贝不仅复制对象本身,而且复制对象包含的引用指向的所有对象。

Spring

IOC AOP | 原理

Spring特性

  1. 轻量级

    • Spring本身轻量
    • Spring非侵入式
    • 本身的处理开销很少
  2. 控制反转IOC

    • 降低耦合
  3. 面向切面

    • 便于维护
  4. 容器

    • 管理对象,提升效率
  5. 框架集合

    • 非常容器集合其他的框架,形成复杂强大的应用程序。

容器

核心组件

Clip_2024-09-11_11-30-46

常用注解

  1. 注入组件注解:@Component、@Controller、@Service、@Repository、@Import

  2. 注入Bean注解:@Autowired、@Inject、@Resource、@Primary

  3. 配置类注解:@Configuration、 @Bean 、@ComponentScan

  4. 切面类注解:@Aspect、@Before、@After、@Around、@PointCut

  5. 其他注解:@JsonIgnore、@Scope、@Singleton、@Prototype、@Request、@Session、@Profile、@Conditional

  6. 属性注入:@Value

    1. ${}是去找外部配置的参数,将值赋过来
    2. #{}是SpEL表达式,去寻找对应变量的内容
    3. #{}直接写字符串就是将字符串的值注入进去
  7. 异步:@EnableAsync、@Async

  8. Json注解

    1、@JsonIgnoreProperties
    此注解是类注解,作用是json序列化时将java bean中的一些属性忽略掉,序列化和反序列化都受影响。

    写法将此标签加在user类的类名上 ,可以多个属性也可以单个属性。
    2、@JsonIgnore
    此注解用于属性或者方法上(最好是属性上),作用和上面的@JsonIgnoreProperties一样。
    3、@JsonFormat
    此注解用于属性或者方法上(最好是属性上),可以方便的把Date类型直接转化为我们想要的模式,比如:
    4、@JsonSerialize
    此注解用于属性或者getter方法上,用于在序列化时嵌入我们自定义的代码,比如序列化一个double时在其后面限制两位小数点。

    5、@JsonDeserialize
    此注解用于属性或者setter方法上,用于在反序列化时可以嵌入我们自定义的代码,类似于上面的@JsonSerialize

    6、@Transient
    如果一个属性并非数据库表的字段映射,就务必将其标示为@Transient,否则ORM框架默认其注解为@Basic;

    7、@JsonIgnoreType
    标注在类上,当其他类有该类作为属性时,该属性将被忽略。

    8、@JsonProperty
    @JsonProperty 可以指定某个属性和json映射的名称。例如我们有个json字符串为{“user_name”:”aaa”},
    而java中命名要遵循驼峰规则,则为userName,这时通过@JsonProperty 注解来指定两者的映射规则即可。这个注解也比较常用。
    9、只在序列化情况下生效的注解
    @JsonPropertyOrder

    在将 java pojo 对象序列化成为 json 字符串时,使用 @JsonPropertyOrder 可以指定属性在 json 字符串中的顺序。

    @JsonInclude

    在将 java pojo 对象序列化成为 json 字符串时,使用 @JsonInclude 注解可以控制在哪些情况下才将被注解的属性转换成 json,例如只有属性不为 null 时。

    @JsonInclude(JsonInclude.Include.NON_NULL)

    这个注解放在类头上,返给前端的json里就没有null类型的字段,即实体类与json互转的时候 属性值为null的不参与序列化。
    另外还有很多其它的范围,例如 NON_EMPTY、NON_DEFAULT等

    10、在反序列化情况下生效的注解
    @JsonSetter

    @JsonSetter 标注于 setter 方法上,类似 @JsonProperty ,也可以解决 json 键名称和 java pojo 字段名称不匹配的问题。

MVC

MVC常用注解

1、@EnableWebMvc
在配置类中开启Web MVC的配置支持。

2、@Controller
3、@RequestMapping
用于映射web请求,包括访问路径和参数。

4、@ResponseBody
支持将返回值放到response内,而不是一个页面,通常用户返回json数据。

5、@RequestBody
允许request的参数在request体中,而不是在直接连接的地址后面。(放在参数前)

6、@PathVariable
用于接收路径参数,比如@RequestMapping(“/hello/{name}”)声明的路径,将注解放在参数前,即可获取该值,通常作为Restful的接口实现方法。

7、@RestController
该注解为一个组合注解,相当于@Controller和@ResponseBody的组合,注解在类上,意味着,该Controller的所有方法都默认加上了@ResponseBody。

8、@ControllerAdvice
全局异常处理
全局数据绑定
全局数据预处理
ControllerAdvice的常用场景

9、@ExceptionHandler
用于全局处理控制器里的异常。

10、@InitBinder
用来设置WebDataBinder,WebDataBinder用来自动绑定前台请求参数到Model中。

SpringBoot

特性

  1. 创建独立的Spring程序
  2. 嵌入式Tomcat
  3. 简化Maven配置
  4. 自动配置Spring
  5. 没有代码生成,不要求配置XMl

Tomcat

Tomcat未完待续

网络

ISO的OSI七层模型

”物联网淑慧是用“

七层模型

HTTP

HTTP是一种无状态传输协议(指的是客户端和服务端不需要建立持久连接)。客户端向服务端发送一次请求,服务端向客户端进行一次回复,交互就结束了。

传输流程

  1. 地址解析

    浏览器从一个URL解析如下信息(http://localhost:8080/index.html

    • 协议:http
    • 主机名:localhost
    • 端口号:8080
    • 资源路径:/idex.html

    其中,主机名需要配合DNS进行域名解析来获取对应的IP地址。

  2. 封装http请求包

    将请求信息和本机信息封装为一个http请求包

  3. 封装TCP包并启动连接

    建立TCP连接(三次握手)

  4. 发送请求

    建立连接后,客户机向服务及发送一些信息:URL(统一资源标识符)、协议版本号、本机信息、MIME(Content-Type)等

  5. 服务器响应

    服务器收到请求后会返回一个响应,包含一个状态行(包括一个协议版本号、一个状态码、MIME信息、实体信息等)。

  6. 服务器关闭连接

    一旦服务器向客户端返回了信息,服务端就会关闭连接,但是如果请求头中包含:Connection:keep-alive,则TCP连接会继续保持打开状态,浏览器可以通过相同的连接发送请求。

HTTPS

HTTPS(Hypertext Transfer Protocol over Secure Socket Layer),是以安全为目标的HTTP 通道,简单讲是 HTTP 的安全版。即 HTTP 下加入 SSL层,HTTPS 的安全基础是 SSL。其所用的端口号是 443。 过程大致如下:
建立连接获取证书
SSL客户端通过TCP 和服务器建立连接之后(443 端口),并且在一般的 tcp 连接协商(握手)过程中请求证书。即客户端发出一个消息给服务器,这个消息里面包含了自己可实现的算法列表和其它一些需要的消息,SSL的服务器端会回应一个数据包,这里面确定了这次通信所需要的算法,然后服务器向客户端返回证书。(证书里面包含了服务器信息:域名。申请证书的公司,公共秘钥)。
证书验证
Client 在收到服务器返回的证书后,判断签发这个证书的公共签发机构,并使用这个机构的公共秘钥确认签名是否有效,客户端还会确保证书中列出的域名就是它正在连接的域名
数据加密和传输

如果确认证书有效,那么生成对称秘钥并使用服务器的公共秘钥进行加密。然后发送给服务器,服务器使用它的私钥对它进行解密,这样两台计算机可以开始进行对称加密进行通信。

preview

Socket

Socket注意

数据库

存储引擎

InnoDB

InnoDB的底层为B+树,每个叶子节点对应一个Page,Page大小一般是固定的16K,非叶子节点只有键值,叶子节点存放实际数据。

适用场景

  1. 经常更新的表,适合处理多重并发的更新请求。
  2. 支持事务。
  3. 可以从灾难中恢复(通过 bin-log 日志等)。
  4. 外键约束。只有他支持外键。
  5. 支持自动增加列属性 auto_increment。

MYISAM

MyIASM是 MySQL默认的引擎,但是它没有提供对数据库事务的支持,也不支持行级锁和外键,因此当 INSERT(插入)或 UPDATE(更新)数据时即写操作需要锁定整个表,效率便会低一些。ISAM 执行读取操作的速度很快,而且不占用大量的内存和存储资源。在设计之初就预想数据组织成有固定长度的记录,按顺序存储的。ISAM 是一种静态索引结构。缺点是它不支持事务处理

Memory

Memory(也叫 HEAP)堆内存:使用存在内存中的内容来创建表。

每个 MEMORY 表只实际对应一个磁盘文件。MEMORY 类型的表访问非常得快,因为它的数据是放在内存中的,并且默认使用HASH 索引。因此一旦服务关闭,表中的数据就会丢失掉。

Memory 同时支持散列索引和 B 树索引,B树索引可以使用部分查询和通配查询,也可以使用<,>和>=等操作符方便数据挖掘,散列索引相等的比较快但是对于范围的比较慢很多。

索引

索引原则

  1. 选择唯一性索引
  2. 为经常排序、分组、联合操作的数据简历索引
  3. 经常被查询的数据应建立索引
  4. 限制索引数目,索引过多时,更新表将会浪费时间
  5. 尽量使用数据量少的索引,索引值过长会浪费时间
  6. 若数据字段长,尽量使用前缀索引
  7. 非常重要的原则:最左前缀匹配原则
  8. 尽量选择区分度高的列作为索引
  9. 索引不能参与计算、带函数的查询不参与索引。
  10. 索引宜扩展,不宜新建

范式

  1. 原子性:如果每列都是不可再分的最小数据单元(也称为最小的原子单元),则满足第一范式(1NF)
  2. 首先满足第一范式,并且表中非主键列不存在对主键的部分依赖。 第二范式要求每个表只描述一件事情。
  3. 满足第二范式,并且表中的列不存在对非主键列的传递依赖

事务

特性

  1. 原子性:完成操作,不可分割
  2. 一致性:事务完成后数据处于一致状态
  3. 隔离性:对数据进行修改的所有并发实事务都是隔离的,互不影响互不依赖。
  4. 永久性:事务对数据的修改永久保持,事务日志保持事务的永久性。

存储过程

可以理解为SQL语言中的函数,它是一组完成了特定功能的SQL语句集合。经过一次编译后不需要再次编译,用户通过制定存储过程的名字和参数来使用。

存储过程优化思路

  1. 尽量利用一些 sql 语句来替代一些小循环,例如聚合函数,求平均函数等。
  2. 中间结果存放于临时表,加索引。
  3. 尽量少使用游标。sql 是个集合语言,对于集合运算具有较高性能。而 cursors 是过程运算。比如
    对一个 100 万行的数据进行查询。游标需要读表 100 万次,而不使用游标则只需要少量几次
    读取。
  4. 事务越短越好。sqlserver 支持并发操作。如果事务过多过长,或者隔离级别过高,都会造成
    并发操作的阻塞,死锁。导致查询极慢,cpu 占用率极地。
  5. 使用 try-catch 处理错误异常。
  6. 查找语句尽量不要放在循环内。

触发器

触发器是一段能自动执行的程序,是一种特殊的存储过程,触发器和普通的存储过程的区别是:触发器是当对某一个表进行操作时触发。诸如:update、insert、delete 这些操作的时候,系统会自动调用执行该表上对应的触发器。

数据库并发策略(锁)

并发锁

乐观锁

乐观锁假设在数据处理过程中,冲突的概率比较小。它在操作数据时不会对数据进行加锁,而是在更新数据的时候,通过检查数据是否被其他事务修改来决定是否执行更新操作。通常使用以下两种策略实现:

  1. 版本号机制,将版本号字段存入表中,下次操作时对比版本号,若版本号一致,则数据未被操作,正常执行。若版本号不同,表明数据已经被修改,,放弃操作或重试。
  2. 时间戳机制,与版本号相同,通过比较数据的实践戳判断数据是否已经修改。

悲观锁

悲观锁认为在数据处理过程中冲突的概率比较大,所以在操作数据之前就会对数据进行加锁,以防止其他事务对数据进行修改。在锁被释放之前,其他事务无法对该数据进行读写操作。

数据锁

行级锁

一种排它锁,防止其他事务修改此行。

冲突较少,但速度较慢

表级锁

表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分 MySQL 引擎支持。最常使用的 MYISAM 与 INNODB 都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。

速度快,但冲突概率大

页级锁

介于表级锁和行级锁之间的一种锁。

折中了行级锁和表级锁的特点。

Redis

对Redis的了解

  • Redis是非关系型数据库,不依赖业务逻辑存储,以简单的key—value形式存储,基于内存存储
  • Redis采用单线程+多路IO复用技术
    • Redis采用单线程模式,无需进行线程切换,减少消耗一定的CPU时间和内存资源,处理请求更加高效
    • Redis主要是基于内存,单线程可以更好的利用CPU缓存。由于数据的访问和操作具有局部性,利用CPU缓存能提高内存访问速度
    • 多路IO复用技术可以同时监听多个连接,当一个连接准备就绪时,就可以进行相应的操作
    • Redis在等待IO操作的过程中,将CPU资源用于执行其他任务,如数据处理和存储,充分利用资源
    • 实现非阻塞IO,当一个客户端的IO操作不可用时,Redis可以立即切换其他就绪的连接进行操作

Redis的数据类型

  1. 字符串(String)

    • Redis最基本的数据类型,一个Key对应一个Value
  2. 列表(List)

    • 单键多值,底层是一个双向链表
    • 对两端的操作性能高,中间查询性能低
  3. 集合(Set)

    • 与List相似,多一个自动排重的功能
    • 是一个无序集合,存储不重复元素
  4. 哈希(Hash)

    • 是一个string类型的fieldvalue的映射表
    • 适合存储对象,类似Java里的Map<String,Object>
  5. 有序集合(Zset)

    • sorted ser与set相似,是没有重复元素的字符串集合
    • 每一个成员都关联了一个评分,评分用来排序
  6. Bitmaps

    • 本身是一个字符串类型,但是可以实现位操作
    • 可以把Bitmaps当作一个数组,每个元素只能存储0和1,数组的下标称为偏移量
    • 主要存储活跃用户,如某一时间段访问网址的用户,将用户id作为偏移量
  7. HyperLogLog

    • 用来做基数统计的数据结构,统计不重复元素
    • 可以去重,添加时相同的元素,无法添加
  8. Geospatital

    • 提供经纬度设置、查询、范围查询、距离查询、经纬度Hash等

Redis的事务

  • Redis事务是一个单独的隔离操作,所有的命令被序列化,按顺序执行

  • 事务主要分为两个部分

    • 组队阶段

      输入Milit命令开始组队阶段,将命令依次放入组队队列里面

    • 执行阶段

      输入Exec命令开始执行组队队列里面的代码

    • 在组队结束后可以输入discard放弃组队

  • 在组队阶段,如果命令出现错误,整个队列里面的命令全部都不会被执行

  • 在执行阶段,命令出现错误,只有出现错误的命令不会执行,不影响其他命令执行

Redis的事务冲突

  • 由于事务的单独隔离操作,当其他客户端发送请求后,会导致对同一个键值对进行操作,导致结果不同
  • 这时采用锁机制进行预防
  1. 悲观锁

    当一个客户端拿到数据后就会进行上锁,直到锁释放才会被其他客户端操作
    效率较低

  2. 乐观锁

    采用版本号机制,每次拿到数据后会进行判断,判断别的客户端是否修改过版本(是否为数据进行过修改)

    • Redis中如果两个事务同时要对一个key进行操作,在组队阶段会先拿到键的版本号,当一个事务进入执行阶段执行后,另一个事务会在执行之前对键进行版本号校验,版本不同,事务就会被终止

Redis的持久化

  • 将内存中的数据写入到磁盘,就是持久化
  1. RDB

    在一定的时间间隔内,将内存中的数据集快照写入磁盘

    过程(写时复制技术

    1. 先复制创建一个**子线程(Fork)**来进行持久化

    2. 将数据写入到一个临时文件中

    3. 等持久化的过程结束,将临时文件替换会持久化文件

      • 由于直接创建持久化文件,当进程断开的时候,数据会丢失

      • 缺点:最后一次持久化的数据可能会丢失

        在durmp.rdb配置文件中,save 20 3意味着在20秒内当3个key发生变化时,进行持久化

  2. AOF

    AOF是以日志的形式记录写操作(增删改),将Redis执行过程中的指令记录下来,当Redis重启会重构数据

    过程

    1. 客户端的命令会被append追加到AOF的缓存区
    2. AOF缓存区会根据持久化策略(每次操作都立即写入日志、每隔一秒同步一次、不主动同步,由操作系统同步)将操作同步到磁盘中的appendonly.aof文件中
    3. AOF根据文件大小执行重写策略(文件大于64M的100%—128M)的时候,用写时复制技术将文件的指令进行压缩
    4. 当Redis重启时,会加载文件重构数据
  3. 备份

    先将.rdb和.aof配置文件复制下来,当Redis重启时会自动加载配置文件,以达到备份的功能

    当RDB和AOF同时开启,默认读取的是AOF的数据

Redis主从复制

  • Redis主从复制是将一台Redis服务器的数据复制到其他Redis服务器
  • 主服务器用来实现写的功能,从服务器实现读的功能,提高系统的并发性能
  • 当主服务器故障时,从服务器可以升级为主服务器,不会操作系统停滞

原理

  1. 当从服务器连接上主服务器后,会主动发送数据同步的消息
  2. 主服务器收到消息后,进行数据持久化,并将RDB文件发送给从服务器
  3. 从服务器收到RDB文件后进行读取,实现数据同步
  4. 当主服务器写的时候,会主动的和从服务器进行数据同步

三大招式

  1. 一主两仆
    • 当从服务器断开后,重写连接会变成单独的主服务器
    • 当主服务器断开后,从服务器不会篡位
  2. 新火相传
    • 从服务器可以组为主服务器(小组长),下面挂多个从服务器
    • 当主服务器断开后,依旧遵循一主两仆,不会篡位
    • 缺点:小组长从服务器断开后,下面的数据不会同步
  3. 反客为主
    • 当主服务器断开后,从服务器会晋升为主服务器

哨兵模式

  • 哨兵模式主要是检测Redis主从服务器的故障,当主服务器发送故障时,自动将从服务器升为主服务器,确保Redis服务的运行
  1. 配置sentinel.config文件,启动哨兵
  2. 哨兵会不断监控主从服务器的运行状态,包括时候在线、是否正常同步等
  3. 哨兵会定期的向主从服务器发送PING命令,如果主服务器在一定时间内没有响应,则认为出现故障
  4. 当检测主服务器故障时,会在从服务器中选择一个合适的服务器作为新的主服务器
  5. 哨兵会向其他服务器发送命令,让新的主服务器与其他服务器同步
  6. 最后将旧的主服务器重新配置为从服务器
  • 选举规则

    1. 优先级靠前的

      在redis.conf中默认配置:slave-priority 100 / replica-priority 100(值越小优先级越高)

    2. 偏移量大的

      与主句数据同步值最高的

    3. runid最小的从服务器

      Redis启动后自动生成的40位的runid

集群

  • Redis集群就是对Redis的水平扩容,将数据分布存储在多个节点上
  • 一个集群至少要有三个主节点
  • 无中心化集群:任何一个节点都可以作为集群的入口,各个节点之间是相互连通的
  1. 客户端可以连接到集群的任意一个节点上
  2. 当执行命令时,会根据键的哈希值对 16384 取模计算出槽位
  3. 每个节点都负责自己范围的键的存储和查询操作

缓存问题

缓存穿透

  • 主要是服务器被恶意攻击,大量的url访问服务器

解决方案

  1. 对空值缓存,如果查询数据库返回的时空值,对这个数据也存入缓存中,避免这个url一直访问服务器
  2. 设置白名单,利用Bitmaps类型作一个可访问名单,id作为偏移量,每次访问的时候与Bitmaps中的值进行比较(1表示用户访问过,无需在对服务器进行查询)
  3. 实时监控,当Redis命中率开始急速下降时,排查访问对象和访问数据,与运维人员配合处理

缓存击穿

  • 主要是某个时间段内的热点新闻
  • 在一点时间内,某一个key过期了,但是由大量返回去查询这个键,这是会去服务器中查询
  • 数据库无法将数据存入缓存中,造成击穿

解决方案

  1. 预先设置热门数据,增加key的过期时间
  2. 实时调整,及时的监控热门数据,增加过期时间
  3. 使用锁机制

缓存雪崩

  • 在某个时间内,大量的key过期,一直去数据库中查找,导致服务器奔溃

解决方案:

  1. 构建多级缓存架构

  2. 使用锁或队列,保证不会有大量的线程对数据库进行读写操作

    缺点:不适合高并发

  3. 设置过期标志更新缓存,记录缓存数据是否过期,如果快过期就会触发通知,让其他线程进行更新

  4. 将缓存失效时间分散开,在失效时间设置随机数,保证不会有大量ket同时失效

分布式锁

  • 对服务器加锁,加锁后,这个集群中的服务器都得加这个锁
  • 锁的四个条件
  1. 互斥性,在任意时间,只有一个客户端持有锁

  2. 不会发送死锁

    设置锁的过期时间,确保服务器断开后锁一直不被释放的问题

  3. 加锁和解锁必须是同一个客户端

    设置uuid,避免a服务器在执行过程中锁过期自动释放后,a服务器执行完释放锁,造成正在使用这个锁的b服务器被释放

  4. 原子性

    使用LUA脚本,确保在服务器执行操作的过程中别的客户端干扰

缓存预热

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

缓存更新

两种策略:

  1. 定时清理过期缓存
  2. 每当一个key查询时,判断该数据是否过期,过期更新

算法

排序算法

冒泡排序

插入排序

快速排序

希尔排序

归并排序

桶排序

基数排序

其他算法

二分查找

剪枝算法

回溯算法

最短路径算法

最大子数组法

最长公共子序列法

最小生成树法

数据结构

队列

链表

hash表

二叉树

红黑树

B树