当前位置: 首页 > news >正文

Java基础知识点汇总(五)

一、限定通配符和非限定通配符

在Java泛型中,通配符(Wildcard)用于表示“未知类型”,解决泛型类型的灵活性问题。根据是否对类型范围进行约束,分为限定通配符非限定通配符,核心区别在于是否限制了匹配的类型范围。

1、非限定通配符(Unbounded Wildcard):?

  • 定义? 表示“任意引用类型”,没有任何继承或实现的约束,可匹配所有类型(如 StringIntegerObject 等)。

  • 适用场景:仅需要操作“所有类型共有的行为”(如 Object 类的方法),无需关注具体类型是什么。

  • 特点

    • 只能“读取”数据,且读取的元素类型被限定为 Object(因为无法确定具体类型)。
    • 不能“写入”数据(除了 null,因为无法确定要写入的类型是否匹配)。
  • 示例
    定义一个打印任意泛型列表元素的方法,无需关心列表中存储的具体类型:

    import java.util.Arrays;
    import java.util.List;public class WildcardExample {// 非限定通配符:可接收 List<String>、List<Integer> 等任意泛型列表public static void printList(List<?> list) {for (Object obj : list) {System.out.println(obj); // 仅调用 Object 类的方法}}public static void main(String[] args) {printList(Arrays.asList("苹果", "香蕉")); // 匹配 List<String>printList(Arrays.asList(10, 20, 30));   // 匹配 List<Integer>}
    }
    

2、限定通配符(Bounded Wildcard):? extends T? super T
限定通配符通过“上界”或“下界”约束匹配的类型范围,确保操作的类型安全。

2.1. 上界限定通配符:? extends T

  • 定义:表示“T 及其所有子类/实现类”,仅匹配继承自 T 的类型。

  • 适用场景:需要“读取”数据(作为“生产者”提供数据),且只能读取 T 类型或其子类型的元素。

  • 特点

    • 可以安全“读取”数据,读取的元素类型为 T(因为子类对象可向上转型为父类)。
    • 不能“写入”数据(除了 null),因为无法确定列表实际存储的是 T 的哪个子类。
  • 示例
    计算任意数字列表的总和(IntegerDouble 等均继承自 Number):

    import java.util.Arrays;
    import java.util.List;public class BoundedWildcardExample {// 上界限定:仅接收 Number 及其子类(如 Integer、Double)的列表public static double sum(List<? extends Number> numbers) {double total = 0;for (Number num : numbers) {total += num.doubleValue(); // 调用 Number 类的方法}return total;}public static void main(String[] args) {System.out.println(sum(Arrays.asList(1, 2, 3)));      // 合法:List<Integer>System.out.println(sum(Arrays.asList(1.5, 2.5)));     // 合法:List<Double>// sum(Arrays.asList("1", "2")); // 编译错误:String 不是 Number 的子类}
    }
    

2.2. 下界限定通配符:? super T

  • 定义:表示“T 及其所有父类/超类”,仅匹配 T 的父类型。

  • 适用场景:需要“写入”数据(作为“消费者”接收数据),且只能写入 T 类型或其子类型的元素。

  • 特点

    • 可以安全“写入”数据,写入的元素必须是 T 或其子类型(子类对象可向上转型为父类)。
    • 读取数据时,类型被限定为 Object(因为无法确定父类的具体类型)。
  • 示例
    向存储 Person 及其父类的列表中添加 StudentStudent 继承自 Person):

    import java.util.ArrayList;
    import java.util.List;class Person {}
    class Student extends Person {}public class SuperWildcardExample {// 下界限定:仅接收 Person 及其父类(如 Object)的列表public static void addStudent(List<? super Person> list) {list.add(new Student()); // 合法:Student 是 Person 的子类}public static void main(String[] args) {addStudent(new ArrayList<Person>());  // 合法:List<Person>addStudent(new ArrayList<Object>());  // 合法:List<Object>(Object 是 Person 的父类)// addStudent(new ArrayList<Student>()); // 编译错误:Student 是 Person 的子类,不是父类}
    }
    

3、核心区别总结

类型语法匹配范围读写特性典型用途
非限定通配符?任意类型只能读(Object),几乎不能写通用操作(如打印)
上界限定通配符? extends TT 及其子类只能读(T),不能写读取/消费数据(生产者)
下界限定通配符? super TT 及其父类可以写(T或子类),读为Object写入/生成数据(消费者)

简单来说,非限定通配符追求“最大灵活性”,而限定通配符通过约束类型范围确保“操作安全性”。实际开发中,可遵循PECS原则选择:

  • 生产者(Producer):需要读取数据时用 ? extends T(Provider Extends);
  • 消费者(Consumer):需要写入数据时用 ? super T(Consumer Super)。

二、序列化、反序列化是什么?

序列化(Serialization)和反序列化(Deserialization)是 Java 中用于处理对象持久化和网络传输的核心机制:

1. 序列化(Serialization)
内存中的对象转换为可存储或传输的二进制流(字节序列) 的过程。
简单说,就是把对象的状态(字段值、类型信息等)"冻结"成一串字节,以便:

  • 保存到文件、数据库等存储介质中(持久化)
  • 通过网络传输到其他进程或服务器

2. 反序列化(Deserialization)
序列化产生的二进制流重新转换为内存中的对象的过程。
即把之前"冻结"的字节序列"解冻",恢复成原来的对象结构和数据。

为什么需要序列化?

  • 跨平台/跨进程通信:网络传输只能传递字节流,序列化让对象可以在不同JVM之间传递。
  • 持久化存储:将对象保存到文件或数据库,下次可以通过反序列化恢复。
  • 分布式计算:在分布式系统中,对象需要在不同节点间传递。

Java中的实现方式
一个类要支持序列化,需满足:

  1. 实现 java.io.Serializable 接口(这是一个标记接口,无需实现任何方法)。
  2. 通常会显式声明 serialVersionUID(如前所述,用于版本控制)。

示例代码

import java.io.*;// 实现Serializable接口,支持序列化
class User implements Serializable {private static final long serialVersionUID = 1L;private String name;private int age;// 构造方法、getter、setter省略
}public class SerializationDemo {public static void main(String[] args) throws Exception {// 创建对象User user = new User("Alice", 25);// 序列化:将对象写入文件try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.ser"))) {out.writeObject(user); // 序列化对象}// 反序列化:从文件恢复对象try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.ser"))) {User restoredUser = (User) in.readObject(); // 反序列化System.out.println(restoredUser.getName()); // 输出 "Alice"}}
}

核心注意点

  • 静态字段(static)不会被序列化(它们属于类,而非对象实例)。
  • transient 修饰的字段不会被序列化(如敏感信息)。
  • 反序列化时不会调用对象的构造方法,而是直接恢复字段值。
  • 序列化版本不兼容(serialVersionUID 不匹配)会导致反序列化失败。

简单理解:序列化是"存档",反序列化是"读档",让对象可以脱离内存长期存在或跨环境移动。

三、serialVersionUID是什么?

在Java中,serialVersionUID是一个用于序列化和反序列化过程中的版本控制标识,它是一个静态常量,类型为long

当一个类实现了Serializable接口(用于支持对象的序列化)时,Java虚拟机会自动为该类生成一个serialVersionUID。这个值是根据类的结构(如类名、方法、字段等)通过特定算法计算得出的。

指定serialVersionUID的主要原因如下:

  1. 保证版本兼容性
    如果不手动指定serialVersionUID,当类的结构发生变化(如添加/删除字段、修改方法等)时,Java会重新计算serialVersionUID。此时,用旧版本类序列化的对象,在使用新版本类反序列化时会抛出InvalidClassException,因为 serialVersionUID 不匹配。

  2. 控制反序列化行为
    当手动指定了serialVersionUID后,即使类的结构发生变化,只要serialVersionUID保持不变,Java就会尝试将旧版本对象反序列化到新版本类中(会按照一定规则处理字段的增减),提高了类的版本兼容性。

示例代码:

import java.io.Serializable;public class User implements Serializable {// 手动指定serialVersionUID,一般我们都直接设置为1Lprivate static final long serialVersionUID = 1L;private String name;private int age;// 构造方法、getter、setter等
}

简单来说,指定serialVersionUID是为了在类的结构发生合理变化时,仍然能够正确地反序列化历史版本的对象,增强了序列化机制的灵活性和兼容性。

如果不指定serialVersionUID会发生什么?
如果不手动指定 serialVersionUID,Java 虚拟机会在编译时自动生成一个,其值基于类的结构信息(如类名、字段、方法、接口等)通过特定算法计算得出。这种自动生成的机制可能导致以下问题:

  1. 类结构变化时反序列化失败
    当类的结构发生任何修改(例如添加/删除字段、修改方法参数、甚至修改注释以外的任何代码),Java 会重新计算并生成一个新的 serialVersionUID。此时:

    • 用旧版本类序列化的对象,在使用新版本类反序列化时,会因为 serialVersionUID 不匹配而抛出 InvalidClassException
    • 这意味着类的微小改动可能导致历史序列化数据完全无法使用
  2. 不同环境可能生成不同值
    自动生成 serialVersionUID 的算法可能依赖于编译器实现或 JVM 版本。同一类在不同编译器(如 javac、Eclipse 编译器)或不同 JDK 版本中编译时,可能生成不同的 serialVersionUID,导致跨环境反序列化失败。

  3. 缺乏版本控制主动权
    不指定 serialVersionUID 时,开发者无法自主控制类的版本兼容性。即使类的改动是向后兼容的(如仅添加一个可选字段),也可能因为自动生成的 serialVersionUID 变化而导致反序列化失败。

示例场景
假设最初有一个类:

import java.io.Serializable;public class User implements Serializable {private String name;// 未指定serialVersionUID
}

用这个类序列化一个对象后,若后来给类添加一个字段:

public class User implements Serializable {private String name;private int age; // 新增字段// 未指定serialVersionUID
}

此时自动生成的 serialVersionUID 已改变,用新版本类反序列化旧对象时会抛出异常。

因此,手动指定 serialVersionUID 是保证序列化兼容性的最佳实践,它让开发者可以自主决定类的版本是否兼容,而非依赖编译器的自动生成机制。

对于不希望序列化的字段——使用 transient 关键字:
这是最常用的方式,在字段声明前添加 transient 修饰符,该字段就会被排除在序列化过程之外。

示例代码:

import java.io.Serializable;public class User implements Serializable {private static final long serialVersionUID = 1L;private String name;       // 会被序列化private transient int age; // 不会被序列化(transient修饰)private transient String password; // 敏感信息通常不序列化
}

特点

  • transient 修饰的字段,在序列化时会被忽略
  • 反序列化时,该字段会被赋值为默认值(如 int 为 0,对象为 null
  • 只能修饰非静态字段(静态字段本身就不会被序列化)

四、进程和线程的区别?

在Java中,进程(Process)线程(Thread) 是操作系统中两个核心的执行单元,它们既有联系又有显著区别,主要体现在资源占用、独立性、通信方式等方面:

1. 定义与本质

  • 进程:是操作系统进行资源分配(如内存、文件句柄、网络端口等)的基本单位,是一个独立运行的程序实例。例如,运行一个Java程序(java MyClass)会启动一个独立进程。
  • 线程:是进程内的执行单元,是CPU调度的基本单位。一个进程可以包含多个线程,它们共享进程的资源,共同完成程序的任务。例如,Java程序中通过new Thread()创建的就是线程。

2. 核心区别

对比维度进程线程
资源占用占用独立的内存空间、文件描述符等,资源消耗大。共享所属进程的资源(内存、文件句柄等),资源消耗小。
独立性进程间相互独立,一个进程崩溃不影响其他进程。线程属于同一进程,一个线程崩溃可能导致整个进程崩溃。
通信方式进程间通信(IPC)复杂,需通过管道、socket、共享内存等。线程间通信简单,可通过共享变量(需处理同步)实现。
切换开销进程切换需要保存和恢复整个进程的状态,开销大。线程切换只需保存线程的局部状态(如程序计数器、栈),开销小。
创建/销毁成本成本高(需分配独立资源)。成本低(共享进程资源)。

3. Java中的体现

  • 进程:每个Java程序运行在独立的JVM进程中,拥有自己的堆内存、方法区等。例如,同时启动两个java -jar程序,会产生两个独立进程,它们的内存空间完全隔离。

  • 线程:Java中通过Thread类或Runnable接口创建线程,所有线程共享所属进程的堆内存(如对象实例),但每个线程有自己的栈内存(存储局部变量、方法调用栈)。

    // 示例:一个进程中的两个线程
    public class ProcessThreadDemo {public static void main(String[] args) {// 主线程System.out.println("主线程: " + Thread.currentThread().getName());// 创建并启动新线程Thread thread = new Thread(() -> {System.out.println("子线程: " + Thread.currentThread().getName());});thread.start();}
    }
    

4. 总结

  • 进程是资源分配的单位,线程是CPU调度的单位。
  • 进程间隔离性强、资源消耗大;线程共享资源、开销小,适合并发任务。
  • Java程序的并发通常通过多线程实现(同一进程内),而多进程通信在分布式场景中更常见(如微服务间调用)。

简单说:进程是“独立的程序”,线程是“程序内的执行流”,多个线程协作可以高效完成并发任务。

五、HashMap原理

Java 的 HashMap 是一种基于哈希表实现的键值对(Key-Value)存储结构,用于高效地进行查找、插入和删除操作。其核心原理围绕哈希函数数组链表/红黑树展开,下面详细解析:

1. 底层数据结构
HashMap 的底层是数组 + 链表 + 红黑树的组合结构(JDK 1.8 及以上):

  • 数组(哈希表):作为主体,每个元素是一个"桶"(Bucket),存储链表或红黑树的头节点。
  • 链表:当多个键(Key)哈希冲突时,会以链表形式存储在同一个桶中。
  • 红黑树:当链表长度超过阈值(默认 8),且数组长度 ≥ 64 时,链表会转换为红黑树,以提高查询效率(红黑树查询时间复杂度为 O(log n),优于链表的 O(n))。

2. 核心原理:哈希与索引计算
HashMap 的核心是通过哈希函数将 Key 映射到数组的索引位置,步骤如下:
(1)计算 Key 的哈希值
调用 Key.hashCode() 方法获取哈希值,再通过 HashMap 内部的哈希算法进一步处理(减少哈希冲突):

static final int hash(Object key) {int h;// 对hashCode进行扰动处理,减少哈希冲突return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • key == null 时,哈希值固定为 0(HashMap 允许 Key 为 null)。
  • 右移 16 位并异或(^)的操作,是为了混合哈希值的高位和低位,减少哈希冲突。

(2)计算数组索引
用处理后的哈希值与数组长度(length)减 1 进行按位与(&) 运算,得到数组索引:

int index = (table.length - 1) & hash;
  • 数组长度始终是 2 的幂次方(如 16、32、64…),因此 length - 1 的二进制是全 1(如 15 是 1111),按位与运算等价于取模(hash % length),但效率更高。

3. 哈希冲突的解决
当两个不同的 Key 计算出相同的索引时,称为哈希冲突HashMap 通过链地址法解决:

  • 冲突的 Key-Value 对会以链表形式存储在同一个桶中。
  • 查找时,先通过索引定位到桶,再遍历链表(或红黑树)比较 Key 的 equals() 方法,找到匹配的 Value。

4. 扩容机制(Resize)
HashMap 中的元素数量(size)超过负载因子(loadFactor)× 数组长度时,会触发扩容:

  • 负载因子:默认 0.75,是权衡空间和时间效率的阈值(值越高,空间利用率高但冲突率高)。
  • 扩容过程
    1. 创建一个新的数组,长度为原数组的 2 倍(保证仍是 2 的幂次方)。
    2. 将原数组中的元素重新计算索引,迁移到新数组中(这个过程称为"重哈希")。
    3. 若原链表长度 ≥ 8 且新数组长度 ≥ 64,会将链表转为红黑树;若长度 ≤ 6,则红黑树转回链表。

5. 关键成员变量

public class HashMap<K,V> {transient Node<K,V>[] table; // 核心数组(哈希表)transient int size; // 实际存储的键值对数量int threshold; // 扩容阈值(= 负载因子 × 数组长度)final float loadFactor; // 负载因子(默认 0.75)
}// 链表节点
static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Node<K,V> next; // 下一个节点的引用
}// 红黑树节点(JDK 1.8+)
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent; // 父节点TreeNode<K,V> left; // 左子树TreeNode<K,V> right; // 右子树TreeNode<K,V> prev; // 链表前驱(用于树转链表)boolean red; // 节点颜色(红/黑)
}

6. 核心操作流程(以 put() 为例)

  1. 计算 Key 的哈希值和数组索引。
  2. 若索引位置为空,直接插入新节点。
  3. 若索引位置不为空:
    • 若头节点的 Key 与插入的 Key 相同(equals() 为 true),则覆盖 Value。
    • 否则,遍历链表/红黑树:
      • 若找到相同 Key,覆盖 Value。
      • 若未找到,在链表尾部插入新节点(JDK 1.8 尾插法,避免死循环)。
  4. 插入后,若链表长度 ≥ 8 且数组长度 ≥ 64,将链表转为红黑树。
  5. size 超过阈值,触发扩容。

7. 线程安全性
HashMap非线程安全的:

  • 多线程并发修改时,可能导致链表成环(扩容时)、数据丢失等问题。
  • 线程安全的替代方案:ConcurrentHashMap(JUC 包)、Hashtable(古老且低效,不推荐)。

总结:
HashMap 通过哈希函数将 Key 映射到数组索引,用链表/红黑树解决哈希冲突,通过扩容机制保证效率,是 Java 中查询效率极高(平均 O(1))的键值对存储结构,广泛用于缓存、配置存储等场景。

六、HashMap 线程不安全体现在哪里?

HashMap 的线程不安全主要体现在多线程并发修改(如 putremoveresize 等操作)时,可能出现数据不一致链表成环扩容丢失数据等问题。以下是具体表现:

  1. 扩容时的链表成环(JDK 1.7 及之前)
    在 JDK 1.7 中,HashMap 扩容(resize)时采用头插法迁移链表节点,多线程并发扩容可能导致链表形成环形结构:
  • 当两个线程同时对同一链表进行扩容迁移时,节点的 next 指针可能相互引用,形成环。
  • 后续查询该链表时,会陷入无限循环(next 指针永远遍历不完),导致 CPU 飙升。

简化示例
假设线程 A 和 B 同时迁移节点 a → b

  • 线程 A 先处理 a,将 a.next 指向 null,准备插入新数组。
  • 线程 B 处理 b,将 b.next 指向 a,此时链表变为 b → a
  • 线程 A 恢复执行,将 a.next 指向 b,最终形成 a ↔ b 的环。
  1. 数据覆盖/丢失
    多线程并发执行 put 操作时,可能导致新插入的键值对被覆盖或丢失:
  • 场景 1:两个线程同时计算出相同的索引,且该位置原本为空。

    • 线程 A 检查到索引位置为空,准备插入节点。
    • 线程 B 也检查到同一位置为空,抢先插入节点。
    • 线程 A 继续执行插入,覆盖线程 B 插入的节点,导致 B 的数据丢失。
  • 场景 2:并发修改同一链表/红黑树。

    • 线程 A 正在遍历链表查找插入位置。
    • 线程 B 同时删除了该链表中的某个节点,导致线程 A 访问到已被删除的节点,可能插入错误位置或丢失数据。
  1. size 计数不准确
    HashMapsize 变量用于记录键值对数量,多线程并发修改时,size++ 操作(非原子操作)可能导致计数错误:
  • 线程 A 和 B 同时读取到 size = 10
  • 线程 A 执行 size++size = 11
  • 线程 B 基于原始值 10 执行 size++ 后仍为 11,导致实际数量比记录值多 1,计数失真。
  1. JDK 1.8 仍不安全
    JDK 1.8 虽然将扩容的头插法改为尾插法,解决了链表成环问题,但仍未解决线程安全问题:
  • 并发 put 时仍可能出现数据覆盖。
  • 红黑树的旋转操作在并发下可能导致结构破坏,引发查询异常。

如何避免线程不安全?

  1. 使用 ConcurrentHashMap:JUC 包提供的线程安全实现,通过分段锁(JDK 1.7)或 CAS + synchronized(JDK 1.8+)保证并发安全,性能优于 Hashtable
  2. 加锁保护:使用 Collections.synchronizedMap(new HashMap<>()),通过全局锁保证安全,但并发效率低。
  3. 避免并发修改:在单线程环境中使用 HashMap,多线程场景下通过线程隔离(如 ThreadLocal)避免共享。

总结HashMap 的线程不安全源于并发修改时的非原子操作和数据结构竞争,核心问题是无法保证多线程操作的可见性、原子性和有序性。在并发场景中,必须使用线程安全的替代方案。

七、ConcurrentHashMap 如何保证线程安全的?

ConcurrentHashMap 是 Java 并发包(JUC)中提供的线程安全哈希表实现,其线程安全机制在 JDK 1.7JDK 1.8+ 中有显著差异,核心思路是通过精细化锁控制减少竞争,同时保证高效并发。

1、JDK 1.7 的实现:分段锁(Segment)
JDK 1.7 中 ConcurrentHashMap 采用 “分段锁”(Segment) 机制,核心思想是将哈希表分为多个独立的“段”,每个段对应一把锁,实现“不同段的操作可以并发执行”。

(1)数据结构

  • 底层由 Segment 数组组成,每个 Segment 本质上是一个小的 HashMap(包含数组 + 链表)。
  • 每个 Segment 独立加锁,多个线程操作不同 Segment 时无需竞争同一把锁。

(2)线程安全保证

  • 分段加锁:当执行 putremove 等修改操作时,只对当前 Key 所在的 Segment 加锁(ReentrantLock),其他 Segment 仍可被其他线程访问。
  • 读操作无锁:获取元素时无需加锁,通过 volatile 保证可见性(Segment 数组和每个 HashEntryvalue 都用 volatile 修饰)。

(3)优缺点

  • 优点:多线程操作不同段时可并发执行,效率高于 Hashtable 的全局锁。
  • 缺点:段数固定(默认 16),若大量线程竞争同一段,仍会有锁竞争;内存占用较高。

2、JDK 1.8+ 的实现:CAS + synchronized
JDK 1.8 彻底重构了 ConcurrentHashMap,移除了 Segment 分段锁,改用 “CAS 无锁算法 + synchronized 细粒度锁”,并引入红黑树优化查询性能。

(1)数据结构

  • 底层与 HashMap 类似:数组(Node[])+ 链表 + 红黑树(链表长度 ≥ 8 时转为红黑树)。

(2)线程安全保证

  • CAS 无锁操作

    • 初始化数组或扩容时,通过 CAS(Compare-And-Swap)保证原子性(如 casTabAt 方法)。
    • value 的简单修改(如 put 时插入新节点到空桶)使用 CAS 避免加锁。
  • synchronized 细粒度锁

    • 当插入节点发生哈希冲突时,对数组桶的头节点synchronized 锁,仅锁定当前桶,不影响其他桶的操作。
    • 例如:线程 A 操作桶 1 的链表,线程 B 操作桶 2 的链表,可同时进行,只有竞争同一桶时才需等待锁。
  • volatile 可见性

    • 数组 transient volatile Node<K,V>[] tablevolatile 修饰,保证数组修改对其他线程可见。
    • 节点 Nodevaluenext 指针也用 volatile 修饰,确保读写可见性。

(3)核心操作示例(put 方法)

  1. 计算 Key 的哈希值和数组索引。
  2. 若索引位置为空,通过 CAS 插入新节点(无锁)。
  3. 若索引位置不为空:
    • 若头节点是 MOVED 状态(表示正在扩容),协助扩容。
    • 否则,对头节点加 synchronized,遍历链表/红黑树:
      • 若找到相同 Key,更新 Value。
      • 若未找到,在链表尾部插入新节点(或红黑树插入)。
  4. 插入后检查是否需要转为红黑树,或触发扩容。

3、JDK 1.8+ 相比 1.7 的改进

  1. 锁粒度更细:从“段级锁”细化到“桶级锁”,减少锁竞争。
  2. 去掉 Segment 结构:内存占用更低,结构更简单。
  3. 引入红黑树:优化长链表的查询性能(O(n) → O(log n))。
  4. CAS 减少加锁:简单操作(如空桶插入)用 CAS 无锁实现,效率更高。

总结
ConcurrentHashMap 保证线程安全的核心是:

  • JDK 1.7:通过“分段锁”隔离不同段的竞争,实现多段并发。
  • JDK 1.8+:通过“CAS 无锁 + 桶级 synchronized 锁”实现更细粒度的并发控制,同时保留 volatile 保证可见性。

这种设计在保证线程安全的同时,最大限度地提高了并发效率,是高并发场景下替代 HashMap(非线程安全)和 Hashtable(低效全局锁)的最佳选择。

八、CAS是什么

Compare-And-Swap(CAS,比较并交换) 是一种无锁原子操作,用于实现多线程环境下的同步控制,避免了传统锁机制的开销和竞争问题。它是并发编程中的核心技术,广泛应用于 JUC 包(如 AtomicIntegerConcurrentHashMap)等场景。

1、CAS 的核心思想
CAS 操作包含三个关键参数:

  • 内存地址(V):要操作的变量在内存中的地址。
  • 预期值(A):线程认为变量当前应该的值。
  • 新值(B):如果变量当前值等于预期值,就将其更新为新值。

操作逻辑
当且仅当内存地址 V 中的值等于预期值 A 时,才将该值更新为 B,否则不做任何操作。整个过程是原子性的(由 CPU 指令直接支持,不可中断)。

2、CAS 的执行流程

  1. 线程读取内存地址 V 中的当前值,记为 当前值
  2. 线程判断 当前值 是否等于预期值 A
    • 若相等,将 V 中的值更新为 B
    • 若不相等,说明该值已被其他线程修改,当前线程不做操作(或重试)。
  3. 无论是否更新,都返回 V 中的旧值(供线程判断是否成功)。

简化示例(模拟 AtomicIntegerincrementAndGet 操作):

public class SimulatedCAS {private int value; // 内存地址V对应的变量// CAS操作:若当前值等于预期值A,则更新为B,返回是否成功public boolean compareAndSwap(int expectedA, int newValueB) {int currentValue = value; // 读取当前值if (currentValue == expectedA) {value = newValueB; // 符合预期,更新为新值return true;}return false; // 不符合预期,不更新}// 原子自增(类似AtomicInteger的incrementAndGet)public int increment() {int current;do {current = value; // 读取当前值作为预期值A} while (!compareAndSwap(current, current + 1)); // 若失败则重试return current + 1;}
}

3、CAS 的底层实现
CAS 操作的原子性依赖于CPU 硬件指令(如 x86 架构的 cmpxchg 指令),由操作系统直接支持:

  • 当 JVM 执行 CAS 操作时,会翻译为对应的 CPU 指令。
  • CPU 保证该指令在执行过程中不被中断,从而实现硬件级别的原子性。

4、CAS 的优势与问题
(1)优势

  1. 无锁开销:避免了传统锁(如 synchronized)的上下文切换、线程阻塞唤醒等开销,适合低冲突场景。
  2. 高并发性能:在竞争不激烈时,CAS 的效率远高于锁机制。
  3. 细粒度控制:可针对单个变量进行原子操作,比锁的粒度更细。

(2)问题

  1. ABA 问题

    • 场景:变量值从 A 被改为 B,又改回 A。CAS 会认为值未变,但实际已被修改。
    • 解决:添加版本号(如 AtomicStampedReference,用“值 + 版本号”作为判断依据)。
  2. 循环重试开销

    • 若并发冲突频繁,CAS 会不断重试(如上述 increment 方法的 do-while 循环),导致 CPU 占用过高。
  3. 只能保证单个变量的原子性

    • CAS 仅能对单个变量进行原子操作,无法直接实现多个变量的原子性(需结合其他机制)。

5、CAS 在 Java 中的应用

  • 原子类java.util.concurrent.atomic 包下的类(如 AtomicIntegerAtomicReference)均基于 CAS 实现。
  • 并发容器ConcurrentHashMap(JDK 1.8+)用 CAS 实现无锁初始化和节点插入。
  • 线程池ThreadPoolExecutor 中用 CAS 维护工作线程数量等状态。

6、总结
CAS 是一种高效的无锁同步机制,通过硬件保证的原子操作实现多线程安全,核心是“比较预期值并更新”。它在低冲突场景下性能优异,但存在 ABA 问题和重试开销,是 Java 并发编程的基础技术之一。

九、ArrayList 和 LinkedList 区别?

ArrayListLinkedList 是 Java 集合框架中两种常用的列表实现,分别基于动态数组双向链表,在底层结构、性能特性和适用场景上有显著区别:

1. 底层数据结构

  • ArrayList:基于动态数组实现,内部维护一个连续的 Object 数组,通过索引(index)快速访问元素。当数组容量不足时,会自动扩容(默认扩容为原容量的 1.5 倍)。

  • LinkedList:基于双向链表实现,每个元素(Node)包含三个部分:

    • 存储的数据(item
    • 前驱节点引用(prev
    • 后继节点引用(next
      元素在内存中不连续,通过节点间的引用关联。

2. 核心性能对比

操作ArrayListLinkedList
随机访问(get/set)效率高,时间复杂度 O(1)(直接通过索引定位)。效率低,时间复杂度 O(n)(需从头/尾遍历到目标位置)。
添加/删除(中间位置)效率低,时间复杂度 O(n)(需移动目标位置后的所有元素)。效率高,时间复杂度 O(1)(只需修改相邻节点的引用,前提是已找到目标位置)。
添加/删除(首尾位置)尾部添加效率高(add() 通常为 O(1),扩容时为 O(n));头部添加效率低(O(n))。首尾操作效率极高(addFirst()/addLast() 均为 O(1)),直接修改头节点或尾节点的引用。
内存占用内存连续,存储元素本身,浪费较少(但扩容会预留空间)。每个元素需额外存储 prevnext 引用,内存开销更大。

3. 其他关键区别

  • 实现的接口

    • ArrayList 仅实现 List 接口。
    • LinkedList 同时实现 List 接口和 Deque 接口(双端队列),因此可作为队列(Queue)或栈(Stack)使用,提供 poll()offer()push()pop() 等方法。
  • 迭代器性能

    • 两者的 Iterator 迭代(for-each 循环)性能相近,但 LinkedListIterator 遍历比随机访问(get(i))高效得多(避免重复从头遍历)。
    • LinkedList 支持双向迭代器(ListIterator),可向前/向后遍历,而 ArrayListListIterator 功能类似但基于数组索引。
  • 扩容机制

    • ArrayList 有扩容成本(当元素数量超过容量时,复制旧数组到新数组)。
    • LinkedList 无需扩容,添加元素时只需创建新节点并调整引用。

4. 适用场景

  • 优先使用 ArrayList

    • 需要频繁通过索引访问元素(如 get(i)set(i, value))。
    • 元素数量固定或变化不大,且以尾部添加/删除为主。
    • 对内存占用较敏感的场景。
  • 优先使用 LinkedList

    • 需要频繁在列表中间或首尾添加/删除元素(如实现队列、栈、链表结构)。
    • 元素数量不确定,且插入删除操作远多于查询操作。

示例代码对比:

// ArrayList 示例(适合随机访问)
List<String> arrayList = new ArrayList<>();
arrayList.add("A");
arrayList.add("B");
String element = arrayList.get(1); // 快速获取索引1的元素(O(1))// LinkedList 示例(适合频繁插入删除)
LinkedList<String> linkedList = new LinkedList<>();
linkedList.addFirst("A"); // 头部添加(O(1))
linkedList.addLast("B");  // 尾部添加(O(1))
linkedList.add(1, "C");   // 中间插入(找到位置后O(1))

总结:

  • ArrayList数组型列表,优势在随机访问,适合读多写少场景。
  • LinkedList链表型列表,优势在中间插入删除,适合写多读少或需双端操作的场景。

选择时需根据实际操作的频率和类型决定,避免因数据结构误用导致性能瓶颈。

十、ThreadLocal 原理

ThreadLocal 是 Java 中用于实现线程本地存储的工具类,它能为每个线程提供一个独立的变量副本,从而避免多线程间的变量共享冲突。

1、核心原理
ThreadLocal 的核心思想是:让每个线程持有一个独立的变量副本,线程对变量的操作仅影响自身副本,不干扰其他线程

其底层通过以下结构实现:

  1. 每个 Thread 线程内部有一个 threadLocals 变量(类型为 ThreadLocal.ThreadLocalMap),这是一个自定义的哈希表,用于存储该线程的所有 ThreadLocal 变量副本。

  2. ThreadLocal 本身不存储数据,仅作为 ThreadLocalMap 的 key,用于在当前线程的 threadLocals 中查找对应的变量副本。

2、数据结构关系

Thread (线程)
└── threadLocals: ThreadLocalMap (线程本地的哈希表)├── 键: ThreadLocal 对象本身└── 值: 该线程对应的变量副本
  • 当线程通过 ThreadLocal.set(value) 存储数据时,实际是在当前线程的 threadLocals 中,以当前 ThreadLocal 为 key,存储 value 副本。
  • 当通过 ThreadLocal.get() 获取数据时,是从当前线程的 threadLocals 中,以当前 ThreadLocal 为 key,取出对应的副本值。

3、核心方法解析

  1. set(T value):为当前线程设置变量副本
public void set(T value) {Thread t = Thread.currentThread(); // 获取当前线程ThreadLocalMap map = getMap(t);    // 获取线程的threadLocalsif (map != null) {map.set(this, value);          // 以当前ThreadLocal为key存储value} else {createMap(t, value);           // 初始化threadLocals并存储}
}
  1. get():获取当前线程的变量副本
public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this); // 以当前ThreadLocal为key查询if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue(); // 若未初始化,设置初始值(默认null)
}
  1. remove():移除当前线程的变量副本
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null) {m.remove(this); // 从线程的threadLocals中删除当前ThreadLocal对应的条目}
}

4、ThreadLocalMap 的特殊设计
ThreadLocalMapThreadLocal 的静态内部类,专为线程本地存储设计,与普通 HashMap 有以下区别:

  • ** Entry 设计**:ThreadLocalMapEntry 继承 WeakReference<ThreadLocal<?>>,即 key(ThreadLocal 对象)是弱引用。这是为了在 ThreadLocal 对象被回收后,自动释放对应的 Entry,避免内存泄漏。
  • 哈希冲突处理:采用线性探测法(而非链表)解决哈希冲突,当索引被占用时,依次检查下一个索引。

5、内存泄漏风险与避免
虽然 Entry 的 key 是弱引用,但仍可能存在内存泄漏:

  • ThreadLocal 对象被回收(key 为 null),但线程仍在运行(如线程池中的核心线程),则 Entry 的 value 会一直被强引用,无法回收,导致内存泄漏。

避免方式

  • 使用完 ThreadLocal 后,主动调用 remove() 方法清除 value。
  • 在线程生命周期结束时,线程对象被回收,threadLocals 也会随之回收。

6、典型应用场景

  1. 线程上下文传递:如在 Web 开发中,存储用户登录信息、请求上下文等,避免在方法间层层传递参数。

    // 定义ThreadLocal存储用户信息
    private static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();// 存储用户信息
    userThreadLocal.set(currentUser);// 在任意地方获取当前线程的用户信息
    User user = userThreadLocal.get();// 使用完毕后移除
    userThreadLocal.remove();
    
  2. 避免线程安全问题:如 SimpleDateFormat 是非线程安全的,可通过 ThreadLocal 为每个线程提供独立实例。

    private static ThreadLocal<SimpleDateFormat> sdfThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
    

7、总结
ThreadLocal 通过让每个线程持有独立的变量副本,实现了线程隔离,避免了多线程共享变量的同步问题。其核心是 Thread 类中的 ThreadLocalMap,以 ThreadLocal 为 key 存储线程私有数据。使用时需注意主动调用 remove() 避免内存泄漏,适用于线程上下文存储、隔离非线程安全对象等场景。


文章转载自:

http://8tSZmJYs.ysckr.cn
http://4dGlEmWH.ysckr.cn
http://pLGbt8jQ.ysckr.cn
http://i3vHmCsH.ysckr.cn
http://XPdwRsqP.ysckr.cn
http://5XMrwITg.ysckr.cn
http://IC8Vd2JC.ysckr.cn
http://7qVbVXjn.ysckr.cn
http://N8RTH85K.ysckr.cn
http://YeMQ4Nl5.ysckr.cn
http://lvXwoZc5.ysckr.cn
http://NQ02dDId.ysckr.cn
http://EtNTF3D6.ysckr.cn
http://cxMnB4jh.ysckr.cn
http://5qHn90U9.ysckr.cn
http://mWYQY133.ysckr.cn
http://MsfJZ78M.ysckr.cn
http://S6SJRFqH.ysckr.cn
http://r9RjGGHV.ysckr.cn
http://RLr9UPB4.ysckr.cn
http://79V0pbkH.ysckr.cn
http://6f9WwdCR.ysckr.cn
http://k0hlZN0L.ysckr.cn
http://aNF6o3vK.ysckr.cn
http://NE7WJFFv.ysckr.cn
http://IquIgAnl.ysckr.cn
http://DbxMY0cf.ysckr.cn
http://PmMSrLCp.ysckr.cn
http://titcThMG.ysckr.cn
http://h7p08ujq.ysckr.cn
http://www.dtcms.com/a/367181.html

相关文章:

  • 什么是压力测试,有哪些方法
  • AI入坑: Trae 通过http调用.net 开发的 mcp server
  • IIS服务器下做浏览器缓存
  • 小白学OpenCV系列3-图像算数运算
  • jQuery 入门:一份献给初学者的完全指南
  • 怎么做到这一点:让 Agent 可以像人类一样 边听边想、边说,而不是“等一句话 → 一次性返回”
  • 风险慎投!IF 狂跌10分,国人发文超80%,这本SCI的1区TOP还能撑多久?
  • 剧本杀APP系统开发:引领娱乐行业新潮流的科技力量
  • Linux2.6内核进程O(1)调度队列
  • 【OpenHarmony文件管理子系统】文件访问接口mod_fileio解析
  • 【全息投影】全息风扇的未来,超薄化、智能化与交互化
  • “SOD-923”封装系列ESD静电二极管 DC0521D9 ESD9X5.0S
  • 架构-亿级流量性能调优实践
  • 开讲了,全栈经验之谈系列:写给进阶中的小伙伴
  • STM32F103C8T6开发板入门学习——寄存器和库函数介绍
  • 0904网络设备配置与管理第二次授课讲义
  • [科普] 卫星导航系统的授时原理与精度分析
  • Linux tail 命令使用说明
  • 机器学习基础-day04-数学方法实现线性回归
  • 如何在MacOS上卸载并且重新安装Homebrew
  • 基于 GEE 计算温度植被干旱指数 TVDI 并可视化分析
  • LED电路图判断灯在低电平时亮、高电平时灭
  • SpringBoot实现国际化(多语言)配置
  • 【代码随想录算法训练营——Day2】数组——209.长度最小的子数组、59.螺旋矩阵II、区间和、开发商购买土地
  • LinuxC++项目开发日志——高并发内存池(1-定长内存池)
  • 【提示词技巧】顺序位置对效果的影响
  • QT-菜单栏、工具栏和状态栏
  • Qt QJsonObject
  • 我辞职了,接替我的人私底下找我,我直接把她删了。明明有个交接群,她是觉得在群里提问会显得自己不够专业吗? 网友:凭啥惯着
  • Docker(②创造nginx容器)