Java热点经典问题集锦_1
# Java热点经典问题集锦_1
# 算法
# 什么是雪花算法,用于何种系统跟场景
雪花算法(Snowflake)是一种用于生成唯一ID的分布式算法。它可以确保在多个节点上生成的ID不会重复,并且可以按照时间顺序进行排序。这种算法主要用于大型分布式系统中,以保证在分布式环境下生成的每个ID都是唯一的。
雪花算法生成的ID由64位组成,其中包括一个41位的时间戳、10位的机器标识符和12位的序列号。时间戳部分精确到毫秒级别,可以保证在同一时刻生成的ID是唯一的;机器标识符可以区分多个节点,避免了同一节点上生成的ID冲突;序列号则可以保证相同节点上生成的ID也是唯一的。
总体来说,雪花算法是一种高效、可靠、安全的分布式ID生成方案,被广泛应用于分布式系统中。
# Java基础问题
# 异常相关面试题
# 概念问题
# 接口幂等性
# 概念定义
所谓的幂等,其实它是一种数学上的概念。而在计算机编程领域里面,幂等一般指的是方法被多次重复执行的时候所产生的影响,和第一次执情的影响是相同的。
# 诞生的原因
之所以要考虑幂等性问题,是因为在网络通信里面存在两种行为,可能会导致接口被重复调用。
第一个:是用户的重复提交或者用户的恶意攻击,导致这个请求会被多次重复执行。
第二个:在分布式网络架构里面,为了避免网络通信导致数据丢失,在服务之间进行通信的时候,会设计一个超时重试的一个机制,或者说,请求消息在网络中停留时间过长,导致的请求重发,而这种重试机制有可能会导致服务端接口被重复调用。
所以在程序设计里面,对于数据变更类的一个接口,需要保证接口的幂等性。
# 解决方案
第一个:我们可以使用数据库的唯一约束来实现幂等。比如说对于数据插入类的场景,像订单创建,因为订单号一定是唯一的,所以如果多次调用就会触发数据库的唯一约束异常,从而避免一次请求创建多个订单的一个问题。
第二个:可以使用redis里面提供的setNX命令。比如说对于MQ消费类的场景,为了避免MQ重复消费导致数据多次被修改的一个问题。我们可以在接收MQ消息的时候,把这个消息通过setNX这个命令写入到redis里面。一旦这个消息被消费过,那么就不会再次消费。
第三个:可以使用状态机来实现幂等。所谓的状态机,是指一条数据的完整运行状态的转换流程。比如说订单状态,因为它的状态只会向前去变更,所以多次修改同一条数据的时候,一旦状态发生变更,那么对于这条数据的修改造成影响只会发生一次。
以下是一个简单例子:
工作节点编号 | 上下文数据 | 节点状态 |
---|---|---|
2131020 | .... | 已结束 |
2131021 | .... | 进行中 |
2131022 | .... | 已就绪 |
比如说我们现在有一个工作流程,在工作流程中有若干多个节点。那么针对我们这个若干多个节点,它的会额外增加一个状态字段。比如说我们原本的1020这个节点原本是已就绪,当发来了启动的命令以后,它变成了进行中。同样的针对于正在进行中的这个节点,在发来了这个启动的命令,他是不做任何变化的。但是结果返回正在执行,这和我们第一次返回的状态是一致的。这也是我们说到的利用额外的状态字段
和业务逻辑
来进行控制。
当然,还有其他的方案,比如:我们也可以在进行调用接口时,额外传入一个参数(或者叫令牌系统之类的提供),后端根据该参数是否首次传递来判断,从而进行幂等的处理。
处理的方案有很多,关键在于你如何选择,你要弄清各种方案之间,它们的优劣所在
,对于你的业务场景,哪一种更优——这才是关键。
# 乐观锁与悲观锁
# 概念与定义
悲观锁:是一种基于悲观态度的数据并发控制机制
,用于防止数据冲突。它采取预防性的措施,在修改数据之前将其锁定,并在操作完成后释放锁定,以确保数据的一致性和完整性。悲观锁通常用于并发环境下的数据库系统,是数据库本身实现锁机制的一种方式。
乐观锁:是一种基于版本控制的并发控制机制
,它并没有使用锁。在乐观锁的思想中,认为数据访问冲突的概率很低,因此不加锁直接进行操作,但在更新数据时会进行版本比对,以确保数据的一致性。
乐观锁的原理主要基于版本号或时间戳来实现。在每次更新数据时,先获取当前数据的版本号或时间戳,然后在更新时比对版本号或时间戳是否一致,若一致则更新成功,否则表示数据已被其他线程修改,更新失败。
具体例子:比如说现在有一条数据,它的初始version等于1,但已经被修改过了,于是它的version变为2。但是之前有个操作还没结束,要将其修改,此时它发起一个version为1的修改请求,但你的版本号要小于当前最新的数据的版本,这个时候就会修改失败,这叫安全性,也就是所谓的乐观锁。
# 适用场景
因为悲观锁需要加锁,在高并发的情况下,这必然会降低程序处理的效率,不过也因为加锁,它让数据的准确性得到了保证。
所以,悲观锁适用
于:
- 高并发且数据竞争激烈的场景:当多个事务需要同时访问和修改同一份数据时,使用悲观锁可以确保数据在任一时刻只被一个事务访问和修改,从而避免数据的不一致性和脏读。
- 数据一致性要求极高的场景:在金融、医疗等行业中,对数据的一致性要求非常高,不允许出现任何的数据不一致或脏读现象。在这些场景中,使用悲观锁可以确保数据在任一时刻只被一个事务访问和修改,从而满足数据一致性的要求。
- 写操作频繁的场景:如果系统中写操作(如更新、删除等)远多于读操作(如查询),那么使用悲观锁可以更有效地保护数据,避免在写操作时被其他事务干扰。
- 事务执行时间较长的场景:当事务的执行时间较长时,使用悲观锁可以确保在该事务执行期间,数据不会被其他事务修改,从而避免数据的不一致性和脏读。
而乐观锁适用
于:
- 写操作较少:在这种场景下,多个事务或线程大部分时间都在读取数据,而写操作的频率相对较低。乐观锁能够减少锁的持有时间,允许多个事务或线程同时读取数据,而不会相互阻塞。
- 数据冲突较少:如果数据更新操作之间的冲突较少,即多个事务或线程同时更新同一份数据的概率较低,那么乐观锁能够发挥很好的性能。因为即使偶尔出现冲突,也只是在更新数据时才会被检测到,而不需要在整个数据处理过程中都锁定资源。
- 重试成本较低:乐观锁在检测到冲突时会回滚事务或提示冲突,需要客户端重新尝试更新操作。因此,如果重试的成本较低(例如,重试不会导致大量计算或I/O操作),那么使用乐观锁是合适的。
- 系统能够容忍一定程度的失败:由于乐观锁在更新数据时可能会因为版本冲突而失败,因此系统需要能够处理这种失败情况。如果系统能够容忍一定程度的失败(例如,通过重试或其他补偿机制来恢复),那么使用乐观锁是可行的。
# 乐观锁的核心
版本号校验必须在更新操作中完成: 乐观锁的核心在于检查 version
或类似的标识是否与之前读取的值一致。只有在单条 SQL 更新语句中完成版本校验和数据更新时,才能避免多个线程写入导致的数据不一致。
- 正确例子
UPDATE resource
SET locked_by = ?, version = version + 1
WHERE id = ? AND version = ?;
2
3
这保证了只有 version
未发生变化的情况下,更新才会成功。
错误示例: 如果版本校验与数据更新分成两个步骤(如先查询后更新),就会导致竞态条件问题。例如:
SELECT version FROM resource WHERE id = ?;
-- version 被线程 A 和线程 B 同时读取
UPDATE resource SET locked_by = ? WHERE id = ? AND version = ?;
2
3
- 这样可能会导致两个线程同时更新成功。
事务隔离级别支持: 数据库必须保证事务隔离级别至少为 READ COMMITTED 或更高。否则,可能会出现未提交事务的修改被其他线程读取的情况,导致并发问题
# Java中的乐观锁
关于乐观锁的实现,Java中的原子类就是一个典型的代表。Java 提供了一系列的原子类(Atomic Classes),这些类位于 java.util.concurrent.atomic
包中。原子类提供了一种线程安全的方式来执行基本的原子操作,而无需使用显式的锁。这些类主要用于在高并发环境中高效地更新共享变量。
以下是AtomicInteger
这个类,它的部分源码:
@IntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
2
3
4
5
6
7
8
它的核心思想就是CAS,先比较操作字段,如果是0,可进行操作,如果不是(操作字段变为1)则进行等待,且在等待时不停查看字段是否变为0(比如通过自旋锁)。
AtomicInteger是一个原子类,表示一个整数值,支持原子操作。
- 常用方法:
get()
、set(int newValue)
、incrementAndGet()
、decrementAndGet()
、addAndGet(int delta)
、compareAndSet(int expect, int update)
。
以上是关于其中一个原子类的简要介绍。
使用原子类(如 AtomicInteger
、AtomicLong
等)可以显著减少线程安全问题,因为它们提供了对基本数据类型和引用类型的原子操作。然而,仅仅使用原子类并不意味着你的代码就完全线程安全了。以下是一些需要注意的方面:
- 原子类(如
AtomicInteger
、AtomicLong
等)只保证它们自身提供的方法是线程安全的。 - 如果原子类的使用依赖于外部状态,那么这些外部状态的改变可能会导致线程安全问题。
原子类本身的方法是线程安全的,主要是因为它们使用了底层的硬件支持和特定的算法来确保操作的原子性。虽然乐观锁(如CAS操作)是实现原子性的常用技术之一,但并不是唯一的方法。
以下是原子类实现线程安全的主要机制:
- CAS操作:使用硬件支持的CAS指令来确保操作的原子性(也就是所谓的乐观锁)。
volatile
关键字:确保内存可见性,避免缓存不一致问题。- 内部锁:在某些情况下使用内部锁来确保线程安全。
- 硬件支持:现代处理器提供的原子操作支持。
- Java内存模型:确保了多线程环境下内存访问的一致性。