Java基础

月伴飞鱼 2024-06-23 15:20:26
编程语言
支付宝打赏 微信打赏

如果文章对你有帮助,欢迎点击上方按钮打赏作者!

不同版本JDK安装网站:https://adoptium.net/zh-CN/

继承,封装,多态

面对对象语言的特性:封装、继承、多态。

封装:

  • 封装也叫作信息隐藏或者数据访问保护。

  • 类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据。

继承:

  • 表示类之间的 is-a 关系。
  • 继承最大的一个好处就是代码复用。
  • 假如两个类有一些相同的属性和方法,就可以将这些相同的部分,抽取到父类中,让两个子类继承父类。
    • 这样,两个子类就可以重用父类中的代码,避免代码重复写多遍。

多态:

  • 子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。
  • 多态提高了代码的可扩展性和复用性。

值传递还是引用传递

Java中方法参数传递方式是按值传递。

如果参数是基本类型,传递的是基本类型的数据拷贝。

如果参数是引用类型,因为栈中存的是对象的地址值,所以传递的是该参量所引用的对象在堆中地址值的拷贝。

编译期与运行期

编译期是指把源码交给编译器编译成计算机可执行文件的过程,运行期是指把编译后的文件交给计算机执行,直到程序结束。

Java中就是把.java文件编译成.class文件,再把编译后的文件交给JVM加载执行。

image-20231016000034082

接口和抽象的区别

抽象类带来的很大的作用就是实现代码复用,比如当有多个子类有相同的属性或方法时,我们可以抽象出一个公共的类,然后子类继承这个抽象类,当然,这个公共类可以是普通父类,也可以是抽象类。

  • 但是抽象类中的方法可以交给子类去实现,让子类有不同的功能。

接口实现了约定和实现相分离,降低了代码间的耦合性,提高代码的可扩展性。

  • 调用者只需要关注抽象的接口,不需要了解具体的实现,具体的实现代码对调用者透明。

反射

在Java运行状态时,只要给定类的名字,就能知道这个类的所有信息,可以构造出指定对象,可以调用它的任意一个属性和方法。

  • 这种动态获取信息以及动态调用对象的方法的功能是反射机制。

关键字

Final

Final可以用来修饰变量、方法或者类。

修饰变量:

  • 这个变量一旦被赋值就不能被修改了,如果尝试给其赋值,会报编译错误。

修饰方法:

  • 该方法不可以被重写。

修饰类:

  • 这个类不可被继承

注意:Final修饰对象时,只是引用不可变,而对象本身的内容依然是可以变化的。

  • 这一点同样适用于数组。

Static

修饰类变量:

  • 如果该变量是 public 的话,表示该变量任何类都可以直接访问,而且无需初始化类,直接使用 类名.static 变量 访问。

修饰方法:

  • 代表该方法和当前类是无关的,任意类都可以直接访问(如果权限是 public 的话)。

  • 被 static 修饰的方法,在类初始化的时候并不会初始化,只有当自己被调用时,才会被执行。

修饰方法块:

  • 方法块(静态块)常常用于在类启动之前,初始化一些值。
  • 静态块只能调用同样被 static 修饰的变量,并且 static 的变量需要写在静态块的前面,不然编译会报错。

Transient

transient用来修饰类变量,意思是当前变量无需进行序列化。

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

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

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

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

静态初始化块:

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

与构造代码块的区别:

  • 构造代码块用于初始化对象,每创建一个对象就会被执行一次。
  • 静态代码块用于初始化类,随着类的加载而执行,不管创建几个对象,都只执行一次。
  • 静态代码块优先于构造代码块的执行。

执行顺序:

所有的静态初始化块都优先执行,其次才是非静态的初始化块和构造函数,它们的执行顺序是:

  • 父类的静态初始化块
  • 子类的静态初始化块
  • 父类的初始化块
  • 父类的构造函数
  • 子类的初始化块
  • 子类的构造函数

Long

Long 自己实现了一种缓存机制,缓存了从 -128 到 127 内的所有 Long 值,如果是这个范围内的 Long 值,就不会初始化,而是从缓存中拿,缓存初始化源码如下:

private static class LongCache {
    private LongCache(){}
    // 缓存,范围从 -128 到 127,+1 是因为有个 0
    static final Long cache[] = new Long[-(-128) + 127 + 1];
 
    // 容器初始化时,进行加载
    static {
        // 缓存 Long 值,注意这里是 i - 128 ,所以再拿的时候就需要 + 128
        for(int i = 0; i < cache.length; i++)
            cache[i] = new Long(i - 128);
    }
}

String

字符串是一个常量,一旦创建了一个 String 对象,就无法改变它的值,它的内容也就不可能发生变化(不考虑反射这种特殊行为)。

String 具备不变性背后的原因是什么:

public final class String
    implements Java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    //...
}

private final 的 char 数组,数组名字叫 value。

它存储着字符串的每一位字符,同时 value 数组是被 final 修饰的,这个 value 一旦被赋值,引用就不能修改了。

除了构造函数之外,并没有任何其他方法会修改 value 数组里面的内容,而且 value 的权限是 private,外部的类也访问不到,所以最终使得 value 是不可变的。

String 类是被 final 修饰的,所以这个 String 类是不会被继承的,因此没有任何人可以通过扩展或者覆盖行为来破坏 String 类的不变性。

String 不可变的好处

1、使用字符串常量池。

2、用作 HashMap 的 key。

3、缓存 HashCode。

4、线程安全

泛型

泛型就是在定义类、接口、方法的时候指定某一种特定类型,让类、接口、方法的使用者来决定具体用哪一种类型的参数。

泛型是在1.5引入的,只在编译期做泛型检查,运行期泛型就会消失,称为 泛型擦除,最终类型都会变成Object

泛型的本质是参数化类型,而类型擦除使得类型参数只存在于编译期,在运行时,JVM是并不知道泛型的存在的。

泛型好处

在编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的。

类型擦除

public static void main(String[] args) {
    List<String> list1=new ArrayList<String>();
    List<Integer> list2=new ArrayList<Integer>();
    System.out.println(list1.getClass()==list2.getClass());
}

ArrayList<String>ArrayList<Integer>在编译时是不同的类型,但是在编译完成后都被编译器简化成了ArrayList

为什么要进行泛型的类型擦除?

主要目的是避免过多的创建类而造成的运行时的过度消耗。

泛型类

public class GenericClass<T>{
    //成员变量
    private T t;
    
    public  void function(T t){

    }
    
    public T functionTwo(T t){
        //注意,这个不是泛型方法!!!
       return t;
    }
}

泛型接口

public interface GenericInterface<T> {
    
    public T get();
    
    public void set(T t);

    public T delete(T t);
    
    default T defaultFunction(T t){
        return t;
    }
}

泛型函数

public class GenericFunction {

    public <T> void function(T t) {
    }

    public <T> T functionTwo(T t) {
        return t;
    }

    public <T> String functionThree(T t) {
        return "";
    }
}

通配符

通配符是为了让Java泛型支持范围限定。

<?>:无界通配符,即类型不确定,任意类型。

<? extends T>:上边界通配符,即?是继承自T的任意子类型,遵守只读不写。

<? super T>:下边界通配符,即?T的任意父类型,遵守只写不读。

<? extends T>上边界通配符:

  • 不作为函数入参,只作为函数返回类型,比如List<? extends T>的使用add函数会编译不通过,get函数则没问题。

<? super T>下边界通配符:

  • 不作为函数返回类型,只作为函数入参,比如List<? super T>add函数正常调用,get函数也没问题,但只会返回Object,所以意义不大。

NIO

非阻塞模式

使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。

非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

通俗理解:

NIO 是可以做到用一个线程来处理多个操作的。

假设有 10000 个请求过来,根据实际情况,可以分配 50 或者 100 个线程来处理,不像之前的阻塞 IO 那样,非得分配 10000 个。

同步模式

同步与异步是基于应用程序和操作系统处理IO事件所采用的方式:

  • 同步:应用程序要直接参与IO读写的操作。

  • 异步:所有的IO操作交给操作系统去处理,应用程序只需要等待通知。

NIO需要线程来进行数据读写和事件处理,所以是同步模式。

ThreadLocal

ThreadLocal 提供了线程本地变量的实例,它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。

使用场景

ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。

ThreadLocal 用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景,避免了传参,类似于全局变量的概念。

关键属性

// threadLocalHashCode 表示当前 ThreadLocal 的 hashCode,用于计算当前 ThreadLocal 在 ThreadLocalMap 中的索引位置
private final int threadLocalHashCode = nextHashCode();
// 计算 ThreadLocal 的 hashCode 值(就是递增)
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// static + AtomicInteger 保证了在一台机器中每个 ThreadLocal 的 threadLocalHashCode 是唯一的
// 被 static 修饰非常关键,因为一个线程在处理业务的过程中,ThreadLocalMap 是会被 set 多个 ThreadLocal 的,多个 ThreadLocal 就依靠 threadLocalHashCode 进行区分
private static AtomicInteger nextHashCode = new AtomicInteger();

ThreadLocalMap

ThreadLocalMap 本身就是一个简单的 Map 结构,key 是 ThreadLocal,value 是 ThreadLocal 保存的值,底层是数组的数据结构。

static class ThreadLocalMap {
        // 数组中的每个节点值,WeakReference 是弱引用,当没有引用指向时,会直接被回收
        static class Entry extends WeakReference<ThreadLocal<?>> {
            // 当前 ThreadLocal 关联的值
            Object value;
            // WeakReference 的引用 referent 就是 ThreadLocal
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        // 数组的初始化大小
        private static final int INITIAL_CAPACITY = 16;
        // 存储 ThreadLocal 的数组
        private Entry[] table;
        // 扩容的阈值,默认是数组大小的三分之二
        private int threshold;
}

ThreadLocal 是如何做到线程之间数据隔离的

主要因为是 ThreadLocalMap 是线程的属性。

ThreadLocals.ThreadLocalMapInheritableThreadLocals.ThreadLocalMap 分别是线程的属性,所以每个线程的 ThreadLocals 都是隔离独享的。

父线程在创建子线程的情况下,会拷贝 inheritableThreadLocals 的值,但不会拷贝 threadLocals 的值。

set 方法

// set 操作每个线程都是串行的,不会有线程安全的问题
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 当前 thradLocal 之前有设置值,直接设置,否则初始化
    if (map != null)
        map.set(this, value);
    // 初始化ThreadLocalMap
    else
        createMap(t, value);
}
private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    // 计算 key 在数组中的下标,其实就是 ThreadLocal 的 hashCode 和数组大小-1取余
    int i = key.threadLocalHashCode & (len-1);
 
    // 整体策略:查看 i 索引位置有没有值,有值的话,索引位置 + 1,直到找到没有值的位置
    // 这种解决 hash 冲突的策略,也导致了其在 get 时查找策略有所不同,体现在 getEntryAfterMiss 中
    for (Entry e = tab[i];
         e != null;
         // nextIndex 就是让在不超过数组长度的基础上,把数组的索引位置 + 1
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        // 找到内存地址一样的 ThreadLocal,直接替换
        if (k == key) {
            e.value = value;
            return;
        }
        // 当前 key 是 null,说明 ThreadLocal 被清理了,直接替换掉
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 当前 i 位置是无值的,可以被当前 thradLocal 使用
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 当数组大小大于等于扩容阈值(数组大小的三分之二)时,进行扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

1、通过递增的 AtomicInteger 作为 ThreadLocal 的 hashCode 的;

2、计算数组索引位置的公式是:

  • hashCode 取模数组大小,由于 hashCode 不断自增,所以不同的 hashCode 大概率上会计算到同一个数组的索引位置(在实际项目中,ThreadLocal 都很少,基本上不会冲突)

3、通过 hashCode 计算的索引位置 i 处如果已经有值了,会从 i 开始,通过 +1 不断的往后寻找,直到找到索引位置为空的地方,把当前 ThreadLocal 作为 key 放进去。

get 方法

public T get() {
    // 因为 threadLocal 属于线程的属性,所以需要先把当前线程拿出来
    Thread t = Thread.currentThread();
    // 从线程中拿到 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 从 map 中拿到 entry,由于 ThreadLocalMap 在 set 时的 hash 冲突的策略不同,导致拿的时候逻辑也不太一样
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 如果不为空,读取当前 ThreadLocal 中保存的值
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 否则给当前线程的 ThreadLocal 初始化,并返回初始值 null
    return setInitialValue();
}
// 自旋 i+1,直到找到为止
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    // 在大量使用不同 key 的 ThreadLocal 时,其实还蛮耗性能的
    while (e != null) {
        ThreadLocal<?> k = e.get();
        // 内存地址一样,表示找到了
        if (k == key)
            return e;
        // 删除没用的 key
        if (k == null)
            expungeStaleEntry(i);
        // 继续使索引位置 + 1
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

扩容

ThreadLocalMap 中的 ThreadLocal 的个数超过阈值时,ThreadLocalMap 就要开始扩容了:

//扩容
private void resize() {
    // 拿出旧的数组
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    // 新数组的大小为老数组的两倍
    int newLen = oldLen * 2;
    // 初始化新数组
    Entry[] newTab = new Entry[newLen];
    int count = 0;
    // 老数组的值拷贝到新数组上
    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                // 计算 ThreadLocal 在新数组中的位置
                int h = k.threadLocalHashCode & (newLen - 1);
                // 如果索引 h 的位置值不为空,往后+1,直到找到值为空的索引位置
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                // 给新数组赋值
                newTab[h] = e;
                count++;
            }
        }
    }
    // 给新数组初始化下次扩容阈值,为数组长度的三分之二
    setThreshold(newLen);
    size = count;
    table = newTab;
}

1、扩容后数组大小是原来数组的两倍。

2、扩容时是没有线程安全问题的,因为 ThreadLocalMap 是线程的一个属性,一个线程同一时刻只能对 ThreadLocalMap 进行操作,因为同一个线程执行业务逻辑必然是串行的,那么操作 ThreadLocalMap 必然也是串行的。

内存泄漏

ThreadLocalMap的每个Entry都是一个对key的弱引用,同时每个Entry都包含了一个对value的强引用。

  • 正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了。

但是,如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,因为有以下的调用链:Thread ---> ThreadLocalMap ---> Entry(key为null) ---> value

因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能会出现OOM。

JDK已经考虑到了这个问题,所以在set,remove,rehash方法中会扫描key为null的Entry,并把对应的value设置为null,这样value对象就可以被回收。

如何避免内存泄露

调用remove方法,就会删除对应的Entry对象,可以避兔内存泄漏,所以使用完ThreadLocal之后,应该调用remove方法。

ThreadLocal<String> localName = new ThreadLocal();
try {
    localName.set("月伴飞鱼");
    // 其它业务逻辑
} finally {
    localName.remove();
}

空指针问题

ThreadLocal在进行get之前,必须先set,否则会报空指针异常。

public class ThreadLocalNPE {
    ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();
    public void set() {
        longThreadLocal.set(Thread.currentThread().getId());
    }
    //拆装箱问题
    public Long get() {//long:NullPointerException
        Long res = longThreadLocal.get();
        return res;
    }
 
    public static void main(String[] args) {
        ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
        System.out.println(threadLocalNPE.get());//NullPointerException
    }
}

如果在每个线程中ThreadLocal.set()进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的 ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。

InheritableThreadLocal

InheritableThreadLocal解决父子线程变量传递的问题。

  • 如果我在后面改了父线程,子线程不会更新它的本地变量Map

这个ThreadLocalMap的局部变量,实际作用是在子线程创建的时候:

  • 父线程会把threadLocal拷贝到子线程中。
public class InheritableThreadLocalSolution {

    // 使用 ThreadLocal 存储当前线程的用户名
    private static final ThreadLocal<String> userNameThreadLocal = new ThreadLocal<>();
    // 使用 InheritableThreadLocal 存储当前线程的用户名,以便子线程可以继承
    private static final InheritableThreadLocal<String> inheritableUserNameThreadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        // 设置父线程的 ThreadLocal 值
        userNameThreadLocal.set("Parent Thread");
        inheritableUserNameThreadLocal.set("Parent Thread (Inheritable)");

        // 创建子线程并启动
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.submit(() -> {
            // 尝试从子线程中获取父线程的 ThreadLocal 值(通常获取不到)
            System.out.println("From child thread (ThreadLocal): " + userNameThreadLocal.get());
            // 尝试从子线程中获取父线程的 InheritableThreadLocal 值
            System.out.println("From child thread (InheritableThreadLocal): " + inheritableUserNameThreadLocal.get());
        });
        executorService.shutdown();
    }
}

InheritableThreadLocal的内存泄漏

当移除父线程的threadlocal,子线程的threadlocal并不会消失。

  • 并且通常来讲子线程是放线程池管理的,不会随着父线程的消失而消失。

所以子线程的threadlocal就一直存在,但是已经用不到了,这就造成了内存泄漏了。

public class InheritableThreadLocalSolution {

    private static final InheritableThreadLocal<String> inheritableUserNameThreadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        inheritableUserNameThreadLocal.set("Parent Thread (Inheritable)");

        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.submit(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("From child thread (InheritableThreadLocal): " + inheritableUserNameThreadLocal.get());
        });

        TimeUnit.SECONDS.sleep(5);
        inheritableUserNameThreadLocal.remove();
        // 提交第二个任务,看看子线程是否还会获得 inheritableThreadLocal的值
        // 结果:会的
        executorService.submit(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            // 父线程以及移除了,但是子线程仍然能获取到
            System.out.println("second From child thread (InheritableThreadLocal): " + inheritableUserNameThreadLocal.get());
        });
        TimeUnit.SECONDS.sleep(10);
        executorService.shutdown();
    }
}

TransmittableThreadLocal

项目地址:https://gitee.com/mirrors/transmittable-thread-local

对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的。

  • 这时父子线程关系的ThreadLocal值传递已经没有意义
    • 需要把 任务提交给线程池时ThreadLocal值传递到 任务执行时

TransmittableThreadLocal解决线程池变量丢失问题。

  • 线程池会复用之前的线程,导致父线程的本地变量更新之后,之前创建的子线程拿不到这个值。

get/set 方法中完成了TransmittableThreadLocal的注册

  • 然后在执行run方法的时候通过TtlRunnable进行了方法包装

在调用之前进行快照形成,并应用快照到当前线程中

  • 最后在线程执行结束之后,run方法内部对线程局部变量做的修改则会被还原。

  • 通过将线程封装成TtlRunnable,然后通过快照还有hold一个总收集变量来解决。

集合类

HashMap

HashMap 底层的数据结构主要是:数组 + 链表 + 红黑树。

其中当链表的长度大于等于 8 时,链表会转化成红黑树,当红黑树的大小小于等于 6 时,红黑树会转化成链表。

图片描述

常见属性

 //初始容量为 16
 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
 
 //最大容量
 static final int MAXIMUM_CAPACITY = 1 << 30;
 
 //负载因子默认值
 static final float DEFAULT_LOAD_FACTOR = 0.75f;
 
 //桶上的链表长度大于等于8时,链表转化成红黑树
 static final int TREEIFY_THRESHOLD = 8;
 
 //桶上的红黑树大小小于等于6时,红黑树转化成链表
 static final int UNTREEIFY_THRESHOLD = 6;
 
 //当数组容量大于 64 时,链表才会转化成红黑树
 static final int MIN_TREEIFY_CAPACITY = 64;
 
 //记录迭代过程中 HashMap 结构是否发生变化,如果有变化,迭代时会 fail-fast
 transient int modCount;
 
 //HashMap 的实际大小,可能不准(因为当你拿到这个值的时候,可能又发生了变化)
 transient int size;
 
 //存放数据的数组
 transient Node<K,V>[] table;
 
 // 扩容的门槛,有两种情况
 // 如果初始化时,给定数组大小的话,通过 tableSizeFor 方法计算,数组大小永远接近于 2 的幂次方,比如你给定初始化大小 19,实际上初始化大小为 32,为 2 的 5 次方。
 // 如果是通过 resize 方法进行扩容,大小 = 数组容量 * 0.75
 int threshold;
 
 //链表的节点
 static class Node<K,V> implements Map.Entry<K,V> {
 
 //红黑树的节点
 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {

新增

1、空数组有无初始化,没有的话初始化。

2、如果通过 key 的 hash 能够直接找到值,跳转到 6,否则到 3。

3、如果 hash 冲突,两种解决方案:链表 or 红黑树。

4、如果是链表,递归循环,把新元素追加到队尾。

5、如果是红黑树,调用红黑树新增的方法。

6、通过 2、4、5 将新元素追加成功,再根据 onlyIfAbsent 判断是否需要覆盖。

7、判断是否需要扩容,需要扩容进行扩容,结束。

图片描述
// 入参 hash:通过 hash 算法计算出来的值。
// 入参 onlyIfAbsent:false 表示即使 key 已经存在了,仍然会用新值覆盖原来的值,默认为 false
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    // n 表示数组的长度,i 为数组索引下标,p 为 i 下标位置的 Node 值
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //如果数组为空,使用 resize 方法初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 如果当前索引位置是空的,直接生成新的节点在当前索引位置上
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 如果当前索引位置有值的处理方法,即我们常说的如何解决 hash 冲突
    else {
        // e 当前节点的临时变量
        Node<K,V> e; K k;
        // 如果 key 的 hash 和值都相等,直接把当前下标位置的 Node 值赋值给临时变量
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 如果是红黑树,使用红黑树的方式新增
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 是个链表,把新节点放到链表的尾端
        else {
            // 自旋
            for (int binCount = 0; ; ++binCount) {
                // e = p.next 表示从头开始,遍历链表
                // p.next == null 表明 p 是链表的尾节点
                if ((e = p.next) == null) {
                    // 把新节点放到链表的尾部 
                    p.next = newNode(hash, key, value, null);
                    // 当链表的长度大于等于 8 时,链表转红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);
                    break;
                }
                // 链表遍历过程中,发现有元素和新增的元素相等,结束循环
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                //更改循环的当前元素,使 p 在遍历过程中,一直往后移动。
                p = e;
            }
        }
        // 说明新节点的新增位置已经找到了
        if (e != null) {
            V oldValue = e.value;
            // 当 onlyIfAbsent 为 false 时,才会覆盖值 
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            // 返回老值
            return oldValue;
        }
    }
    // 记录 HashMap 的数据结构发生了变化
    ++modCount;
    //如果 HashMap 的实际大小大于扩容的门槛,开始扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

链表的新增

当链表长度大于等于 8 时,此时的链表就会转化成红黑树,转化的方法是:treeifyBin,此方法有一个判断,当链表长度大于等于 8,并且整个数组大小大于 64 时,才会转成红黑树,当数组大小小于 64 时,只会触发扩容,不会转化成红黑树。

JDK1.7中链表插入采用的是头插法,JDK1.8中插入使用的是尾插法。

  • 头插法:扩容时,扩容的逻辑会导致节点互相引用,导致死循环

为什么是8?

链表查询的时间复杂度是 O(n),红黑树的查询复杂度是 O(log(n))

在链表数据不多的时候,使用链表进行遍历也比较快,只有当链表数据比较多的时候,才会转化成红黑树,但红黑树需要的占用空间是链表的 2 倍,考虑到转化时间和空间损耗。

在考虑设计 8 这个值的时候,参考了泊松分布概率函数,由泊松分布中得出结论,链表各个长度的命中概率为:

* 0:    0.60653066
* 1:    0.30326533
* 2:    0.07581633
* 3:    0.01263606
* 4:    0.00157952
* 5:    0.00015795
* 6:    0.00001316
* 7:    0.00000094
* 8:    0.00000006

当链表的长度是 8 的时候,出现的概率是 0.00000006,不到千万分之一,所以说正常情况下,链表的长度不可能到达 8 ,而一旦到达 8 时,肯定是 hash 算法出了问题,所以在这种情况下,为了让 HashMap 仍然有较高的查询性能,所以让链表转化成红黑树。

红黑树新增节点过程

1、首先判断新增的节点在红黑树上是不是已经存在:

  • 如果节点没有实现 Comparable 接口,使用 equals 进行判断。

  • 如果节点自己实现了 Comparable 接口,使用 compareTo 进行判断。

2、新增的节点如果已经在红黑树上,直接返回;不在的话,判断新增节点是在当前节点的左边还是右边,左边值小,右边值大。

3、自旋递归 1 和 2 步,直到当前节点的左边或者右边的节点为空时,停止自旋,当前节点即为我们新增节点的父节点。

4、把新增节点放到当前节点的左边或右边为空的地方,并于当前节点建立父子节点关系。

5、进行着色和旋转,结束。

扩容机制

扩容阈值:

  • 阈值 = 容量 x 负载因子,假设当前 HashMap的容量是 16,负载因子是默认值 0.75,当 size 到达 16 x 0.75= 12 的时候,就会触发扩容。

查找

根据 hash 算法定位数组的索引位置,equals 判断当前节点是否是我们需要寻找的 key,是的话直接返回,不是的话往下。

判断当前节点有无 next 节点,有的话判断是链表类型,还是红黑树类型。

分别走链表和红黑树不同类型的查找方法。

// 采用自旋方式从链表中查找 key,e 初始为为链表的头节点
do {
    // 如果当前节点 hash 等于 key 的 hash,并且 equals 相等,当前节点就是我们要找的节点
    // 当 hash 冲突时,同一个 hash 值上是一个链表的时候,我们是通过 equals 方法来比较 key 是否相等的
    if (e.hash == hash &&
        ((k = e.key) == key || (key != null && key.equals(k))))
        return e;
    // 否则,把当前节点的下一个节点拿出来继续寻找
} while ((e = e.next) != null);

HashMap为什么是线程不安全的

同时 put 碰撞导致数据丢失。

  • 多个线程同时使用 put 来添加元素,而且两个 put 的 key 是一样的,它们发生了碰撞,这样最终就只会保留一个数据。

可见性问题无法保证。

  • 如果线程 1 给某个 key 放入了一个新值,那么线程 2 在获取对应的 key 的值的时候,它的可见性是无法保证的。

死循环造成 CPU 100%:

  • 扩容的时候,会反转散列桶中的节点顺序,当有多个线程同时进行扩容的时候,如果两个线程同时反转的话,便可能形成一个循环。

ArrayList

ArrayList整体架构是一个数组结构:

image-20231016184326651

基本概念:

DEFAULT_CAPACITY:表示数组的初始大小,默认是 10。

size:表示当前数组的大小,类型是 int,没有使用 volatile 修饰,非线程安全。

modCount:统计当前数组被修改的版本次数,数组结构有变动,就会加 1。

初始化

ArrayList无参构造器初始化时,默认大小是空数组,并不是默认的DEFAULT_CAPACITY 10,10 是在第一次 add 时扩容的值。

新增

判断是否需要扩容,如果需要则执行扩容操作。

赋值新元素。

public boolean add(E e) {
    // 确保数组大小是否足够,不够则执行扩容,size 为当前数组的大小
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 直接赋值
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

// 计算 capacity
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果数组为空,则返回默认容量(10) 和最小容量(比如初始化 List add 一个 element 时 minCapacity 为 1)之间的 max 值
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

// 确保容量足够
private void ensureExplicitCapacity(int minCapacity) {
    // 记录数组被修改 
    modCount++;

    // 如果我们期望的最小容量大于目前数组的长度,则执行扩容操作
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

// 扩容,并把现有数据拷贝到新的数组中去
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // 新的容量为旧的容量的 1.5 倍(capacity >> 1  ==> capacity / 2)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 如果新的容量小于我们期望值 minCapacity,则将新的容量值赋值为期望值
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 如果新的容量大于 JVM 所能分配的数组的最大值,那么就用 Integer 的最大值
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 通过复制进行扩容
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

扩容的规则并不是翻倍,而是 原来容量大小 + 原来容量大小的一半(原来容量的 1.5 倍)。

ArrayList 中数组的最大容量是 Integer.MAX_VALUE,超过这个值,JVM 就不会给数组分配内存空间了。

新增时,并没有对值进行严格的校验,所以 ArrayList 是允许 null 值的。

扩容

扩容会先新建一个符合我们预期容量 newCapacity 的新数组,然后把旧数组的数据拷贝过去。

public static int[] copyOf(int[] original, int newLength) {
    int[] copy = new int[newLength];
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;
}

public static native void arraycopy(Object src,  int  srcPos, Object dest, int destPos, int length);

线程安全

只有当 ArrayList 作为共享变量时,才会有线程安全问题,当 ArrayList 是方法内部局部变量时,是没有线程安全问题的。

本质是因为 ArrayList 自身的 elementData、size、modCount 在进行各种操作时,都没有加锁,而且这些变量的类型并非是可见的(volatile)的,所以如果多个线程对这些变量进行操作时,可能会有值被覆盖的情况。

ArrayList和LinkedList使用不当,性能差距会如此之大

LinkedList

LinkedList 适用于要求有顺序、并且会按照顺序进行迭代的场景,底层数据结构是一个双向链表:

image-20231015001829797

链表每个节点我们叫做 Node,Node 有 prev 属性,代表前一个节点的位置,next 属性,代表后一个节点的位置。

first 是双向链表的头节点,它的前一个节点是 null。

last 是双向链表的尾节点,它的后一个节点是 null。

当链表中没有数据时,first 和 last 是同一个节点,前后指向都是 null。

因为是个双向链表,只要机器内存足够强大,是没有大小限制的。

    transient Node<E> first;//第一个节点
    transient Node<E> last;//最后一个节点
private static class Node<E> {
    E item;// 节点值
    Node<E> next; // 指向的下一个节点
    Node<E> prev; // 指向的前一个节点

    // 初始化参数顺序分别是:前一个节点、本身节点值、后一个节点
    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

追加(新增)

追加节点时,add 方法默认是从尾部开始追加,addFirst 方法是从头部开始追加。

尾部追加节点比较简单,只需要简单地把指向位置修改下即可。

// 从尾部开始追加节点
public void addLast(E e) {
        linkLast(e);
    }
void linkLast(E e) {
    // 把尾节点数据暂存
    final Node<E> l = last;
    // 新建新的节点,初始化入参含义:
    // l 是新节点的前一个节点,当前值是尾节点值
    // e 表示当前新增节点,当前新增节点后一个节点是 null
    final Node<E> newNode = new Node<>(l, e, null);
    // 新建节点追加到尾部
    last = newNode;
    //如果链表为空(l 是尾节点,尾节点为空,链表即空),头部和尾部是同一个节点,都是新建的节点
    if (l == null)
        first = newNode;
    //否则把前尾节点的下一个节点,指向当前尾节点。
    else
        l.next = newNode;
    //大小和版本更改
    size++;
    modCount++;
}

节点删除

节点删除的方式和追加类似,可以选择从头部删除,也可以选择从尾部删除,删除操作会把节点的值,前后指向节点都置为 null,帮助 GC 进行回收。

链表结构的节点新增、删除都非常简单,仅仅把前后节点的指向修改下就好了,所以 LinkedList 新增和删除速度很快。

public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}
//从头删除节点 f 是链表头节点
private E unlinkFirst(Node<E> f) {
    // 拿出头节点的值,作为方法的返回值
    final E element = f.item;
    // 拿出头节点的下一个节点
    final Node<E> next = f.next;
    //帮助 GC 回收头节点
    f.item = null;
    f.next = null;
    // 头节点的下一个节点成为头节点
    first = next;
    //如果 next 为空,表明链表为空
    if (next == null)
        last = null;
    //链表不为空,头节点的前一个节点指向 null
    else
        next.prev = null;
    //修改链表大小和版本
    size--;
    modCount++;
    return element;
}

节点查询

链表查询某一个节点是比较慢的,需要挨个循环查找。

LinkedList 并没有采用从头循环到尾的做法,而是采取了简单二分法,首先看看 index 是在链表的前半部分,还是后半部分。

如果是前半部分,就从头开始寻找,反之亦然。通过这种方式,使循环的次数至少降低了一半,提高了查找的性能。

// 根据链表索引位置查询节点
Node<E> node(int index) {
    // 如果 index 处于队列的前半部分,从头开始找,size >> 1 是 size 除以 2 的意思。
    if (index < (size >> 1)) {
        Node<E> x = first;
        // 直到 for 循环到 index 的前一个 node 停止
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {// 如果 index 处于队列的后半部分,从尾开始找
        Node<E> x = last;
        // 直到 for 循环到 index 的后一个 node 停止
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

HashSet

HashSet原理:使用的就是组合 HashMap。

  • 把 HashMap 当作自己的一个局部变量。
// 把 HashMap 组合进来,key 是 Hashset 的 key,value 是下面的 PRESENT
private transient HashMap<E,Object> map;
// HashMap 中的 value
private static final Object PRESENT = new Object();

1、在使用 HashSet 时,比如 add 方法,只有一个入参,但组合的 Map 的 add 方法却有 key,value 两个入参,相对应上 Map 的 key 就是我们 add 的入参,value 就是第二行代码中的 PRESENT,用一个默认值 PRESENT 来代替 Map 的 Value;

2、如果 HashSet 是被共享的,当多个线程访问的时候,就会有线程安全问题,因为在后续的所有操作中,并没有加锁。

TreeSet

TreeSet底层组合的是 TreeMap,所以继承了 TreeMap key 能够排序的功能,迭代的时候,也可以按照 key 的排序顺序进行迭代。

TreeMap

TreeMap 底层的数据结构就是红黑树,和 HashMap 的红黑树结构一样。

不同的是:TreeMap 利用了红黑树左节点小,右节点大的性质,根据 key 进行排序,使每个元素能够插入到红黑树大小适当的位置,维护了 key 的大小关系,适用于 key 需要排序的场景。

因为底层使用的是平衡红黑树的结构,所以 containsKey、get、put、remove 等方法的时间复杂度都是 log(n)

TreeMap 常见的属性有:

//比较器,如果外部有传进来 Comparator 比较器,首先用外部的
//如果外部比较器为空,则使用 key 自己实现的 Comparable#compareTo 方法
//比较手段和上面日常工作中的比较 demo 是一致的
private final Comparator<? super K> comparator;
 
//红黑树的根节点
private transient Entry<K,V> root;
 
//红黑树的已有元素大小
private transient int size = 0;
 
//树结构变化的版本号,用于迭代过程中的快速失败场景
private transient int modCount = 0;
 
//红黑树的节点
static final class Entry<K,V> implements Map.Entry<K,V> {}

新增节点

判断红黑树的节点是否为空,为空的话,新增的节点直接作为根节点:

Entry<K,V> t = root;
//红黑树根节点为空,直接新建
if (t == null) {
    // compare 方法限制了 key 不能为 null
    compare(key, key); // type (and possibly null) check
    // 成为根节点
    root = new Entry<>(key, value, null);
    size = 1;
    modCount++;
    return null;
}

根据红黑树左小右大的特性,进行判断,找到应该新增节点的父节点:

Comparator<? super K> cpr = comparator;
if (cpr != null) {
    //自旋找到 key 应该新增的位置,就是应该挂载那个节点的头上
    do {
        //一次循环结束时,parent 就是上次比过的对象
        parent = t;
        // 通过 compare 来比较 key 的大小
        cmp = cpr.compare(key, t.key);
        //key 小于 t,把 t 左边的值赋予 t,因为红黑树左边的值比较小,循环再比
        if (cmp < 0)
            t = t.left;
        //key 大于 t,把 t 右边的值赋予 t,因为红黑树右边的值比较大,循环再比
        else if (cmp > 0)
            t = t.right;
        //如果相等的话,直接覆盖原值
        else
            return t.setValue(value);
        // t 为空,说明已经到叶子节点了
    } while (t != null);
}

在父节点的左边或右边插入新增节点:

//cmp 代表最后一次对比的大小,小于 0 ,代表 e 在上一节点的左边
if (cmp < 0)
    parent.left = e;
//cmp 代表最后一次对比的大小,大于 0 ,代表 e 在上一节点的右边,相等的情况第二步已经处理了。
else
    parent.right = e;

1、着色旋转,达到平衡,结束。

2、新增节点时,就是利用了红黑树左小右大的特性,从根节点不断往下查找,直到找到节点是 null 为止,节点为 null 说明到达了叶子结点。

3、查找过程中,发现 key 值已经存在,直接覆盖。

4、TreeMap 是禁止 key 是 null 值的。

LinkedHashMap

LinkedHashMap 是继承 HashMap 的,所以它拥有 HashMap 的所有特性,再此基础上,还提供了两大特性:

  • 按照插入顺序进行访问。
  • 实现了访问最少最先删除功能,其目的是把很久都没有访问的 key 自动删除。

LinkedHashMap链表结构

// 链表头
transient LinkedHashMap.Entry<K,V> head;
 
// 链表尾
transient LinkedHashMap.Entry<K,V> tail;
 
// 继承 Node,为数组的每个元素增加了 before 和 after 属性
static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}
 
// 控制两种访问模式的字段,默认 false
// true 按照访问顺序,会把经常访问的 key 放到队尾
// false 按照插入顺序提供访问
final boolean accessOrder;

LinkedHashMap 的数据结构很像是把 LinkedList 的每个元素换成了 HashMap 的 Node,像是两者的结合体,也正是因为增加了这些结构,从而能把 Map 的元素都串联起来,形成一个链表,而链表就可以保证顺序了,就可以维护元素插入进来的顺序。

如何按照顺序新增

LinkedHashMap 初始化时,默认 accessOrder 为 false,就是会按照插入顺序提供访问,插入方法使用的是父类 HashMap 的 put 方法,不过覆写了 put 方法执行中调用的 newNode/newTreeNode 和 afterNodeAccess 方法。

newNode/newTreeNode 方法,控制新增节点追加到链表的尾部,这样每次新节点都追加到尾部,即可保证插入顺序了。

// 新增节点,并追加到链表的尾部
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    // 新增节点
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    // 追加到链表的尾部
    linkNodeLast(p);
    return p;
}
// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    // 新增节点等于位节点
    tail = p;
    // last 为空,说明链表为空,首尾节点相等
    if (last == null)
        head = p;
    // 链表有数据,直接建立新增节点和上个尾节点之间的前后关系即可
    else {
        p.before = last;
        last.after = p;
    }
}

LinkedHashMap 通过新增头节点、尾节点,给每个节点增加 before、after 属性,每次新增时,都把节点追加到尾节点等手段,在新增的时候,就已经维护了按照插入顺序的链表结构了。

IO模型

同步阻塞

用户空间的应用程序执行一个系统调用,这会导致应用程序阻塞,直到数据准备好,并且将数据从内核复制到用户进程,最后进程再处理数据,在等待数据到处理数据的两个阶段,整个进程都被阻塞,不能处理别的网络IO。

图片

同步非阻塞

非阻塞的系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。

进程在返回之后,可以干点别的事情,然后再发起系统调用。

重复上面的过程,循环往复的进行系统调用,这个过程通常被称之为轮询。

轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。

需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。

图片

IO多路复用

IO多路复用,这是一种进程预先告知内核的能力,让内核发现进程指定的一个或多个IO条件就绪了,就通知进程。

IO复用的实现方式目前主要有Select、Poll和Epoll。

图片

信号驱动

首先允许Socket进行信号驱动IO,并安装一个信号处理函数,进程继续运行并不阻塞。

当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。

此种IO方式存在的一个很大的问题:

  • Linux中信号队列是有限制的,如果超过这个数字问题就无法读取数据。
图片

异步非阻塞

当用户线程进行了系统调用,立刻就可以开始去做其它的事,用户线程不阻塞。

  • 准备数据:当内核一直等到数据准备好了,它就会将数据从内核内核缓冲区,拷贝到用户缓冲区。
  • 内核会给用户线程发送一个信号,或者回调用户线程注册的回调接口,告诉用户线程操作完成了。
  • 用户线程读取用户缓冲区的数据,完成后续的业务操作。

信号驱动IO,异步IO的主要区别在于:

  • 信号驱动由内核告诉我们何时可以开始一个IO操作(数据在内核缓冲区中)。
  • 而异步IO则由内核通知IO操作何时已经完成(数据已经在用户空间中)。
图片

BIO,NIO,AIO的区别:

BIO:

  • 同步并阻塞,服务实现模式为一个连接对应一个线程,即客户端发送一个连接,服务端要有一个线程来处理。
  • 如果连接多了,线程数量不够,就只能等待,即会发生阻塞。
  • 适用连接数目比较小且固定的架构。

NIO:

  • 同步非阻塞,服务实现模式是一个线程可以处理多个连接,即客户端发送的连接都会注册到多路复用器上,然后进行轮询连接,有I/O请求就处理。
  • 适用连接数目多且连接比较短的架构,如:聊天服务器,弹幕系统等,编程比较复杂。

AIO:

  • 异步非阻塞,引入了异步通道,采用的是Proactor模式。
  • 适用连接数目多且连接长的架构,如相册服务器。

SPI机制

SPI 全称为 Service Provider Interface,是一种服务发现机制。

它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。

这一机制为很多框架扩展提供了可能,比如在Dubbo、JDBC中都使用到了SPI机制。

image-20220813151427856

具体使用

定义一个接口,SPIService

public interface SPIService {
    void execute();
}

定义两个实现类

public class SpiImpl1 implements SPIService{
    public void execute() {
        System.out.println("SpiImpl1.execute()");
    }
}

public class SpiImpl2 implements SPIService{
    public void execute() {
        System.out.println("SpiImpl2.execute()");
    }
}

在ClassPath路径下配置添加一个文件。

文件名字是接口的全限定类名,内容是实现类的全限定类名,多个实现类用换行符分隔。

com.viewscenes.netsupervisor.spi.SpiImpl1
com.viewscenes.netsupervisor.spi.SpiImpl2

通过ServiceLoader.load或者Service.providers方法拿到实现类的实例。

其中,Service.providers包位于sun.misc.Service,而ServiceLoader.load包位于java.util.ServiceLoader

public class Test {
    public static void main(String[] args) {    
        Iterator<SPIService> providers = Service.providers(SPIService.class);
        ServiceLoader<SPIService> load = ServiceLoader.load(SPIService.class);

        while(providers.hasNext()) {
            SPIService ser = providers.next();
            ser.execute();
        }
        System.out.println("--------------------------------");
        Iterator<SPIService> iterator = load.iterator();
        while(iterator.hasNext()) {
            SPIService ser = iterator.next();
            ser.execute();
        }
    }
}

基本原理:通过反射的方式,创建实现类的实例并返回。

新特性

JDK 17

Sealed类:

Sealed类是一种新的类修饰符,用于限制类的继承

  • 可以控制哪些类可以继承自它,这样可以使得代码更加安全、可维护

Pattern Matching for Switch:

新的Switch语法,可以用于模式匹配

  • 可以根据不同的模式执行不同的操作,从而使得代码更加简洁、易读、易维护

可以减少代码量,避免出现大量的if-else语句

改进的垃圾回收器(ZGC(新型垃圾收集器)):

改进了垃圾回收器,提高了垃圾回收的效率和吞吐量

  • 可以更加高效地回收内存,从而提高应用程序的性能和响应速度

风格的内存管理:

引入了C++风格的内存管理,包括对堆内存分配的优化和对垃圾回收的改进

  • C++风格的内存管理可以使得Java应用程序更加高效,从而提高应用程序的性能和响应速度

JDK21

序列化集合接口:

新增序列集合接口 SequencedCollection

  • 常用的 ArrayListLinkedList等都实现了这个接口

ZGC增加分代:

增加了分代功能,比如 CMS 收集器区分老年代和年轻代

  • 这样一来,可以更频繁的回收年轻代

虚拟线程(协程):

虚拟线程可以看作是一种用户级线程,与操作系统的线程或进程不同

  • 它是由编程语言或库提供的,而不是由操作系统管理的
支付宝打赏 微信打赏

如果文章对你有帮助,欢迎点击上方按钮打赏作者!