欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 美景 > 每日学习Java之一万个为什么

每日学习Java之一万个为什么

2025/3/25 12:08:39 来源:https://blog.csdn.net/m0_63497607/article/details/146407814  浏览:    关键词:每日学习Java之一万个为什么

文章目录

  • @FunctionInterface default并不会被计数
  • Lambda使用
      • **1. 集合操作**
        • **(1) 遍历集合**
        • **(2) 过滤集合**
        • **(3) 转换集合**
        • **(4) 排序集合**
      • **2. 多线程编程**
        • **(1) 创建线程**
        • **(2) 使用线程池**
      • **3. 比较器**
      • **4. 事件监听器**
      • **5. 函数式接口**
        • **(1) Function 示例**
        • **(2) Predicate 示例**
        • **(3) Consumer 示例**
      • **6. 方法引用**
        • **(1) 静态方法引用**
        • **(2) 实例方法引用**
      • **总结**
  • java.lang.reflect
      • **1. 核心类**
        • **(1) `Class<T>`**
        • **(2) `Field`**
          • 常用方法:
        • **(3) `Method`**
          • 常用方法:
        • **(4) `Constructor<T>`**
          • 常用方法:
        • **(5) `Array`**
          • 常用方法:
      • **2. 接口**
        • **(1) `Member`**
          • 常用方法:
        • **(2) `AnnotatedElement`**
          • 常用方法:
      • **3. 异常类**
        • **(1) `IllegalAccessException`**
        • **(2) `IllegalArgumentException`**
        • **(3) `InvocationTargetException`**
        • **(4) `NoSuchFieldException`**
        • **(5) `NoSuchMethodException`**
      • **4. 其他工具类**
        • **(1) `Modifier`**
          • 常用方法:
      • **5. 总结**
  • Linux 安装jdk/ tomcat/ mysql8/ redis
  • Redis特点
      • 1. **高性能**
      • 2. **丰富的数据结构**
      • 3. **持久化能力**
      • 4. **事务支持**
      • 5. **发布订阅模式**
      • 6. **分片与集群**
      • 7. **过期时间设置**
      • 8. **Lua脚本支持**
      • 9. **安全性**
      • 10. **多种语言客户端支持**
  • i++为什么不是原子性(存在并发问题)
  • 多线程什么时候会快?线程间通信会拖慢速度吗?
      • 多线程什么时候会快?
      • 线程间通信是否会拖慢速度?
  • Redis 单线程模型 + IO 多路复用 (异步IO/阻塞IO)
  • Redis 中的数据操作
      • **1. Key 的相关操作**
      • **2. String 类型相关操作**
      • **3. List 类型相关操作**
      • **4. Set 类型相关操作**
      • **5. Hash 类型相关操作**
      • **6. Sorted Set 类型相关操作**
  • Redis处理关系型数据的三种办法
      • **场景:用户信息管理**
      • **设计思路**
      • **示例代码**
        • **(1) 插入用户数据**
        • **(2) 查询用户数据**
        • **(3) 更新用户数据**
        • **(4) 删除用户数据**
      • **批量操作**
        • **(1) 批量查询**
        • **(2) 批量插入**
      • **扩展:范围查询**
        • **(1) 使用集合存储用户 ID**
        • **(2) 遍历用户信息**
      • **优点**
      • **缺点**
  • Redis配置文件常见属性及其功能
      • 1. **网络相关配置**
      • 2. **持久化相关配置**
      • 3. **内存管理**
      • 4. **安全**
      • 5. **复制**
      • 6. **慢查询日志**
      • 7. **其他**
  • SpringData Redis使用
  • RedisTemplate 中的序列化器
      • 常见的序列化器
      • 配置 `RedisTemplate` 的序列化器
      • 不同序列化器的选择场景
      • 注意事项
  • 如何切换jedis
  • LUA脚本
      • 基础语法
      • 表(Table)
      • 模块和包
      • 与 C/C++ 集成
      • 示例:简单的 Lua 脚本
  • Redis持久化策略
  • Redis内存淘汰策略
  • 什么是哈希桶
  • 缓存三件套
  • 什么是扰动函数,哪些设计考虑到了扰动函数
      • 扰动函数的定义与设计应用
        • 1. **扰动函数的定义**
      • **(1) 化学中的“扰动力函数”(Forging Function)**
      • **(2) 计算机科学中的“扰动函数”**
        • **Java HashMap 的扰动函数**
      • **2. 设计中考虑扰动函数的场景**
        • **(1) 哈希表(如 HashMap)的设计**
        • **(2) 缓存系统的设计(如 Redis)**
        • **(3) 密码学与安全协议**
        • **(4) 机器学习与优化算法**
      • **3. 设计扰动函数的核心考量**
  • HashMap中的精妙设计
  • ConcurrentHashMap的优化
      • **1. 分段锁(Segment Locking)机制(Java 7 及之前)**
        • **基本思想**
        • **工作原理**
        • **优点**
        • **缺点**
      • **2. CAS + 细粒度锁(Java 8 及之后)**
        • **优化点**
      • **3. 读操作的优化**
        • **无锁读**
        • **volatile 保证可见性**
      • **4. 写操作的优化**
        • **CAS 和锁结合**
        • **批量插入**
      • **5. 扩容优化**
        • **渐进式扩容**
        • **多线程协作**
      • **6. 其他优化**
        • **计数器优化**
        • **弱一致性迭代器**
      • **7. 性能对比**
      • **8. 使用场景**
      • **9. 注意事项**
      • **总结**
  • LongAdder思想
      • **1. 基本思想**
        • **分段锁机制(Striped Locking)**
        • **惰性初始化(Lazy Initialization)**
      • **2. 工作流程**
      • **3. 优势与劣势**
        • **优势**
        • **劣势**
      • **4. 使用场景**

@FunctionInterface default并不会被计数

函数式接口中限制1个抽象接口,其他默认实现和静态实现不会被计数

Lambda使用

在 Java 中,Lambda 表达式是一种简洁的方式来实现函数式接口(只有一个抽象方法的接口)。它广泛应用于集合操作、多线程编程和事件处理等场景。


1. 集合操作

(1) 遍历集合

使用 forEach 方法遍历集合。

import java.util.Arrays;
import java.util.List;public class Main {public static void main(String[] args) {List<String> names = Arrays.asList("Alice", "Bob", "Charlie");// 使用 Lambda 表达式遍历names.forEach(name -> System.out.println(name));}
}

(2) 过滤集合

使用 Stream API 的 filter 方法过滤集合中的元素。

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;public class Main {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);// 使用 Lambda 表达式过滤偶数List<Integer> evenNumbers = numbers.stream().filter(n -> n % 2 == 0).collect(Collectors.toList());System.out.println(evenNumbers); // 输出 [2, 4, 6]}
}

(3) 转换集合

使用 map 方法对集合中的元素进行转换。

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;public class Main {public static void main(String[] args) {List<String> names = Arrays.asList("Alice", "Bob", "Charlie");// 使用 Lambda 表达式将名字转为大写List<String> upperCaseNames = names.stream().map(name -> name.toUpperCase()).collect(Collectors.toList());System.out.println(upperCaseNames); // 输出 [ALICE, BOB, CHARLIE]}
}

(4) 排序集合

使用 sorted 方法对集合进行排序。

import java.util.Arrays;
import java.util.List;public class Main {public static void main(String[] args) {List<String> names = Arrays.asList("Charlie", "Alice", "Bob");// 使用 Lambda 表达式按字典顺序排序names.sort((a, b) -> a.compareTo(b));System.out.println(names); // 输出 [Alice, Bob, Charlie]}
}

2. 多线程编程

(1) 创建线程

使用 Lambda 表达式简化线程创建。

public class Main {public static void main(String[] args) {// 使用 Lambda 表达式创建并启动线程Thread thread = new Thread(() -> System.out.println("Thread is running"));thread.start();}
}

(2) 使用线程池

通过 ExecutorService 和 Lambda 表达式提交任务。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Main {public static void main(String[] args) {ExecutorService executor = Executors.newFixedThreadPool(2);// 提交任务executor.submit(() -> System.out.println("Task 1 is running"));executor.submit(() -> System.out.println("Task 2 is running"));executor.shutdown();}
}

3. 比较器

使用 Lambda 表达式实现 Comparator 接口。

import java.util.Arrays;
import java.util.List;public class Main {public static void main(String[] args) {List<String> names = Arrays.asList("Charlie", "Alice", "Bob");// 使用 Lambda 表达式按长度排序names.sort((a, b) -> Integer.compare(a.length(), b.length()));System.out.println(names); // 输出 [Bob, Alice, Charlie]}
}

4. 事件监听器

在 GUI 编程中,使用 Lambda 表达式替代匿名内部类。

import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;public class Main {public static void main(String[] args) {JButton button = new JButton("Click Me");// 使用 Lambda 表达式添加事件监听器button.addActionListener(e -> System.out.println("Button clicked!"));JFrame frame = new JFrame();frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);frame.add(button);frame.pack();frame.setVisible(true);}
}

5. 函数式接口

Java 提供了一些内置的函数式接口,例如 FunctionPredicateConsumer 等,它们可以与 Lambda 表达式结合使用。

(1) Function 示例

Function<T, R> 表示一个接受参数类型为 T 并返回结果类型为 R 的函数。

import java.util.function.Function;public class Main {public static void main(String[] args) {Function<Integer, String> convert = num -> "Number: " + num;System.out.println(convert.apply(42)); // 输出 Number: 42}
}

(2) Predicate 示例

Predicate<T> 表示一个接受参数类型为 T 并返回布尔值的函数。

import java.util.function.Predicate;public class Main {public static void main(String[] args) {Predicate<Integer> isEven = num -> num % 2 == 0;System.out.println(isEven.test(4)); // 输出 trueSystem.out.println(isEven.test(5)); // 输出 false}
}

(3) Consumer 示例

Consumer<T> 表示一个接受参数类型为 T 并不返回任何结果的操作。

import java.util.function.Consumer;public class Main {public static void main(String[] args) {Consumer<String> print = message -> System.out.println(message);print.accept("Hello, World!"); // 输出 Hello, World!}
}

6. 方法引用

Lambda 表达式还可以通过方法引用进一步简化代码。

(1) 静态方法引用
import java.util.function.Function;public class Main {public static void main(String[] args) {Function<String, Integer> stringLength = String::length;System.out.println(stringLength.apply("Hello")); // 输出 5}
}

(2) 实例方法引用
import java.util.function.Consumer;public class Main {public static void main(String[] args) {Consumer<String> print = System.out::println;print.accept("Hello, Lambda!"); // 输出 Hello, Lambda!}
}

总结

Lambda 表达式在 Java 中的应用非常广泛,特别是在以下场景:

  • 集合操作(如 forEachfiltermap 等)。
  • 多线程编程(如 RunnableCallableExecutorService 等)。
  • 比较器(如 Comparator)。
  • 事件监听器(如 GUI 编程中的按钮点击事件)。
  • 函数式接口(如 FunctionPredicateConsumer 等)。

通过 Lambda 表达式,代码变得更加简洁、易读且高效。

java.lang.reflect

Java 反射机制主要位于 java.lang.reflect 包中,它提供了一系列类和接口,用于在运行时动态地获取类的信息(如类、方法、字段、构造器等)并进行操作。以下是 java.lang.reflect 包中的主要内容及其功能概述:


1. 核心类

(1) Class<T>
  • 位于 java.lang 包中,是反射的核心类。
  • 表示一个类或接口的运行时类型信息。
  • 提供了获取类的构造器、方法、字段等功能。
Class<?> clazz = String.class;

(2) Field
  • 表示类中的字段(成员变量)。
  • 提供了获取和设置字段值的功能。
常用方法:
  • getName():返回字段的名称。
  • getType():返回字段的类型。
  • get(Object obj):获取指定对象实例中该字段的值。
  • set(Object obj, Object value):设置指定对象实例中该字段的值。
Field field = clazz.getDeclaredField("fieldName");
field.setAccessible(true); // 如果字段是私有的
field.set(instance, "new value");

(3) Method
  • 表示类中的方法。
  • 提供了调用方法的功能。
常用方法:
  • getName():返回方法的名称。
  • getReturnType():返回方法的返回类型。
  • getParameterTypes():返回方法的参数类型数组。
  • invoke(Object obj, Object... args):调用指定对象实例上的方法。
Method method = clazz.getMethod("methodName", String.class);
Object result = method.invoke(instance, "argument");

(4) Constructor<T>
  • 表示类的构造器。
  • 提供了创建类实例的功能。
常用方法:
  • newInstance(Object... initargs):使用指定的参数创建类的新实例。
Constructor<?> constructor = clazz.getConstructor(String.class);
Object instance = constructor.newInstance("constructor argument");

(5) Array
  • 提供了操作数组的静态方法。
  • 主要用于动态创建和访问数组。
常用方法:
  • newInstance(Class<?> componentType, int length):创建一个指定类型的数组。
  • get(Object array, int index):获取数组中指定索引位置的值。
  • set(Object array, int index, Object value):设置数组中指定索引位置的值。
Object array = Array.newInstance(Integer.class, 5);
Array.set(array, 0, 10);
System.out.println(Array.get(array, 0)); // 输出 10

2. 接口

(1) Member
  • 表示类的成员(字段、方法、构造器)。
  • FieldMethodConstructor 的共同父接口。
常用方法:
  • getName():返回成员的名称。
  • getDeclaringClass():返回声明该成员的类。

(2) AnnotatedElement
  • 表示可以被注解修饰的元素(类、方法、字段等)。
  • 提供了检查注解的功能。
常用方法:
  • isAnnotationPresent(Class<? extends Annotation> annotationClass):检查是否包含指定的注解。
  • getAnnotation(Class<T> annotationClass):获取指定类型的注解。
  • getAnnotations():获取所有注解。
if (clazz.isAnnotationPresent(MyAnnotation.class)) {MyAnnotation annotation = clazz.getAnnotation(MyAnnotation.class);System.out.println(annotation.value());
}

3. 异常类

(1) IllegalAccessException
  • 当试图通过反射访问无法访问的成员(例如私有字段或方法)时抛出。
(2) IllegalArgumentException
  • 当传递给反射方法的参数不合法时抛出。
(3) InvocationTargetException
  • 当通过反射调用的方法本身抛出异常时,会包装成此异常抛出。
(4) NoSuchFieldException
  • 当试图获取不存在的字段时抛出。
(5) NoSuchMethodException
  • 当试图获取不存在的方法时抛出。

4. 其他工具类

(1) Modifier
  • 提供了处理修饰符(如 publicprivatestatic 等)的工具方法。
  • 常用于判断类、方法或字段的访问权限。
常用方法:
  • isPublic(int mod):判断是否为 public
  • isPrivate(int mod):判断是否为 private
  • isStatic(int mod):判断是否为 static
  • toString(int mod):将修饰符转换为字符串表示形式。
int modifiers = field.getModifiers();
System.out.println(Modifier.toString(modifiers));

5. 总结

java.lang.reflect 包的主要内容如下:

类/接口功能描述
Class<T>获取类的元信息
Field访问和修改字段
Method调用方法
Constructor<T>创建类的实例
Array操作数组
Member表示类的成员(字段、方法、构造器)
AnnotatedElement检查注解
Modifier处理修饰符

通过这些类和接口,Java 反射机制允许开发者在运行时动态地分析和操作类及其成员,从而实现更灵活的程序设计。反射可能会带来性能开销,并且破坏封装性,因此应谨慎使用。

Linux 安装jdk/ tomcat/ mysql8/ redis

  • 通过本地 / 远程连接进行文件上传
  • 防火墙和Net配置完毕可以通过yum镜像下载
  • 通过tar相关命令解压解包 以及 redis需要的gcc环境,编译安装

Redis特点

Redis 是一种开源的、基于内存的数据结构存储系统,它可以用作数据库、缓存和消息中间件。

1. 高性能

  • 基于内存:Redis 主要将数据存储在内存中,这使得它的读写速度非常快。
  • 异步复制:支持主从异步复制,保证了高可用性和数据冗余,同时不影响性能。
  • 非阻塞I/O:使用多路复用技术处理网络I/O,确保在处理大量客户端连接时依然保持高效。

2. 丰富的数据结构

  • 字符串(String):最基本的数据类型,可用于存储文本或二进制数据。
  • 列表(List):链表结构,支持从两端插入和移除元素。
  • 集合(Set):无序且不重复的元素集合,支持集合间的交集、并集、差集等操作。
  • 有序集合(Sorted Set):每个元素关联一个分数,依据分数排序,适用于排行榜等场景。
  • 哈希(Hash):键值对的集合,适合存储对象。
  • 位图(Bitmaps)HyperLogLogs地理空间(Geospatial) 等特殊用途的数据结构。

3. 持久化能力

  • RDB快照:定期将内存中的数据集以快照形式保存到磁盘上。
  • AOF日志:记录服务器接收到的所有修改性指令序列,并将其追加到文件中。通过重放这些命令来恢复数据。

4. 事务支持

  • 虽然没有传统关系型数据库那样的复杂事务机制,但Redis提供了MULTIEXECDISCARDWATCH等命令实现简单的事务控制,保证一系列命令执行的原子性。

5. 发布订阅模式

  • 支持发布/订阅模式的消息通信机制,便于构建实时应用如聊天室或事件驱动架构。

6. 分片与集群

  • 分片(Sharding):通过客户端实现数据分布于多个Redis实例间的技术。
  • Redis Cluster:官方提供的分布式解决方案,支持自动分片和故障转移功能。

7. 过期时间设置

  • 可以为键设置生存时间(TTL),过期后键会自动被删除,非常适合做缓存。

8. Lua脚本支持

  • 允许用户编写Lua脚本来扩展Redis的功能,实现复杂的业务逻辑。

9. 安全性

  • 提供密码验证机制,可以限制访问;还可以通过配置防火墙规则进一步加强安全防护。

10. 多种语言客户端支持

  • Redis 拥有广泛的社区支持,几乎所有的主流编程语言都有对应的客户端库。

这些特性使得 Redis 在众多应用场景中表现出色,无论是作为缓存解决方案还是直接用作数据库都非常合适。

i++为什么不是原子性(存在并发问题)

i++ 汇编其实是 赋值 增加 赋值 三步,每一步是原子性的,每一步之间会被插队,所以i++整体不是原子的。

多线程什么时候会快?线程间通信会拖慢速度吗?

多线程什么时候会快?

  1. I/O 密集型任务

    • 当程序需要频繁地进行 I/O 操作(如文件读写、网络请求等),这些操作通常会导致线程阻塞等待外部资源。在这种情况下,使用多线程可以让其他线程在某个线程等待时继续执行任务,从而提高整体效率。
  2. CPU 密集型任务

    • 对于需要大量计算的任务,如果可以将任务分解为多个独立的小任务,并且这些小任务可以在不同的 CPU 核心上并行执行,那么使用多线程或多个进程可以显著加速处理过程。然而,需要注意的是,过多的线程创建可能会导致上下文切换开销增加,反而降低性能。
  3. 异步处理

    • 在某些场景下,比如服务器端开发,可能需要同时处理大量的客户端请求。通过使用多线程或者异步非阻塞I/O模型,可以使服务器同时服务于更多的用户,提高吞吐量。

线程间通信是否会拖慢速度?

线程间通信确实有可能成为性能瓶颈,尤其是在以下几个方面:

  1. 锁竞争

    • 如果多个线程需要访问共享资源,则必须确保对这些资源的访问是同步的,这通常涉及到加锁机制。频繁的锁争用会导致线程被阻塞,等待获取锁,从而降低了并发度和性能。
  2. 上下文切换

    • 频繁的线程间通信可能导致更多的上下文切换,即操作系统需要保存当前线程的状态并将控制权转移给另一个线程。这种切换是有代价的,它消耗了额外的CPU周期,特别是在高频率发生时。
  3. 数据传输成本

    • 线程之间传递消息或数据也需要时间,尤其是当数据量较大时。此外,如果设计不当,可能会引入不必要的内存复制或其他低效的操作。
  4. 复杂性增加

    • 增加了代码的复杂性和潜在的错误来源,例如死锁、竞态条件等问题,这些问题不仅影响性能,还可能导致程序行为异常。

为了减轻线程间通信带来的负面影响,可以考虑以下策略:

  • 减少锁的粒度:尽量缩小临界区范围,只锁定必要的部分。
  • 使用无锁算法:在某些情况下,可以采用原子操作或其他无锁技术来避免传统意义上的锁。
  • 优化通信模式:选择合适的线程间通信机制,比如队列、管道等,并尽量减少不必要的通信。
  • 批处理:合并多个小的数据传输请求为一个大的请求,以减少实际的交互次数。

Redis 单线程模型 + IO 多路复用 (异步IO/阻塞IO)

  • 单线程: 负责 命令的执行工作

  • 异步IO: 负责 连接客户端 执行命令

  • 命令执行等待区:所有命令在这里排队执行,相当于火车的检票口

Redis 中的数据操作


1. Key 的相关操作

命令功能描述
DEL key删除指定的 key。
EXISTS key检查 key 是否存在,存在返回 1,否则返回 0。
EXPIRE key seconds设置 key 的过期时间(单位:秒)。
TTL key查看 key 的剩余生存时间(单位:秒)。如果 key 没有设置过期时间,返回 -1。
PERSIST key移除 key 的过期时间,使其变为持久化。
TYPE key返回 key 所存储的值的类型(如 string、list、set 等)。
RENAME key newkey将 key 重命名为 newkey。
KEYS pattern查找所有符合给定模式的 key(例如 KEYS user:*)。

2. String 类型相关操作

命令功能描述
SET key value设置 key 的值为 value。
GET key获取 key 的值。
INCR key将 key 中存储的数字值自增 1。
DECR key将 key 中存储的数字值自减 1。
INCRBY key increment将 key 中存储的数字值增加指定的增量值。
DECRBY key decrement将 key 中存储的数字值减少指定的减量值。
APPEND key value如果 key 已经存在并且是一个字符串,则将 value 追加到 key 原来的值后面。
STRLEN key返回 key 所存储的字符串值的长度。
MSET key1 value1 key2 value2同时设置多个 key-value 对。
MGET key1 key2同时获取多个 key 的值。

3. List 类型相关操作

命令功能描述
LPUSH key value将一个值插入到列表头部(左侧)。
RPUSH key value将一个值插入到列表尾部(右侧)。
LPOP key移除并返回列表的第一个元素(左侧)。
RPOP key移除并返回列表的最后一个元素(右侧)。
LRANGE key start stop返回列表中指定范围内的元素(索引从 0 开始)。
LLEN key返回列表的长度。
LREM key count value从列表中移除指定数量的值等于 value 的元素(count > 0 从头开始,count < 0 从尾开始)。
LINDEX key index返回列表中指定索引位置的元素。
LTRIM key start stop修剪列表,只保留指定范围内的元素。

4. Set 类型相关操作

命令功能描述
SADD key member向集合中添加一个成员。
SMEMBERS key返回集合中的所有成员。
SREM key member从集合中移除一个成员。
SCARD key返回集合中成员的数量。
SISMEMBER key member判断 member 是否是集合的成员,是则返回 1,否则返回 0。
SINTER key1 key2返回两个集合的交集。
SUNION key1 key2返回两个集合的并集。
SDIFF key1 key2返回第一个集合与第二个集合的差集。
SRANDMEMBER key [count]随机返回集合中的一个或多个成员。

5. Hash 类型相关操作

命令功能描述
HSET key field value设置哈希表中字段的值。
HGET key field获取哈希表中字段的值。
HMSET key field1 value1 field2 value2同时设置哈希表中多个字段的值。
HMGET key field1 field2同时获取哈希表中多个字段的值。
HGETALL key返回哈希表中所有的字段和值。
HDEL key field删除哈希表中的一个字段。
HEXISTS key field判断哈希表中是否存在指定字段,存在返回 1,否则返回 0。
HLEN key返回哈希表中字段的数量。
HINCRBY key field increment将哈希表中字段的值增加指定的增量值。

6. Sorted Set 类型相关操作

命令功能描述
ZADD key score member向有序集合中添加一个成员,并指定分数。
ZRANGE key start stop [WITHSCORES]返回有序集合中指定范围内的成员(按分数从小到大排序)。
ZREVRANGE key start stop [WITHSCORES]返回有序集合中指定范围内的成员(按分数从大到小排序)。
ZCARD key返回有序集合中成员的数量。
ZSCORE key member返回有序集合中指定成员的分数。
ZREM key member从有序集合中移除一个成员。
ZRANK key member返回成员在有序集合中的排名(按分数从小到大排序)。
ZREVRANK key member返回成员在有序集合中的排名(按分数从大到小排序)。
ZINCRBY key increment member将有序集合中成员的分数增加指定的增量值。

Redis处理关系型数据的三种办法

以数据行为主,字段+对应数据行数据是一个kv,一个数据行有n个kv。

String格式有两种:

  • 可以用特殊格式的key :v ,比如 id name age 将 两个半kv放在key中,v放最后一个值
  • k : 序列化 v

Hash中 直接用 k : 多个 kv

以下是一个具体的例子,展示如何使用 Redis 的 String 类型和长 key 格式来处理类似关系型数据库的数据。


场景:用户信息管理

假设我们需要存储用户的基本信息,包括用户的 ID、姓名、年龄和邮箱地址。在一个关系型数据库中,这些信息可能存储在一个名为 users 的表中,表的结构如下:

idnameageemail
1Alice25alice@example.com
2Bob30bob@example.com

我们可以使用 Redis 的 String 类型和长 key 格式来模拟这种结构。


设计思路

  • 每个用户的信息可以通过一个唯一的 key 来表示。
  • 使用长 key 格式设计规则:user:<id>:<field>,其中:
    • <id> 是用户的唯一标识(类似于主键)。
    • <field> 是用户的某个属性字段(如 nameageemail 等)。

例如:

  • 用户 ID 为 1 的姓名存储为 user:1:name
  • 用户 ID 为 1 的年龄存储为 user:1:age
  • 用户 ID 为 1 的邮箱存储为 user:1:email

每个 key 对应的值就是该字段的具体内容。


示例代码

(1) 插入用户数据
# 用户 1 的信息
SET user:1:name "Alice"
SET user:1:age "25"
SET user:1:email "alice@example.com"# 用户 2 的信息
SET user:2:name "Bob"
SET user:2:age "30"
SET user:2:email "bob@example.com"
(2) 查询用户数据
# 获取用户 1 的姓名
GET user:1:name
# 返回 "Alice"# 获取用户 2 的邮箱
GET user:2:email
# 返回 "bob@example.com"
(3) 更新用户数据
# 更新用户 1 的年龄
SET user:1:age "26"# 验证更新是否成功
GET user:1:age
# 返回 "26"
(4) 删除用户数据
# 删除用户 2 的信息
DEL user:2:name
DEL user:2:age
DEL user:2:email

批量操作

Redis 支持批量操作,可以通过 MGETMSET 实现对多个字段的读取或写入。

(1) 批量查询
MGET user:1:name user:1:age user:1:email
# 返回 ["Alice", "25", "alice@example.com"]
(2) 批量插入
MSET user:3:name "Charlie" user:3:age "35" user:3:email "charlie@example.com"

扩展:范围查询

由于 Redis 的 String 类型本身不支持范围查询,但我们可以通过其他方式实现类似的查询功能。例如:

(1) 使用集合存储用户 ID

可以使用 Redis 的 Set 数据类型来存储所有用户的 ID,方便后续遍历。

SADD users 1 2 3
(2) 遍历用户信息

结合 SMEMBERSMGET 可以实现对所有用户信息的遍历。

# 获取所有用户 ID
SMEMBERS users
# 返回 [1, 2, 3]# 构造批量查询 key
# 假设需要查询所有用户的姓名
MGET user:1:name user:2:name user:3:name
# 返回 ["Alice", "Bob", "Charlie"]

优点

  1. 简单直观:通过长 key 格式,可以非常清晰地表达数据之间的关系。
  2. 灵活性高:可以根据需要动态添加或删除字段,无需固定的表结构。
  3. 高效查询:单个字段的查询和更新都非常高效,因为 Redis 的 String 类型是基于内存操作的。

缺点

  1. 缺乏复杂查询能力:Redis 的 String 类型本身不支持复杂的 SQL 查询(如 JOIN、GROUP BY 等)。如果需要复杂查询,可能需要额外的逻辑处理。
  2. 存储冗余:长 key 格式可能会导致存储冗余,尤其是当字段较多时,key 的长度会增加存储开销。
  3. 难以管理大规模数据:如果用户数量庞大,手动构造和管理长 key 可能会变得复杂。

Redis配置文件常见属性及其功能

Redis 配置文件(通常名为 redis.conf)包含了许多配置选项,用于定制 Redis 实例的行为。以下是 Redis 配置文件中一些常见的属性及其功能说明:

1. 网络相关配置

  • bind

    • 功能:指定 Redis 服务器监听的 IP 地址,默认情况下是 127.0.0.1,这意味着 Redis 只接受来自本机的连接。如果要让 Redis 接受外部连接,可以设置为服务器的实际 IP 地址或使用 0.0.0.0 来监听所有可用接口。
    • 示例:bind 0.0.0.0
  • port

    • 功能:定义 Redis 监听的端口号,默认是 6379
    • 示例:port 6379
  • timeout

    • 功能:客户端空闲多少秒后关闭连接(以秒为单位)。默认值为 0,表示不超时。
    • 示例:timeout 300

2. 持久化相关配置

  • save

    • 功能:RDB 持久化的触发条件,格式为 save <seconds> <changes>,即在指定的时间内至少发生了多少次修改时会触发一次快照保存。
    • 示例:save 900 1 表示如果在 900 秒内至少有 1 次更改,则执行 BGSAVE。
  • appendonly

    • 功能:是否开启 AOF(Append Only File)日志记录模式,默认是 no。开启后,每个写操作都会被追加到 .aof 文件中。
    • 示例:appendonly yes
  • appendfsync

    • 功能:控制 AOF 文件同步策略。可选值包括 alwayseverysecno,分别代表每次修改都同步、每秒钟同步一次和依赖操作系统同步。
    • 示例:appendfsync everysec

3. 内存管理

  • maxmemory

    • 功能:设置 Redis 实例的最大内存使用量。当达到此限制时,Redis 将根据 maxmemory-policy 的设定来移除键。
    • 示例:maxmemory 512mb
  • maxmemory-policy

    • 功能:决定当达到最大内存限制时,Redis 如何选择要移除的键。常见的策略包括 volatile-lruallkeys-lru 等。
    • 示例:maxmemory-policy allkeys-lru

4. 安全

  • requirepass

    • 功能:设置访问 Redis 服务器所需的密码。这有助于保护 Redis 实例免受未经授权的访问。
    • 示例:requirepass yourpassword
  • rename-command

    • 功能:重命名或禁用某些命令,增强安全性。例如,可以禁用 FLUSHALL 命令。
    • 示例:rename-command FLUSHALL ""

5. 复制

  • slaveof

    • 功能:配置当前实例作为另一个 Redis 实例的从库。需要指定主库的 IP 地址和端口。
    • 示例:slaveof master-ip 6379
  • masterauth

    • 功能:如果主库设置了密码,则在此处指定密码以便从库能够成功连接。
    • 示例:masterauth master-password

6. 慢查询日志

  • slowlog-log-slower-than

    • 功能:定义一个命令执行时间超过多少微秒会被记录到慢查询日志中。默认值是 10000 微秒(即 10 毫秒)。
    • 示例:slowlog-log-slower-than 5000
  • slowlog-max-len

    • 功能:慢查询日志的最大条目数。一旦超过这个数量,旧的日志条目将被删除。
    • 示例:slowlog-max-len 128

7. 其他

  • databases

    • 功能:设置数据库的数量,默认是 16。不同的数据库之间数据是隔离的。
    • 示例:databases 16
  • logfile

    • 功能:指定 Redis 日志文件的位置。如果设置为空字符串,则日志输出到标准输出设备。
    • 示例:logfile /var/log/redis/redis-server.log
  • dir

    • 功能:指定工作目录,在该目录下 Redis 将创建 RDB 文件和其他临时文件。
    • 示例:dir ./

以上列出了一些常用的 Redis 配置项,实际应用中可能还需要根据具体的业务需求调整更多参数。正确配置这些参数可以帮助优化 Redis 性能,并确保其安全稳定地运行。

SpringData Redis使用

  • 引入场景启动器

  • 设置redis配置

    • host
    • port
    • lettuce (默认连接池):pool : 参数
    • client-type
  • 启动redis

RedisTemplate 中的序列化器

在使用 Spring Data Redis 时,RedisTemplate 是一个核心组件,用于与 Redis 进行交互。RedisTemplate 提供了对 Redis 数据的序列化和反序列化的支持,而这些操作是由不同的**序列化器(Serializer)**来完成的。

默认情况下,RedisTemplate 使用 Java 的 JdkSerializationRedisSerializer,它会将对象序列化为二进制数据存储到 Redis 中。然而,这种序列化方式可能会导致存储的数据不可读,并且与其他语言或工具的兼容性较差。因此,在实际开发中,我们通常会根据需求自定义序列化器。


常见的序列化器

Spring Data Redis 提供了多种内置的序列化器,以下是一些常用的序列化器:

  1. JdkSerializationRedisSerializer

    • 默认使用的序列化器。
    • 使用 Java 的序列化机制(ObjectOutputStreamObjectInputStream)。
    • 序列化后的数据是二进制格式,占空间较大,且不易读。
    • 示例:
      redisTemplate.setKeySerializer(new JdkSerializationRedisSerializer());
      
  2. StringRedisSerializer

    • 用于处理字符串数据。
    • 将键或值直接作为字符串进行存储和读取。
    • 示例:
      redisTemplate.setKeySerializer(new StringRedisSerializer());
      redisTemplate.setValueSerializer(new StringRedisSerializer());
      
  3. GenericJackson2JsonRedisSerializer

    • 使用 Jackson 库将对象序列化为 JSON 格式。
    • 序列化后的数据是可读的 JSON 字符串,适合跨语言使用。
    • 示例:
      redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
      
  4. OxmSerializer

    • 使用 XML 格式进行序列化。
    • 适用于需要以 XML 格式存储数据的场景。
  5. ByteArrayRedisSerializer

    • 直接存储原始字节数组。
    • 示例:
      redisTemplate.setValueSerializer(new ByteArrayRedisSerializer());
      
  6. 自定义序列化器

    • 可以通过实现 org.springframework.data.redis.serializer.RedisSerializer<T> 接口来自定义序列化逻辑。

配置 RedisTemplate 的序列化器

在 Spring 中,可以通过配置 RedisTemplate 来指定键和值的序列化器。以下是一个完整的示例:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<String, Object> template = new RedisTemplate<>();// 设置键的序列化器template.setKeySerializer(new StringRedisSerializer());template.setHashKeySerializer(new StringRedisSerializer());// 设置值的序列化器template.setValueSerializer(new GenericJackson2JsonRedisSerializer());template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());// 设置连接工厂template.setConnectionFactory(redisConnectionFactory);return template;}
}

不同序列化器的选择场景

  • StringRedisSerializer

    • 适用于键或值是简单字符串的情况。
    • 如果你的 Redis 数据只需要存储简单的字符串,可以使用此序列化器。
  • GenericJackson2JsonRedisSerializer

    • 适用于需要存储复杂对象并且希望数据可读的场景。
    • 如果你需要跨语言使用 Redis 数据,或者希望 Redis 中的数据是人类可读的 JSON 格式,推荐使用此序列化器。
  • JdkSerializationRedisSerializer

    • 适用于纯 Java 环境下,不需要考虑跨语言兼容性的场景。
    • 如果你对性能要求较高,同时不介意存储二进制数据,可以选择此序列化器。
  • 自定义序列化器

    • 如果内置的序列化器无法满足需求,例如需要加密或压缩数据,可以实现自定义序列化器。

注意事项

  1. 一致性

    • 在同一个 Redis 实例中,确保键和值的序列化方式一致,否则可能会导致反序列化失败。
  2. 数据迁移

    • 如果更改了序列化器,旧数据可能无法被正确解析,因此需要清理或迁移数据。
  3. 性能

    • 不同的序列化器对性能的影响不同。例如,JSON 序列化器比 JDK 序列化器更耗时,但生成的数据更小、更易读。
  4. 安全性

    • 如果存储的数据是敏感信息,建议使用加密序列化器或在存储前对数据进行加密。

通过合理选择和配置 RedisTemplate 的序列化器,可以让 Redis 更好地适应你的应用场景,同时提高数据的可读性和兼容性。

如何切换jedis

排除lettuce依赖,导入jedis

LUA脚本

Lua 是一种轻量级、高性能的嵌入式脚本语言,广泛用于游戏开发、嵌入式系统、Web 开发以及其他需要脚本支持的应用程序中。它的设计目标是简单、小巧且易于集成到 C 和 C++ 程序中。

基础语法

  1. 变量定义
    在 Lua 中,你可以使用 = 来给变量赋值。Lua 支持动态类型,这意味着你不需要在声明时指定变量的类型。

    message = "Hello, World!"
    number = 42
    
  2. 注释
    单行注释以 -- 开始,多行注释则使用 --[[]]

    -- 这是一个单行注释
    --[[ 这是一个多行注释 ]]
    
  3. 控制结构
    包括条件语句(如 if)、循环语句(如 while, for)等。

    if number > 40 thenprint("The number is greater than 40")
    endfor i = 1, 5 doprint(i)
    end
    
  4. 函数
    函数使用 function 关键字定义,并可以返回多个结果。

    function greet(name)return "Hello, " .. name .. "!"
    end
    print(greet("Lua"))
    

表(Table)

表是 Lua 的唯一数据结构,它可以用来表示数组、集合、记录和其他数据结构。

-- 创建一个表
local array = {"Lua", "Scripting", "Language"}-- 访问表中的元素
print(array[1])  -- 输出: Lua-- 使用表作为字典
local dict = {name = "Lua", type = "Scripting Language"}
print(dict["name"])  -- 输出: Lua

模块和包

Lua 支持模块化编程,通过 require 函数来加载模块。

-- 定义一个模块
local myModule = {}function myModule.sayHello()print("Hello from module!")
endreturn myModule-- 在另一个文件中使用该模块
local myModule = require("myModule")
myModule.sayHello()  -- 输出: Hello from module!

与 C/C++ 集成

Lua 设计之初就考虑到了与其他语言的互操作性,特别是 C 和 C++。通过 Lua 提供的 C API,可以轻松地将 Lua 脚本嵌入到 C/C++ 应用程序中,或者反过来从 Lua 调用 C/C++ 编写的函数。

示例:简单的 Lua 脚本

下面是一个简单的 Lua 脚本示例,它定义了一个函数,然后调用这个函数并打印输出。

-- 定义一个函数
function add(a, b)return a + b
end-- 调用函数并打印结果
result = add(3, 5)
print(result)  -- 输出: 8

Redis持久化策略

Redis的持久化策略主要包括**RDB(快照持久化)AOF( append-only file)**两种方式,核心要点如下:

  1. RDB(基于快照)

    • 原理:在指定时间间隔内将内存数据快照写入磁盘,生成.rdb文件。
    • 优点:文件紧凑,适合全量备份;恢复速度快,适合灾难恢复。
    • 缺点:可能丢失快照间隔内的数据;生成快照时CPU占用较高。
    • 触发:可配置save指令(如save 900 1)或手动执行SAVE/BGSAVE
  2. AOF(基于日志追加)

    • 原理:将每个写操作以协议格式追加到.aof文件,记录所有写命令。
    • 优点:支持appendfsync策略(always/每秒/无)平衡性能与安全性,数据丢失风险更低。
    • 缺点:文件体积较大;需定期重写(AOF Rewrite)压缩。
    • 触发:实时记录写命令,通过bgrewriteaof优化文件。
  3. 混合策略

    • 组合使用:同时开启RDB和AOF,利用RDB快速恢复和AOF数据可靠性,但需权衡性能。
    • 配置建议:根据业务需求选择:
      • 高性能低损耗:优先RDB+合理间隔。
      • 数据强一致性:AOF+appendfsync everysec(每秒同步)。

核心目标:在数据持久性、性能损耗和恢复效率间平衡,确保数据安全与系统稳定性。


Redis内存淘汰策略

Redis的内存淘汰策略是一种用于管理Redis实例中数据生命周期的机制,当Redis使用的内存达到预设的最大限制时,这些策略决定了哪些键值对应该被删除以释放空间。以下是对Redis内存淘汰策略的具体介绍:

  1. noeviction:这是Redis的默认策略(≥v3.0)。当内存使用达到最大限制时,Redis会拒绝新的写入操作,并返回错误,此时只响应读操作。这种策略适用于数据保留非常重要且不能丢失的场景,或者在内存充足的环境下使用。
  2. allkeys-lru:在所有键中使用LRU(最近最少使用)算法进行淘汰。Redis会维护一个近似的LRU列表,虽然不完全精确,但对大多数使用场景来说是足够的。这种策略适用于缓存应用,其中需要保留最近被访问的数据以便快速响应后续的读取请求。
  3. allkeys-lfu:在所有键中使用LFU(最不经常使用)算法进行淘汰。LFU算法根据键的访问频率来淘汰数据,访问次数最少的键优先被淘汰。这种策略适用于有明显热点数据的应用场景,可以确保热点数据不被轻易淘汰。
  4. volatile-lru:仅在设置了过期时间的键中,基于LRU算法淘汰数据。这种策略适用于部分数据有时效性要求的场景,只针对设置了过期时间的键进行淘汰。
  5. volatile-lfu:仅在设置了过期时间的键中,基于LFU算法淘汰数据。同样只针对设置了过期时间的键,但淘汰依据是访问频率。
  6. allkeys-random:随机从所有key中淘汰数据。这种策略适用于对数据淘汰无特定要求的场景。
  7. volatile-random:随机从设置了ttl key中淘汰数据。只针对设置了过期时间的键进行随机淘汰。
  8. volatile-ttl:根据键的剩余过期时间进行淘汰,越早过期的键越先被淘汰。这种策略适用于缓存数据时效性要求严格的场景。

总的来说,Redis的内存淘汰策略提供了多种方式来管理和优化内存使用,用户可以根据具体需求选择合适的策略。在实际使用中,还需要注意监控Redis的内存使用情况,并根据需要调整相关参数以优化性能和资源利用。


什么是哈希桶

存储某个数的最终哈希值,但是哈希值或者说哈希码应该是一个序列,这个序列需要被映射到一个链表或者数组下标,直接映射可能会使得空间利用效率很低。JDK HashMap中是通过 hash&(length-1)来映射到目标下标,这个下标就被称为哈希桶,保证最终哈希桶在 容量之内。

缓存三件套

缓存三件套指的是数据访问越过缓存直接访问数据库的三种情况:

  • 缓存击穿:当热Key过期时,大量请求访问数据库

    • 解决办法:互斥锁 / 永不过期 / 双缓存
  • 缓存穿透:访问不存在的数据

    • 解决办法:Bool(布隆)过滤器,用多个哈希槽确定某个key是否存在,有小几率误判。 / 缓存数据
  • 缓存雪崩:多个Key过期时,大量请求访问数据库

    • 解决办法:随机TTL、多级缓存、限流降级、集群

那么如何记忆呢? 顾名思义即可。击穿就是 多发子弹击穿标靶(热key过期),穿透就是 穿过了掩体(访问缓存不存在的数据),雪崩(好多掩体没了)大量key过期。

什么是扰动函数,哪些设计考虑到了扰动函数

扰动函数的定义与设计应用

1. 扰动函数的定义

扰动函数(Disturbance Function)在不同领域有不同的含义,以下是两种主要场景的解释:


(1) 化学中的“扰动力函数”(Forging Function)

根据知识库中的化学术语描述:

  • 定义:扰动力函数是描述化学反应平衡受扰动时的数学表达式。
  • 核心思想
    • 化学反应体系在宏观约束条件(如温度、压力、浓度等)固定时,存在稳定的化学平衡。
    • 当外部条件改变(如添加反应物、改变温度等),平衡被打破,系统会从原平衡状态向新平衡状态过渡。
    • 扰动力函数通过量化浓度偏移(如 ΔC = C - C_eq)和反应动力学参数,描述这一动态过程。
  • 数学表达
    • 根据化学弛豫原理,扰动力函数(如 Δ)可以将高阶速率方程简化为一级反应动力学形式:
      [
      -\frac{dI}{dt} = \Delta - \frac{\Delta_e}{\tau}
      ]
    • 其中,Δ 表示扰动引起的浓度偏移,τ 是弛豫时间常数。

(2) 计算机科学中的“扰动函数”

在编程和数据结构中,扰动函数通常指对哈希码(Hash Code)进行二次处理的算法,目的是优化哈希值的分布,减少哈希冲突。

Java HashMap 的扰动函数

根据知识库中的描述(CSDN博客内容):

  • 作用:对对象的 hashCode() 值进行扰动处理,提高哈希值的均匀性。
  • 实现原理
    1. 步骤
      • 输入对象的 hashCode() 值(32位 int)。
      • 高位与低位异或:将哈希码的高16位与低16位进行异或操作,打乱原始位分布:
        int hash = hashCode();
        hash ^= (hash >>> 16); // 高16位右移16位后与低16位异或
        
      • 最终哈希值:通过 (n - 1) & hash 计算数组下标(n 为数组长度)。
    2. 设计目的
      • 减少哈希冲突:某些对象的 hashCode() 实现可能质量较低(如只使用低几位),扰动函数通过混合高低位,增强低位的随机性。
      • 均匀分布:确保哈希值在数组索引范围内更均匀地分布,避免链表过长导致查询性能下降。

2. 设计中考虑扰动函数的场景

(1) 哈希表(如 HashMap)的设计
  • 问题:哈希冲突(不同对象的哈希码映射到同一索引)可能导致链表过长,降低查询效率。
  • 解决方案
    • 扰动函数:通过位操作(如异或、移位)对哈希码进行二次处理,提升分布均匀性。
    • 扩展机制:当链表长度超过阈值(如8),转换为红黑树(JDK 1.8+),进一步优化查询效率。
  • 知识库引用

    “HashMap的hash方法也就是扰动函数是为了防止一些实现比较差hashCode()方法…减少碰撞。”


(2) 缓存系统的设计(如 Redis)
  • 问题:缓存雪崩(大量缓存同时失效,导致数据库压力激增)。
  • 解决方案
    • 随机过期时间(扰动 TTL):在设置缓存过期时间时,对基础时间(如 24小时)进行随机扰动(如 ±10%),避免大量缓存同时失效。
    • 设计目的:通过时间分布的扰动,平滑请求峰值,防止数据库雪崩。
  • 知识库关联

    “随机过期时间(Random TTL)…避免key同时过期”(缓存雪崩解决方案)。


(3) 密码学与安全协议
  • 场景:生成随机数或密钥时,避免可预测性。
  • 扰动函数应用
    • 对种子值进行扰动(如异或系统时间、内存地址等),增加随机性,防止被破解。

(4) 机器学习与优化算法
  • 场景:优化模型训练时的参数初始化或梯度下降路径。
  • 扰动函数应用
    • 在初始参数或梯度方向中引入随机扰动,避免陷入局部最优解。

3. 设计扰动函数的核心考量

无论在哪个领域,设计扰动函数时通常需要考虑以下原则:

  1. 均匀性:确保扰动后的结果分布更均匀。
  2. 不可预测性:避免被恶意利用(如哈希攻击或缓存穿透)。
  3. 计算效率:扰动操作需简单高效,不影响系统性能。
  4. 适应性:能应对不同场景的输入(如哈希码的多样性或缓存失效模式)。

HashMap中的精妙设计

HashMap的精妙设计体现在以下几个核心点:

  1. 哈希与扰动:通过hashCode()计算键的哈希值,并采用高位异或低位的扰动函数(如hash = hash ^ (hash >>> 16)),提升哈希值分布均匀性,减少冲突。
  2. 链表与红黑树:当链表长度超过阈值(默认8)时,自动转为红黑树,将查询时间复杂度从O(n)降至O(log n),平衡存储与效率。
  3. 动态扩容:容量达到阈值(负载因子默认0.75)时扩容为原容量的2倍,通过分摊机制保证均摊O(1)复杂度;扩容时通过“雨水灌溉”法(交替分配元素)减少哈希碰撞。
  4. 键值特性:允许一个null键和多个null值,键的equals()hashCode()确保唯一性,值可自由存储。
  5. 性能优化:通过高位运算和结构化存储(数组+链表/红黑树)实现高效存取,兼顾空间与时间,成为Java中最常用的键值对容器。

ConcurrentHashMap的优化

ConcurrentHashMap 是 Java 并发编程中非常重要的一个线程安全的哈希表实现。相比传统的 Hashtable 或通过 Collections.synchronizedMap 包装的 HashMapConcurrentHashMap 提供了更高的并发性能和更好的扩展性。以下是 ConcurrentHashMap 的优化设计思想与实现细节。


1. 分段锁(Segment Locking)机制(Java 7 及之前)

基本思想

在 Java 7 中,ConcurrentHashMap 使用了分段锁机制来实现线程安全。整个哈希表被划分为多个段(Segment),每个段独立加锁。这样,不同线程可以同时操作不同的段,从而减少锁的竞争。

工作原理
  • 每个 Segment 是一个独立的小哈希表。
  • 写操作时,只需锁定相关的 Segment,而不是整个哈希表。
  • 读操作通常是无锁的,因为数据本身是线程安全的。
优点
  • 减少了锁的粒度,提升了并发性能。
  • 支持一定程度的并行写操作。
缺点
  • Segment 数量固定(默认为 16),无法动态调整,可能限制扩展性。
  • 在高并发场景下,某些热点 Segment 仍可能成为瓶颈。

2. CAS + 细粒度锁(Java 8 及之后)

Java 8 对 ConcurrentHashMap 进行了重大改进,摒弃了分段锁机制,转而使用更高效的 CAS(Compare-And-Swap)操作和细粒度锁。

优化点
  1. 基于 Node 的链表和红黑树

    • 每个桶(Bucket)由一个 Node 表示,存储键值对。
    • 当链表长度超过阈值(默认为 8)时,会将链表转换为红黑树,以提高查找效率。
  2. CAS 操作

    • 插入、更新等操作尽量使用无锁的 CAS 操作完成。
    • 如果 CAS 失败(说明有竞争),再升级为锁。
  3. 细粒度锁

    • 锁定的是具体的桶(Node),而不是整个段或哈希表。
    • 不同线程可以同时操作不同的桶,进一步提升了并发性能。
  4. 动态扩容

    • 扩容时采用分批迁移的方式,避免一次性迁移所有数据导致的性能问题。
    • 每次只迁移一部分桶的数据,其他线程可以继续访问未迁移的部分。

3. 读操作的优化

无锁读
  • 读操作不涉及锁,直接从哈希表中获取数据。
  • 即使在扩容过程中,读操作仍然可以通过旧表和新表协作完成。
volatile 保证可见性
  • 哈希表中的值使用 volatile 修饰,确保多线程间的可见性。
  • 这样,即使没有显式的锁,读线程也能看到最新的写入结果。

4. 写操作的优化

CAS 和锁结合
  • 写操作优先尝试使用 CAS 完成,只有在 CAS 失败时才加锁。
  • 这种方式减少了锁的使用频率,降低了线程阻塞的可能性。
批量插入
  • 在初始化或批量插入数据时,ConcurrentHashMap 会尽量减少锁的争用,提升性能。

5. 扩容优化

渐进式扩容
  • 扩容时,ConcurrentHashMap 不会一次性迁移所有数据,而是逐步进行。
  • 每次迁移一个桶的数据,其他线程可以继续访问未迁移的部分。
  • 迁移完成后,旧表会被丢弃。
多线程协作
  • 扩容过程中允许多个线程协作完成数据迁移,充分利用多核 CPU 的优势。

6. 其他优化

计数器优化
  • ConcurrentHashMap 使用了类似 LongAdder 的思想来维护元素数量,避免了对全局计数器的频繁竞争。
  • 计数器被分成多个部分,每个部分独立更新,最终通过累加得到总数。
弱一致性迭代器
  • ConcurrentHashMap 提供了弱一致性的迭代器,允许在遍历时继续修改数据。
  • 迭代器不会抛出 ConcurrentModificationException,但可能看不到最新的修改。

7. 性能对比

特性HashtableCollections.synchronizedMapConcurrentHashMap
线程安全性
锁粒度整个表整个表桶级锁或无锁
并发性能
读操作是否加锁
扩容机制全局锁全局锁渐进式扩容

8. 使用场景

ConcurrentHashMap 适用于以下场景:

  1. 高并发环境:需要频繁地进行读写操作,且要求较高的并发性能。
  2. 统计与计数:例如统计访问次数、记录事件等。
  3. 缓存系统:作为线程安全的缓存存储结构。

9. 注意事项

  1. 弱一致性

    • ConcurrentHashMap 的迭代器和一些方法(如 size())提供的是弱一致性,可能无法反映最新的状态。
  2. 不支持 null 键或值

    • ConcurrentHashMap 不允许插入 null 键或值,以避免歧义。
  3. 内存消耗

    • 由于采用了红黑树和细粒度锁等机制,ConcurrentHashMap 的内存开销可能比普通 HashMap 更高。

总结

ConcurrentHashMap 的优化主要体现在以下几个方面:

  • 分段锁到细粒度锁:从粗粒度的段锁进化为细粒度的桶锁,甚至无锁操作。
  • CAS 操作:减少了锁的使用,提升了并发性能。
  • 动态扩容:采用渐进式扩容和多线程协作,避免了全表锁。
  • 数据结构优化:链表转红黑树,提升了查询效率。

这些优化使得 ConcurrentHashMap 成为了高并发场景下最常用的线程安全哈希表实现之一。

LongAdder思想

LongAdder 是 Java 8 引入的一个类,位于 java.util.concurrent.atomic 包中。它主要用于高并发环境下高效地执行计数操作。LongAdder 的设计理念旨在解决在高竞争条件下 AtomicLong 的性能瓶颈问题。

1. 基本思想

分段锁机制(Striped Locking)
  • 基本概念LongAdder 采用了一种称为“分段”的策略来减少线程间的竞争。它将计数器内部划分为多个部分(或称段),每个部分可以独立进行加减操作。
  • 原理:当有多个线程尝试更新计数值时,它们会被分配到不同的段上进行操作,从而减少了直接的竞争和锁的争用。最终的结果是通过累加所有段的值得到的。
惰性初始化(Lazy Initialization)
  • LongAdder 在创建时不会立即分配所有的段,而是根据实际需要动态增加段的数量。这种惰性初始化的方式有助于节省内存资源。

2. 工作流程

  1. 初始状态LongAdder 初始化时只有一个基础单元(base unit),此时的操作类似于 AtomicLong
  2. 低竞争情况:如果当前只有少量线程或者没有竞争发生,所有的增减操作都会直接作用于这个基础单元。
  3. 高竞争情况:一旦检测到较高的竞争度(例如,多个线程试图同时修改同一个基础单元),LongAdder 会创建额外的单元,并将新的增减请求分散到这些新创建的单元上。
  4. 结果汇总:当需要获取总计数值时,LongAdder 会对所有单元(包括基础单元和其他附加单元)的值进行求和。

3. 优势与劣势

优势
  • 高并发性能:相较于 AtomicLong,在高并发场景下,LongAdder 能够提供更好的性能表现,因为它减少了线程之间的竞争。
  • 可扩展性:随着竞争加剧,LongAdder 可以自动扩展其内部结构以适应更高的并发量。
劣势
  • 空间成本:为了支持多线程并行操作,LongAdder 需要维护更多的内部状态,这可能会导致比 AtomicLong 更高的内存消耗。
  • 复杂性增加:由于引入了分段机制,逻辑相对复杂了一些,但这主要是对开发者透明的,用户无需关心其实现细节。

4. 使用场景

LongAdder 特别适用于那些读写频繁且对精确度要求不是特别严格的统计场景,如:

  • 统计网站访问次数
  • 监控系统中的指标收集
  • 并发任务完成数量统计等

对于不需要实时精确计数的应用场景,LongAdder 提供了一个非常有效的解决方案。然而,在需要精确一致性的场合(比如财务计算),可能仍需选择 AtomicLong 或其他同步机制。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词