1 IO流

1.1 分类

01.IO流分为3种
    第1种:按照【流的流向】划分:【输入流】、【输出流】
    第2种:按照【操作单元】划分:【字节流】、【字符流】
    第3种:按照【流的角色】划分:【节点流】、【处理流】

02.按照【操作单元】划分
    b.字节流(Byte Streams):用于处理字节数据,适用于所有类型的I/O操作
        a.输入流,基类【InputStream】
            FilelnputStream                                     【字节流】,从文件中读取字节数据
            BufferedInputStream                                 【缓冲流】,为输入流提供缓冲功能,提高读取性能
            DatalnputStream                                     【二进制流】,读取基本数据类型的数据
        b.输出流,基类【OutputStream】
            FileOutputStream                                    【字节流】,将字节数据写入文件
            BufferedOutputStream                                【缓冲流】,为输出流提供缓冲功能,提高写入性能
            DataOutputStream                                    【二进制流】,写入基本数据类型的数据
    b.字符流(Character Streams):用于处理字符数据,适用于文本文件
        a.输入流,基类【Reader】
            FileReader                                          【字符流】,从文件中读取字符数据
            BufferedReader                                      【缓冲流】,为字符输入流提供缓冲功能,提高读取性能
            InputStreamReader                                   【转换流】,将字节流转换为字符流
        b.输出流,基类【Writer】
            FileWriter                                          【字符流】,将字符数据写入文件
            BufferedWriter                                      【缓冲流】,为字符输出流提供缓冲功能,提高写入性能
            OutputStreamWriter                                  【转换流】,将字符流转换为字节流

03.字节流、字符流
    a.数据类型
        字节流:处理原始字节数据,适用于二进制文件
        字符流:处理字符数据,适用于文本文件
    b.抽象类
        字节流:基于 InputStream 和 OutputStream
        字符流:基于 Reader 和 Writer
    c.字符编码
        字节流:不考虑字符编码问题,直接处理字节
        字符流:自动处理字符编码,适合处理文本数据
    d.使用场景
        字节流:适用于处理非文本数据,如图像、音频
        字符流:适用于处理文本数据,如读取和写入文本文件

04.缓冲流、转换流
    a.功能不同
        缓冲流:用于提高 I/O 操作的效率,通过缓冲区减少对底层设备的访问
        转换流:用于在字节流和字符流之间进行转换,处理不同字符编码的文本数据
    b.使用场景
        缓冲流:适用于需要频繁读写操作的场景,如文件复制、网络传输等
        转换流:适用于需要处理不同字符编码的文本数据,如读取或写入非 UTF-8 编码的文件
    c.API不同
        缓冲流:提供了按行读取、写入等高级功能
        转换流:提供了字符编码转换的功能

1.2 文件流:File

00.以文件操作为例,主要的操作流程如下
    1.使用File类打开一个文件
    2.通过字节流或字符流的子类,指定输出的位置
    3.进行读/写操作
    4.关闭输入/输出

01.常用方法
    a.文件/目录信息
        isFile()判断是否是一个文件,返回是一个布尔值,true表示文件。false不是文件
        isDirectory()判断是否是一个目录(文件夹),true表示文件夹。false不是文件夹
        exists()判断文件或目录是否存在,true表示存在,false表示不存在
        getName()获取文件或目录的名称(不包括路径)。
        getPath()获取文件或目录的相对路径
        getAbsolutePath()获取文件或目录的绝对路径
        length()获取文件的大小(字节数)
        lastModified()获取文件或目录的最后修改时间(返回毫秒数)
    b.文件/目录操作
        createNewFile() 创建一个新的空文件(如果文件已存在则返回 false),目录必须要存在,否则会出现异常
        mkdir() 创建单级目录(如果父目录不存在则失败)
        mkdirs()创建多级目录(包括所有必要的父目录),如果没有父级目录,也会给你创建
        delete()删除文件或空目录(如果删除成功返回 true)
        renameTo(File dest) 将文件或目录重命名或移动到目标路径
    c.目录内容
        list()返回目录中的文件和子目录的名称数组(String[])
        listFiles()返回目录中的文件和子目录的 File 对象数组(File[])
        list(FilenameFilter filter)返回符合过滤条件的文件和子目录的名称数组
        listFiles(FileFilter filter)返回符合过滤条件的文件和子目录的 File 对象数组
    d.文件读取的值是-1表示的是什么意思
        如果文件数据已经读取完了,再去读取数据,读取的数据值是: -1,表示已经读取完了

02.常见场景
    a.new File(url)的路径问题
        new File(url)中url
        可以是一个文件路径,也可以是目录路径,也可以是一个不存在的路径
        因此我们的 File 可能是一个文件或者文件夹或者不存在的对象
    b.是否是一个文件夹:File.isDirectory
        package part;

        import java.io.*;

        public class Java01 {
            public static void main(String[] args) {
                // 数据源文件对象
                File srcFile =new File("./test");
                // 是否是一个文件夹
                if(srcFile.isDirectory()){
                    System.out.println("Directory");
                }
            }
        }
    c.判断这个文件对象是否存在:srcFile.exists()
        package part;

        import java.io.*;

        public class Java01 {
            public static void main(String[] args) {
                // 数据源文件对象
                File srcFile =new File("./test");
                // 判断文件或目录是否存在,返回来的是一个布尔值
                if(srcFile.exists()){
                    System.out.println("存在这个对象");
                }else{
                    System.out.println("不存在这个对象");
                }
            }
        }
    d.创建目录:File.mkdirs(),如果没有父级目录,也会给你创建
        package part;

        import java.io.*;

        public class Java01 {
            public static void main(String[] args) {
                // 数据源文件对象
                File srcFile =new File("./testqq/213123");
                // 文件对象是否存在
                if(!srcFile.exists()){
                    //如果文件对象不存在的话,我会创建一个目录
                    // 如果我的项目中没有这个目录,他会创建一个这样的多级目录的哈
                    srcFile.mkdirs();
                }
            }
        }
    e.创建文件:File.createNewFile
        package part;

        import java.io.*;

        public class Java01 {
            public static void main(String[] args) {
                // 数据源文件对象,如果/testqq/213123不存在的话,就会出现异常的哈
                File srcFile =new File("./testqq/213123/xuexie.txt");
                try {
                    // 创建一个新文件,可能会有异常的哈,因此使用try-catch进行捕获
                    // 创建的目录必须要存在,否则会出现异常。
                    srcFile.createNewFile();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    f.获取文件的基本信息:名称-对路径-最后修改的时间-大小(字节)
        package part;

        import java.io.*;

        public class Java01 {
            public static void main(String[] args) {
                // 数据源文件对象
                File srcFile =new File("./test/hello.txt");
                // 文件的名称 hello.txt
                System.out.println(srcFile.getName());
                // 文件的绝对路径 D:\javaprojiect\goodStudy\javaStudy\.\test\hello.txt
                System.out.println(srcFile.getAbsolutePath());
                // 文件最后修改的时间,时间戳 1738681949390
                System.out.println(srcFile.lastModified());
                // 文件的大小 22
                System.out.println(srcFile.length());
            }
        }
    g.获取文件夹的相关信息:名称-绝对路径-最后修改时间
        package part;

        import java.io.*;

        public class Java01 {
            public static void main(String[] args) {
                // 数据源文件对象
                File srcFile =new File("./test");
                // 文件夹的名称 test
                System.out.println(srcFile.getName());
                // 文件夹的绝对路径 D:\javaprojiect\goodStudy\javaStudy\.\test
                System.out.println(srcFile.getAbsolutePath());
                // 文件夹最后修改的时间,时间戳 1738681949390
                System.out.println(srcFile.lastModified());
            }
        }
    h.复制文件
        package part;

        import java.io.*;

        public class Java01 {
            public static void main(String[] args) {
                // 数据源文件对象
                File srcFile =new File("./test/hello.txt");
                // 数据目的的文件对象(自动生成)
                File srcCopy =new File("./test/helloCpy.txt");
                if(srcFile.exists()){
                    System.out.println("File exists");
                }
                // 文件输入流(管道对象)
                FileInputStream fis = null;
                // 文件输出流(管道对象)
                FileOutputStream fos = null;
                try {
                    // 可能会有异常,需要使用try-catch
                   fis = new FileInputStream(srcFile);
                   fos = new FileOutputStream(srcCopy);
                   // 打开阀门,开始读取数据了哈(输出),这里也会产生异常的,使用 IOException
                   int srcData = fis.read();
                   // 打开阀门,开始写入数据了哈(写入)
                    fos.write(srcData);
                } catch (IOException e) {
                    // 这里使用
                    throw new RuntimeException(e);
                } finally {
                    if(fis != null){
                        // fis.close 可能也会存在异常,因此也需要给新增上try-catch
                        try {
                            fis.close();
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    if(fos != null){
                        // fos.close 可能也会存在异常,因此也需要给新增上try-catch
                        try {
                            fos.close();
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
            }
        }

1.3 字节流:InputStream、OutputStream

01.字节流
    a.介绍
        字节流是 Java I/O 的一种基础流,用于处理原始的字节数据。它们主要用于处理二进制数据,如图像、音频、视频文件等
    b.API
        两个主要抽象类是InputStream、OutputStream

02.常用API
    a.InputStream: 字节输入流的抽象类
        常用子类:FileInputStream、BufferedInputStream
        方法:int read()、int read(byte[] b)、void close()
    b.OutputStream: 字节输出流的抽象类
        常用子类:FileOutputStream、BufferedOutputStream
        方法:void write(int b)、void write(byte[] b)、void close()

03.用法
    try (FileInputStream fis = new FileInputStream("input.dat");
         FileOutputStream fos = new FileOutputStream("output.dat")) {
        byte[] buffer = new byte[1024];
        int bytesRead;
        while ((bytesRead = fis.read(buffer)) != -1) {
            fos.write(buffer, 0, bytesRead);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }

1.4 字符流:Reader、Writer

01.字符流
    a.介绍
        字符流是专门用于处理字符数据的流,适用于文本文件的读写,字符流能够自动处理字符编码问题
    b.API
        两个主要抽象类是Reader、Writer

02.常用 API
    a.Reader: 字符输入流的抽象类
        常用子类:FileReader、BufferedReader
        方法:int read()、int read(char[] cbuf)、void close()
    b.Writer: 字符输出流的抽象类
        常用子类:FileWriter、BufferedWriter
        方法:void write(int c)、void write(char[] cbuf)、void close()

03.用法
    try (FileReader fr = new FileReader("input.txt");
         FileWriter fw = new FileWriter("output.txt")) {
        char[] buffer = new char[1024];
        int charsRead;
        while ((charsRead = fr.read(buffer)) != -1) {
            fw.write(buffer, 0, charsRead);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }

1.5 缓冲流:BufferedReader、BufferedWriter

01.缓冲流
    a.介绍
        它通过在内存中维护一个缓冲区来减少对底层设备的访问次数,从而提高性能
    b.API
        BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter

02.常用API
    a.BufferedInputStream: 用于为 InputStream 提供缓冲功能
        构造方法:BufferedInputStream(InputStream in)、BufferedInputStream(InputStream in, int size)
        方法:int read()、int read(byte[] b, int off, int len)
    b.BufferedOutputStream: 用于为 OutputStream 提供缓冲功能
        构造方法:BufferedOutputStream(OutputStream out)、BufferedOutputStream(OutputStream out, int size)
        方法:void write(int b)、void write(byte[] b, int off, int len)
    c.BufferedReader: 用于为 Reader 提供缓冲功能,支持按行读取
        构造方法:BufferedReader(Reader in)、BufferedReader(Reader in, int size)
        方法:String readLine()
    d.BufferedWriter: 用于为 Writer 提供缓冲功能
        构造方法:BufferedWriter(Writer out)、BufferedWriter(Writer out, int size)
        方法:void write(String s, int off, int len)、void newLine()

03.用法
    try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
         BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
        String line;
        while ((line = reader.readLine()) != null) {
            writer.write(line);
            writer.newLine();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }

1.6 转换流:InputStreamReader、OutputStreamWriter

01.转换流
    a.介绍
        转换流用于在字节流和字符流之间进行转换
        它们可以将字节流转换为字符流,或将字符流转换为字节流,通常用于处理不同字符编码的文本数据
    b.API
        InputStreamReader、OutputStreamWriter

02.常用API
    a.InputStreamReader: 将 InputStream 转换为 Reader
        构造方法:InputStreamReader(InputStream in)、InputStreamReader(InputStream in, String charsetName)
        方法:int read()、int read(char[] cbuf, int offset, int length)
    b.OutputStreamWriter: 将 Writer 转换为 OutputStream
        构造方法:OutputStreamWriter(OutputStream out)、OutputStreamWriter(OutputStream out, String charsetName)
        方法:void write(int c)、void write(char[] cbuf, int off, int len)

03.用法
    try (InputStreamReader reader = new InputStreamReader(new FileInputStream("input.txt"), "UTF-8");
         OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream("output.txt"), "UTF-8")) {
        char[] buffer = new char[1024];
        int length;
        while ((length = reader.read(buffer)) != -1) {
            writer.write(buffer, 0, length);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }

1.7 同步、异步、阻塞、非阻塞

00.汇总
    同步IO、异步IO:程序等待IO操作完成的方式
    阻塞IO、非阻塞IO:程序在等待IO操作完成时是否能继续执行其他操作

01.程序等待IO操作完成的方式
    a.同步I/O
        a.定义
            同步I/O是指程序发起一个I/O操作后,必须等待该操作完成并获取到结果后,才能继续执行后续代码
        b.特点
            同步I/O操作会阻塞程序的执行,直到I/O操作完成。这意味着程序在等待I/O操作期间不能执行其他任务
        c.示例
            在Java中,使用java.io包中的FileInputStream或FileOutputStream进行文件读写就是同步I/O的例子
    b.异步I/O
        a.定义
            异步I/O是指程序发起一个I/O操作后,不需要等待该操作完成,而是可以继续执行后续代码
            I/O操作完成后,程序会通过回调、事件或轮询等方式得到通知
        b.特点
            异步I/O不会阻塞程序的执行,允许程序在I/O操作进行的同时执行其他任务,提高了程序的并发性和效率
        c.示例
            在Java中,使用java.nio.channels.AsynchronousFileChannel进行文件读写就是异步I/O的例子

02.程序在等待IO操作完成时是否能继续执行其他操作
    a.阻塞I/O
        a.定义
            阻塞I/O是指程序发起一个I/O操作后,如果数据尚未准备好(如数据未到达、文件未找到等)
            程序会一直等待,直到数据准备好并完成I/O操作
        b.特点
            阻塞I/O会导致程序的执行线程被挂起,直到I/O操作完成。在这期间,线程不能执行其他任务
        c.示例
            在Java中,使用java.io包中的Socket进行网络通信时,InputStream.read()方法就是阻塞I/O的例子
    b.非阻塞I/O
        a.定义
            非阻塞I/O是指程序发起一个I/O操作后,如果数据尚未准备好
            I/O操作会立即返回一个错误或特定值,而不是等待数据准备好
        b.特点
            非阻塞I/O允许程序在数据未准备好时继续执行其他任务,而不是等待
            程序通常需要轮询或检查I/O操作的状态,以确定何时数据准备好
        c.示例
            在Java中,使用java.nio包中的SocketChannel设置为非阻塞模式
            并使用read()方法进行网络通信,就是非阻塞I/O的例子

2 序列化

2.1 定义

01.概念
    序列化:就是将【对象】转化成【字节序列】的过程
    反序列化:就是将【字节序列】转化成【对象】的过程

02.什么情况下需要序列化
    1.网络上传输
    2.持久化存储到文件中

03.原因
    【对象的寿命,随着JVM的停止允许,而丢失状态】
    【有时候需要把在内存中的各种对象的状态(也就是实例变量,不是方法)保存下来,并在需要的时候,再将对象恢复】
    【虽然,我们可以通过各种各样的方法来保存“对象的状态”,但JAVA提供了一种保存对象状态的机制,那就是“序列化”】
    ---------------------------------------------------------------------------------------------------------
    【有些对象可能很重要且占用不少内存,但“可能暂时不使用该对象”,若直接放入内存显然浪费、若丢弃又需要额外创建对象】
    【“一种折中的方法”,将“对象的状态”暂时放入“文件”、“数据库”,然后根据需要进行“磁盘读取”,这就是“序列化”】

04.序列化的优点
    【保存到“文件”“数据库”】:将一个已经实例化的类转成文件存储,【隔一段时间后】,可以恢复【类的所有变量和状态】
    【方便在网络中进行传送】:对象、文件、数据等格式,【序列化后,无论原来是什么,都可以变为byte[]字节流】
    【RMI(远程方法的调用)】:分布式对象,利用【对象序列化】运行远程主机上的服务,就像【在本地机上运行对象】一样

05.Java序列化算法
    所有保存到磁盘的对象都有一个序列化编码号。
    当程序试图序列化一个对象时,会先检查此对象是否已经序列化过。
    若对象从未被序列化过,才会将此对象序列化为字节序列输出;如果此对象已经序列化过,则直接输出编号即可。

2.2 实现:4种

00.汇总
    1.Serializable接口
    2.Externalizable接口
    3.替代方案:Serializable接口,添加writeObject(ObjectOutputStream out)、readObject(ObjectInputStream in)
    4.单例模式:需要重写readResolve()方法保证“单例模式创建的对象,与序列化后的对象”是同一个,否则会破坏单例原则

01.Serializable接口
    public class SerializeDemo01 {
        enum Sex {
            MALE,
            FEMALE
        }

        static class Person implements Serializable {
            private static final long serialVersionUID = 1L;
            private String name = null;
            private Integer age = null;
            private Sex sex;

            public Person() { }

            public Person(String name, Integer age, Sex sex) {
                this.name = name;
                this.age = age;
                this.sex = sex;
            }

            @Override
            public String toString() {
                return "Person{" + "name='" + name + '\'' + ", age=" + age + ", sex=" + sex + '}';
            }
        }

        /**
         * 序列化
         */
        private static void serialize(String filename) throws IOException {
            File f = new File(filename); // 定义保存路径
            OutputStream out = new FileOutputStream(f); // 文件输出流
            ObjectOutputStream oos = new ObjectOutputStream(out); // 对象输出流
            oos.writeObject(new Person("Jack", 30, Sex.MALE)); // 保存对象
            oos.close();
            out.close();
        }

        /**
         * 反序列化
         */
        private static void deserialize(String filename) throws IOException, ClassNotFoundException {
            File f = new File(filename); // 定义保存路径
            InputStream in = new FileInputStream(f); // 文件输入流
            ObjectInputStream ois = new ObjectInputStream(in); // 对象输入流
            Object obj = ois.readObject(); // 读取对象
            ois.close();
            in.close();
            System.out.println(obj);
        }

        public static void main(String[] args) throws IOException, ClassNotFoundException {
            final String filename = "d:/text.dat";
            serialize(filename);
            deserialize(filename);
        }
    }
    // Output:
    // Person{name='Jack', age=30, sex=MALE}

02.Externalizable接口
    public class ExternalizeDemo02 {
        enum Sex {
            MALE,
            FEMALE
        }

        static class Person implements Externalizable {
            transient private Integer age = null;
            // 其他内容略

            private void writeObject(ObjectOutputStream out) throws IOException {
                out.defaultWriteObject();
                out.writeInt(age);
            }

            private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
                in.defaultReadObject();
                age = in.readInt();
            }

            @Override
            public void writeExternal(ObjectOutput out) throws IOException {
                out.writeObject(name);
                out.writeInt(age);
            }

            @Override
            public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
                name = (String) in.readObject();
                age = in.readInt();
            }
        }
         // 其他内容略
    }
    // Output:
    // call Person()
    // name: Jack, age: 30, sex: null

03.替代方案:Serializable接口,添加writeObject(ObjectOutputStream out)、readObject(ObjectInputStream in)
    public class SerializeDemo03 {
        static class Person implements Serializable {
            transient private Integer age = null;
            // 其他内容略

            private void writeObject(ObjectOutputStream out) throws IOException {
                out.defaultWriteObject();
                out.writeInt(age);
            }

            private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
                in.defaultReadObject();
                age = in.readInt();
            }
            // 其他内容略
        }
        // 其他内容略
    }

04.单例模式:需要重写readResolve()方法保证“单例模式创建的对象,与序列化后的对象”是同一个,否则会破坏单例原则
    public class SerializeDemo04 {
        enum Sex {
            MALE, FEMALE
        }

        static class Person implements Serializable {
            static final Person instatnce = new Person("Tom", 31, Sex.MALE);

            public static Person getInstance() {
                return instatnce;
            }

            private void writeObject(ObjectOutputStream out) throws IOException {
                out.defaultWriteObject();
                out.writeInt(age);
            }

            // private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            //     in.defaultReadObject();
            //     age = in.readInt();
            // }

            // 将 readObject 替换为 readResolve 方法,直接返回 Person 的单例对象
            private Object readResolve() {
                return instatnce;
            }
        }
    }

2.3 问题:15个

01.反序列化的顺序与序列化时的顺序是否一致?
    一致

02.为什么Java序列化算法不会重复序列化同一个对象?
    Java序列化算法【不会重复序列化同一个对象,只会记录已序列化对象的编号】
    如果序列化一个可变对象后,更改了对象内容,再次序列化,并不会再次将此对象转换为字节序列,而只是保存序列化编号

03.序列化时,Java对象的哪些部分会被保存?
    序列化时,只对对象的成员变量进行保存,而不管对象的方法
    当一个对象的实例变量引用其他对象,序列化该对象时也会把引用对象进行序列化

04.为什么声明为static和transient类型的成员数据不能被序列化?
    声明为static和transient类型的成员数据不能被序列化,因为static代表类的状态,transient代表对象的临时数据

05.序列化时,如果一个对象的成员变量是另一个对象,会发生什么?
    如果一个对象的成员变量是另一个对象,那么这个对象的数据成员也会被保存。这是能用“序列化解决深拷贝的重要原因”

06.被序列化的类的成员变量如果不是基本类型或String,需要满足什么条件?
    如果一个可序列化的类的成员不是基本类型,也不是String,那这个引用类型也必须是可序列化的;否则,此类无法序列化
    也就是说,被序列化的类必须属于【Enum、Array和实现Serializable接口】中的一种,否则会抛出异常

07.哪些基础类已经实现了Serializable接口?
    很多基础类已经实现了Serializable接口,比如String、ArrayList、HashMap、LinkedList、TreeSet等。

08.为什么并非所有的对象都可以序列化?
    并非所有的对象都可以序列化,原因有两个:
    一是安全性(private在序列化过程中不受保护)
    二是资源分配(如socket,thread类)

09.当一个父类实现序列化,子类是否需要显式实现Serializable接口?
    当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口

10.当子类实现序列化,而父类没有实现序列化,会发生什么?
    当子类实现序列化,而父类没有实现序列化,子类序列化时会调用父类的无参构造函数,可能会出现默认值(如int为0)

11.父类是Serializable,子类是否可以被序列化?
    父类是Serializable,所有子类都可以被序列化

12.子类是Serializable,父类不是,会发生什么?
    子类是Serializable,父类不是,则子类可以正确序列化,但父类的属性不会被序列化(不报错,但数据丢失)

13.如果序列化的属性是对象,这个对象需要满足什么条件?
    如果序列化的属性是对象,则这个对象也必须是Serializable,否则会报错

14.反序列化时,如果对象的属性有修改或删减,会发生什么?
    反序列化时,如果对象的属性有修改或删减,则修改的部分属性会丢失,但不会报错

15.反序列化时,如果serialVersionUID被修改,会发生什么?
    反序列化时,如果serialVersionUID被修改,则反序列化会失败

2.4 注意:7个

00.汇总
    01.序列化的类
    02.子类序列化,父类未序列化:父类属性丢失,注意子类序列化时会调用“父类的无参构造函数”
    03.成员是引用的序列化:被序列化的类必须属于【Enum、Array和实现Serializable接口】中的一种,否则抛出异常
    04.同一对象不会序列化多次:依次将4个对象写入输入流,同一个“流”中,反序列化的顺序与序列化时的顺序一致
    05.序列化算法潜在的问题:同一对象不会序列化多次,更改了对象内容,再次序列化,并不会再次将此对象变为字节序列,而只保存编号
    06.可选的自定义序列化:writeObject()、readObject()、readObjectNoData()
    07.更彻底的自定义序列化:writeReplace()、readResolve()

01.序列化的类
    a.String
        public final class String implements java.io.Serializable,Comparable<String>,CharSequence {
           private static final long serialVersionUID = -6849794470754667710L;
        }
    b.ArrayList、HashMap、LinkedList、TreeSet
        ArrayList   transient Object[] elementData;
        HashMap     transient Node<K,V>[] table;
        LinkedList  transient Node<E> first;
        TreeSet     private transient NavigableMap<E,Object> m;
        但是,ArrayList、HashMap、LinkedList等数据存储字段,修饰符都是transient,但可以正常序列化、反序列化;
        实际上,各个集合类型,对于序列化和反序列化是有单独的实现的,并没有采用虚拟机默认的方式;
    c.ArrayList序列化
        以ArrayList为例,都采用“Serializable + writeObject、readObject”方法,在transient情况下,实现序列化;
        ArrayList序列化和反序列化主要思路:“根据集合中实际存储的元素个数”来进行操作,避免不必要的空间浪费
        (ArrayList的扩容机制,决定了集合中实际存储的元素个数肯定比集合的可容量要小)
        ----------------------------------------------------
        实现 Serializable 接口,
        并添加 writeObject(ObjectOutputStream out) 与 readObject(ObjectInputStream in) 方法。
        序列化和反序列化过程中会自动回调这两个方法。
        ----------------------------------------------------
        public class ArrayList<E> extends AbstractList<E>
                implements List<E>, RandomAccess, Cloneable, java.io.Serializable
        {
            private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
                int expectedModCount = modCount;
                //序列化当前ArrayList中非transient以及非静态字段
                s.defaultWriteObject();
                //序列化数组实际个数
                s.writeInt(size);
                // 逐个取出数组中的值进行序列化
                for (int i=0; i<size; i++) {
                    s.writeObject(elementData[i]);
                }
                //防止在并发的情况下对元素的修改
                if (modCount != expectedModCount) {
                    throw new ConcurrentModificationException();
                }
            }
            private void readObject(java.io.ObjectInputStream s) throws java.io.IOException{
                elementData = EMPTY_ELEMENTDATA;
                // 反序列化非transient以及非静态修饰的字段,其中包含序列化时的数组大小 size
                s.defaultReadObject();
                // 忽略的操作
                s.readInt(); // ignored
                if (size > 0) {
                    // 容量计算
                    int capacity = calculateCapacity(elementData, size);
                    SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
                    //检测是否需要对数组扩容操作
                    ensureCapacityInternal(size);
                    Object[] a = elementData;
                    // 按顺序反序列化数组中的值
                    for (int i=0; i<size; i++) {
                        a[i] = s.readObject();
                    }
                }
            }
        }

02.子类序列化,父类未序列化:父类属性丢失,注意子类序列化时会调用“父类的无参构造函数”
    a.描述
        Java约定:【构建一个对象,必须先有父对象,才有子对象,因此每次创建会调用“父类的无参构造函数”来符合这一设定】
        因此,【序列化时,需要注意“父类的无参构造函数,对变量进行初始化问题;可能会出现默认值int为0,出现null值”】
    b.代码
        a.子类序列化,父类未序列化
            class People{
                int num;
                public People(){}           // 默认的无参构造函数,没有进行初始化
                public People(int num){     // 有参构造函数
                    this.num = num;
                }
                public String toString(){
                    return "num:"+num;
                }
            }
            class Person extends People implements Serializable{
                private static final long serialVersionUID = 1L;
                String name;
                int age;
                public Person(int num,String name,int age){
                    super(num);             // 调用父类中的构造函数
                    this.name = name;
                    this.age = age;
                }
                public String toString(){
                    return super.toString()+"\tname:"+name+"\tage:"+age;
                }
            }
        b.序列化
            Person person = new Person(10,"tom", 22); // 调用带参数的构造函数num=10,name = "tim",age =22
            oos.writeObject(person);                  // 写出对象
        c.反序列化
            Person person = (Person)ois.readObject(); // 反序列化,调用父类中的无参构函数。
            System.out.println(person);               // 结果:num:0   name:tom    age:22
        d.分析结果
            发现由于父类中无参构造函数并没有对num初始化,所以num使用默认值为0

03.成员是引用的序列化:被序列化的类必须属于【Enum、Array和实现Serializable接口】中的一种,否则抛出异常
    a.代码
        public class Teacher {          // 实现Serializable接口

        }
        public class Person implements Serializable {
            public Person(String name, Teacher teacher) {
                this.name = name;
                this.teacher = teacher;
            }

            private Teacher teacher;
            private int age;
            private String name;
            private String sex;
        }
        public class SerializeAndDeserializeTest {
            public static void main(String[] args) throws Exception {
                Teacher teacher = new Teacher();
                Person person = new Person("Lh", teacher);
                person.setName("gacl");
                person.setAge(25);
                person.setSex("男");

                ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("d:/text.dat")));
                oo.writeObject(person);
                oo.close();
            }
        }
    b.分析
        Exception in thread "main" java.io.NotSerializableException: com.java.master.serializable.Teacher
        因为Teacher类的对象是不可序列化的,这导致了Person对象不可序列化。

04.同一对象不会序列化多次:依次将4个对象写入输入流,同一个“流”中,反序列化的顺序与序列化时的顺序一致
    a.代码
        public class WriteTeacher {
            public static void main(String[] args) throws Exception {
                Person person = new Person("路飞", 20);
                Teacher t1 = new Teacher("雷利", person);
                Teacher t2 = new Teacher("红发香克斯", person);

                // 序列化:依次将4个对象写入输入流
                ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:/text.dat");
                oos.writeObject(t1);
                oos.writeObject(t2);
                oos.writeObject(person);
                oos.writeObject(t2);
                }
            }
        }
        public class ReadTeacher {
            public static void main(String[] args) {
                // 反序列化
                ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/text.dat"));
                Teacher t1 = (Teacher) ois.readObject();
                Teacher t2 = (Teacher) ois.readObject();
                Person p = (Person) ois.readObject();
                Teacher t3 = (Teacher) ois.readObject();

                System.out.println(t1 == t2);                           // false
                System.out.println(t1.getPerson() == p);                // true
                System.out.println(t2.getPerson() == p);                // true

                System.out.println(t2 == t3);                           // true
                System.out.println(t1.getPerson() == t2.getPerson());   // true
            }
        }
    b.分析
        存放顺序如下:
            Person person = new Person("路飞", 20);
            Teacher t1 = new Teacher("雷利", person);
            Teacher t2 = new Teacher("红发香克斯", person);
            oos.writeObject(t1);
            oos.writeObject(t2);
            oos.writeObject(person);
            oos.writeObject(t2);
        取出顺序如下:
            Teacher t1 = (Teacher) ois.readObject();
            Teacher t2 = (Teacher) ois.readObject();
            Person p = (Person) ois.readObject();
            Teacher t3 = (Teacher) ois.readObject();
        同一个“流”中,反序列化的顺序与序列化时的顺序一致:
            t1 == t2                   // false t1、t2分别取出流中的“第一次Teacher、第二次Teacher”,两次对象不同
            t1.getPerson() == p        // true  序列化Teacher时,同时序列化成员变量Person,而同一对象不会序列化多次
            t2.getPerson() == p        // true  序列化Teacher时,同时序列化成员变量Person,而同一对象不会序列化多次
            t2 == t3                   // true  t2、t3分别取出流中的“第二次Teacher、第三次Teacher”,两次对象不同
            t1.getPerson() == t2.getPerson()  // true 序列化Teacher时,同时序列化Person,而同一对象不会序列化多次

05.序列化算法潜在的问题:同一对象不会序列化多次,更改了对象内容,再次序列化,并不会再次将此对象变为字节序列,而只保存编号
    a.代码
        public static void main(String[] args) {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:/text.dat"));
            ObjectInputStream ios = new ObjectInputStream(new FileInputStream("d:/text.dat"));

            // 第一次序列化person
            Person person = new Person("猪八戒", 23);
            oos.writeObject(person);
            System.out.println(person);

            // 修改name
            person.setName("孙悟空");
            System.out.println(person);                 // com.java.master.serializable.Person@7a07c5b4

            // 第二次序列化person
            oos.writeObject(person);                    // com.java.master.serializable.Person@7a07c5b4

            // 依次反序列化出p1、p2
            Person p1 = (Person) ios.readObject();
            Person p2 = (Person) ios.readObject();

            System.out.println(p1 == p2);                           // true
            System.out.println(p1.getName().equals(p2.getName()));  // true
        }
    b.分析
        修改内容后,并不会再次将此对象变为字节序列,而只保存编号

06.可选的自定义序列化:writeObject()、readObject()、readObjectNoData()
    a.代码
        public class Person implements Serializable {
           private String name;
           private int age;
           //省略构造方法,get及set方法

           private void writeObject(ObjectOutputStream out) throws IOException {
               //将名字反转写入二进制流
               out.writeObject(new StringBuffer(this.name).reverse());
               out.writeInt(age);
           }

           private void readObject(ObjectInputStream ins) throws IOException,ClassNotFoundException{
               //将读出的字符串反转恢复回来
               this.name = ((StringBuffer)ins.readObject()).reverse().toString();
               this.age = ins.readInt();
           }
        }
    b.分析
        transient修饰,默认【整数:0,小数:0.0,字符:'\u0000',布尔:flase】【引用:null/对象内部默认属性值/""】
        ---------------------------------------------------------------------------------------------------
        private void writeObject(java.io.ObjectOutputStream out) throws IOException;
        private void readObject(java.io.ObjectIutputStream in) throws IOException,ClassNotFoundException;
        private void readObjectNoData() throws ObjectStreamException;
        writeObject:自定义序列化规则,比如哪些属性、依据的规则
        readObject:自定义反序列化规则,比如哪些属性、依据的规则
        readObjectNoData:使用不同类接收反序列化对象,或序列化流被篡改时,调用readObjectNoData()初始化反序列化对象

07.更彻底的自定义序列化:writeReplace()、readResolve()
    a.代码
        public class Person implements Serializable {
          private String name;
          private int age;
          // 省略构造方法,get及set方法

          private Object writeReplace() throws ObjectStreamException {
              ArrayList<Object> list = new ArrayList<>(2);
              list.add(this.name);
              list.add(this.age);
              return list;
          }

           public static void main(String[] args) throws Exception {
              try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
                   ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"))) {
                  Person person = new Person("zs", 23);
                  oos.writeObject(person);

                  ArrayList list = (ArrayList)ios.readObject();
                  System.out.println(list);                                     // [zs, 23]
              }
          }
        }
        public class Person implements Serializable {
            private String name;
            private int age;
            //省略构造方法,get及set方法
             private Object readResolve() throws ObjectStreamException{
                return new ("brady", 23);
            }
            public static void main(String[] args) throws Exception {
                try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
                     ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"))) {
                    Person person = new Person("zs", 23);
                    oos.writeObject(person);

                    HashMap map = (HashMap)ios.readObject();                    // {brady=23}
                    System.out.println(map);
                }
            }
        }
    b.分析
        writeReplace():此方法可将任意对象代替目标序列化对象(在序列化时,先writeReplace,后writeObject)
        readResolve():此方法可将任意对象代替目标反序列化对象(在反序列化时,先readeObject,后readResolve)
        ---------------------------------------------------------------------------------------------------
        readResolve、writeReplace访问修饰符,可以是private、protected、public
        如果父类重写了这两个方法,子类都需要根据自身需求重写,这显然不是一个好的设计。
        对于final修饰的类重写readResolve,可以是public;非final修饰的类重写readResolve,不建议public,推荐private

2.5 @Serial:标识

01.@Serial 注解概述
    a.定义
        @Serial 注解是 JDK 14 引入的,它的主要作用是标识与序列化相关的字段或方法
        特别是用于标识 serialVersionUID、readObject、writeObject、readResolve 和 writeReplace 等序列化特殊方法
    b.作用
        它并不是强制性的,但它提供了一种更具可读性和语义化的方式来标注序列化代码。
    c.示例
        @Serial
        private static final long serialVersionUID = 5652464866930818765L;
        -----------------------------------------------------------------------------------------------------
        使用 @Serial 来明确表示 serialVersionUID 是与 Java 序列化机制相关的字段。

02.serialVersionUID 的作用
    a.版本控制
        serialVersionUID 是 Java 序列化机制中用于版本控制的一个字段
        它确保同一个类在不同版本间可以安全地进行序列化和反序列化
    b.序列化与反序列化
        a.序列化
            在序列化时,Java 会将对象的 serialVersionUID 与数据一起保存到文件中
        b.反序列化
            在反序列化时,Java 会检查当前类的 serialVersionUID 是否与序列化时的 serialVersionUID 相匹配
            如果不匹配,Java 会抛出 InvalidClassException,提示类不兼容

03.配置 serialVersionUID 是否必要?
    a.自动生成的风险
        如果没有手动定义 serialVersionUID,Java 编译器会根据类的结构(字段、方法等)自动生成一个版本号
        然而,自动生成的 serialVersionUID 可能在类的某些小变动(如增加字段)后发生改变
        从而导致序列化和反序列化不兼容
    b.显式定义的好处
        建议在需要序列化的类中显式定义 serialVersionUID,以确保类的版本控制可预期。
        -----------------------------------------------------------------------------------------------------
        @Serial
        private static final long serialVersionUID = 1L;
        -----------------------------------------------------------------------------------------------------
        定义 serialVersionUID 的好处在于:即使对类进行了一些轻微的修改
        只要这些修改不影响序列化机制,仍然可以确保兼容性

04.Lombok 能简化 serialVersionUID 的配置吗?
    a.现状
        Lombok 是一个非常流行的 Java 库,它可以通过注解来简化代码,比如 @Getter、@Setter、@Data 等
        不过,Lombok 并没有提供与 serialVersionUID 相关的注解
    b.手动配置
        如果希望在类中定义 serialVersionUID,仍然需要手动配置。
        -----------------------------------------------------------------------------------------------------
        import lombok.Data;
        import java.io.Serializable;

        @Data
        public class User implements Serializable {
            @Serial
            private static final long serialVersionUID = 123456789L;

            private String name;
            private String email;
        }
        -----------------------------------------------------------------------------------------------------
        尽管 Lombok 在很多方面为我们提供了便捷,但在 serialVersionUID 的配置上并没有自动化支持

2.6 transient:忽略

01.定义
    a.说明
        transient关键字用于声明【一个字段不应被序列化】
    b.场景
        用于防止某些字段在序列化过程中被保存,可以用于保护敏感数据、减少序列化开销或避免序列化不需要的字
    c.注意事项
        默认值:被声明为transient的字段在反序列化后将被设置为其默认值。例如,引用类型字段将被设置为null,整数类型字段将被设置为0,布尔类型字段将被设置为false。
        自定义序列化:如果需要在序列化和反序列化过程中执行自定义逻辑,可以实现writeObject和readObject方法。
        兼容性:在类的定义中添加或删除transient关键字可能会影响序列化的兼容性,因此在修改类定义时需要谨慎。

02.代码示例
    a.代码
        import java.io.*;

        class User implements Serializable {
            private static final long serialVersionUID = 1L;

            private String username;
            private transient String password; // 使用transient关键字

            public User(String username, String password) {
                this.username = username;
                this.password = password;
            }

            @Override
            public String toString() {
                return "User{" +
                        "username='" + username + '\'' +
                        ", password='" + password + '\'' +
                        '}';
            }
        }

        public class TransientExample {
            public static void main(String[] args) {
                User user = new User("john_doe", "password123");

                // 序列化对象
                try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
                    oos.writeObject(user);
                } catch (IOException e) {
                    e.printStackTrace();
                }

                // 反序列化对象
                try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
                    User deserializedUser = (User) ois.readObject();
                    System.out.println("Deserialized User: " + deserializedUser);
                } catch (IOException | ClassNotFoundException e) {
                    e.printStackTrace();
                }
            }
        }
    b.说明
        定义类:User类实现了Serializable接口,表示该类的对象可以被序列化
        使用transient关键字:password字段被声明为transient,表示该字段不应被序列化
        序列化对象:使用ObjectOutputStream将User对象写入文件user.ser
        反序列化对象:使用ObjectInputStream从文件user.ser读取User对象

2.7 Serializable接口

01.Serializable接口:标识接口,没有方法
    a.序列化类的要求
        被序列化的类必须属于【Enum、Array和实现Serializable接口】中的一种,否则将抛出NotSerializableException异常
    b.默认序列化机制
        【不仅会序列化“当前对象本身”,还会对“其父类的字段”,“该对象引用的其它对象(成员变量是对象类型)”也进行序列化】;
        【注意,此处的“父类、引用的其他对象”,也必须满足序列化要求,即Enum、Array 和 Serializable中一种】
        【不包括类中的静态变量,“序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量”】
    c.serialVersionUID
        serialVersionUID 字段必须是 static final long 类型,建议使用 private 修饰符(仅声明,子类继承该字段无意义)
        数组类不能声明一个明确的 serialVersionUID,因此它们具有默认的计算值,但是数组类没有这种 serialVersionUID 要求
        ----------------------------------------------------------------------------------------------------
        serialVersionUID 是 Java 为【每个序列化类】产生的版本标识。
        它可以用来保证在反序列时,发送方发送的和接受方接收的是可兼容的对象。
        如果接收方接收的类的 serialVersionUID 与发送方发送的 serialVersionUID 不一致,会抛出InvalidClassException
        ----------------------------------------------------------------------------------------------------
        在某些场合,希望【类的不同版本对序列化兼容】,因此需要确保【类的不同版本具有“相同”的serialVersionUID】
        在某些场合,不希望【类的不同版本对序列化兼容】,因此需要确保【类的不同版本具有“不同”的serialVersionUID】
        ----------------------------------------------------------------------------------------------------
        若没有显式声明 serialVersionUID,【默认 serialVersionUID 值根据类名、接口名、成员方法及属性生成】
        虽然可以生成不同的值,但【不同JDK编译可能生成不同值,会导致异常】,因此建议【给每个类指定serialVersionUID值】
    d.transient
        当某些变量不想被序列化,同是又不适合使用static关键字声明,那么此时就需要用transient关键字来声明该变量
        被transient修饰的属性是默认值:对于引用类型是null;基本类型是0;boolean类型是false
        public class SerializeDemo02 {
            static class Person implements Serializable {
                transient private Integer age = null;
                // 略
            }
        }
        // Output: name: Jack, age: null, sex: MALE
    e.可选的自定义序列化
        writeObject:自定义序列化规则,比如哪些属性、依据的规则
        readObject:自定义反序列化规则,比如哪些属性、依据的规则
        readObjectNoData:使用不同类接收反序列化对象,或序列化流被篡改时,调用readObjectNoData()初始化反序列化对象
    f.更彻底的自定义序列化
        readResolve、writeReplace访问修饰符,可以是private、protected、public
        如果父类重写了这两个方法,子类都需要根据自身需求重写,这显然不是一个好的设计。
        对于final修饰的类重写readResolve,可以是public;非final修饰的类重写readResolve,不建议public,推荐private

2.8 Externalizable接口

01.Externalizable接口:继承 Serializable 接口,两个抽象方法(“writeExternal()”、"readExternal()")
    a.失效问题
        可序列化类实现 Externalizable 接口之后,基于 Serializable 接口的默认序列化机制就会失效
    b.相比 Serializable 接口
        若直接使用 Externalizable 接口,
        默认【writeExternal、readExternal未作任何处理,则“该序列化行为将不会保存/读取任何一个字段”】
        序列化时,【先调用“该类的无参构造方法,且类型必须为public”创建新对象,然后“将'待保存字段'分别填充到新对象中”】
    c.替代方案
        实现 Serializable 接口,
        并添加 writeObject(ObjectOutputStream out) 与 readObject(ObjectInputStream in) 方法。
        序列化和反序列化过程中会自动回调这两个方法。
    d.单例模式的序列化问题
        单例模式(某个类的创建的唯一性),如果该类进行“序列化”,会出现“单例模式创建的对象,与序列化后的对象,并不相等”,
        因此,为了保证“单例类创建类的唯一性”,需要重写readResolve()方法,直接返回 Person 的单例对象

3 NIO

3.1 概念:IO多路复用

01.存在一些问题
    a.问题1
        服务端接收客户端的accept()方法是一个阻塞式方法,该方法会一直阻塞,直到有客户端发来连接。
        因此,服务端在等待期间(阻塞)会什么也不做,造成资源浪费
    b.问题2
        如果在高并发环境下,服务端需要通过开辟线程来分别处理各个客户端请求
        但是每开辟一个线程大约会消耗1MB内存,因此在高并发环境下对内存等资源的损耗也是非常大的。

02.NIO
    a.NIO概念
        NIO:new I0,non blocking io(jdk1.4之后提供的),高效率实现IO技术
    b.三个组成
        缓冲区buffer
        通道Channel
        选择器Selector
    c.选择器是实现【IO多路复用】的关键
        一种通过【单个线程】监视【多个输入输出通道】的技术。
        它允许一个线程同时处理多个IO操作,而不需要为每个连接创建一个线程。

3.2 通道:channel

01.Channel与流的区别
    a.双向性
        Channel:双向,意味着你可以通过同一个通道进行读和写操作
        传统流:单向,即一个流要么是输入流,要么是输出流
    b.异步性
        Channel:支持异步IO操作,这使得它们可以与选择器(Selector)一起使用,实现非阻塞IO。
    c.Buffer结合
        Channel:数据的读写必须通过缓冲区(Buffer)进行。通道本身不存储数据,而是直接与缓冲区交互。这种设计使得数据处理更加灵活和高效。

02.Channel的特性
    a.数据传输
        通道负责在缓冲区和实体(如文件、网络套接字)之间传输数据。它们不存储数据,只负责数据的传输。
    b.双向性
        通道是双向的,可以同时进行读和写操作,这与传统IO流的单向性形成对比

03.获取Channel对象的方法
    a.方式1
        FileChannel.open() -> Channel对象
    b.方式2
        FilelnputStream / FileOutputsStream / RandomAccessFile.getChannel() -> Channel对象
    c.方式3
        Socket / ServerSocket / DatagramSocket.getChannel() -> Channel对象

3.3 缓冲区:buffer

00.总结
    Channel负责传输, Buffer负责存储

01.基本概念
    a.容量(Capacity)
        缓冲区的容量是它可以容纳的最大数据量。容量在缓冲区创建时被设定,并且不能改变
    b.位置(Position)
        位置是下一个要读或写的元素的索引。位置会在读写操作时自动更新
    c.限制(Limit)
        限制是缓冲区中第一个不能被读或写的元素的索引。限制在写模式下等于容量,在读模式下等于写入的数据量
    d.标记(Mark)
        标记是一个用于记录当前position的索引。可以通过mark()方法设置标记,通过reset()方法恢复到标记的位置

02.基本操作
    a.分配缓冲区
        使用allocate()方法创建一个缓冲区。例如,ByteBuffer.allocate(1024)创建一个容量为1024字节的字节缓冲区
    b.写入数据到缓冲区
        使用put()方法将数据写入缓冲区
    c.翻转缓冲区
        使用flip()方法将缓冲区从写模式切换到读模式。flip()会将限制设置为当前位置,并将位置重置为0
    d.读取数据从缓冲区
        使用get()方法从缓冲区读取数据
    e.清空缓冲区
        使用clear()方法清空缓冲区,准备再次写入。clear()不会清除数据,只是重置位置和限制
    f.重绕缓冲区
        使用rewind()方法重置位置为0,允许重新读取缓冲区中的数据

03.缓冲区的类型
    ByteBuffer:字节
    CharBuffer:字符
    IntBuffer:整数
    LongBuffer:长整数
    FloatBuffer:浮点
    DoubleBuffer:双精度浮点
    ShortBuffer:短整数

3.4 选择器:selector

01.基本概念
    a.非阻塞IO
        选择器与非阻塞通道一起使用。通道可以注册到选择器上,选择器可以监视这些通道的事件,而不需要阻塞线程。
    b.事件类型
        连接就绪(OP_CONNECT):表示通道成功连接到服务器。
        接受就绪(OP_ACCEPT):表示服务器套接字通道准备好接受新连接。
        读就绪(OP_READ):表示通道中有数据可以读取。
        写就绪(OP_WRITE):表示可以向通道写入数据。
    c.选择键(SelectionKey)
        每个注册到选择器的通道都会关联一个选择键。选择键包含通道和选择器之间的关系信息,以及通道的就绪事件。

02.使用选择器的步骤
    a.打开选择器
        使用Selector.open()方法创建一个选择器实例
    b.注册通道
        将通道设置为非阻塞模式
        使用channel.register(selector, ops)方法将通道注册到选择器上,并指定要监视的事件类型(ops)
    c.选择就绪通道
        调用selector.select()方法阻塞等待,直到至少有一个通道准备好进行IO操作
        通过selector.selectedKeys()获取已就绪通道的选择键集合
    d.处理就绪事件
        遍历选择键集合,处理每个键对应的事件
        根据事件类型执行相应的IO操作(如接受连接、读取数据、写入数据等)
    e.移除已处理的键
        处理完一个选择键后,需要将其从集合中移除,以避免重复处理。

03.关键点
    a.非阻塞模式
        通道必须配置为非阻塞模式,以便与选择器一起使用
    b.选择器的使用
        选择器用于监视多个通道的事件,通过调用select()方法,程序可以阻塞等待,直到至少有一个通道准备好进行IO操作
    c.事件处理
        通过检查SelectionKey的状态,可以确定通道上发生了什么事件(如接受连接、读、写等),并进行相应的处理

3.5 BIO、NIO、AIO

00.总结
    a.BIO
        传统IO
        模式简单使用方便,并发处理能力低
    b.NIO
        传统IO的升级,通过【非阻塞IO】和【多路复用】提高了并发处理能力
    c.AIO
        NIO的升级,也叫NIO2,通过【异步IO】进一步提高了效率

01.BIO(Blocking IO)
    a.特点
        阻塞模式:在BIO中,IO操作是阻塞的。当一个线程执行读写操作时,它会被阻塞,直到操作完成
        线程模型:通常为每个连接分配一个线程来处理IO操作。这意味着在高并发场景下,可能需要大量线程来处理连接
        简单易用:BIO模型简单直观,适合于连接数较少且固定的场景
    b.适用场景
        适用于连接数较少且固定的应用程序,如传统的C/S架构系统

02.NIO(Non-blocking IO)
    a.特点
        非阻塞模式:NIO允许非阻塞IO操作,线程可以在等待数据准备好时执行其他任务
        多路复用:通过Selector实现IO多路复用,一个线程可以监视多个通道的IO事件,提高了资源利用率
        缓冲区:NIO使用缓冲区来进行数据的读写操作,提供了更灵活的数据处理方式
    b.适用场景
        适用于连接数较多且连接时间较长的应用程序,如高并发的网络服务器

03.AIO(Asynchronous IO)
    a.特点
        异步模式:AIO是异步非阻塞的,IO操作可以在后台完成,完成后通过回调机制通知应用程序
        简化编程:由于异步操作,线程不需要等待IO操作完成,可以继续执行其他任务
        高效:适合于高延迟的IO操作,因为线程可以在等待IO操作完成时执行其他任务
    b.适用场景
        适用于高并发且IO操作耗时较长的应用程序,如需要处理大量连接的高性能服务器

3.6 channel、buffer、selector

00.总结
    Buffer:【存储数据】,用于存储数据,管理数据的读写
    Channel:【传输数据】,用于在数据源和缓冲区之间传输数据,支持双向和非阻塞操作
    Selector:【监视IO事件】,用于监视多个通道的IO事件,实现IO多路复用,提高并发处理能力

01.缓冲区(Buffer)
    a.作用
        缓冲区是一个用于存储数据的容器。所有从通道中读取的数据都必须先放入缓冲区,
        同样,写入通道的数据也必须先从缓冲区中获取。
    b.特点
        缓冲区是线性的数据结构,具有固定的容量。
        提供了位置(position)、限制(limit)、容量(capacity)等属性来管理数据的读写。
        支持多种数据类型的缓冲区,如ByteBuffer、CharBuffer、IntBuffer等。
    c.使用场景
        在进行IO操作时,缓冲区用于临时存储数据,以便在通道和应用程序之间传输。

02.通道(Channel)
    a.作用
        通道是一个双向的数据传输通道,可以用于从数据源(如文件、网络套接字)读取数据到缓冲区,或者将缓冲区中的数据写入到数据源。
    b.特点
        通道是双向的,可以同时进行读和写操作。
        支持非阻塞IO操作,与选择器结合使用时,可以实现高效的多路复用。
        常见的通道类型包括FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel等。
    c.使用场景
        通道用于在数据源和缓冲区之间传输数据,是NIO中数据传输的核心组件。

03.选择器(Selector)
    a.作用
        选择器用于监视多个通道的IO事件(如连接、读、写等),从而实现IO多路复用。
        它允许一个线程同时管理多个通道,提高了处理大量并发连接的效率。
    b.特点
        选择器可以监视多个通道的事件,通过select()方法阻塞等待,直到至少有一个通道准备好进行IO操作。
        使用选择键(SelectionKey)来表示通道和选择器之间的关系,以及通道的就绪事件。
        常用于网络服务器的开发,可以高效地处理大量并发连接。
    c.使用场景
        选择器用于实现非阻塞IO操作,特别适合于需要同时处理多个连接的网络应用程序。

4 NIO

4.1 实现文件发送

00.核心思想
    通过 FileChannel 读取文件数据并通过 SocketChannel 将数据发送给接收端

01.核心组件
    a.FileChannel:
        用于从文件中读取数据。
        通过 FileInputStream 或 RandomAccessFile 获取。
    b.SocketChannel:
        用于建立客户端与服务器端的连接,传输文件数据。
        可以设置为非阻塞模式。
    c.Selector(可选):
        如果需要同时管理多个客户端(如文件服务器),可以使用 Selector 进行多路复用。
    d.ByteBuffer:
        作为数据传输的中间缓冲区,用于存储从文件读取的数据并写入到网络中。

02.文件发送的基本步骤
    a.建立文件读取通道:
        使用 FileChannel 打开需要发送的文件。
        读取文件数据到缓冲区。
    b.建立网络通道:
        使用 SocketChannel 建立客户端与服务器的连接。
        在客户端通过 SocketChannel 发送文件数据。
    c.数据传输:
        使用 ByteBuffer 作为缓冲区,分块读取文件数据,并将其通过 SocketChannel 写入网络。
    d.关闭资源:
        传输完成后,关闭文件通道和网络通道。

03.思路图解
    +----------------+        +----------------+        +----------------+
    | FileChannel    | -----> | ByteBuffer     | -----> | SocketChannel   |
    | (读取文件数据) |        | (存储数据)     |        | (发送数据到网络)|
    +----------------+        +----------------+        +----------------+

04.文件发送的具体实现步骤
    a.文件发送端(客户端)
        打开文件,获取 FileChannel。
        连接服务器,获取 SocketChannel。
        使用 ByteBuffer 循环读取文件数据,并通过 SocketChannel 写入网络。
        关闭资源。
    b.文件接收端(服务器)
        使用 ServerSocketChannel 接收客户端连接。
        获取 SocketChannel,读取客户端发送的数据。
        将数据写入文件,完成文件保存。
        关闭资源。

4.2 导致CPU100%?空轮询

01.NIO为什么会导致CPU100%?空轮询
    a.总结
        在 Java 中总共有三种 IO 类型:BIO(Blocking I/O,阻塞I/O)、NIO(Non-blocking I/O,非阻塞I/O)和 AIO(Asynchronous I/O,异步I/O),它们的区别如下:
        在 JDK 1.4 之前,只有 BIO 一种模式,其开发过程相对简单,新来一个连接就会创建一个新的线程处理,但随着请求并发度的提升,BIO 很快遇到了性能瓶颈。
        所以在 JDK 1.4 以后开始引入了 NIO 技术,NIO 可以在一个线程中处理多个 IO 操作,提高了资源的利用率和系统的吞吐量。
        而到了 JDK 1.7 发布了 AIO 模型,它可以实现当线程发起一个 IO 操作后,可以直接返回,无需等待 IO 操作完成。操作系统会在整个 IO 操作完成后,通过回调函数通知应用程序。
    b.空轮询和CPU100%
        随着 NIO 逐渐使用,人们却发现了 NIO 的一个经典问题,也就是臭名昭著的 Epoll(多路复用实现技术)空轮询的问题。
        空轮询的问题是指,在 Linux 系统下,使用 Java 中的 NIO 时,即使 Selector(多路复用器)轮询结果为空,
        也没有 wakeup 或新消息要处理时,NIO 依旧会进行空轮询,导致 CPU 一直上升,最终造成 CPU 使用率 100% 的问题。
    c.空轮询的原因
        当连接出现了 RST(强制连接关闭),因为 poll 和 epoll 对于突然中断的连接 Socket 会对返回的 eventSet 事件集合置为
        POLLHUP 或者 POLLERR,eventSet 事件集合发生了变化,这就导致 Selector 会被唤醒,进而导致 CPU 100% 问题,
        其根本原因就是 JDK 没有处理好这种情况,比如 SelectionKey 中就没定义有异常事件的类型,导致异常无法被捕捉和处理,
        从而一直空轮询。

02.如何解决空轮询?
    a.方案1:升级 Java 版本
        早期的 JDK 版本中(JDK 1.7 之前),这个 bug 较为常见,但后续的 JDK 更新中,
        Oracle 和 OpenJDK 团队已经着手解决了这一问题,确保使用最新的 Java 版本可以减少遇到此问题的风险。
        但网上依然有人发现即使在 JDK 1.8 中,使用原生的 NIO 依然会发生空轮询的问题,只是发生的概率变低了而已。
    b.方案2:使用第三方库
        对于无法升级 Java 版本的情况,或担心新版本的 JDK 中依然存在空轮询问题的团队可以考虑
        使用已经解决了此问题的第三方库,如 Netty。Netty 通过主动检测和处理空轮询情况,
        当检测到可能的空轮询时,会采取措施如临时增加 Selector 的等待时间,
        或者重建 Selector,以此来避免 CPU 资源的浪费。

4.3 一个Channel操作多个Buffer

00.说明
    a.介绍
        一个 Channel 操作多个 Buffer 的场景通常涉及 Scattering(分散)和 Gathering(聚集)操作。
        Scattering 和 Gathering 是 NIO 的核心特性之一,用于在多个缓冲区之间高效地处理数据。
    b.总结
        一个 Channel 操作多个 Buffer 的核心是 Scattering Read 和 Gathering Write。
        分散读取:将数据从 Channel 分配到多个缓冲区。
        聚集写入:将多个缓冲区的数据合并写入到 Channel。
        通过正确管理缓冲区状态(如 flip() 和 clear()),可以高效地操作多个缓冲区。

01.场景说明
    a.Scattering(分散读取):
        将一个 Channel 中的数据按照顺序依次读取到多个 Buffer 中。
        适用于分段读取,比如从网络中读取一个协议数据包时,可能有固定大小的头部缓冲区和可变大小的内容缓冲区。
    b.Gathering(聚集写入):
        将多个 Buffer 中的数据依次写入到一个 Channel 中。
        适用于分段发送,比如发送一个协议数据包时,可以将头部和内容分别存储在不同的缓冲区中,再一次性写入到网络中。

02.核心思路
    a.准备多个缓冲区:
        创建多个 Buffer(如 ByteBuffer),每个缓冲区负责存储特定的数据部分(如协议头、消息体等)。
        确保每个缓冲区的容量和数据布局符合需求。
    b.分散读取(Scattering Read):
        使用 Channel 的 read(ByteBuffer[] buffers) 方法,将数据从 Channel 依次读取到多个缓冲区中。
        每个缓冲区按顺序填充,直到缓冲区满或数据读取完成。
    c.聚集写入(Gathering Write):
        使用 Channel 的 write(ByteBuffer[] buffers) 方法,将多个缓冲区中的数据依次写入到 Channel。
        每个缓冲区的数据按顺序写入,直到所有缓冲区的数据都被写完。
    d.处理缓冲区状态:
        在读取或写入操作后,需要切换缓冲区的模式(读模式/写模式)以正确操作数据。
        使用 flip()、clear() 或 compact() 方法管理缓冲区状态。

03.注意事项
    a.缓冲区容量:
        在分散读取时,如果某个缓冲区容量不足,可能会丢失数据或需要手动调整缓冲区大小。
        在聚集写入时,确保缓冲区的数据准备好(如切换到读模式 flip())。
    b.缓冲区顺序:
        缓冲区的顺序在数组中非常重要,分散读取和聚集写入都会严格按照数组顺序操作缓冲区。
    c.缓冲区状态管理:
        读取操作后,需要调用 flip() 切换到读模式。
        写入操作后,需要调用 clear() 或 compact() 准备下一次写入。
    d.数据完整性:
        如果数据较大,可以分块处理,确保每次操作的数据不会丢失。
    e.通道类型:
        分散和聚集操作通常用于 FileChannel 和 SocketChannel。
        对于 SocketChannel,分散读取和聚集写入在网络协议处理(如 HTTP、TCP 等)中非常有用。

04.实际应用场景
    a.协议解析:
        分散读取用于解析网络协议时,将协议的头部和数据部分分开存储。
        聚集写入用于发送网络协议包时,将头部和数据部分合并发送。
    b.日志处理:
        分散读取从文件中读取多段日志内容到不同的缓冲区中。
        聚集写入将多段日志内容写入到一个文件中。
    c.文件传输:
        使用分散读取从多个文件部分中读取数据。
        使用聚集写入将这些数据发送到网络或写入另一个文件。

05.分散读取(Scattering Read)
    a.应用场景
        从 Channel 读取数据时,将不同部分的数据存储到不同的缓冲区中。
    b.代码
        import java.io.RandomAccessFile;
        import java.nio.ByteBuffer;
        import java.nio.channels.FileChannel;

        public class ScatteringExample {
            public static void main(String[] args) throws Exception {
                // 打开文件通道
                RandomAccessFile file = new RandomAccessFile("example.txt", "r");
                FileChannel channel = file.getChannel();

                // 创建多个缓冲区
                ByteBuffer header = ByteBuffer.allocate(8); // 头部缓冲区(8字节)
                ByteBuffer body = ByteBuffer.allocate(32);  // 内容缓冲区(32字节)

                // 将缓冲区数组传递给 channel.read(),进行分散读取
                ByteBuffer[] buffers = { header, body };
                channel.read(buffers);

                // 切换到读模式,准备读取数据
                header.flip();
                body.flip();

                // 打印读取的数据
                System.out.println("Header:");
                while (header.hasRemaining()) {
                    System.out.print((char) header.get());
                }

                System.out.println("\nBody:");
                while (body.hasRemaining()) {
                    System.out.print((char) body.get());
                }

                // 关闭通道和文件
                channel.close();
                file.close();
            }
        }
    c.输出示例
        假设文件内容为:
        ABCDEFGH1234567890
        -----------------------------------------------------------------------------------------------------
        输出可能为:
        Header:
        ABCDEFGH
        Body:
        1234567890

06.聚集写入(Gathering Write)
    a.应用场景
        将多个缓冲区的数据合并写入到一个 Channel 中(例如,发送一个协议包)。
    b.代码
        import java.io.FileOutputStream;
        import java.nio.ByteBuffer;
        import java.nio.channels.FileChannel;

        public class GatheringExample {
            public static void main(String[] args) throws Exception {
                // 打开文件输出流
                FileOutputStream fos = new FileOutputStream("output.txt");
                FileChannel channel = fos.getChannel();

                // 创建多个缓冲区
                ByteBuffer header = ByteBuffer.allocate(8);
                ByteBuffer body = ByteBuffer.allocate(32);

                // 写入数据到缓冲区
                header.put("Header1".getBytes());
                body.put("This is the body.".getBytes());

                // 切换到读模式,准备写入数据
                header.flip();
                body.flip();

                // 将缓冲区数组传递给 channel.write(),进行聚集写入
                ByteBuffer[] buffers = { header, body };
                channel.write(buffers);

                // 关闭通道和文件
                channel.close();
                fos.close();
            }
        }
    c.输出示例
        假设程序将数据写入文件 output.txt,文件内容为:
        Header1This is the body.

4.4 ByteBuffer是Buffer缓冲区的一个具体子类

01.定义
    a.缓冲区(Buffer):
        是 Java NIO 中的一个抽象类,位于 java.nio 包中。
        它是所有缓冲区类型的父类,定义了缓冲区的通用功能和操作。
        缓冲区用于存储和管理数据,为通道(Channel)提供数据的读写操作。
        缓冲区本质上是一个带有状态的数组,提供了位置(position)、限制(limit)、容量(capacity)等属性来管理数据的存储和读取。
    b.ByteBuffer:
        是缓冲区的一个具体实现类,继承自 Buffer。
        用于存储字节数据(byte 类型)。
        提供了额外的方法来处理基本数据类型(如 int、long 等)和直接缓冲区(Direct Buffer)的功能。
        是 NIO 中最常用的缓冲区类型之一。
    c.总结
        缓冲区(Buffer) 是一个抽象类,定义了缓冲区的通用功能和操作。
        ByteBuffer 是缓冲区的一个具体实现类,专门用于处理字节数据,并扩展了许多与字节相关的操作。
        Buffer 是通用概念,而 ByteBuffer 是具体实现。 如果需要处理其他类型的数据(如字符、整数等),可以使用其他具体的缓冲区实现(如 CharBuffer、IntBuffer 等)。

02.类层次结构
    a.介绍
        缓冲区是一个抽象类,ByteBuffer 是其具体子类
    b.以下是类的继承关系:
        java.nio.Buffer (抽象类)
            ↳ java.nio.ByteBuffer (子类,用于字节数据)
            ↳ java.nio.CharBuffer (子类,用于字符数据)
            ↳ java.nio.IntBuffer (子类,用于整型数据)
            ↳ java.nio.FloatBuffer (子类,用于浮点型数据)
            ↳ java.nio.LongBuffer (子类,用于长整型数据)
            ↳ java.nio.DoubleBuffer (子类,用于双精度浮点型数据)
            ↳ java.nio.ShortBuffer (子类,用于短整型数据)
        -----------------------------------------------------------------------------------------------------
        Buffer 是一个抽象类,而 ByteBuffer 是其中一个具体的子类,专门用于存储字节数据。

03.功能区别
    | 特性         | 缓冲区(Buffer)                     | ByteBuffer
    |--------------|-------------------------------------|-----------------------------
    | 抽象程度     | 是一个抽象类,定义了缓冲区的通用功能。 | 是 Buffer 的具体实现类,存储 byte 数据。
    | 存储数据类型 | 不具体定义数据类型(通用缓冲区)。     | 专门用于存储字节(byte 类型)。
    | 功能扩展     | 只提供缓冲区的基本操作(如 flip())。 | 提供了额外的方法(如读取/写入基本数据类型)。
    | 使用场景     | 用于定义缓冲区的基础结构。            | 用于具体的字节数据操作,例如网络通信、文件 I/O。

04.缓冲区的通用功能
    a.介绍
        所有缓冲区(包括 ByteBuffer)都继承了 Buffer 的通用功能。
        Buffer 定义了一些核心的属性和方法,用于管理缓冲区的数据存储和操作:
    b.属性
        capacity(容量):缓冲区的最大容量(初始化后不能改变)。
        position(位置):当前读/写操作的位置,表示下一次读/写的索引。
        limit(限制):表示缓冲区中有效数据的上限,不能读/写超过此位置。
        mark(标记):一个临时存储的位置,可以通过 reset() 恢复到此位置。
    c.方法
        clear():清空缓冲区,为写入数据做好准备。
        flip():切换缓冲区模式(从写模式切换到读模式)。
        rewind():重置 position 为 0,重新读取缓冲区的数据。
        mark() 和 reset():标记当前位置,并可以通过 reset() 恢复到标记的位置。
        remaining():返回从当前位置到限制位置之间的剩余空间。
        hasRemaining():判断是否还有剩余数据。

05.ByteBuffer的特有功能
    a.介绍
        ByteBuffer 继承了上述通用功能,并扩展了一些特定于字节操作的功能:
    b.创建方式
        a.非直接缓冲区:
            ByteBuffer buffer = ByteBuffer.allocate(1024); // 创建堆内存缓冲区
        b.直接缓冲区:
            ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024); // 创建堆外内存缓冲区
    c.读写数据
        a.存储数据:
            buffer.put((byte) 65);  // 存储一个字节
            buffer.put(new byte[]{1, 2, 3});  // 存储一个字节数组
        b.读取数据:
            byte b = buffer.get();  // 读取一个字节
            byte[] bytes = new byte[3];
            buffer.get(bytes);  // 读取字节数组
    d.操作基本数据类型
        ByteBuffer 提供了直接操作基本数据类型的方法:
        buffer.putInt(12345);  // 写入一个整数
        int value = buffer.getInt();  // 读取一个整数
    e.分片和只读缓冲区
        a.分片
            创建当前缓冲区的一个视图,称为“切片”:
            ByteBuffer slice = buffer.slice();
        b.只读缓冲区
            将缓冲区转换为只读模式:
            ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();

4.5 非直接缓冲区/直接缓冲区(零拷贝)

00.总结
    a.区别
        | 特性        | 直接缓冲区(Direct Buffer)      | 非直接缓冲区(Non-Direct Buffer)
        |-------------|---------------------------------|---------------------------
        | 内存位置     | 堆外内存(操作系统直接管理)     | JVM 堆内存
        | 分配方式     | ByteBuffer.allocateDirect()    | ByteBuffer.allocate()
        | 访问速度     | 相对较慢                        | 相对较快
        | I/O 性能     | 较高(避免内存复制)            | 较低(需要内存复制)
        | 创建销毁成本 | 较高                            | 较低
        | 垃圾回收     | 不受 JVM 垃圾回收直接管理        | 受 JVM 垃圾回收管理
        | 适用场景     | 长期、频繁的 I/O 操作           | 临时、小型数据操作
    b.直接缓冲区(Direct Buffer)
        直接缓冲区:jvm管辖的内存中,能够直接通过adress变量指向操作系统内存中的物理映射文件
        直接缓冲区:直接操作的是操作系统中的内存数据,因此无法用GC回收。
        以上,也称为零拷贝

01.定义
    a.直接缓冲区(Direct Buffer):
        使用操作系统的内存(即堆外内存)直接分配,不通过 JVM 的堆内存。
        数据在直接缓冲区中,I/O 操作可以直接在内存和通道之间传输,而无需将数据从 JVM 堆内存复制到操作系统内存。
        使用 ByteBuffer 的 allocateDirect() 方法创建直接缓冲区。
    b.非直接缓冲区(Non-Direct Buffer):
        使用 JVM 的堆内存分配。
        数据存储在 JVM 的堆内存中,I/O 操作需要将数据从堆内存复制到操作系统的内存。
        使用 ByteBuffer 的 allocate() 方法创建非直接缓冲区。

02.性能区别
    a.直接缓冲区:
        优势:因为数据直接位于操作系统的内存中,避免了堆内存与操作系统内存之间的数据复制,I/O 操作性能较高,适合频繁的大量数据传输。
        劣势:创建和销毁直接缓冲区的成本较高(因为分配堆外内存比堆内存更昂贵),而且访问直接缓冲区的速度可能比访问非直接缓冲区慢(堆外内存访问涉及更多的系统调用)。
        适用场景:适合长期使用的、需要频繁进行 I/O 操作的场景。
    b.非直接缓冲区:
        优势:分配和销毁成本较低,访问速度较快,因为它位于 JVM 的堆内存中,受到 JVM 的垃圾回收机制管理。
        劣势:I/O 操作需要额外的数据复制(从堆内存到操作系统内存),性能可能较低。
        适用场景:适合临时使用的小型数据操作场景。

03.内存分配方式
    a.直接缓冲区:
        分配的内存不受 JVM 堆大小的限制(因为它使用的是堆外内存)。
        不受 JVM 垃圾回收机制的直接管理,生命周期由程序和操作系统控制。
        如果没有显式释放(例如丢弃缓冲区引用),可能会导致内存泄漏。
    b.非直接缓冲区:
        使用 JVM 堆内存分配,受到堆内存大小的限制。
        由 JVM 的垃圾回收机制自动管理生命周期。

04.创建方式
    a.直接缓冲区:
        ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024); // 分配直接缓冲区
    b.非直接缓冲区:
        ByteBuffer nonDirectBuffer = ByteBuffer.allocate(1024); // 分配非直接缓冲区

05.选择直接缓冲区还是非直接缓冲区?
    a.直接缓冲区
        频繁进行 I/O 操作(例如文件读写、大量网络数据传输),因为它能减少内存复制,提高性能。
    b.非直接缓冲区
        需要临时存储数据,或操作的数据量较小,因为它分配和销毁成本较低,并且由 JVM 自动管理。

5 Netty:基于NIO

5.1 概念

00.Netty的定位
    虽然Netty本身不是直接的I/O操作,但它是一个用于构建网络应用的框架。
    Netty广泛应用于需要处理网络I/O的场景,通过利用Java NIO的特性,提供了强大的功能来处理复杂的网络I/O任务。
    它是许多高性能服务器应用(如游戏服务器、聊天服务器、HTTP服务器等)的首选框架。

01.Netty与IO的关系
    a.基于NIO
        Netty 是基于 Java NIO 构建的。Java NIO 提供了非阻塞 I/O 操作的能力,
        而 Netty 对 NIO 的复杂性进行了封装,提供了更高层次的 API,使得开发网络应用程序更加简单和高效。
    b.异步和事件驱动
        Netty 采用异步和事件驱动的模型,允许处理大量并发连接而不会阻塞线程。
        这种模型在高性能、高并发的网络应用中非常有用,如聊天服务器、HTTP 服务器等。
    c.丰富的协议支持
        Netty 支持多种协议的实现,包括 HTTP、WebSocket、TCP、UDP 等,
        这使得它在构建多种网络应用程序时非常灵活和强大。
    d.可扩展性
        Netty 提供了良好的可扩展性和模块化设计,可以根据需求轻松扩展功能。
        它支持通过编写自定义的 ChannelHandler 来处理不同的协议和业务逻辑。

02.Netty的特点
    高性能:Netty 能够有效地管理大量并发连接,适合构建高性能的网络应用程序。
    简化开发:通过抽象和封装底层 I/O 操作,Netty 简化了开发过程。
    丰富的组件:提供了许多开箱即用的组件和工具,用于处理各种协议和网络任务。
    跨平台:Netty 是纯 Java 实现的,可以在任何支持 Java 的平台上运行。

5.2 零拷贝

00.总结
    零拷贝通过【直接共享数据的内存地址】,【避免中间的拷贝】过程,从而提高了数据传输的效率。

01.零拷贝
    a.介绍
        零拷贝(Zero-copy)技术是一种计算机操作系统中用于提高数据传输效率的优化策略。
        在传统的数据传输过程中,需要将数据从一个缓冲区拷贝到另一个缓冲区,然后再传输给目标。
        这涉及到多次的 CPU 和内存之间的数据拷贝操作,会消耗 CPU 的时间和内存带宽。
    b.原理
        利用 Linux 下的 MMap、sendFile 等手段来实现,使得数据能够直接从磁盘映射到内核缓冲区,
        然后通过 DMA 传输到网卡缓存,整个过程中 CPU 只负责管理和调度,而无需执行实际的数据复制指令。
    c.特点
        减少数据拷贝:零拷贝技术直接共享数据内存地址,避免了传统 IO 中多次数据拷贝,提高了数据传输效率。
        降低 CPU 负荷:通过减少 CPU 参与数据复制的次数,降低了 CPU 的使用率。
        优化上下文切换:相对于传统 IO 多次用户态和内核态之间的切换,sendFile() 等技术减少了上下文切换次数,从而降低了系统开销。
        应用场景广泛:在 Java 中,FileChannel 的 transferTo/transferFrom 以及直接缓冲区(DirectBuffer)都利用了零拷贝技术,同时高性能框架(如 Kafka、Netty)也在底层使用了此技术优化数据传输。

02.场景
    a.NIO(New I/O)通道
        java.nio.channels.FileChannel 提供了 transferTo() 和 transferFrom() 方法,
        可以直接将数据从一个通道传输到另一个通道,例如从文件通道直接传输到 Socket 通道,
        整个过程无需将数据复制到用户空间缓冲区,从而实现了零拷贝。
    b.Socket Direct Buffer
        在 JDK 1.4 及更高版本中,Java NIO 支持使用直接缓冲区(DirectBuffer),
        这类缓冲区是在系统堆外分配的,可以直接由网卡硬件进行 DMA 操作,
        减少数据在用户态与内核态之间复制次数,提高网络数据发送效率。
    c.Apache Kafka 或者 Netty 等高性能框架
        这些框架在底层实现上通常会利用 Java NIO 的上述特性来优化数据传输,
        如 Kafka 生产者和消费者在传输消息时会用到零拷贝技术以提升性能。

5.3 核心组件

01.总结
    启动器       Bootstrap/ServerBootstrap
    事件循环器   EventLoopGroup/EventLoop
    通道         Channel
    通道处理器   ChannelHandler
    通道管道     ChannelPipeline

02.核心组件
    a.组件1:Bootstrap/ServerBootstrap【启动器】
        Bootstrap 是“引导”的意思,它主要负责整个 Netty 程序的启动、初始化、服务器连接等过程,它相当于一条主线,串联了 Netty 的其他核心组件。
        PS:Netty 中的引导器共分为两种类型:一个为用于客户端引导的 Bootstrap,另一个为用于服务端引导的 ServerBootStrap。
    b.组件2:Channel【通道】
        Channel 是网络数据的传输通道,它代表了到实体(如硬件设备、文件、网络套接字或能够执行 I/O 操作的程序组件)的开放连接,如读操作和写操作。
        Channel 提供了基本的 API 用于网络 I/O 操作,如 register、bind、connect、read、write、flush 等。Netty 自己实现的 Channel 是以 JDK NIO Channel 为基础的,相比较于 JDK NIO,Netty 的 Channel 提供了更高层次的抽象,同时屏蔽了底层 Socket 的复杂性,赋予了 Channel 更加强大的功能,你在使用 Netty 时基本不需要再与 Java Socket 类直接打交道。
        常见的 Channel 类型有以下几个:
        NioServerSocketChannel 异步 TCP 服务端。
        NioSocketChannel 异步 TCP 客户端。
        OioServerSocketChannel 同步 TCP 服务端。
        OioSocketChannel 同步 TCP 客户端。
        NioDatagramChannel 异步 UDP 连接。
        OioDatagramChannel 同步 UDP 连接。
        当然 Channel 也会有多种状态,如连接建立、连接注册、数据读写、连接销毁等状态。
    c.组件3:EventLoopGroup/EventLoop【事件循环器】
        a.总结
            EventLoopGroup 是一个处理 I/O 操作和任务的线程组。
            在 Netty 中,EventLoopGroup 负责接受客户端的连接,以及处理网络事件,
            如读/写事件。它包含多个 EventLoop,每个 EventLoop 包含一个 Selector 和一个重要的组件,
            用于处理注册到其上的 Channel 的所有 I/O 事件
        b.EventLoopGroup、EventLoop和Channel
            它们三者的关系如下:
            一个 EventLoopGroup 往往包含一个或者多个 EventLoop。EventLoop 用于处理 Channel 生命周期内的所有 I/O 事件,如 accept、connect、read、write 等 I/O 事件。
            EventLoop 同一时间会与一个线程绑定,每个 EventLoop 负责处理多个 Channel。
            每新建一个 Channel,EventLoopGroup 会选择一个 EventLoop 与其绑定。该 Channel 在生命周期内都可以对 EventLoop 进行多次绑定和解绑。
        c.线程模型
            Netty 通过创建不同的 EventLoopGroup 参数配置,就可以支持 Reactor 的三种线程模型:
            单线程模型:EventLoopGroup 只包含一个 EventLoop,Boss 和 Worker 使用同一个EventLoopGroup;
            多线程模型:EventLoopGroup 包含多个 EventLoop,Boss 和 Worker 使用同一个EventLoopGroup;
            主从多线程模型:EventLoopGroup 包含多个 EventLoop,Boss 是主 Reactor,Worker 是从 Reactor,它们分别使用不同的 EventLoopGroup,主 Reactor 负责新的网络连接 Channel 创建,然后把 Channel 注册到从 Reactor。
    d.组件4:ChannelHandler【通道处理器】
        ChannelHandler 是 Netty 处理 I/O 事件或拦截 I/O 操作的组件。当发生某种 I/O 事件时(如数据接收、连接打开、连接关闭等),ChannelHandler 会被调用并处理这个事件。
        例如,数据的编解码工作以及其他转换工作实际都是通过 ChannelHandler 处理的。站在开发者的角度,最需要关注的就是 ChannelHandler,我们很少会直接操作 Channel,都是通过 ChannelHandler 间接完成。
    e.组件5:ChannelPipeline【通道管道】
        ChannelPipeline 是 ChannelHandler 的容器,提供了一种方式,以链式的方式组织和处理跨多个 ChannelHandler 之间的交互逻辑。当数据在管道中流动时,它会按照 ChannelHandler 的顺序被处理。

5.4 线程模型

01.介绍
    线程模型被称为【Reactor(响应式)模型/模式】
    它是基于NIO多路复用模型的一种升级,【将IO事件和业务处理进行分离,使用一个或多个线程来执行任务的一种机制】

02.Reactor三大组件
    a.Reactor(反应器)
        Reactor 负责监听和分发事件,它是整个 Reactor 模型的调度中心。
        Reactor 监视一个或多个输入通道,如监听套接字上的连接请求或读写事件。
        当检测到事件发生时,Reactor 会将其分发给预先注册的处理器(Handler)进行处理。
        在 Netty 中,这个角色经常是由 EventLoop 或其相关的 EventLoopGroup 来扮演,它们负责事件的循环处理、任务调度和 I/O 操作。
    b.Acceptor(接收器)
        用于处理 IO 连接请求。当 Reactor 检测到有新的客户端连接请求时,会通知 Acceptor,
        后者通过 accept() 方法接受连接请求,并创建一个新的 SocketChannel(在 Netty 中是 Channel)来表示这个连接。
        随后,Acceptor 通常会将这个新连接的 Channel 注册到 Worker Reactor 或 EventLoop 中,
        以便进一步处理该连接上的读写事件。
    c.Handlers(处理器)
        Handlers 负责具体的事件处理逻辑,即执行与事件相关的业务操作。
        在 Netty 中,Handler 是一个或多个 ChannelHandler 的实例,它们形成一个责任链(ChannelPipeline),
        每个 Handler 负责处理一种或一类特定的事件(如解码、编码、业务逻辑处理等)。
        数据或事件在 ChannelPipeline 中从一个 Handler 传递到下一个,直至处理完毕或被消费。
        Handler 可以分为入站(inbound)和出站(outbound)两种,分别处理流入的数据或流出的数据。

03.Reactor三大模型
    a.单线程模型
        在单线程模型中,所有的事件处理操作都由单个 Reactor 实例在单个线程下完成。
        Reactor 负责监控事件、分发事件和执行事件处理程序(Handlers)
        -------------------------------------------------------------------------------------------------
        // 假设有一个单线程的Reactor,负责监听、接收连接、读写操作
        class SingleThreadReactor {
            EventLoop eventLoop; // 单个事件循环线程

            SingleThreadReactor() {
                eventLoop = new EventLoop(); // 初始化单个事件循环
            }

            void start(int port) {
                ServerSocketChannel serverSocket = ServerSocketChannel.open();
                serverSocket.socket().bind(new InetSocketAddress(port)); // 绑定端口

                eventLoop.execute(() -> { // 在事件循环中执行
                    while (true) {
                        SocketChannel clientSocket = serverSocket.accept(); // 接受连接
                        if (clientSocket != null) {
                            handleConnection(clientSocket); // 处理连接
                        }
                    }
                });
                eventLoop.run(); // 启动事件循环
            }

            void handleConnection(SocketChannel clientSocket) {
                // 读写操作,这里简化处理
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                while (clientSocket.read(buffer) > 0) {
                    // 处理读取的数据
                    buffer.flip();
                    // 假设处理数据逻辑...
                    buffer.clear();
                }
                // 写操作逻辑类似
            }
        }
        -------------------------------------------------------------------------------------------------
        优缺点分析
        优点:简单、线程安全性好、适合编写简单的网络应用。
        缺点:处理能力受限于单个线程的处理能力,无法充分利用多核 CPU,可能会影响性能。
    b.多线程模型
        在多线程模型中,连接 Acceptor 和业务处理(Handlers)是由不同线程分开执行的,其中 Handlers 是由线程池(多个线程)来执行的,
        -------------------------------------------------------------------------------------------------
        // 假设有两个线程,一个用于监听连接,一个用于处理连接后的操作
        class MultiThreadReactor {
            EventLoop acceptLoop;
            EventLoop workerLoop;

            MultiThreadReactor() {
                acceptLoop = new EventLoop(); // 接收连接的线程
                workerLoop = new EventLoop(); // 处理连接的线程
            }

            void start(int port) {
                ServerSocketChannel serverSocket = ServerSocketChannel.open();
                serverSocket.socket().bind(new InetSocketAddress(port));

                acceptLoop.execute(() -> { // 在接受线程中监听
                    while (true) {
                        SocketChannel clientSocket = serverSocket.accept();
                        if (clientSocket != null) {
                            workerLoop.execute(() -> handleConnection(clientSocket)); // 将新连接交给工作线程处理
                        }
                    }
                });

                acceptLoop.run(); // 启动接受线程
                workerLoop.run(); // 启动工作线程
            }

            // handleConnection 方法与单线程模型中的相同
        }
        -------------------------------------------------------------------------------------------------
        优缺点分析
        优点:此模式可以提高并发性能,充分利用多核 CPU,并且保持简单的编程模型。
        缺点:多线程数据共享和数据同步比较复杂,并且 Reactor 需要处理所有事件监听和响应,在高并发场景依然会出现性能瓶颈。
    c.主从多线程模型
        主从多线程模型是一个主 Reactor 线程加多个子 Reactor 子线程,以及多个工作线程池来处理业务的
        -------------------------------------------------------------------------------------------------
        import io.netty.bootstrap.ServerBootstrap;
        import io.netty.channel.ChannelFuture;
        import io.netty.channel.ChannelInitializer;
        import io.netty.channel.EventLoopGroup;
        import io.netty.channel.nio.NioEventLoopGroup;
        import io.netty.channel.socket.SocketChannel;
        import io.netty.channel.socket.nio.NioServerSocketChannel;

        public class MainReactorModel {
            public static void main(String[] args) {
                // 主Reactor,用于接受连接
                EventLoopGroup bossGroup = new NioEventLoopGroup();
                // 从Reactor,用于处理连接后的读写操作
                EventLoopGroup workerGroup = new NioEventLoopGroup();

                try {
                    ServerBootstrap bootstrap = new ServerBootstrap();
                    bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            // 在这里添加业务处理器,如解码器、编码器、业务逻辑处理器
                            ch.pipeline().addLast(new MyBusinessHandler());
                        }
                    });

                    ChannelFuture future = bootstrap.bind(8080).sync();
                    System.out.println("Server started at port 8080");
                    future.channel().closeFuture().sync(); // 等待服务器关闭
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    bossGroup.shutdownGracefully();
                    workerGroup.shutdownGracefully();
                }
            }
        }
        -------------------------------------------------------------------------------------------------
        优缺点分析
        优点:可以充分利用多核 CPU 的资源,提高系统的整体性能和并发处理能力。
        缺点:模型相对复杂,实现和维护成本较高。

5.5 粘包问题

00.总结
    粘包:【数据传输时,接收方「未能正常读取」到一条完整数据的情况】
    拆包:【发送方发送的一个大数据包被接收方拆分成多个小数据包进行接收的现象】

01.粘包
    a.粘包概念
        粘包和拆包问题也叫做粘包和半包问题,
        它是指在【数据传输时,接收方未能正常读取到一条完整数据的情况】(只读取了部分数据,或多读取到了另一条数据的情况)
        就叫做粘包或拆包问题。
    b.粘包问题
        粘包问题是指在网络通信中,发送方连续发送的多个小数据包被接收方一次性接收的现象。
        这可能是因为底层传输层协议(如 TCP)会将多个小数据包合并成一个大的数据块进行传输,
        导致接收方在接收数据时一次性接收了多个数据包,造成粘连。
        -----------------------------------------------------------------------------------------------------
        例如以下案例,正常情况下客户端发送了两条消息,分别为“ABC”和“DEF”,
        那么接收端也应该收到两条消息“ABC”和“DEF”才对,但是接收端却收到了“ABCD”这样的消息,这种情况就叫做粘包
    c.拆包/半包问题
        拆包问题是指发送方发送的一个大数据包被接收方拆分成多个小数据包进行接收的现象。
        这可能是因为底层传输层协议(如 TCP)将一个大数据包拆分成多个小的数据块进行传输,
        导致接收方在接收数据时分别接收了多个小数据包,造成拆开。
        -----------------------------------------------------------------------------------------------------
        例如以下案例,客户端发送了一条消息“ABC”,而接收端却收到了“AB”和“C”两条信息,这种情况就叫做半包
        PS:大部分情况下我们都把粘包问题和拆包问题看成同一个问题,所以下文就用粘包问题来替代粘包和拆包问题。

02.Netty如何解决粘包问题
    d.为什么会有粘包问题?
        粘包问题通常发生在 TCP/IP 协议中,因为 TCP 是面向连接的传输协议,
        它是以“流”的形式传输数据的,而“流”数据是没有明确的开始和结尾边界的,所以就会出现粘包问题。
    e.常见解决方案
        固定大小方法:发送方和接收方固定发送数据大小,当字符长度不够时用空字符弥补,有了固定大小之后就知道每条消息的具体边界了,这样就没有粘包的问题了。
        自定义数据协议(定义数据长度):在 TCP 协议的基础上封装一层自定义数据协议,在自定义数据协议中,包含数据头(存储数据的大小)和 数据的具体内容,这样服务端得到数据之后,通过解析数据头就可以知道数据的具体长度了,也就没有粘包的问题了。
        特殊分割符:以特殊的字符结尾,比如以“\n”结尾,这样我们就知道数据的具体边界了,从而避免了粘包问题。
        -----------------------------------------------------------------------------------------------------
        以上三种方案中,
        第一种固定大小的方法可能会造成网络流量的浪费,以及传输性能慢的问题;
        第二种解决方案实现难度大,且不利于维护,
        所以比较推荐的是第三种方案,使用特殊分隔符来区分消息的边界,从而避免粘包问题。
    f.Netty解决方案
        使用定长解码器(FixedLengthFrameDecoder):每个数据包都拥有固定的长度,接收端根据固定长度对数据进行切分,从而解决了粘包问题。
        使用行分隔符解码器(LineBasedFrameDecoder):以行为单位进行数据包的解码,从而解决粘包问题。
        使用分隔符解码器(DelimiterBasedFrameDecoder):使用特定的分隔符来标识消息边界,这样接收端可以根据分隔符正确切分消息。
        使用长度字段解码器(LengthFieldBasedFrameDecoder):在消息头部加入表示消息长度的字段,接收端根据长度字段来确定消息的边界,而从解决粘包问题。
        -----------------------------------------------------------------------------------------------------
        在 Netty 中,解码器(Decoder)起着非常重要的作用。解码器主要负责将从网络中接收到的原始字节流数据转换为应用程序能够理解的 Java 对象或消息格式。
        使用解码器可以解决粘包和拆包问题、协议转换问题、消息编码(如文本转换为字节流)等问题。

5.6 简单示例

01.Netty简单示例
    a.服务端
        import io.netty.bootstrap.ServerBootstrap;
        import io.netty.channel.ChannelFuture;
        import io.netty.channel.ChannelInitializer;
        import io.netty.channel.EventLoopGroup;
        import io.netty.channel.nio.NioEventLoopGroup;
        import io.netty.channel.socket.SocketChannel;
        import io.netty.channel.socket.nio.NioServerSocketChannel;
        import io.netty.handler.codec.string.StringDecoder;
        import io.netty.handler.codec.string.StringEncoder;

        public class NettyServer {

            public static void main(String[] args) throws Exception {
                // 创建BossGroup和WorkerGroup,它们都是EventLoopGroup的实现
                // BossGroup负责接收进来的连接
                EventLoopGroup bossGroup = new NioEventLoopGroup(1);
                // WorkerGroup负责处理已经被接收的连接
                EventLoopGroup workerGroup = new NioEventLoopGroup();

                try {
                    // 创建服务器端的启动对象,配置参数
                    ServerBootstrap bootstrap = new ServerBootstrap();
                    // 设置两个线程组
                    bootstrap.group(bossGroup, workerGroup)
                            // 设置服务器通道实现类型
                            .channel(NioServerSocketChannel.class)
                            // 设置通道初始化器,主要用来配置管道中的处理器
                            .childHandler(new ChannelInitializer<SocketChannel>() {
                                @Override
                                protected void initChannel(SocketChannel ch) throws Exception {
                                    // 向管道加入处理器
                                    // 解码器:ByteBuf -> String
                                    ch.pipeline().addLast(new StringDecoder());
                                    // 编码器:String -> ByteBuf
                                    ch.pipeline().addLast(new StringEncoder());

                                    // 自定义的处理器
                                    ch.pipeline().addLast(new ServerHandler());
                                }
                            });

                    System.out.println("服务器 is ready...");

                    // 绑定一个端口并且同步,生成了一个ChannelFuture对象
                    ChannelFuture cf = bootstrap.bind(6668).sync();

                    // 对关闭通道进行监听
                    cf.channel().closeFuture().sync();
                } finally {
                    // 优雅关闭线程组
                    bossGroup.shutdownGracefully();
                    workerGroup.shutdownGracefully();
                }
            }
        }
    b.客户端
        import io.netty.bootstrap.Bootstrap;
        import io.netty.channel.ChannelFuture;
        import io.netty.channel.ChannelInitializer;
        import io.netty.channel.EventLoopGroup;
        import io.netty.channel.nio.NioEventLoopGroup;
        import io.netty.channel.socket.SocketChannel;
        import io.netty.channel.socket.nio.NioSocketChannel;
        import io.netty.handler.codec.string.StringDecoder;
        import io.netty.handler.codec.string.StringEncoder;

        public class NettyClient {

            public static void main(String[] args) throws Exception {
                // 创建EventLoopGroup,相当于线程池
                EventLoopGroup group = new NioEventLoopGroup();

                try {
                    // 创建客户端启动对象
                    Bootstrap bootstrap = new Bootstrap();

                    // 设置相关参数
                    bootstrap.group(group) // 设置线程组
                            .channel(NioSocketChannel.class) // 设置客户端通道实现类型
                            .handler(new ChannelInitializer<SocketChannel>() { // 设置处理器
                                @Override
                                protected void initChannel(SocketChannel ch) throws Exception {
                                    // 向管道加入处理器
                                    ch.pipeline().addLast(new StringDecoder());
                                    ch.pipeline().addLast(new StringEncoder());
                                    // 自定义的处理器
                                    ch.pipeline().addLast(new ClientHandler());
                                }
                            });

                    System.out.println("客户端 is ready...");

                    // 发起异步连接操作
                    ChannelFuture future = bootstrap.connect("127.0.0.1", 6668).sync();

                    // 发送消息
                    future.channel().writeAndFlush("Hello Server!");

                    // 对关闭通道进行监听
                    future.channel().closeFuture().sync();
                } finally {
                    group.shutdownGracefully(); // 优雅关闭线程组
                }
            }
        }

5.7 延迟任务:时间轮调度算法

01.延迟任务实现
    a.介绍
        在 Netty 中,我们需要使用 HashedWheelTimer 类来实现延迟任务
    b.代码
        public class DelayTaskExample {
            public static void main(String[] args) {
                System.out.println("程序启动时间:" + LocalDateTime.now());
                NettyTask();
            }

            private static void NettyTask() {
                // 创建延迟任务实例
                HashedWheelTimer timer = new HashedWheelTimer(3, // 间隔时间
                                                              TimeUnit.SECONDS, // 间隔时间单位
                                                              100); // 时间轮中的槽数
                // 创建任务
                TimerTask task = new TimerTask() {
                    @Override
                    public void run(Timeout timeout) throws Exception {
                        System.out.println("执行任务时间:" + LocalDateTime.now());
                    }
                };
                // 将任务添加到延迟队列中
                timer.newTimeout(task, 0, TimeUnit.SECONDS);
            }
        }
        -----------------------------------------------------------------------------------------------------
        以上程序的执行结果如下:
        程序启动时间:2024-06-04T10:16:23.033
        执行任务时间:2024-06-04T10:16:26.118
        从上述执行结果可以看出,我们使用 HashedWheelTimer 实现了延迟任务的执行。

02.时间轮调度算法
    a.定义
        概念1:使用时间轮算法可以实现【海量任务新增和取消任务的时间度为 O(1)】
        概念2:时间轮定时器最大的优势就是,任务的新增和取消都是 O(1) 时间复杂度,而且只需要【一个线程就可以驱动时间轮】进行工作
    b.HashedWheelTimer底层是【时间轮调度算法】
        那么问题来了,HashedWheelTimer 是如何实现延迟任务的?什么是时间轮调度算法?
        查看 HashedWheelTimer 类的源码会发现,其实它是底层是通过时间轮调度算法来实现的,
        以下是 HashedWheelTimer 核心实现源码(HashedWheelTimer 的创建源码)如下:
        -----------------------------------------------------------------------------------------------------
        private static HashedWheelBucket[] createWheel(int ticksPerWheel) {
        // 省略其他代码
        ticksPerWheel = normalizeTicksPerWheel(ticksPerWheel);
        HashedWheelBucket[] wheel = new HashedWheelBucket[ticksPerWheel];
        for (int i = 0; i < wheel.length; i ++) {
            wheel[i] = new HashedWheelBucket();
        }
        return wheel;
        }
        private static int normalizeTicksPerWheel(int ticksPerWheel) {
        int normalizedTicksPerWheel = 1;
        while (normalizedTicksPerWheel < ticksPerWheel) {
            normalizedTicksPerWheel <<= 1;
        }
        return normalizedTicksPerWheel;
        }
        private static final class HashedWheelBucket {
            private HashedWheelTimeout head;
            private HashedWheelTimeout tail;
            // 省略其他代码
        }
    c.分析源码
        在 HashedWheelTimer  中,使用了 HashedWheelBucket 数组实现时间轮的概念,每个 HashedWheelBucket
        表示时间轮中一个 slot(时间槽),HashedWheelBucket 内部是一个双向链表结构,双向链表的每个节点持有
        一个 HashedWheelTimeout 对象,HashedWheelTimeout 代表一个定时任务,每个 HashedWheelBucket
        都包含双向链表 head 和 tail 两个 HashedWheelTimeout 节点,这样就可以实现不同方向进行链表遍历
        -----------------------------------------------------------------------------------------------------
        时间轮算法的设计思想就来源于钟表,如上图所示,时间轮可以理解为一种环形结构,像钟表一样被分为多个 slot 槽位。每个 slot 代表一个时间段,每个 slot 中可以存放多个任务,使用的是链表结构保存该时间段到期的所有任务。时间轮通过一个时针随着时间一个个 slot 转动,并执行 slot 中的所有到期任务。
        任务的添加是根据任务的到期时间进行取模,然后将任务分布到不同的 slot 中。如上图所示,时间轮被划分为 8 个 slot,每个 slot 代表 1s,当前时针指向 2 时,假如现在需要调度一个 3s 后执行的任务,应该加入 2+3=5 的 slot 中;如果需要调度一个 12s 以后的任务,需要等待时针完整走完一圈 round 零 4 个 slot,需要放入第 (2+12)%8=6 个 slot。
        那么当时针走到第 6 个 slot 时,怎么区分每个任务是否需要立即执行,还是需要等待下一圈 round,甚至更久时间之后执行呢?所以我们需要把 round 信息保存在任务中。例如图中第 6 个 slot 的链表中包含 3 个任务,第一个任务 round=0,需要立即执行;第二个任务 round=1,需要等待 1*8=8s 后执行;第三个任务 round=2,需要等待 2×8=8s 后执行。所以当时针转动到对应 slot 时,只执行 round=0 的任务,slot 中其余任务的 round 应当减 1,等待下一个 round 之后执行。
        可以看出时间轮有点类似 HashMap,如果多个任务如果对应同一个 slot,处理冲突的方法采用的是拉链法。在任务数量比较多的场景下,适当增加时间轮的 slot 数量,可以减少时针转动时遍历的任务个数。
        时间轮定时器最大的优势就是,任务的新增和取消都是 O(1) 时间复杂度,而且只需要一个线程就可以驱动时间轮进行工作。