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

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

目录

布隆过滤器

一、核心思想

二、执行逻辑详解

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 数量 需要一个最优值。太少的哈希函数容易冲突,太多的哈希函数会很快将位数组“填满”,反而增加冲突。

  • 对于给定的 n 和 m,可以计算出一个使误判率最小的最佳哈希函数数量 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(表示设置失败)

二、简单示例

# 第一次设置,key "mykey" 不存在
127.0.0.1:6379> SETNX mykey "Hello"
(integer) 1  # 返回 1,设置成功# 尝试再次设置相同的 key
127.0.0.1:6379> SETNX mykey "World"
(integer) 0  # 返回 0,设置失败# 检查值,仍然是 "Hello"
127.0.0.1:6379> GET mykey
"Hello"

三、SETNX 的核心特性

1. 原子性

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

2. 简单性

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

3. 无过期时间

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

四、经典应用场景

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

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

# 客户端1获取锁
127.0.0.1:6379> SETNX lock:order123 "client1"
(integer) 1  # 获取锁成功# 客户端2尝试获取同一个锁(此时锁还被client1持有)
127.0.0.1:6379> SETNX lock:order123 "client2"  
(integer) 0  # 获取锁失败,合理# 客户端1释放锁
127.0.0.1:6379> DEL lock:order123
(integer) 1# 客户端2再次尝试获取锁(此时锁已释放)
127.0.0.1:6379> SETNX lock:order123 "client2"
(integer) 1  # 获取锁成功,合理

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

  •  命令执行的原子性:每个命令执行期间不会被中断

  •  自然的互斥访问:SETNX 在同一时刻只能有一个客户端成功

  •  顺序一致性:所有客户端看到相同的命令执行顺序

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

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

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

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

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

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

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

  • EX:设置过期时间(秒)

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

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

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

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

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

Lua脚本的原子性解决方案

-- Lua脚本:检查值匹配再删除
if redis.call("GET", KEYS[1]) == ARGV[1] thenreturn redis.call("DEL", KEYS[1])
elsereturn 0
end

在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. 值验证:只有锁的值与预期值匹配时才执行删除

非原子操作的问题

# 错误的做法:分两步操作
127.0.0.1:6379> GET lock:order123
"client1"
# 在这两步之间,锁可能被其他客户端修改!127.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. 推送数据:将解析后的结构化数据推送给订阅者

// Canal解析出的数据格式示例
{"database": "shop","table": "products", "type": "UPDATE",  // 操作类型"data": [{"id": 1001,"name": "iPhone", "price": 5999,     // 新价格"stock": 50}],"old": [{"price": 5499     // 旧价格}]
}

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

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

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

// 伪代码:在业务代码中手动维护缓存一致性
public void updateProduct(Product product) {// 获取写锁Lock writeLock = redis.getLock("product:" + product.getId());try {// 1. 更新数据库productMapper.update(product);// 2. 删除/更新缓存redis.delete("product:" + product.getId());} finally {writeLock.unlock();}
}public Product getProduct(Long id) {// 获取读锁Lock readLock = redis.getLock("product:" + id);try {// 先查缓存,再查数据库...} finally {readLock.unlock();}
}

缺点:

  • 代码侵入性强:每个数据库操作都要手动维护缓存

  • 容易遗漏:复杂的业务逻辑可能忘记更新缓存

  • 性能开销:锁竞争影响性能

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

// Canal客户端:监听数据库变更,自动更新缓存
@CanalEventListener
public class CacheUpdateListener {@ListenPointpublic void onProductUpdate(ProductChangeEvent event) {if (event.getType() == UPDATE || event.getType() == DELETE) {// 自动删除对应的缓存redis.delete("product:" + event.getId());}if (event.getType() == INSERT || event.getType() == UPDATE) {// 或者更新缓存redis.set("product:" + event.getId(), event.getNewData());}}
}

优点:

  • 解耦:缓存同步与业务代码完全分离

  • 可靠:基于 binlog,不会遗漏任何数据变更

  • 通用:一套方案适用于所有表的缓存同步

完整的数据同步架构

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

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

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

多路复用IO(I/O Multiplexing)

核心思想:

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

Spring的注解

@Repository

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

@Repository的作用

1. 标识数据访问层组件

@Repository
public class UserDaoImpl implements UserDao {@Autowiredprivate JdbcTemplate jdbcTemplate;public User findById(Long id) {String sql = "SELECT * FROM users WHERE id = ?";return jdbcTemplate.queryForObject(sql, new UserRowMapper(), id);}
}

2. 自动异常转换

  • 将特定持久化技术的异常(如JDBC的SQLException)转换为Spring的统一数据访问异常

  • 提供一致的异常处理体验

3. Bean自动扫描与注册

在Spring配置中:

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

@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接口所在的包路径

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

使用后,包内所有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 内共享

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

二级缓存

开启二级缓存

1. 全局配置

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

2. Mapper配置

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

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

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

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

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

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):如果需要,最多等待给定的时间以完成计算,然后检索其结果(如果可用)。

public interface Future<V> {// 尝试取消任务boolean cancel(boolean mayInterruptIfRunning);// 判断任务是否被取消boolean isCancelled();// 判断任务是否完成(正常完成、异常、取消都算完成)boolean isDone();// 获取计算结果(阻塞直到计算完成)V get() throws InterruptedException, ExecutionException;// 获取计算结果(带超时时间)V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

FutureTask类


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

FutureTask有两种构造方法:

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

使用Callable和FutureTask

Callable<String> callable = () -> {Thread.sleep(1000);return "Hello, World!";
};FutureTask<String> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();// 做一些其他事情
// ...// 获取结果
try {String result = futureTask.get(); // 这里会阻塞直到任务完成System.out.println(result);
} catch (InterruptedException | ExecutionException e) {e.printStackTrace();
}

使用Runnable和FutureTask

Runnable runnable = () -> {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}
};FutureTask<String> futureTask = new FutureTask<>(runnable, "Task completed");
Thread thread = new Thread(futureTask);
thread.start();// 获取结果
try {String result = futureTask.get(); // 返回"Task completed"System.out.println(result);
} catch (InterruptedException | ExecutionException e) {e.printStackTrace();
}

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

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

使用示例

import java.util.concurrent.*;public class FutureExample {public static void main(String[] args) throws Exception {ExecutorService executor = Executors.newSingleThreadExecutor();// 提交Callable任务,返回FutureFuture<String> future = executor.submit(() -> {Thread.sleep(2000); // 模拟耗时操作return "任务执行完成";});System.out.println("主线程继续执行...");// 获取结果(会阻塞直到任务完成)String result = future.get();System.out.println("结果: " + result);executor.shutdown();}
}

FutureTask 直接使用

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

实际应用场景

1. 并行计算

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

2. 超时控制

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

3. 任务取消

FutureTask<String> futureTask = new FutureTask<>(() -> {while (!Thread.currentThread().isInterrupted()) {// 执行任务,定期检查中断状态}return "任务被取消";
});Thread thread = new Thread(futureTask);
thread.start();// 5秒后取消任务
Thread.sleep(5000);
futureTask.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()检查打断标志

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

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

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

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

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

重要注意事项:

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

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

总结

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

http://www.dtcms.com/a/499338.html

相关文章:

  • 七、WEB APIs(二)
  • LLMs-from-scratch :多种字节对编码(BPE)对比
  • 济南哪里有网站建设公司网站类网站开发源代码
  • 做笔记的网站源码wordpress手机版论坛
  • 网站推广有哪些举措域名需要跟网站名称一致么
  • 具身神经-机器人通讯架构与实现系列
  • [GO]gin框架:ShouldBindJSON与其他常见绑定方法
  • KUKA库卡焊接机器人二氧化碳节气
  • 机器人、具身智能的起步——线性系统理论|【三】线性、因果与时不变
  • 服务器做php网站吗wordpress评论贴图
  • 网站建设与管理的心得怎样做音乐网站
  • 请例举 Android 中常用布局类型,并简述其用法以及排版效率
  • Android 约束布局(ConstraintLayout)的权重机制:用法与对比解析
  • 编程与数学 03-007 《看潮资源管理器》项目开发 07 主窗口设计(3-3)
  • 基于单片机的架空线路接地故障检测与报警系统
  • 鸿蒙实现滴滴出行项目之乘客支付订单功能
  • 如何把自己做的网站放到网上360建筑网怎样取消发布的消息
  • 做网站有哪个空间网站建设优化推广贵州
  • 西电25年A测 语音识别机械臂方案与教程
  • 数据结构——队列的链式存储结构
  • 媒体135网站口碑好的宜昌网站建设
  • 湖南省建设银行网站官网深圳龙华网站建设公司
  • 网站后台管理系统源码网站空间文件夹
  • 元宇宙与公共服务的深度融合:重构民生服务的效率与温度
  • 深入解析十字链表:从理论到实践的全面指南
  • 红色页面网站护肤品网站建设的摘要
  • GB28181视频服务wvp部署(一)
  • 吴忠住房和城乡建设局网站小学生编程网课前十名
  • 浅谈 OpenAPI Schema—— 接口契约的标准语言
  • TSDF 体素模型与光线投射