JAVA面试复习笔记(待完善)

作者:paishishaba日期:2025/10/20

目录

布隆过滤器

一、核心思想

二、执行逻辑详解

1. 添加元素

2. 查询元素

三、为什么会有误判?

四、关键参数与性能权衡

五、执行逻辑总结与特点

六、典型应用场景

Redis 的 SETNX 命令

一、基本语法和语义

二、简单示例

三、SETNX 的核心特性

1. 原子性

2. 简单性

3. 无过期时间

四、经典应用场景

1. 分布式锁(最经典的应用)

五、SETNX 的局限性及改进方案

问题1:非原子性的设置过期时间

解决方案:使用 SET 命令的 NX 和 EX 参数

问题2:可能误删其他客户端的锁

解决方案:使用 Lua 脚本确保原子性

六、SETNX vs 新的 SET 语法

Redis的持久化

Canal

Canal 的工作原理:

缓存和 MySQL 数据同步方案对比

方案1:基于读写锁的同步(应用程序控制)

方案2:基于 Canal + binlog 的同步(解耦方案)

完整的数据同步架构

多路复用IO(I/O Multiplexing)

Spring的注解

@Repository

@Repository的作用

@Mapper注解

SpringMVC SpringBoot

Mybatis

延迟加载

Mybatis的一级二级缓存

一级缓存

基本概念

工作机制

一级缓存结构

缓存失效场景

配置选项

二级缓存

开启二级缓存

二级缓存使用示例

缓存回收策略

实体类序列化要求

两级缓存执行流程

ArrayList

Futrue、FutureTask

Future接口

FutureTask类

实例

使用示例

实际应用场景

线程状态

Java线程的打断机制


布隆过滤器

布隆过滤器是一种空间效率极高的概率型数据结构,用于判断一个元素是否一定不在一个集合中可能在集合中。它的核心特点是:高效、省空间,但有一定程度的误判率

一、核心思想

布隆过滤器的执行逻辑基于两个基本操作:添加查询。它背后是一个巨大的位数组 和一组哈希函数

  1. 位数组:初始时,所有位都设置为0。
  2. 哈希函数:多个相互独立、均匀分布的哈希函数。

二、执行逻辑详解

1. 添加元素

当一个元素被加入到布隆过滤器时,会执行以下步骤:

  1. 哈希计算:将此元素分别通过 k 个不同的哈希函数进行计算,得到 k 个哈希值。
  2. 取模定位:将每个哈希值对位数组的长度 m 取模,得到 k 个在数组范围内的位置索引。
  3. 置位:将位数组中这 k 个位置上的位都设置为 1
2. 查询元素

当需要查询一个元素是否存在于布隆过滤器中时,执行以下步骤:

  1. 哈希计算:同样,将此元素通过那 k 个哈希函数进行计算,得到 k 个哈希值。
  2. 取模定位:同样,对每个哈希值取模,得到 k 个位置索引。
  3. 检查位:检查位数组中这 k 个位置上的位。
  • 如果其中任何一个位的值为 0:那么可以肯定地得出结论——“该元素一定不在集合中”
  • 如果所有位的值都是 1:那么可以得出结论——“该元素可能在集合中”

三、为什么会有误判?

根本原因:哈希冲突。

  1. 你添加了元素 A,它将位置 1, 3, 5 设置成了 1
  2. 你添加了元素 B,它将位置 2, 4, 6 设置成了 1
  3. 现在查询一个从未添加过的元素 C。
  4. 经过哈希计算,元素 C 对应的位置恰好是 1, 4, 6
  5. 你检查位数组,发现位置 1, 4, 6 都已经被其他元素(A和B)设置成了 1

这时,布隆过滤器就会错误地认为元素 C 是存在的。这就是假阳性

总结:

  • 肯定不存在” 是100%准确的。因为只要有一个位是0,就证明这个元素从未被添加过。
  • “可能存在” 是不确定的。可能是因为元素真的存在,也可能是由其他元素设置的位偶然组合而成的。

四、关键参数与性能权衡

布隆过滤器的行为由三个参数决定:

  1. n:预期要添加的元素数量。
  2. m:位数组的大小(位数)。
  3. k:哈希函数的数量。

它们之间的关系决定了误判率

  • 位数组 m 越大,误判率越低(因为有更多的位来分散信息,冲突可能性降低),但占用空间越大。
  • 哈希函数 k 数量 需要一个最优值。太少的哈希函数容易冲突,太多的哈希函数会很快将位数组“填满”,反而增加冲突。
  • 对于给定的 nm,可以计算出一个使误判率最小的最佳哈希函数数量 k

经验公式:
当哈希函数数量 𝑘=𝑚𝑛ln⁡2k=nm​ln2 时,误判率最小。其中,为了达到指定的误判率 𝑝p,位数组大小 𝑚m 应满足 𝑚=−𝑛ln⁡𝑝(ln⁡2)2m=−(ln2)2nlnp​。

五、执行逻辑总结与特点

特性描述
空间效率非常高,只需要一个位数组和几个哈希函数。
时间效率添加和查询操作都是 O(k),常数时间,非常快。
确定性回答“不存在”是100%正确的;回答“存在”是有概率正确的。
缺点1. 误判率:存在假阳性。 2. 无法删除:由于多位共享,传统布隆过滤器无法安全删除元素(删除一个元素可能会影响其他元素)。(注:有变种如计数布隆过滤器支持删除)

六、典型应用场景

利用其“不存在则一定不存,存在则可能存在”的逻辑,布隆过滤器常用于前置快速判断,以减轻核心系统的压力。

  1. 缓存系统
    • 逻辑:先查询布隆过滤器,如果“肯定不存在”,则无需查询后端数据库,直接返回空。这可以防止缓存穿透攻击。
  2. 网页爬虫
    • 逻辑:判断一个URL是否已经被爬取过。如果布隆过滤器说“可能存在”,则大概率已经爬过,可以跳过,节省资源。
  3. 数据库
    • 逻辑:在查询数据库前,先用布隆过滤器判断数据是否存在,避免对不存在的键进行昂贵的磁盘IO操作。
  4. 恶意网站检测
    • 逻辑:浏览器本地维护一个布隆过滤器,快速判断一个网站是否在恶意网站黑名单中。如果“可能存在”,再发起一次精确查询。

Redis 的 SETNX 命令

SETNX 是 SET if Not eXists 的缩写,意思是"如果不存在则设置"。

一、基本语法和语义

SETNX key value

执行逻辑:

  1. Redis 会检查指定的 key 是否存在。
  2. 如果 key 不存在
    • 将 key 设置为指定的 value
    • 返回 1(表示设置成功)
  3. 如果 key 已经存在
    • 不进行任何操作,保持原有的 key-value 不变
    • 返回 0(表示设置失败)

二、简单示例

1# 第一次设置,key "mykey" 不存在
2127.0.0.1:6379> SETNX mykey "Hello"
3(integer) 1  # 返回 1,设置成功
4
5# 尝试再次设置相同的 key
6127.0.0.1:6379> SETNX mykey "World"
7(integer) 0  # 返回 0,设置失败
8
9# 检查值,仍然是 "Hello"
10127.0.0.1:6379> GET mykey
11"Hello"

三、SETNX 的核心特性

1. 原子性

这是 SETNX 最重要的特性!检查和设置这两个操作是在一个原子操作中完成的,不存在竞态条件。

2. 简单性

命令非常简单,只有成功(1)或失败(0)两种结果。

3. 无过期时间

传统的 SETNX 命令本身不能设置过期时间,如果需要过期时间,需要配合 EXPIRE 命令使用。

四、经典应用场景

1. 分布式锁(最经典的应用)

SETNX 是实现 Redis 分布式锁最简单的方式:

1# 客户端1获取锁
2127.0.0.1:6379> SETNX lock:order123 "client1"
3(integer) 1  # 获取锁成功
4
5# 客户端2尝试获取同一个锁(此时锁还被client1持有)
6127.0.0.1:6379> SETNX lock:order123 "client2"  
7(integer) 0  # 获取锁失败,合理
8
9# 客户端1释放锁
10127.0.0.1:6379> DEL lock:order123
11(integer) 1
12
13# 客户端2再次尝试获取锁(此时锁已释放)
14127.0.0.1:6379> SETNX lock:order123 "client2"
15(integer) 1  # 获取锁成功,合理

Redis 通过单线程模型保证了:

  • 命令执行的原子性:每个命令执行期间不会被中断
  • 自然的互斥访问:SETNX 在同一时刻只能有一个客户端成功
  • 顺序一致性:所有客户端看到相同的命令执行顺序

这正是为什么 Redis 的 SETNX 能够作为分布式锁的基础,而不需要额外的锁机制来协调客户端之间的竞争。

五、SETNX 的局限性及改进方案

问题1:非原子性的设置过期时间

1# 这种写法有风险!
2if redis.setnx("lock", "value") == 1:
3    redis.expire("lock", 10)  # 如果在这条命令执行前程序崩溃,锁将永远不会释放!

解决方案:使用 SET 命令的 NX 和 EX 参数

Redis 2.6.12 之后,推荐使用 SET 命令的扩展语法:

1# 原子性的设置值和过期时间
2SET key value NX EX 10
  • NX:等同于 SETNX,只在 key 不存在时设置
  • EX:设置过期时间(秒)

问题2:可能误删其他客户端的锁

简单的 DEL 操作可能删除其他客户端持有的锁。

解决方案:使用 Lua 脚本确保原子性

Lua脚本更像是"存储过程",而MySQL的事务提供了ACID特性

Lua脚本为什么能解决这个问题?

Lua脚本的原子性解决方案

1-- Lua脚本:检查值匹配再删除
2if redis.call("GET", KEYS[1]) == ARGV[1] then
3    return redis.call("DEL", KEYS[1])
4else
5    return 0
6end

在Redis中执行:

127.0.0.1:6379> EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end" 1 lock:order123 "client1"

  1. 原子性执行:Redis保证Lua脚本在执行期间不会被其他命令打断
  2. 检查+删除的原子组合:GET和DEL操作在脚本中是一个不可分割的整体
  3. 值验证:只有锁的值与预期值匹配时才执行删除

非原子操作的问题

1# 错误的做法:分两步操作
2127.0.0.1:6379> GET lock:order123
3"client1"
4# 在这两步之间,锁可能被其他客户端修改!
5
6127.0.0.1:6379> DEL lock:order123  # 如果锁已经被修改,这里就会误删

六、SETNX vs 新的 SET 语法

特性SETNX + EXPIRESET with NX & EX
原子性非原子(两条命令)原子操作
过期时间需要额外命令内置支持
推荐度不推荐推荐
Redis版本所有版本2.6.12+

Redis的持久化

特性RDBAOF
存储内容数据快照操作命令
文件格式二进制(紧凑)文本(Redis协议)
文件大小较小较大
恢复速度快(直接加载数据)慢(需要重放所有命令)
数据安全性可能丢失最后一次快照后的数据根据配置,最多丢失1秒数据
性能影响保存时对性能影响大写入时对性能影响小

Canal

  • Canal是阿里巴巴开源的一个基于MySQL数据库Binlog的增量订阅和消费组件。它模拟MySQL Slave的交互协议,伪装自己为MySQL Slave,向MySQL Master发送dump请求,MySQL Master收到请求后,开始推送Binlog给Canal。
  • Canal解析Binlog,并将其转换成更容易处理的结构化数据,供下游系统(如缓存、消息队列等)使用。
  • 常见用途:数据库同步、缓存更新、搜索索引更新等。

Binlog(二进制日志)是什么?

  • Binlog是MySQL的一种日志,它记录了对数据库执行的所有更改操作(如INSERT、UPDATE、DELETE等),但不包括SELECT这类不修改数据的操作。
  • Binlog是MySQL服务器层维护的,与存储引擎无关,也就是说无论使用InnoDB还是其他引擎,只要开启了Binlog,就会记录。
  • Binlog主要用于:
    • 主从复制(Replication):主服务器将Binlog发送给从服务器,从服务器重放这些日志以保持数据一致。
    • 数据恢复:通过重放Binlog来恢复数据到某个时间点。

Canal 的工作原理:

MySQL主库 ──binlog──> Canal Server ──解析后的数据──> 应用程序(如缓存更新)

执行流程:

  1. 伪装从库:Canal 把自己伪装成 MySQL 的从库(slave)
  2. 请求binlog:向 MySQL 主库发送 dump 请求,获取 binlog
  3. 解析binlog:解析 binlog 中的变更事件
  4. 推送数据:将解析后的结构化数据推送给订阅者
1// Canal解析出的数据格式示例
2{
3  "database": "shop",
4  "table": "products", 
5  "type": "UPDATE",  // 操作类型
6  "data": [
7    {
8      "id": 1001,
9      "name": "iPhone", 
10      "price": 5999,     // 新价格
11      "stock": 50
12    }
13  ],
14  "old": [
15    {
16      "price": 5499     // 旧价格
17    }
18  ]
19}

缓存和 MySQL 数据同步方案对比

"读写锁"是一种方案,但 Canal + binlog 是另一种更优雅的方案:

方案1:基于读写锁的同步(应用程序控制)

1// 伪代码:在业务代码中手动维护缓存一致性
2public void updateProduct(Product product) {
3    // 获取写锁
4    Lock writeLock = redis.getLock("product:" + product.getId());
5    
6    try {
7        // 1. 更新数据库
8        productMapper.update(product);
9        
10        // 2. 删除/更新缓存
11        redis.delete("product:" + product.getId());
12        
13    } finally {
14        writeLock.unlock();
15    }
16}
17
18public Product getProduct(Long id) {
19    // 获取读锁
20    Lock readLock = redis.getLock("product:" + id);
21    
22    try {
23        // 先查缓存,再查数据库...
24    } finally {
25        readLock.unlock();
26    }
27}

缺点:

  • 代码侵入性强:每个数据库操作都要手动维护缓存
  • 容易遗漏:复杂的业务逻辑可能忘记更新缓存
  • 性能开销:锁竞争影响性能

方案2:基于 Canal + binlog 的同步(解耦方案)

1// Canal客户端:监听数据库变更,自动更新缓存
2@CanalEventListener
3public class CacheUpdateListener {
4    
5    @ListenPoint
6    public void onProductUpdate(ProductChangeEvent event) {
7        if (event.getType() == UPDATE || event.getType() == DELETE) {
8            // 自动删除对应的缓存
9            redis.delete("product:" + event.getId());
10        }
11        
12        if (event.getType() == INSERT || event.getType() == UPDATE) {
13            // 或者更新缓存
14            redis.set("product:" + event.getId(), event.getNewData());
15        }
16    }
17}

优点:

  • 解耦:缓存同步与业务代码完全分离
  • 可靠:基于 binlog,不会遗漏任何数据变更
  • 通用:一套方案适用于所有表的缓存同步

完整的数据同步架构

在实际项目中,通常会采用这样的架构:

1MySQL ──binlog──> Canal ──MQ──> 多个消费者
2                              ├── 缓存服务(更新Redis)
3                              ├── 搜索服务(更新Elasticsearch)
4                              ├── 大数据服务(更新数据仓库)
5                              └── 消息推送服务

在现代分布式系统中,Canal + binlog 的方案更加流行,因为它提供了更好的解耦性和可维护性。

多路复用IO(I/O Multiplexing)

核心思想:

一个线程监控多个 I/O 操作,哪个准备好了就处理哪个

Spring的注解

@Repository

它是一个数据访问层的标记,同时能够将数据访问异常转换为Spring的统一数据访问异常

@Repository的作用

1. 标识数据访问层组件

1@Repository
2public class UserDaoImpl implements UserDao {
3    @Autowired
4    private JdbcTemplate jdbcTemplate;
5    
6    public User findById(Long id) {
7        String sql = "SELECT * FROM users WHERE id = ?";
8        return jdbcTemplate.queryForObject(sql, new UserRowMapper(), id);
9    }
10}

2. 自动异常转换

  • 将特定持久化技术的异常(如JDBC的SQLException)转换为Spring的统一数据访问异常
  • 提供一致的异常处理体验

3. Bean自动扫描与注册

在Spring配置中:

1@Configuration
2@ComponentScan("com.example.dao") // 扫描带有@Repository的类
3public class AppConfig {
4}

@Mapper注解

@Mapper注解并非由Spring、SpringMVC或SpringBoot框架提供,它是MyBatis框架的核心注解

注解所属框架主要作用
@MapperMyBatis标记一个接口为MyBatis的映射器(Mapper),MyBatis会在编译时为其动态生成代理实现类-2-5。这样你就可以直接通过接口方法执行SQL操作,无需编写实现类。
@RepositorySpring作为Spring的** stereotype注解**之一,用于标识一个类为数据访问层(DAO)的Bean-2。它的主要作用是让Spring在扫描时能识别并将其纳入容器管理,同时能够将平台特定的数据访问异常转换为Spring统一的异常-2

虽然@Mapper是MyBatis的注解,但它设计的目的就是为了与Spring框架无缝整合。

使用@MapperScan:这是更推荐的方式。@MapperScan也是MyBatis提供的注解,你只需在SpringBoot的启动类上使用它,并指定Mapper接口所在的包路径

1@SpringBootApplication
2@MapperScan("com.example.mapper") // 扫描该包下的所有接口
3public class Application {
4    public static void main(String[] args) {
5        SpringApplication.run(Application.class, args);
6    }
7}

使用后,包内所有Mapper接口都无需再单独添加@Mapper@Repository注解,Spring和MyBatis会自动完成所有处理,非常方便

SpringMVC SpringBoot

注解所属框架引入版本
@GetMappingSpring MVCSpring 4.3+
@PostMappingSpring MVCSpring 4.3+
@PutMappingSpring MVCSpring 4.3+
@DeleteMappingSpring MVCSpring 4.3+
@PatchMappingSpring MVCSpring 4.3+
@RequestMappingSpring MVCSpring 2.5+

Spring MVC:提供Web开发能力,包括这些注解

Spring Boot:通过自动配置,让Spring MVC开箱即用

Mybatis

延迟加载

MyBatis的延迟加载(Lazy Loading)是一种在需要时才加载相关对象数据的机制,目的是减少不必要的数据库查询,提升性能。

工作原理:

  1. 当查询主对象时,MyBatis不会立即加载与主对象关联的子对象(如一对一、一对多关联),而是返回一个代理对象。
  2. 当程序第一次访问关联对象时,代理对象会触发一次额外的查询,去数据库加载关联对象的数据。

实现方式:
MyBatis通过动态代理技术实现延迟加载。例如,当查询一个订单(Order)时,订单中有一个用户(User)对象(多对一关联)和一个订单明细(OrderDetail)列表(一对多关联)。如果启用延迟加载,那么当获取订单时,不会立即加载用户和订单明细,直到你调用order.getUser()或order.getOrderDetails()时,MyBatis才会执行相应的查询。

配置延迟加载:
在MyBatis的配置文件中,可以设置lazyLoadingEnabledtrue来启用延迟加载。还可以使用aggressiveLazyLoading(早期版本)或lazyLoadTriggerMethods等参数来控制加载行为。

注意:在MyBatis 3.4.1及以后版本,aggressiveLazyLoading的默认值改为false,而lazyLoadingEnabled的默认值也是false

使用延迟加载的注意事项:

  1. 延迟加载可以减少不必要的数据库查询,但也可能导致“N+1查询问题”(当遍历一个集合时,每个元素都会触发一次查询,导致多次查询)。
  2. 在Web应用中,如果延迟加载发生在视图渲染阶段,而数据库连接已经关闭,则会抛出异常。解决方法是使用OpenSessionInView模式或在事务范围内完成数据加载。
配置项说明默认值
lazyLoadingEnabled是否启用延迟加载false
aggressiveLazyLoading侵略性延迟加载(任何方法调用都会加载)false (3.4.1+)
lazyLoadTriggerMethods触发加载的方法equals,clone,hashCode,toString

优点

  • 减少不必要的数据传输
  • 提高初始查询速度
  • 节省内存资源

缺点

  • 可能产生"N+1查询"问题
  • 增加代码复杂度
  • 需要注意会话生命周期管理

Mybatis的一级二级缓存

特性一级缓存二级缓存
作用范围SqlSession内部Mapper命名空间
默认状态开启关闭
共享性不能共享跨SqlSession共享
存储位置内存内存/磁盘/第三方存储
生命周期随SqlSession销毁随应用关闭销毁
适用场景单次会话内重复查询全局频繁查询且更新少

一级缓存

基本概念
  • 范围:SqlSession 级别(默认开启)
  • 生命周期:与 SqlSession 相同
  • 共享性:同一个 SqlSession 内共享
工作机制
1// 示例:一级缓存演示
2SqlSession sqlSession = sqlSessionFactory.openSession();
3UserMapper mapper = sqlSession.getMapper(UserMapper.class);
4
5// 第一次查询,访问数据库
6User user1 = mapper.selectUserById(1L);
7System.out.println("第一次查询,执行SQL");
8
9// 第二次查询相同数据,从一级缓存获取
10User user2 = mapper.selectUserById(1L); 
11System.out.println("第二次查询,从缓存获取");
12
13// 验证是同一个对象
14System.out.println(user1 == user2); // 输出:true
15
16sqlSession.close();
一级缓存结构
1// 伪代码:PerpetualCache 实现
2public class PerpetualCache implements Cache {
3    private String id;
4    private Map<Object, Object> cache = new HashMap<>();
5    
6    @Override
7    public void putObject(Object key, Object value) {
8        cache.put(key, value);
9    }
10    
11    @Override
12    public Object getObject(Object key) {
13        return cache.get(key);
14    }
15}
缓存失效场景
1// 1. 执行增删改操作
2UserMapper mapper = sqlSession.getMapper(UserMapper.class);
3User user1 = mapper.selectUserById(1L); // 查询,加入缓存
4
5mapper.updateUser(user1); // 更新操作,清空一级缓存
6
7User user2 = mapper.selectUserById(1L); // 重新查询数据库
8
9// 2. 手动清空缓存
10sqlSession.clearCache(); // 手动清空一级缓存
11
12// 3. 关闭SqlSession
13sqlSession.close(); // 关闭会话,缓存销毁
配置选项
1<!-- 在settings中配置本地缓存作用域 -->
2<settings>
3    <!-- SESSION: 同一个SqlSession共享(默认) -->
4    <!-- STATEMENT: 缓存仅对当前语句有效,相当于关闭一级缓存 -->
5    <setting name="localCacheScope" value="SESSION"/>
6</settings>

二级缓存

开启二级缓存

1. 全局配置

1<!-- mybatis-config.xml -->
2<settings>
3    <!-- 开启二级缓存(默认就是true,可省略) -->
4    <setting name="cacheEnabled" value="true"/>
5</settings>

2. Mapper配置

1<!-- UserMapper.xml -->
2<mapper namespace="com.example.mapper.UserMapper">
3    <!-- 开启本Mapper的二级缓存 -->
4    <cache
5        eviction="FIFO"           <!-- 回收策略:FIFO -->
6        flushInterval="60000"     <!-- 刷新间隔:60秒 -->
7        size="512"                <!-- 引用数目:512个 -->
8        readOnly="true"/>         <!-- 只读:true -->
9    
10    <select id="selectUserById" parameterType="long" resultType="User">
11        SELECT * FROM users WHERE id = #{id}
12    </select>
13</mapper>
二级缓存使用示例
1// 多个SqlSession共享二级缓存
2SqlSession sqlSession1 = sqlSessionFactory.openSession();
3UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
4User user1 = mapper1.selectUserById(1L); // 查询数据库
5sqlSession1.close(); // 重要:必须关闭,数据才会进入二级缓存
6
7SqlSession sqlSession2 = sqlSessionFactory.openSession();
8UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
9User user2 = mapper2.selectUserById(1L); // 从二级缓存获取
10sqlSession2.close();
11
12System.out.println(user1 == user2); // 输出:false(不同对象,但数据相同)
缓存回收策略
策略描述适用场景
LRU最近最少使用最常用策略
FIFO先进先出按顺序淘汰
SOFT软引用内存不足时GC回收
WEAK弱引用更积极地GC回收

一级缓存在session.close(); 的时候 一级缓存就被完全清理,HashMap被丢弃

实体类序列化要求
1// 使用二级缓存的实体类建议实现Serializable(因为二级不一定使用
2默认的PerpetualCache(HashMap存储),二级缓存更常使用外部缓存(redis))
3public class User implements Serializable {
4    private static final long serialVersionUID = 1L;
5    
6    private Long id;
7    private String name;
8    // getter/setter...
9}
两级缓存执行流程
1// 缓存查询顺序
2public class Executor {
3    public <E> List<E> query(MappedStatement ms, Object parameter) {
4        // 1. 生成缓存Key
5        CacheKey key = createCacheKey(ms, parameter);
6        
7        // 2. 先查询二级缓存
8        List<E> list = (List<E>) tcm.getObject(cache, key);
9        if (list != null) {
10            return list;
11        }
12        
13        // 3. 查询一级缓存
14        list = (List<E>) localCache.getObject(key);
15        if (list != null) {
16            return list;
17        }
18        
19        // 4. 查询数据库
20        list = queryFromDatabase(ms, parameter);
21        
22        // 5. 放入一级缓存
23        localCache.putObject(key, list);
24        
25        return list;
26    }
27}

虽然默认都是HashMap,但二级缓存更常使用外部缓存:

1// 情况1:使用默认的PerpetualCache(HashMap存储)
2public class User {  // 不实现Serializable也可以
3    private Long id;
4    private String name;
5    // 在默认的PerpetualCache+HashMap中能正常工作
6}
7
8// 情况2:使用分布式缓存(Redis等)  
9public class User implements Serializable {  // 必须实现
10    private static final long serialVersionUID = 1L;
11    private Long id;
12    private String name;
13}

ArrayList

ArrayList有两个相关的构造方法:

  1. ArrayList(int initialCapacity):构造一个具有指定初始容量的空列表。
  2. ArrayList():构造一个初始容量为10的空列表(注意,在JDK8中,默认构造方法初始容量为10,但实际是在第一次添加元素时才分配容量为10的数组)。
  • new ArrayList(10):创建时直接分配容量为10的数组,0次扩容
  • new ArrayList():创建时空数组,首次添加元素时扩容1次到默认容量10

Futrue、FutureTask

Future是Java并发编程中的一个接口,它代表一个异步计算的结果。Future提供了检查计算是否完成、等待计算完成以及获取计算结果的方法。如果计算尚未完成,get方法会阻塞直到计算完成。

FutureTask是Future的一个基础实现类,它实现了Runnable接口,因此可以由一个线程来执行。FutureTask可以包装一个Callable或Runnable对象,因为Callable可以返回结果,而Runnable不能,所以当包装Runnable时,需要额外提供一个结果(或者使用null)。

Future接口

Future接口定义了以下方法:

  • boolean cancel(boolean mayInterruptIfRunning):尝试取消执行此任务。如果任务已经完成、已经取消或由于其他原因无法取消,则此尝试将失败。如果成功,并且此任务在调用cancel时尚未启动,则此任务不应运行。如果任务已经启动,则mayInterruptIfRunning参数决定是否中断执行此任务的线程。
  • boolean isCancelled():如果此任务在正常完成之前被取消,则返回true。
  • boolean isDone():如果此任务完成,则返回true。完成可能是由于正常终止、异常或取消,在所有这些情况下,此方法都将返回true。
  • V get():等待计算完成,然后检索其结果。
  • V get(long timeout, TimeUnit unit):如果需要,最多等待给定的时间以完成计算,然后检索其结果(如果可用)。
1public interface Future<V> {
2    // 尝试取消任务
3    boolean cancel(boolean mayInterruptIfRunning);
4    
5    // 判断任务是否被取消
6    boolean isCancelled();
7    
8    // 判断任务是否完成(正常完成、异常、取消都算完成)
9    boolean isDone();
10    
11    // 获取计算结果(阻塞直到计算完成)
12    V get() throws InterruptedException, ExecutionException;
13    
14    // 获取计算结果(带超时时间)
15    V get(long timeout, TimeUnit unit) 
16        throws InterruptedException, ExecutionException, TimeoutException;
17}

FutureTask类

FutureTask类实现了RunnableFuture接口,而RunnableFuture接口继承了Runnable和Future接口。因此,FutureTask既可以作为Runnable被线程执行,又可以作为Future得到计算的结果。

FutureTask有两种构造方法:

  • FutureTask(Callable<V> callable):创建一个FutureTask,它在运行时将执行给定的Callable。
  • FutureTask(Runnable runnable, V result):创建一个FutureTask,它在运行时将执行给定的Runnable,并安排get方法在成功完成时返回给定的结果。
1// 可以这样被线程执行
2public class FutureTask<V> implements RunnableFuture<V> {
3    // ...
4}
5
6public interface RunnableFuture<V> extends Runnable, Future<V> {
7    void run();
8}
实例

使用Callable和FutureTask

1Callable<String> callable = () -> {
2    Thread.sleep(1000);
3    return "Hello, World!";
4};
5
6FutureTask<String> futureTask = new FutureTask<>(callable);
7Thread thread = new Thread(futureTask);
8thread.start();
9
10// 做一些其他事情
11// ...
12
13// 获取结果
14try {
15    String result = futureTask.get(); // 这里会阻塞直到任务完成
16    System.out.println(result);
17} catch (InterruptedException | ExecutionException e) {
18    e.printStackTrace();
19}

使用Runnable和FutureTask

1Runnable runnable = () -> {
2    try {
3        Thread.sleep(1000);
4    } catch (InterruptedException e) {
5        e.printStackTrace();
6    }
7};
8
9FutureTask<String> futureTask = new FutureTask<>(runnable, "Task completed");
10Thread thread = new Thread(futureTask);
11thread.start();
12
13// 获取结果
14try {
15    String result = futureTask.get(); // 返回"Task completed"
16    System.out.println(result);
17} catch (InterruptedException | ExecutionException e) {
18    e.printStackTrace();
19}

FutureTask是一个可取消的异步计算,它实现了Future和Runnable接口,因此既可以作为Future来获取结果,也可以作为Runnable被线程执行。它提供了对计算过程的生命周期管理。

在并发编程中,我们通常将耗时的操作封装在Callable或Runnable中,然后用FutureTask来执行,并通过FutureTask来获取结果或控制任务的执行。

使用示例

1import java.util.concurrent.*;
2
3public class FutureExample {
4    public static void main(String[] args) throws Exception {
5        ExecutorService executor = Executors.newSingleThreadExecutor();
6        
7        // 提交Callable任务,返回Future
8        Future<String> future = executor.submit(() -> {
9            Thread.sleep(2000); // 模拟耗时操作
10            return "任务执行完成";
11        });
12        
13        System.out.println("主线程继续执行...");
14        
15        // 获取结果(会阻塞直到任务完成)
16        String result = future.get();
17        System.out.println("结果: " + result);
18        
19        executor.shutdown();
20    }
21}

FutureTask 直接使用

1public class FutureTaskExample {
2    public static void main(String[] args) throws Exception {
3        // 创建FutureTask,包装Callable
4        FutureTask<String> futureTask = new FutureTask<>(() -> {
5            Thread.sleep(2000);
6            return "FutureTask执行结果";
7        });
8        
9        // 创建线程执行
10        Thread thread = new Thread(futureTask);
11        thread.start();
12        
13        System.out.println("主线程做其他事情...");
14        
15        // 获取结果
16        String result = futureTask.get();
17        System.out.println("结果: " + result);
18    }
19}
特性Future接口FutureTask类
身份接口,定义规范具体实现类
执行方式通过ExecutorService提交可直接作为Runnable被Thread执行
功能完整性只有获取结果的方法完整的任务生命周期管理
使用场景线程池任务提交的返回值需要更精细控制的任务执行

实际应用场景

1. 并行计算

1ExecutorService executor = Executors.newFixedThreadPool(3);
2
3Future<Integer> future1 = executor.submit(() -> calculate1());
4Future<Integer> future2 = executor.submit(() -> calculate2());
5Future<Integer> future3 = executor.submit(() -> calculate3());
6
7// 并行执行,最后汇总结果
8int result = future1.get() + future2.get() + future3.get();

2. 超时控制

1Future<String> future = executor.submit(() -> {
2    // 可能很耗时的操作
3    return fetchDataFromNetwork();
4});
5
6try {
7    // 最多等待3秒
8    String result = future.get(3, TimeUnit.SECONDS);
9} catch (TimeoutException e) {
10    future.cancel(true); // 超时取消任务
11    System.out.println("任务超时");
12}

3. 任务取消

1FutureTask<String> futureTask = new FutureTask<>(() -> {
2    while (!Thread.currentThread().isInterrupted()) {
3        // 执行任务,定期检查中断状态
4    }
5    return "任务被取消";
6});
7
8Thread thread = new Thread(futureTask);
9thread.start();
10
11// 5秒后取消任务
12Thread.sleep(5000);
13futureTask.cancel(true);

线程状态

状态触发条件恢复条件是否消耗CPU
RUNNABLE线程已启动,具备运行条件获得CPU时间片获得时间片时消耗
BLOCKED竞争synchronized锁失败锁可用时不消耗CPU
WAITING调用wait()、join()等被notify()或线程结束不消耗CPU
TIMED_WAITING调用sleep()、wait(timeout)等超时或被唤醒不消耗CPU

Java线程的打断机制

在Java中,每个线程都有一个布尔类型的打断标志(interrupt status)。当我们调用一个线程的interrupt()方法时,这个线程的打断标志会被设置为true。

但是,这并不会立即停止线程的执行,而是需要线程自己检查这个标志并做出相应的处理。

与打断相关的方法有三个:

  • interrupt():实例方法,用于中断线程。如果该线程正处于阻塞状态(如调用了sleep、wait、join等方法),那么它会立即抛出InterruptedException,并且打断标志会被清除(即设置为false)。如果线程没有阻塞,则只是设置打断标志为true。
  • isInterrupted():实例方法,用于检查线程的打断标志,不会清除打断标志。
  • static interrupted():静态方法,用于检查当前线程的打断标志,并且会清除打断标志(即如果当前线程的打断标志为true,则调用后返回true,并将打断标志设置为false)。

示例1:使用isInterrupted()检查打断标志

1public class InterruptExample1 {
2    public static void main(String[] args) throws InterruptedException {
3        Thread thread = new Thread(() -> {
4            // 循环检查打断标志
5            while (!Thread.currentThread().isInterrupted()) {
6                System.out.println("线程运行中...");
7            }
8            System.out.println("线程结束,打断标志为: " + Thread.currentThread().isInterrupted());
9        });
10
11        thread.start();
12        Thread.sleep(10); // 主线程休眠10毫秒,确保子线程运行
13        thread.interrupt(); // 中断线程
14    }
15}

示例2:使用static interrupted()方法

1public class InterruptExample2 {
2    public static void main(String[] args) throws InterruptedException {
3        Thread thread = new Thread(() -> {
4            while (true) {
5                // 使用静态方法检查,并清除标志
6                if (Thread.interrupted()) {
7                    System.out.println("检测到打断,退出循环。");
8                    System.out.println("再次检查打断标志: " + Thread.currentThread().isInterrupted());
9                    break;
10                }
11            }
12        });
13
14        thread.start();
15        thread.interrupt(); // 设置打断标志为true
16    }
17}

示例3:线程在阻塞时被中断(例如在sleep时)

1public class InterruptExample3 {
2    public static void main(String[] args) throws InterruptedException {
3        Thread thread = new Thread(() -> {
4            try {
5                Thread.sleep(5000); // 线程休眠5秒
6            } catch (InterruptedException e) {
7                // 在阻塞过程中被中断,会抛出InterruptedException,并且打断标志会被清除(变为false)
8                System.out.println("线程在休眠时被中断,打断标志为: " + Thread.currentThread().isInterrupted());
9                // 我们可以选择重新设置打断标志,或者直接返回
10                // Thread.currentThread().interrupt(); // 重新中断,以便上层代码能知道
11            }
12        });
13
14        thread.start();
15        Thread.sleep(1000); // 主线程休眠1秒,确保子线程进入休眠
16        thread.interrupt(); // 中断子线程的休眠
17    }
18}

重要注意事项:

当线程在阻塞状态(如sleep、wait、join)时被中断,会立即抛出InterruptedException,并且打断标志会被清除(变成false)。因此,在捕获InterruptedException后,通常有两种选择:

  1. 要么重新设置打断标志(因为异常捕获后打断标志为false,所以需要再次调用interrupt()设置标志),这样上层代码可以检测到中断。
  2. 要么不处理异常,直接退出。

总结

  • isInterrupted():检查其他线程的中断状态,不改变状态
  • interrupted():检查当前线程的中断状态,清除状态
  • interrupt():设置线程的中断标志为true
  • 阻塞方法被中断时会抛出InterruptedException并清除中断状态

JAVA面试复习笔记(待完善)》 是转载文章,点击查看原文


相关推荐


Windows Server,如何使用WSFC+nginx实现集群故障转移
IT橘子皮2025/10/19

在 Windows Server 环境中结合 WSFC(Windows Server Failover Clustering)和 Nginx 实现集群故障转移,核心目标是构建一个既具备应用层高可用性(由 Nginx 负责),又具备基础设施层高可用性(由 WSFC 保障 Nginx 服务本身)的稳固架构。下面这张图清晰地展示了这套架构的完整工作流程: 上图展示了WSFC如何通过心跳检测监控Nginx主节点的状态,并在故障发生时自动将服务(包括虚拟IP和Nginx进程)转移到备节点。下面我们详细拆


AI修图革命:IOPaint+cpolar让废片拯救触手可及
倔强的石头_2025/10/18

文章目录 前言【视频教程】1.什么是IOPaint?2.本地部署IOPaint3.IOPaint简单实用4.公网远程访问本地IOPaint5.内网穿透工具安装6.配置公网地址7.使用固定公网地址远程访问总结 前言 旅行拍照时意外拍到路人闯入?证件照背景不合规?传统修图软件学习成本高,在线工具又担心隐私泄露?IOPaint的出现给出了完美解方——这款开源AI修图工具支持一键擦除多余物体、修复老照片瑕疵,所有操作在本地完成,无需上传原始图片。特别适合摄影爱好者和自媒体创作者,其


“签名”这个概念是非对称加密独有的吗?
你的人类朋友2025/10/16

前言 🍃 你好啊,我是你的人类朋友 ☺️ 本篇文章主要来自于我之前在工作中犯的一个对“签名”的概念的误解 问大家一个问题: "签名"这个概念是非对称加密独有的吗? 先说答案:不是。 虽然【数字签名】确实是非对称加密技术的重要应用,但【"签名"】这个概念在密码学中有着更广泛的含义和应用。 今天就来澄清这个误解,也是我之前在学习信息安全的过程中犯的一个错误 🙀。 正文 一、签名的本质目的 在密码学中,"签名"的核心目的是提供: 身份认证 - 证明消息确实来自声称的发送方 完整性验证 - 确保消


Redis(63)Redis的Lua脚本如何使用?
Victor3562025/10/15

Redis 提供了对 Lua 脚本的支持,使得用户可以在服务器端执行原子操作,从而能够降低网络开销、提高性能和确保操作的原子性。以下是关于如何使用 Redis 的 Lua 脚本的详细指南,并结合实例代码进行说明。 1. 使用 Lua 脚本的基本命令 Redis 提供了两个主要命令来执行 Lua 脚本: EVAL EVALSHA EVAL 命令 EVAL 命令用于直接执行 Lua 脚本。 语法: EVAL script numkeys key [key ...] arg [arg ...]


html5的新增标签
期待~明天2025/10/14

HTML5新增结构标签 HTML5引入了多个语义化标签,用于更清晰地描述网页结构: <header>:定义文档或节的页眉<footer>:定义文档或节的页脚<nav>:定义导航链接<article>:定义独立的内容块<section>:定义文档中的节<aside>:定义页面内容之外的内容(如侧栏) HTML5新增多媒体标签 为支持原生多媒体内容,新增以下标签: <audio>:嵌入音频内容<video>:嵌入视频内容<source>:为多媒体元素定义多种媒体资源<track>:为视频添


在实际项目中如何设计一个高可用的Spring Cloud Gateway集群?
IT橘子皮2025/10/12

在高可用架构中,Spring Cloud Gateway 集群的设计是关键环节。下面我将从核心架构、关键配置到生产级优化,为你梳理高可用网关集群的设计方案。 🏗️ 集群架构设计 构建高可用网关集群的核心是消除单点故障,并通过分层设计实现流量可靠分发。 ​核心模式​:标准的解决方案是部署多个 Gateway 实例,并在前端配置一个负载均衡器(如云服务商的SLB/ELB/ALB或自建的Nginx/HAProxy)作为统一的流量入口。所有外部请求先到达负载均衡器,再由其分发到后端的各个Gatewa


先用js快速开发,后续引入ts是否是一个好的实践?
你的人类朋友2025/10/11

前言 我在业余时间做一些小项目时,每次进行技术选型,都会有一个疑问: 应该直接使用 TypeScript 开发,还是先用 JavaScript 快速启动,后续再引入 TypeScript? 今天干脆来整理一下思路,方便后续复用这些想法 正文 一、快速开发的优势 先用 JavaScript 进行快速开发确实有其明显优势: 开发速度更快 无需类型定义和接口声明 跳过类型检查的编译步骤 ⭐ 【重要】特别适合【原型开发】和【概念验证】,个人认为这个是最重要的 学习成本低 更容易上手 ⭐ 【重要】减


深入解析 Vue 3 源码:computed 的底层实现原理
excel2025/10/9

在 Vue 3 的响应式系统中,computed 是一个非常重要的功能,它用于创建基于依赖自动更新的计算属性。本文将通过分析源码,理解 computed 的底层实现逻辑,帮助你从源码层面掌握它的原理。 一、computed 的基本使用 在使用层面上,computed 有两种常见用法: 1. 只读计算属性 const count = ref(1) const plusOne = computed(() => count.value + 1) console.log(plusOne.value)


CICD工具选型指南,Jenkins vs Arbess哪一款更好用?
高效研发之旅2025/10/8

Jenkins是一款常用的CICD工具,Arbess作为一款新兴的国产开源免费的CICD工具,两款工具各有特点。本文将从安装配置、功能特性、用户体验等几个方面对两款软件进行详细对比。 1、安装配置 项目 Jenkins Arbess 安装难度需要预装Java环境,需要手动配置端口和后台服务。一键安装,私有部署不同环境均支持傻瓜式一键安装。配置难度需要配置国内镜像源,安装核心插件零配置,安装后即刻可用,无需额外配置。支持操作系统支持Windows、ma


【征文计划】基于Rokid CXR-M SDK 打造AI 实时会议助手:从连接到自定义界面的完整实践
_摘星_2025/10/6

【征文计划】基于Rokid CXR-M SDK 打造AI 实时会议助手:从连接到自定义界面的完整实践 > **摘要**:本文基于 Rokid CXR-M SDK,详细阐述如何构建一个面向商务会议场景的“AI 实时会议助手”应用。通过手机端与 Rokid 智能眼镜的协同,实现语音转写、要点提炼、提词引导、多语翻译与会后纪要自动生成。文章涵盖从环境配置、蓝牙/Wi-Fi 连接、设备控制、AI 场景交互到自定义 UI 渲染的完整开发流程,并提供关键代码示例与最佳实践建议。 > > ![](https:

首页编辑器站点地图

Copyright © 2025 聚合阅读

License: CC BY-SA 4.0