余声-个人博客


  • 首页

  • 分类

  • 归档

  • 标签

Mysql事务

发表于 2024-11-10 | 更新于 2025-09-14 | 分类于 数据库
字数统计 | 阅读时长

一、事务

事务指的是逻辑上的一组操作,组成这组操作的各个单元要么全都成功,要么全都失败。
事务作用:保证在一个事务中多次SQL操作要么全都成功,要么全都失败。

1、特性

  • 原子性:事务的操作要么都发生,要么都不发生;
  • 一致性:事务前后数据的完整性必须保持一致;
  • 隔离性:一个用户的事务不能被其他用户的事务干扰;多个并发事务之间的数据相互隔离;隔离性由隔离级别保障!
  • 持久性:事务一旦提交,对数据的修改时永久性的;

2、事务并发问题

  • 脏读:一个事务读到了另一个事务未提交的数据
  • 不可重复读:一个事务读到了另一个事务已经提交(update)的数据。引发事务中的多次查询结果不
    一致
  • 虚读 /幻读:一个事务读到了另一个事务已经插入(insert)的数据。导致事务中多次查询的结果不一
    致
  • 丢失更新的问题!

3、隔离级别

  • read uncommitted 读未提交【RU】,一个事务读到另一个事务没有提交的数据
    存在:3个问题(脏读、不可重复读、幻读)。
  • read committed 读已提交【RC】,一个事务读到另一个事务已经提交的数据
    存在:2个问题(不可重复读、幻读)。
    解决:1个问题(脏读)
  • repeatable read:可重复读【RR】,在一个事务中读到的数据始终保持一致,无论另一个事务是
    否提交
    解决:3个问题(脏读、不可重复读、幻读)
  • serializable 串行化,同时只能执行一个事务,相当于事务中的单线程
    解决:3个问题(脏读、不可重复读、幻读)

二、事务底层

1、丢失更新问题

  • 两个事务针对同一个数据进行修改操作时会丢失更新!
  • 解决方案:
    • 基于锁并发控制LBCC
    • 基于版本并发控制MVCC

三、MVCC

核心思想是读不加锁,读写不冲突

MVCC 实现原理关键在于数据快照,不同的事务访问不同版本的数据快照,从而实现事务下对数据的隔离级别

MVCC,全称Multiversion Concurrency Control,即多版本并发控制,是数据库领域中一种用于管理并发数据访问的机制。与数据库锁相似,MVCC也是一种并发控制的解决方案,但它侧重于通过维护数据的多个版本来避免读写冲突,从而提高并发性能。

MVCC的基本原理

在数据库中,对数据的操作主要分为读和写两种。在并发场景下,会出现读-读并发、读-写并发和写-写并发三种情况。其中,读-读并发通常不会引发问题,写-写并发则常通过加锁来解决,而读-写并发则可以通过MVCC机制来高效处理。

MVCC的核心思想是,对于同一份数据,每个事务在读取时都会看到一个特定的、一致的数据版本,这个版本是在该事务开始时刻生成的。这样,即使有其他事务在修改数据,也不会影响到当前事务的读取结果。

快照读与当前读

MVCC的实现依赖于快照读的概念。快照读是指读取的是快照数据,即快照生成时的数据状态。在MySQL中,普通的SELECT语句(不加锁)通常就是快照读。与快照读相对应的是当前读,它读取的是最新数据,通常用于加锁的SELECT操作或数据的增删改操作。

Undo Log与快照

Undo Log是MySQL中用于回退的事务日志。在事务提交之前,MySQL会先记录更新前的数据到Undo Log中。这些“更新前的数据”实际上就是快照数据。因此,Undo Log是MVCC实现的重要手段。

每当一条记录发生变更时,MySQL都会先将其快照存储到Undo Log中,并更新记录中的隐式字段。这些隐式字段包括:

  • db_row_id:隐藏主键,用于创建聚簇索引。
  • db_trx_id:对这条记录做了最新一次修改的事务的ID。
  • db_roll_ptr:回滚指针,指向这条记录的上一个版本(即Undo Log中的上一个快照的地址)。

这样,每个快照都通过db_trx_id和db_roll_ptr字段形成了一个快照链表。

Read View与可见性

然而,即使有了Undo Log和快照链表,我们仍然需要确定在当前事务中应该读取哪个快照。这时,就需要用到Read View了。

Read View是InnoDB中一个至关重要的概念,它是实现MVCC的基础。Read View主要用来解决可见性问题,即它会告诉当前事务应该看到哪个版本的数据。具体来说,Read View会根据当前事务的ID和其他活跃事务的ID来构建一个视图,然后基于这个视图来确定哪些数据版本对当前事务是可见的。

通过Read View,InnoDB能够确保每个事务在读取数据时都能看到一个一致的快照,从而避免了读写冲突,提高了并发性能。

综上所述,MVCC通过维护数据的多个版本、利用快照读和Undo Log以及Read View等机制,实现了高效的并发控制。这使得数据库能够在高并发环境下保持数据的一致性和完整性,同时提高了系统的性能和吞吐量。

小结

  • MVCC指在使用RC、RR隔离级别下,使不同事务的 读-写 、 写-读 操作并发执行,提升系统性能
  • MVCC核心思想是读不加锁,读写不冲突。
  • RC、RR这两个隔离级别的一个很大不同就是生成 ReadView 的时机不同
  • RC在每一次进行普通 SELECT 操作前都会生成一个 ReadView
  • RR在第一次进行普通 SELECT 操作前生成一个 ReadView ,之后的查询操作都重复这个ReadView

Mysql事务

发表于 2024-11-10 | 更新于 2025-09-14 | 分类于 数据库
字数统计 | 阅读时长

什么是索引下推

官方索引条件下推:

Index Condition Pushdown,简称ICP。是MySQL5.6对使用索引从表中检索行的
一种优化。ICP可以减少存储引擎必须访问基表的次数以及MySQL服务器必须访问存储引擎的次数。可
用于 InnoDB 和 MyISAM 表,对于InnoDB表ICP仅用于辅助索引。
可以通过参数optimizer_switch控制ICP的开始和关闭。

索引下推和覆盖索引是减少回表的一种手段

  • ICP可以有效减少回表查询次数和返回给服务层的记录数,从而
    减少了磁盘IO次数和服务层与存储引擎的交互次数。

问题

以InnoDB的辅助索引为例,来讲解ICP的作用:MySQl在使用组合索引在检索数据时是使用最左前缀原
则来定位记录,左侧前缀之后不匹配的后缀,MySQL会怎么处理?

配置

#optimizer_switch优化相关参数开关
mysql> show VARIABLES like ‘optimizer_switch’;
#关闭ICP
SET optimizer_switch = ‘index_condition_pushdown=off’;
#开启ICP
SET optimizer_switch = ‘index_condition_pushdown=on’;

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CREATE TABLE `test1`  (
`id` int NOT NULL,
`id1` int NULL DEFAULT NULL,
`id2` int NULL DEFAULT NULL,
`id3` int NULL DEFAULT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `id1`(`id1`, `id2`, `id3`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of test1
-- ----------------------------
INSERT INTO `test1` VALUES (1, 2, 3, 4, 'zhangsan ');
INSERT INTO `test1` VALUES (2, 2, 3, 5, 'lisi');
INSERT INTO `test1` VALUES (3, 2, 4, 5, 'wangwu ');

SET FOREIGN_KEY_CHECKS = 1;
1
2

EXPLAIN SELECT * FROM `test1` WHERE id1 = 1 AND id2 > 1 AND id3 = 3;

如果使用了索引下推,Extra 中是Using index condition

注意!!!

如果你的 EXPLAIN 输出显示:

1
2
3
Extra: Using where; Using index
Using where:表示部分条件是在 MySQL 层处理的,而不是完全依赖索引。
Using index:说明查询是覆盖索引的(即所有查询的数据都可以从索引中获取,避免回表)。

这说明,虽然联合索引 (id1, id2, id3) 被使用,但优化器可能认为使用 索引覆盖扫描 比触发索引下推更高效。

volatile笔记

发表于 2024-11-10 | 更新于 2025-09-14 | 分类于 笔记
字数统计 | 阅读时长

volatile能保证原子性吗?

volatile本身不是锁

一、原子性

原子性是指一个操作或者多个操作要么全部执行,要么全部不执行,中间不会被其他线程的操作打断。在Java中,原子性通常通过同步机制(如synchronized)或者原子类(如AtomicInteger、AtomicLong等)来保证。

为什么需要原子性

在并发编程中,多个线程可能会同时访问和修改同一个共享资源。如果没有适当的同步机制,就可能会出现数据不一致、数据丢失等问题。原子性操作可以确保这些共享资源在并发访问时的正确性和一致性。

如何实现原子性操作

  1. 使用synchronized关键字:
    synchronized关键字可以用来修饰方法或代码块,以确保同一时间只有一个线程可以执行被修饰的代码。这是保证原子性的一种常见方式。

  2. 使用原子类:
    Java提供了一些原子类(如AtomicInteger、AtomicLong、AtomicReference等),这些类内部使用了高效的机制来确保操作的原子性。这些类通常用于实现计数器、状态标志等需要原子性操作的场景。

  3. 使用锁(Lock):
    Java的java.util.concurrent.locks包提供了一些高级的锁机制(如ReentrantLock、ReadWriteLock等),这些锁提供了比synchronized更灵活的同步控制。

  4. 使用低级别的原子操作:
    在某些情况下,开发者可能需要直接使用Java的Unsafe类或者JNI(Java Native Interface)来调用操作系统的原子操作指令。这种方式通常用于实现高性能的并发数据结构或算法。

示例:使用AtomicInteger实现原子性自增

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.concurrent.atomic.AtomicInteger;

public class Test {
private AtomicInteger number = new AtomicInteger(0);

public void increase() {
number.incrementAndGet(); // 原子性自增操作
}

public static void main(String[] args) {
Test atomicDemo = new Test();
for (int j = 0; j < 10; j++) {
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
atomicDemo.increase();
}
}, String.valueOf(j)).start();
}

// 等待所有线程执行完毕
while (Thread.activeCount() > 2) {
Thread.yield();
}

System.out.println(Thread.currentThread().getName() + " final number result = " + atomicDemo.number.get());
}
}

在这个示例中,我们使用了AtomicInteger的incrementAndGet方法来实现原子性自增操作。由于AtomicInteger内部使用了高效的原子操作机制,因此可以保证在多线程环境下的正确性。运行这个程序,你会发现每次的结果都是10000,这与使用volatile修饰的变量时的结果不同。

二、volatile是如何保证可见性和有序性的?

volatile和可见性

对于volatile变量,当对volatile变量进行写操作的时候,JVM会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中。 所以,如果一个变量被volatile所修饰的话,在每次数据变化之后,其值都会被强制刷入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个volatile在并发编程中,其值在多个缓存中是可见的。

三、synchronized和volatile 区别

synchronized的特点和缺点

synchronized是一种加锁机制,用于确保在同一时刻只有一个线程可以执行某个方法或代码块。它的主要优点在于能够确保线程安全,防止多个线程同时修改共享资源导致的数据不一致问题。然而,synchronized也存在一些缺点:

  1. 性能损耗:虽然JDK 1.6及以后的版本对synchronized进行了多种优化(如适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等),但加锁和解锁的过程仍然会带来一定的性能损耗。

  2. 产生阻塞:synchronized实现的锁本质上是一种阻塞锁。当多个线程同时访问同一段同步代码时,只有一个线程能够获取锁并进入临界区,其他线程则需要在Entry Set中等待。这可能导致线程阻塞和上下文切换,从而影响系统的并发性能。

volatile的特点和优势

volatile是Java虚拟机提供的一种轻量级同步机制,它主要用于确保变量的可见性,并禁止指令重排。与synchronized相比,volatile具有以下特点和优势:

  1. 性能更高:volatile变量的读操作与普通变量几乎无差别,写操作虽然由于需要插入内存屏障而稍慢一些,但在大多数场景下,其开销仍然比锁要低。

  2. 禁止指令重排:volatile通过内存屏障来确保变量的可见性和有序性。这意味着,当一个线程修改了volatile变量的值后,其他线程能够立即看到这个修改。同时,volatile还能够防止编译器和处理器对指令进行重排序,从而避免某些潜在的并发问题。

  3. 非阻塞:与synchronized不同,volatile不会造成线程的阻塞。它只是确保变量的可见性和有序性,而不会限制线程对共享资源的访问。

为什么需要volatile

尽管synchronized提供了强大的线程同步功能,但在某些场景下,我们仍然需要volatile。这主要是因为:

  1. 可见性问题:在某些情况下,我们可能只需要确保变量的可见性,而不需要对共享资源进行加锁。这时,使用volatile就足够了。

  2. 指令重排问题:在某些并发场景中,指令重排可能会导致潜在的问题。而volatile能够禁止指令重排,从而避免这些问题。

  3. 性能考虑:在某些对性能要求较高的场景中,使用volatile可能比使用synchronized更加合适。因为volatile的开销更低,不会造成线程的阻塞和上下文切换。

综上所述,synchronized和volatile各有其特点和适用场景。在Java并发编程中,我们需要根据具体的需求和场景来选择合适的同步机制。

ArrayList初始容量是多少

发表于 2024-10-16 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

👌ArrayList初始容量是多少?

题目详细答案

ArrayList 是 Java 中用于动态数组的一个类。它可以在添加或删除元素时自动调整其大小。然而,ArrayList 有一个默认的初始容量,这个容量是在你创建 ArrayList 实例时如果没有明确指定容量参数时所使用的。

在 Java 的 ArrayList 实现中,默认的初始容量是 10。这意味着当你创建一个新的 ArrayList 而不指定其容量时,它会以一个内部数组长度为 10 的数组来开始。当添加的元素数量超过这个初始容量时,ArrayList 的内部数组会进行扩容,通常是增长为原来的 1.5 倍。

1
ArrayList<String> list = new ArrayList<>(); // 默认的初始容量是 10

但是,如果你知道你将要在 ArrayList 中存储多少元素,或者预计会存储多少元素,那么最好在创建时指定一个初始容量,这样可以减少由于扩容而导致的重新分配数组和复制元素的操作,从而提高性能。

1
ArrayList<String> list = new ArrayList<>(50); // 初始容量设置为 50

自从1.7之后,arraylist初始化的时候为一个空数组。但是当你去放入第一个元素的时候,会触发他的懒加载机制,使得数量变为10。

1
2
3
4
5
6
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}

所以我们的arraylist初始容量的确是10。只不过jdk8变为懒加载来节省内存。进行了一点优化。

/sp17yx6dqwcrail5>

ArrayList如何保证线程安全

发表于 2024-10-16 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

👌ArrayList如何保证线程安全?

题目详细答案

借助锁

可以通过在访问 ArrayList 的代码块上使用 synchronized 关键字来手动同步对 ArrayList 的访问。这要求所有访问 ArrayList 的代码都知道并使用相同的锁。

1
2
3
4
5
6
7
8
9
10
List<String> list = new ArrayList<>();
// ... 填充列表 ...

synchronized(list) {
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String element = it.next();
// 处理元素...
}
}

使用 Collections.synchronizedList

Collections.synchronizedList 方法返回一个线程安全的列表,该列表是通过在每个公共方法(如 add(), get(), iterator() 等)上添加同步来实现的,其中同步是基于里面的同步代码块实现。但是,和手动同步一样,它也不能解决在迭代过程中进行结构修改导致的问题。

1721369333026-315b65c8-9222-48af-add6-fde77b52b82b.png

1
List<String> list = Collections.synchronizedList(new ArrayList<>());

使用并发集合

Java 并发包 java.util.concurrent 提供了一些线程安全的集合类,如 CopyOnWriteArrayList。这些类提供了不同的线程安全保证和性能特性。

CopyOnWriteArrayList是一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行新的复制来实现的。因此,迭代器不会受到并发修改的影响,并且遍历期间不需要额外的同步。但是,当有很多写操作时,这种方法可能会很昂贵,因为它需要在每次修改时复制整个底层数组。

1
List<String> list = new CopyOnWriteArrayList<>();

选择解决方案时,需要考虑并发模式、读写比例以及性能需求。如果你的应用主要是读操作并且偶尔有写操作,CopyOnWriteArrayList是一个好选择。如果你的应用有大量的写操作,那么可能需要使用其他并发集合或手动同步策略。

/hkx8lgbi0mny13uc>

ArrayList是线程安全的吗

发表于 2024-10-16 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

👌ArrayList是线程安全的吗?

题目详细答案

ArrayList不是线程安全的。在多线程环境下,如果多个线程同时对ArrayList进行操作,可能会出现数据不一致的情况。

当多个线程同时对ArrayList进行添加、删除等操作时,可能会导致数组大小的变化,从而引发数据不一致的问题。例如,当一个线程在对ArrayList进行添加元素的操作时(这通常分为两步:先在指定位置存放元素,然后增加size的值),另一个线程可能同时进行删除或其他操作,导致数据的不一致或错误。

比如下面的这个代码,就是实际上ArrayList 放入元素的代码:

1
2
elementData[size] = e;
size = size + 1;
  1. elementData[size] = e; 这一行代码是将新的元素 e 放置在 ArrayList 的内部数组 elementData 的当前大小 size 的位置上。这里假设 elementData 数组已经足够大,可以容纳新添加的元素(实际上 ArrayList 在必要时会增长数组的大小)。
  2. size = size + 1; 这一行代码是更新 ArrayList 的大小,使其包含新添加的元素。

如果两个线程同时尝试向同一个 ArrayList 实例中添加元素,那么可能会发生以下情况:

  • 线程 A 执行 elementData[size] = eA;(假设当前 size 是 0)
  • 线程 B 执行 elementData[size] = eB;(由于线程 A 尚未更新 size,线程 B 看到的 size 仍然是 0)
  • 此时,elementData[0] 被线程 B 的 eB 覆盖,线程 A 的 eA 丢失
  • 线程 A 更新 size = 1;
  • 线程 B 更新 size = 1;(现在 size 仍然是 1,但是应该是 2,因为有两个元素被添加)

为了解决ArrayList的线程安全问题,可以采取以下几种方式:

  1. 使用Collections类的synchronizedList方法:将ArrayList转换为线程安全的List。这种方式通过在对ArrayList进行操作时加锁来保证线程安全,但可能会带来一定的性能损耗。

  2. 使用CopyOnWriteArrayList类:它是Java并发包中提供的线程安全的List实现。CopyOnWriteArrayList在对集合进行修改时,会创建一个新的数组来保存修改后的数据,这样就不会影响到其他线程对原数组的访问。因此,它适合在读操作远远多于写操作的场景下使用。

  3. 使用并发包中的锁机制:如Lock或Semaphore等,显式地使用锁来保护对ArrayList的操作,可以确保在多线程环境下数据的一致性。但这种方式需要开发人员自行管理锁的获取和释放,容易出现死锁等问题。

还可以考虑使用其他线程安全的集合类,如Vector或ConcurrentLinkedQueue等,它们本身就是线程安全的,可以直接在多线程环境下使用。

/sok8k2bhymx7np2u>

ArrayList是如何扩容的

发表于 2024-10-16 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

👌ArrayList是如何扩容的?

题目详细答案

ArrayList的扩容机制是Java集合框架中一个重要的概念,它允许ArrayList在需要时自动增加其内部数组的大小以容纳更多的元素。主要有以下的步骤

1、初始容量和扩容因子:

当创建一个新的ArrayList对象时,它通常会分配一个初始容量,这个初始容量默认为10。

ArrayList的扩容因子是一个用于计算新容量的乘数,默认为1.5。

2、扩容触发条件:

当向ArrayList中添加一个新元素,并且该元素的数量超过当前数组的容量时,就会触发扩容操作。

3、扩容策略:

扩容时,首先根据当前容量和扩容因子计算出一个新的容量。新容量的计算公式为:

newCapacity = oldCapacity + (oldCapacity >> 1),这实际上是将原容量增加50%(即乘以1.5)。

如果需要的容量大于Integer.MAX_VALUE - 8(因为数组的长度是一个int类型,其最大值是Integer.MAX_VALUE,但ArrayList需要预留一些空间用于内部操作),则会使用Integer.MAX_VALUE作为新的容量。

4、扩容过程:

创建一个新的数组,其长度为新计算的容量。

将原数组中的所有元素复制到新数组中。

将ArrayList的内部引用从原数组更新为新数组。

将新元素添加到新数组的末尾。

5、性能影响:

扩容过程涉及到内存分配和元素复制,可能会对性能产生一定的影响。因此,在使用ArrayList时,如果可能的话,最好预估需要存储的元素数量,并设置一个合适的初始容量,以减少扩容的次数。

6、扩容源码

1719246984492-7f242ac2-c31e-4cdd-a074-bc448c5d3148.png

/ghpqbh9ig3hu1coh>

ArrayList的添加与删除元素为什么慢

发表于 2024-10-16 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

👌ArrayList的添加与删除元素为什么慢?

题目详细答案

主要是由于其内部实现基于数组的特性所导致的。

ArrayList的添加与删除操作慢,主要是因为其内部实现基于数组,而数组在插入和删除元素时需要移动其他元素来保证连续性和顺序性,这个过程需要耗费较多的时间。

相对于基于链表的数据结构(如LinkedList),ArrayList的插入和删除操作的时间复杂度是O(n)级别的,而链表的时间复杂度为O(1)。

添加元素

尾部添加:

当在ArrayList的尾部添加元素时,如果当前数组的容量还未达到最大值,只需要将新元素添加到数组的末尾即可,此时时间复杂度为O(1)。

但是,当数组容量已满时,需要进行扩容操作。扩容操作通常会将数组的容量增加到当前容量的1.5倍或2倍,并将原数组中的所有元素复制到新的更大的数组中。这一过程的时间复杂度为O(n),其中n为当前数组中的元素数量。

指定位置插入:

当在ArrayList的指定位置(非尾部)插入元素时,需要将目标位置之后的所有元素向后移动一个位置,然后将新元素插入到指定位置。这个过程涉及到移动元素的操作,时间复杂度为O(n),在最坏情况下,如头部插入,需要移动所有的元素。

删除元素

尾部删除:

当删除的元素位于列表末尾时,只需要将末尾元素移除即可,时间复杂度为O(1)。

指定位置删除:

当在ArrayList的指定位置(非尾部)删除元素时,需要将删除点之后的所有元素向前移动一个位置,以填补被删除元素的位置。这个过程同样涉及到移动元素的操作,时间复杂度为O(n),在最坏情况下,如头部删除,需要移动除了被删除元素之外的所有元素。

/ngcbam50s7fa0twf>

BlockingQueue是什么

发表于 2024-10-16 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

👌BlockingQueue是什么?

题目详细答案

BlockingQueue是 Java 中定义在java.util.concurrent包下的一个接口,它扩展了Queue接口,并添加了阻塞操作。BlockingQueue提供了一种线程安全的机制,用于在多线程环境中处理生产者-消费者问题。

特点和功能

阻塞操作:BlockingQueue提供了阻塞的put和take方法:

put(E e):如果队列已满,则阻塞直到有空间可插入元素。

take():如果队列为空,则阻塞直到有元素可取。

线程安全:所有方法都使用内部锁或其他同步机制来确保线程安全。

多种实现:BlockingQueue有多种实现方式,适用于不同的场景:

ArrayBlockingQueue:基于数组的有界阻塞队列。

LinkedBlockingQueue:基于链表的可选有界阻塞队列。

PriorityBlockingQueue:支持优先级排序的无界阻塞队列。

DelayQueue:支持延迟元素的无界阻塞队列。

SynchronousQueue:不存储元素的阻塞队列,每个插入操作必须等待一个对应的移除操作。

LinkedTransferQueue:基于链表的无界阻塞队列,支持传输操作。

代码 Demo

如何使用BlockingQueue实现生产者-消费者模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class BlockingQueueExample {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);

// 生产者线程
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
System.out.println("Producing: " + i);
queue.put(i); // 如果队列已满,阻塞
Thread.sleep(100); // 模拟生产时间
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});

// 消费者线程
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
Integer value = queue.take(); // 如果队列为空,阻塞
System.out.println("Consuming: " + value);
Thread.sleep(150); // 模拟消费时间
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});

producer.start();
consumer.start();
}
}

LinkedBlockingQueue被用作BlockingQueue的实现。生产者线程不断地向队列中添加元素,而消费者线程不断地从队列中取出元素。如果队列已满,生产者线程会阻塞,直到有空间可插入元素;如果队列为空,消费者线程会阻塞,直到有元素可取。

主要方法

BlockingQueue提供了一些常用的方法,这些方法分为四类:

  1. 抛出异常:

add(E e):如果队列已满,抛出IllegalStateException。

remove():如果队列为空,抛出NoSuchElementException。

element():如果队列为空,抛出NoSuchElementException。

  1. 返回特殊值:

offer(E e):如果队列已满,返回false。

poll():如果队列为空,返回null。

peek():如果队列为空,返回null。

  1. 阻塞操作:

put(E e):如果队列已满,阻塞直到有空间可插入元素。

take():如果队列为空,阻塞直到有元素可取。

  1. 超时操作:

offer(E e, long timeout, TimeUnit unit):在指定的时间内插入元素,如果队列已满,等待直到超时或插入成功。

poll(long timeout, TimeUnit unit):在指定的时间内取出元素,如果队列为空,等待直到超时或取出成功。

/wuyri8q61obeufmq>

Comparable 和 Comparator 的区别

发表于 2024-10-16 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

👌Comparable 和 Comparator 的区别?

题目详细答案

Comparable和Comparator是 Java 中用于排序的两个接口,它们有不同的用途和实现方式。

Comparable接口

Comparable接口用于定义对象的自然排序顺序。实现此接口的类必须覆盖compareTo方法,该方法用于比较当前对象与指定对象的顺序。

类实现Comparable接口,并覆盖compareTo方法。

代码 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.*;

class Person implements Comparable<Person> {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age); // 按年龄排序
}

@Override
public String toString() {
return name + " (" + age + ")";
}
}

public class ComparableExample {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Alice", 30));
people.add(new Person("Bob", 25));
people.add(new Person("Charlie", 35));

Collections.sort(people);

for (Person person : people) {
System.out.println(person);
}
}
}

Comparator接口

Comparator接口用于定义对象的自定义排序顺序。它允许你定义多个排序标准,而不需要修改对象的类。

创建一个或多个实现Comparator接口的类,并覆盖compare方法。

代码 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import java.util.*;

class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}

@Override
public String toString() {
return name + " (" + age + ")";
}
}

class NameComparator implements Comparator<Person> {
@Override
public int compare(Person p1, Person p2) {
return p1.getName().compareTo(p2.getName()); // 按名字排序
}
}

class AgeComparator implements Comparator<Person> {
@Override
public int compare(Person p1, Person p2) {
return Integer.compare(p1.getAge(), p2.getAge()); // 按年龄排序
}
}

public class ComparatorExample {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Alice", 30));
people.add(new Person("Bob", 25));
people.add(new Person("Charlie", 35));

// 按名字排序
Collections.sort(people, new NameComparator());
System.out.println("按名字排序:");
for (Person person : people) {
System.out.println(person);
}

// 按年龄排序
Collections.sort(people, new AgeComparator());
System.out.println("按年龄排序:");
for (Person person : people) {
System.out.println(person);
}
}
}

主要区别

接口实现位置:

Comparable:对象类自身实现Comparable接口,定义其自然排序顺序。

Comparator:单独的类或匿名类实现Comparator接口,定义自定义排序顺序。

方法名称:

Comparable:实现compareTo方法。

Comparator:实现compare方法。

排序标准:

Comparable:只能有一个排序标准(自然顺序)。

Comparator:可以有多个排序标准,可以根据需要定义不同的Comparator实现。

使用场景:

Comparable:适用于单一的自然排序顺序,例如字典顺序、数字顺序等。

Comparator:适用于需要多个排序标准的场景,例如按名字排序、按年龄排序等。

/yz4xnv56p75pof6a>

ConcurrentHashMap的原理

发表于 2024-10-16 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

👌ConcurrentHashMap的原理?

题目详细答案

ConcurrentHashMap 是 Java 中一种高效的线程安全哈希表,主要用于在多线程环境下进行高并发的读写操作。它的设计和实现使得在大多数情况下能够提供比其他同步哈希表(如 HashMap)更高的并发性能。以下是 ConcurrentHashMap 的主要原理和机制

分段锁机制

在早期版本(Java 7及之前),ConcurrentHashMap 使用了分段锁机制(Segmented Locking)来实现高并发性。

分段锁:ConcurrentHashMap 将整个哈希表分成多个段(Segment),每个段维护一个独立的哈希表和锁。这样,在不同段上的操作可以并发进行,从而提高并发度。

1
2
3
4
5
6
7
8
9
10
// 伪代码示例
class ConcurrentHashMap<K, V> {
Segment<K, V>[] segments;

static class Segment<K, V> {
final ReentrantLock lock = new ReentrantLock();
HashEntry<K, V>[] table;
// 其他字段和方法
}
}

锁粒度:由于每个段都有自己的锁,只有在操作同一个段时才需要竞争锁,这大大降低了锁竞争的几率,提高了并发性能。

CAS 操作和无锁机制

在 Java 8 及之后,ConcurrentHashMap 进行了重构,摒弃了分段锁机制,转而采用了更加细粒度的锁和无锁机制(CAS 操作)。

CAS 操作:CAS(Compare-And-Swap)是一种无锁的原子操作,用于在不加锁的情况下实现线程安全。ConcurrentHashMap使用Unsafe类中的 CAS 方法来更新某些字段,从而避免了锁的开销。

1
2
3
4
// 伪代码示例
boolean casTabAt(Node<K, V>[] tab, int i, Node<K, V> c, Node<K, V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

细粒度锁:在 Java 8 中,ConcurrentHashMap使用了更加细粒度的锁(synchronized和ReentrantLock),只在必要时锁定特定的桶(bin)或节点,从而进一步提高并发性能。

红黑树

为了应对哈希冲突,ConcurrentHashMap 在链表长度超过一定阈值(默认是8)时,将链表转换为红黑树,以提高查找效率。

链表:在哈希冲突较少时,使用链表存储冲突的键值对。

红黑树:当链表长度超过阈值时,转换为红黑树,以便在大量冲突时仍能保持较高的查找效率。

1
2
3
4
// 伪代码示例
if (binCount >= TREEIFY_THRESHOLD) {
treeifyBin(tab, hash);
}

扩容机制(Rehashing)

ConcurrentHashMap 采用了渐进式扩容机制来避免扩容过程中长时间的全表锁定。

渐进式扩容:在扩容过程中,ConcurrentHashMap并不会一次性将所有数据迁移到新的哈希表中,而是采用渐进式扩容的方式,在每次插入或删除操作时,逐步迁移部分数据。

1
2
3
4
// 伪代码示例
void transfer(Node<K, V>[] tab, Node<K, V>[] nextTab) {
// 渐进式迁移数据
}

读写操作

读取操作:读取操作大部分情况下是无锁的,因为ConcurrentHashMap使用了volatile变量和 CAS 操作来保证读取的可见性和一致性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 伪代码示例
V get(Object key) {
Node<K, V>[] tab; Node<K, V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 其他读取逻辑
}
return null;
}

写入操作:写入操作则需要在必要时使用锁或 CAS 操作来保证线程安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 伪代码示例
V put(K key, V value) {
return putVal(key, value, false);
}

final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K, V>[] tab = table;;) {
Node<K, V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K, V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K, V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key || (ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K, V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K, V>(hash, key, value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K, V> p;
binCount = 2;
if ((p = ((TreeBin<K, V>)f).putTreeVal(hash, key, value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}

/gr29od28mo8ulrk5>

ConcurrentHashMap 能保证复合操作的原子性吗

发表于 2024-10-16 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

ConcurrentHashMap 能保证复合操作的原子性吗?

题目详细答案

不能,ConcurrentHashMap 是 Java 中用于处理并发访问的线程安全集合类。它通过分段锁机制来提高并发性能。然而,尽管 ConcurrentHashMap 能够保证单个操作的线程安全性,但它不能保证复合操作的原子性。

单个操作的线程安全性

ConcurrentHashMap中的单个操作,如put(),get(),remove(),是线程安全的。这意味着多个线程可以同时执行这些操作而不会导致数据不一致或抛出异常。

复合操作的原子性

复合操作是指多个基本操作的组合,例如“检查-然后-执行”模式(check-then-act),如:

如果键不存在,则添加一个新的键值对。如果键存在,则更新其值。

这些操作在ConcurrentHashMap中不是原子性的,因为在执行复合操作的过程中,可能会有其他线程对ConcurrentHashMap进行修改,导致数据不一致。例如:

1
2
3
4
5
6
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

// 检查是否包含键,然后添加键值对
if (!map.containsKey("key")) {
map.put("key", 1);
}

上面的代码片段不是线程安全的,因为在containsKey()和put()之间,另一个线程可能已经插入了相同的键。

解决方案

为了保证复合操作的原子性,可以使用ConcurrentHashMap提供的原子方法,如computeIfAbsent(),compute(),merge()等。这些方法允许在单个操作中执行复杂的计算,从而保证操作的原子性。

使用computeIfAbsent()

1
map.computeIfAbsent("key", k -> 1);

如果键"key"不存在,则将其值设置为1。此操作是原子性的。

使用compute()

1
map.compute("key", (k, v) -> (v == null) ? 1 : v + 1);

此方法允许对键进行计算,如果键不存在,则v为null,否则可以对其值进行更新。

使用merge()

1
map.merge("key", 1, Integer::sum);

如果键"key"存在,则将其值与1相加,否则将其值设置为1。此操作也是原子性的。

/bg29eue26kgthvqq>

Enumeration和Iterator接口的区别

发表于 2024-10-16 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

👌Enumeration和Iterator接口的区别?

题目详细答案

Enumeration和Iterator是 Java 中用于遍历集合的两个接口。虽然它们有相似的功能,但它们有不同的设计和使用方式。

Enumeration接口

Enumeration是一个较老的接口,存在于 Java 1.0 中。它主要用于遍历旧的集合类,如Vector和Hashtable。

常用方法

boolean hasMoreElements(): 如果枚举中仍有更多元素,则返回true。

Object nextElement(): 返回枚举中的下一个元素。

代码 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.*;

public class EnumerationExample {
public static void main(String[] args) {
Vector<String> vector = new Vector<>();
vector.add("A");
vector.add("B");
vector.add("C");

Enumeration<String> enumeration = vector.elements();
while (enumeration.hasMoreElements()) {
String element = enumeration.nextElement();
System.out.println(element);
}
}
}

Iterator接口

Iterator是在 Java 2 (JDK 1.2) 中引入的。它是集合框架的一部分,适用于所有集合类(如ArrayList、HashSet、HashMap等)。Iterator提供了更灵活的遍历方法,并允许在遍历过程中安全地移除元素。

方法

boolean hasNext(): 如果迭代器中仍有更多元素,则返回true。

E next(): 返回迭代器中的下一个元素。

void remove(): 从集合中移除迭代器返回的最后一个元素(可选操作)。

代码 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.*;

public class IteratorExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
if (element.equals("B")) {
iterator.remove(); // 安全地移除元素
}
}

System.out.println("After removal: " + list);
}
}

主要区别

接口引入时间:

Enumeration:引入于 Java 1.0。

Iterator:引入于 Java 2 (JDK 1.2)。

方法名称和功能:

Enumeration:使用hasMoreElements()和nextElement()方法。

Iterator:使用hasNext()和next()方法,并增加了remove()方法。

元素移除:

Enumeration:不支持在遍历过程中移除元素。

Iterator:支持在遍历过程中安全地移除元素(通过remove()方法)。

适用范围:

Enumeration:主要用于旧的集合类,如Vector和Hashtable。

Iterator:适用于所有集合类,是集合框架的一部分。

/vk496pkev2ub6nh4>

HashMap怎么计算hashCode的?

发表于 2024-10-16 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

👌HashMap怎么计算hashCode的?

HashMap使用键的hashCode()方法来生成哈希值,并对其进行一些处理,以提高哈希表的性能和均匀分布。

调用键的hashCode()方法

首先,HashMap调用键对象的hashCode()方法来获取一个整数哈希码。这个哈希码是由键对象的类定义的,通常是通过某种算法基于对象的内部状态计算出来的。

1
int hashCode= key.hashCode();

扰动函数 (Perturbation Function)

为了减少哈希冲突并使哈希码更加均匀地分布,HashMap对原始哈希码进行了一些额外的处理。这种处理被称为扰动函数。Java 8 及以后的HashMap实现使用以下算法来计算最终的哈希值:

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这个算法的步骤如下:

  1. 获取键的哈希码:h = key.hashCode()
  2. 右移 16 位:h >>> 16
  3. 异或运算:h ^ (h >>> 16)

这种方法通过将高位和低位的哈希码混合在一起,减少了哈希冲突的概率,从而使得哈希码更加均匀地分布在哈希表的桶中。

计算数组索引

计算出扰动后的哈希值后,HashMap使用这个值来确定键值对在哈希表中的位置。通常,HashMap使用哈希值对数组的长度取模(取余数)来计算索引:

1
int index = (n - 1) & hash;

其中,n是哈希表数组的长度。n通常是 2 的幂,这样(n - 1)就是一个全 1 的二进制数,这使得按位与操作&可以有效地替代取模操作%,从而提高性能。

demo

假设我们有一个键对象,其hashCode()返回值为 123456。那么,计算哈希值的过程如下:

  1. 调用hashCode()方法:int hashCode = 123456;
  2. 扰动函数计算:
    • h = 123456
    • h >>> 16 = 123456 >>> 16 = 1(右移 16 位)
    • hash = h ^ (h >>> 16) = 123456 ^ 1 = 123457
  3. 计算数组索引(假设数组长度n为 16,即n - 1为 15):
    • index = (15) & 123457 = 15 & 123457 = 1

最终,键值对将存储在哈希表数组的索引 1 位置。

/str1ewvagoc4qesr>

HashMap的主要参数都有哪些

发表于 2024-10-16 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

👌HashMap的主要参数都有哪些?

题目详细答案

初始容量(Initial Capacity)

初始容量是HashMap在创建时分配的桶(bucket)数组的大小。默认初始容量是 16。可以在创建HashMap时通过构造函数指定初始容量。

1
HashMap<K, V> map = newHashMap<>(initialCapacity);

负载因子(Load Factor)

负载因子是一个衡量HashMap何时需要调整大小(即扩容)的参数。默认负载因子是 0.75,这意味着当HashMap中的条目数达到当前容量的 75% 时,HashMap会进行扩容。负载因子越低,哈希表中的空闲空间越多,冲突越少,但空间利用率也越低。

1
HashMap<K, V> map = newHashMap<>(initialCapacity, loadFactor);

阈值(Threshold)

阈值是HashMap需要扩容的临界点,计算方式为初始容量 * 负载因子。当实际存储的键值对数量超过这个阈值时,HashMap会进行扩容。

桶(Bucket)

HashMap内部使用一个数组来存储链表或树(在 Java 8 及之后的版本中,当链表长度超过一定阈值时,会转化为树)。每个数组元素称为一个桶(bucket)。哈希值经过计算后决定了键值对存储在哪个桶中。

哈希函数(Hash Function)

HashMap使用哈希函数将键的哈希码转换为数组索引。Java 的HashMap使用了扰动函数(perturbation function)来减少哈希冲突:

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

链表和树(Linked List and Tree)

在桶中的键值对存储方式上,HashMap使用链表来处理哈希冲突。在 Java 8 及之后的版本中,当链表的长度超过阈值(默认是 8)时,链表会转换为红黑树,以提高查找效率。

红黑树转换阈值(Treeify Threshold)

这是一个阈值,当单个桶中的链表长度超过这个值时,链表会转换为红黑树。默认值是 8。

最小树化容量(Minimum Treeify Capacity)

这是一个阈值,当HashMap的容量小于这个值时,即使链表长度超过Treeify Threshold,也不会将链表转换为红黑树,而是会先进行扩容。默认值是 64。

扩容因子(Resize Factor)

当HashMap的大小超过阈值时,容量会加倍。即新的容量是旧容量的两倍。

迭代器(Iterators)

HashMap提供了键、值和条目的迭代器,用于遍历HashMap中的元素。迭代器是快速失败的(fail-fast),即在迭代过程中,如果HashMap结构被修改(除了通过迭代器自身的remove方法),迭代器会抛出ConcurrentModificationException。

版本(ModCount)

HashMap维护了一个内部版本号modCount,用于跟踪HashMap的结构修改次数。这在迭代器中用于检测并发修改。

这些参数和属性共同决定了HashMap的性能和行为。理解这些参数可以帮助开发者更好地使用HashMap,并在需要时进行适当的调整以满足特定的性能需求。

HashMap的负载因子初始值为什么是0.75

发表于 2024-10-16 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

👌HashMap的负载因子初始值为什么是0.75?

题目详细答案

HashMap的负载因子(load factor)初始值设为 0.75 是一个经过权衡的结果,主要考虑了性能和内存使用之间的平衡。

性能与内存使用的平衡

查找性能:在HashMap中,查找操作的时间复杂度接近 (O(1))。然而,当哈希表中的元素过多时,链地址法中的链表会变长,查找时间会增加。负载因子为 0.75 意味着在表达到 75% 满时进行扩容,这样可以保持链表的长度较短,从而保证查找操作的高效性。

内存使用:如果负载因子设置得太低(例如 0.5),HashMap会更频繁地扩容,需要更多的内存来存储未使用的桶。负载因子为 0.75 是一个较为合理的设置,可以在保证查找性能的同时,节约内存。

扩容频率

较高的负载因子(如 1.0)会减少扩容的频率,但会导致较长的链表或更多的哈希碰撞,从而影响查找性能。较低的负载因子(如 0.5)会增加扩容的频率,虽然可以减少碰撞,但会导致更多的空间浪费。

0.75 是一个折中的选择,它既能保证较少的哈希碰撞,又不会频繁地进行扩容,从而在性能和内存使用之间取得平衡。

实际应用中的经验

在实际应用中,0.75 被证明是一个有效的默认值。它在大多数情况下提供了良好的性能和较为合理的内存使用。尽管特定应用可能有不同的需求,但对于通用场景,这个默认值是经过大量实践验证的。

负载因子的灵活性

虽然 0.75 是默认值,开发者在创建HashMap时可以根据具体需求指定不同的负载因子。例如:

1
Map<Integer, String> map = newHashMap<>(initialCapacity, 0.5f);

在上述代码中,HashMap的负载因子被设置为 0.5,这可能适用于需要更高查找性能但内存使用不是主要考虑因素的场景。

HashMap默认负载因子为 0.75 是一个经过深思熟虑的选择,旨在平衡查找性能和内存使用。它在大多数情况下提供了良好的性能表现,同时避免了频繁扩容和过多的内存浪费。开发者可以根据具体需求调整负载因子,以适应不同的应用场景。

/nokyemihqmhdvppw>

HashMap,扩容过程,怎么解决哈希冲突

发表于 2024-10-16 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

👌HashMap,扩容过程,怎么解决哈希冲突?

题目详细答案

HashMap 扩容过程

Hashmap 的扩容(rehashing)主要发生在以下两种情况下:

  1. 当添加元素时,如果当前数组为空,会进行初始化:默认情况下,会创建一个长度为 16 的数组,并且加载因子(load factor)默认为 0.75。
  2. 当数组中的元素数量大于或等于数组长度与加载因子的乘积时:例如,当数组长度为 16,加载因子为 0.75,并且元素数量达到 12 时(16 * 0.75 = 12),会触发扩容。扩容时,数组长度会翻倍(通常是 2 的幂),并重新哈希所有元素到新的数组中。

在扩容过程中,hashmap 会重新计算每个元素的哈希值,并根据新的数组长度重新定位其索引位置。由于数组长度翻倍,哈希值的位运算结果可能会改变,导致元素在新数组中的位置与旧数组不同。

哈希冲突解决

哈希冲突(hash collision)是指不同的键计算出相同的哈希值,从而在哈希表中映射到同一个位置。HashMap 通过以下策略来解决哈希冲突:

  1. 链表法(链表或红黑树):在 HashMap 中,每个位置(索引)可以存储一个链表(或红黑树,当链表长度超过一定阈值时)。当发生哈希冲突时,新的元素会被添加到对应的链表中。在 Java 8 及之后的版本中,当链表长度达到 8 且数组长度大于 64 时,链表会转换为红黑树以优化性能。
  2. 哈希函数:为了降低哈希冲突的概率,HashMap 使用了一个精心设计的哈希函数来计算键的哈希值。这个哈希函数考虑了键对象的哈希码(hashCode)以及键在数组中的索引位置,通过一些位运算得到最终的哈希值。这样可以确保哈希值的分布尽可能均匀,减少冲突的可能性。
  3. 初始容量和加载因子:初始容量和加载因子也会影响哈希冲突的概率。较大的初始容量和较小的加载因子可以降低哈希冲突的概率,但也会增加空间开销。因此,在选择这些参数时需要根据具体需求进行权衡。

总的来说,HashMap 通过链表法(或红黑树)和精心设计的哈希函数来解决哈希冲突,并通过扩容和重新哈希来保持哈希表的性能和效率。

/nduaqs2uds8td4tw>

HashSet如何实现线程安全

发表于 2024-10-16 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

👌HashSet如何实现线程安全?

题目详细答案

HashSet本身不是线程安全的。如果多个线程在没有外部同步的情况下同时访问一个HashSet,并且至少有一个线程修改了集合,那么它必须保持同步。

使用Collections.synchronizedSet

Java 提供了一个简单的方法来创建一个同步的集合,通过Collections.synchronizedSet方法。这个方法返回一个线程安全的集合包装器。

1
Set<String> synchronizedSet = Collections.synchronizedSet(newHashSet<>());

使用这个方法后,所有对集合的访问都将是同步的。但是,需要注意的是,对于迭代操作,必须手动同步:

1
2
3
4
5
6
7
Set<String> synchronizedSet = Collections.synchronizedSet(newHashSet<>());
synchronized (synchronizedSet) {
Iterator<String> iterator = synchronizedSet.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}

使用ConcurrentHashMap

如果需要更高效的并发访问,可以使用ConcurrentHashMap来实现类似HashSet的功能。ConcurrentHashMap提供了更细粒度的锁机制,在高并发环境下性能更好。

1
Set<String> concurrentSet = ConcurrentHashMap.newKeySet();

ConcurrentHashMap.newKeySet()返回一个基于ConcurrentHashMap的Set实现,它是线程安全的,并且在高并发环境下性能优越。

使用CopyOnWriteArraySet

对于读操作远多于写操作的场景,可以使用CopyOnWriteArraySet。它的实现基于CopyOnWriteArrayList,在每次修改时都会复制整个底层数组,因此在写操作较少时性能较好。

1
Set<String> copyOnWriteArraySet = newCopyOnWriteArraySet<>();

手动同步

如果你不想使用上述任何一种方法,也可以手动同步HashSet的访问。可以使用synchronized关键字来保护对HashSet的访问:

1
2
3
4
Set<String> hashSet = newHashSet<>();
synchronized (hashSet) {
// 对 hashSet 的操作
}

选择合适的方案

如果你的应用程序是单线程的,或只有少量的线程访问集合,可以使用Collections.synchronizedSet。

如果你的应用程序有大量的并发读写操作,可以使用ConcurrentHashMap.newKeySet。

如果你的应用程序读操作远多于写操作,可以使用CopyOnWriteArraySet。

代码 Demo

再给大家弄一个使用ConcurrentHashMap实现线程安全Set的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashSetExample {
public static void main(String[] args) {
Set<String> concurrentSet = ConcurrentHashMap.newKeySet();

// 多线程环境下的操作示例
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
concurrentSet.add(Thread.currentThread().getName() + "-" + i);
}
};

Thread thread1 = new Thread(task, "Thread1");
Thread thread2 = new Thread(task, "Thread2");

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

try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("Set size: " + concurrentSet.size());
}
}

/uic9i6eyh8to1udk>

Hashset的底层原理

发表于 2024-10-16 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

👌Hashset的底层原理?

题目详细答案

HashSet是 Java 中一个常用的集合类,它用于存储不重复的元素。HashSet的底层实现依赖于HashMap。

基本结构

HashSet底层使用HashMap来存储元素。具体来说,每当你向HashSet中添加一个元素时,这个元素实际上是作为HashMap的键来存储的,而HashMap的值是一个固定的常量对象。

1
2
3
4
5
6
7
8
9
10
11
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {
private transient HashMap<E,Object> map;

private static final Object PRESENT = new Object();

public HashSet() {
map = new HashMap<>();
}

// 其他构造函数和方法
}

添加元素 (add方法)

当你调用HashSet的add方法时,HashSet会将元素作为键插入到HashMap中,值为一个常量对象PRESENT。

1
2
3
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}

HashMap的put方法会检查键是否已经存在,如果不存在则插入新键值对。如果键已经存在,则更新键值对并返回旧值。因此,HashSet能够保证元素唯一性。

元素查找 (contains方法)

HashSet的contains方法实际上是调用HashMap的containsKey方法。

1
2
3
public boolean contains(Object o) {
return map.containsKey(o);
}

HashMap的containsKey方法通过计算键的hashCode值来快速定位元素的位置,然后进行比较。

删除元素 (remove方法)

HashSet的remove方法调用HashMap的remove方法来删除元素。

1
2
3
public boolean remove(Object o) {
return map.remove(o) == PRESENT;
}

/zrp0iuq43z8fg3ov>

Java提供了哪些队列

发表于 2024-10-16 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

👌Java提供了哪些队列?

题目详细答案

LinkedList

基于链表实现的双向链表,实现了List、Deque和Queue接口,支持在头部和尾部进行快速插入和删除操作。

使用场景:

需要频繁插入和删除元素的场景。

需要双端队列(Deque)功能的场景,如在头部和尾部进行操作。

PriorityQueue

基于优先级堆(Priority Heap)实现的无界队列。元素按照自然顺序或指定的比较器顺序排列。不允许插入null元素。

使用场景:

需要按优先级处理元素的场景,如任务调度、事件处理等。

需要动态调整元素顺序的场景。

ArrayDeque

基于数组实现的双端队列(Deque),没有容量限制,可以动态扩展,比LinkedList更高效,尤其是在栈和队列操作方面。

使用场景:

需要高效的栈或队列操作的场景。

需要双端队列功能,但不需要线程安全的场景。

ConcurrentLinkedQueue

基于链表实现的无界非阻塞队列。使用无锁算法,提供高效的并发性能。线程安全,适用于高并发环境。

使用场景:

高并发环境下的无界队列。

需要高效的非阻塞并发操作的场景。

LinkedBlockingQueue

基于链表实现的可选有界阻塞队列,支持阻塞的put和take操作,线程安全,适用于生产者-消费者模式。

使用场景:

生产者-消费者模式,特别是在需要限制队列大小的场景。需要线程安全的阻塞队列。

ArrayBlockingQueue

基于数组实现的有界阻塞队列,必须指定容量,支持阻塞的put和take操作。线程安全,适用于生产者-消费者模式。

使用场景:

生产者-消费者模式,特别是在需要固定大小的队列时。需要线程安全的有界阻塞队列。

DelayQueue

支持延迟元素的无界阻塞队列,元素只有在其延迟时间到期后才能被取出。线程安全,适用于并发环境。

使用场景:

需要延迟处理元素的场景,如任务调度、缓存过期处理等。

定时任务执行场景。

LinkedBlockingDeque

基于链表实现的可选有界阻塞双端队列,支持阻塞的put和take操作。线程安全,适用于生产者-消费者模式。

使用场景:生产者-消费者模式,特别是在需要限制队列大小的双端队列场景。需要线程安全的阻塞双端队列。

/gfv9i9dpcf71awio>

<i class="fa fa-angle-left"></i>1…8910…12<i class="fa fa-angle-right"></i>

239 日志
22 分类
30 标签
GitHub
© 2025 javayun
由 Hexo 强力驱动
主题 - NexT.Gemini