NoProvider
Jihongchang(讨论 | 贡献)2025年8月28日 (四) 09:19的版本 (建立内容为“NoProvider.java<syntaxhighlight lang="java"> import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.TestCompo…”的新页面)
NoProvider.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestComponent;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.Queue;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@TestComponent
public class NoProvider {
@Autowired
private JdbcTemplate jdbcTemplate;
private final Queue<String> noQueue = new ConcurrentLinkedQueue<>();
// 线程安全锁,仅在填充队列时使用
private final Lock lock = new ReentrantLock();
// Spring Bean 的默认作用域是单例,所以单例 Bean 的属性会被所有线程共享
private int lastMaxNoInt = -1; //记录上次查询到的最大数值,用于分页查询
private static final int TARGET_SIZE = 1000;
// 分页查询时使用的页容量
private static final int PAGE_SIZE = 1000;
private static final String SQL = "select * from student where no > ? order by no asc limit ?";
public String poll() {
// 在大部分时候对 poll 的调用时,noQueue 里都不是空的,所以把不为空的、不用上锁的逻辑写在前面
// 无锁尝试获取元素
String no = noQueue.poll();
if (no != null) {
return no;
}
/*
如果同时多个线程走到这里,除第一个线程上锁成功外,其他线程都将等待;
核心原理:Lock 的内存语义(对应JMM规范)
Java 中的 Lock接口(如ReentrantLock的实现)严格遵循 JMM 的内存语义,
其作用等价于 synchronized 的“加锁-解锁”,但更灵活。具体到这个场景:
(1)第一个线程“解锁(unlock)”时的动作
当第1个线程执行 lock.unlock() 时,
JMM 会强制将该线程工作内存中修改后的 lastMaxNoInt 值刷新到主内存(相当于“写回”主内存)。
注:线程不会直接操作主内存,而是先将主内存的数据加载到自己的“工作内存”(CPU 缓存、寄存器等)中修改,
解锁时必须把修改结果同步回主内存,避免数据只存在于当前线程的工作内存中。
(2)下一个线程“加锁(lock)”时的动作
当下一个线程执行lock.lock()并成功获取锁时,
JMM会强制该线程清空自己的工作内存中 lastMaxNoInt 的旧值,
然后从主内存重新加载 lastMaxNoInt 的最新值(也就是第一个线程刚刷回主内存的新值)到自己的工作内存中。
这一步保证了下一个线程进入临界区后,拿到的 lastMaxNoInt 一定是最新的,
而不是自己工作内存中缓存的旧值。
*/
// 队列空,加锁填充
lock.lock();
/*
在操作锁(或其他需要手动释放的资源,如文件流、数据库连接)时,
try-finally不是“可选写法”,而是必须的安全保障:
直接在 return 前 unlock 的写法,无法处理“代码执行过程中抛出异常”的场景,会导致锁泄露;
try-finally 能确保锁在任何情况下都被释放,这在多线程共享的 Spring Bean 中至关重要(一旦锁泄露,整个服务可能瘫痪)。
这也是 Java 并发编程中操作锁的 标准写法,目的是保证锁的可靠性和程序的健壮性。
*/
try {
// 双重检查:防止多线程同时进入填充逻辑
no = noQueue.poll();
if (no != null) {
return no;
}
fillQueueToTargetSize();
return noQueue.poll();
} finally {
lock.unlock();
}
}
private void fillQueueToTargetSize() {
while (noQueue.size() < TARGET_SIZE) {
String noStart = String.format("%08d", lastMaxNoInt);
List<String> noList = jdbcTemplate.queryForList(SQL, new Object[]{lastMaxNoInt, PAGE_SIZE}, String.class);
if (noList.isEmpty()) {
fillQueueToTargetSizeWhenNoListIsEmpty();
break;
}
// 每一个 no 肯定都比 lastMaxNoInt 大
for (int i = 0; noQueue.size() < TARGET_SIZE && i < noList.size(); i++) {
String no = noList.get(i);
int noInt = Integer.parseInt(no);
while (noQueue.size() < TARGET_SIZE && ++lastMaxNoInt < noInt) {
String newNo = String.format("%08d", lastMaxNoInt);
noQueue.offer(newNo);
}
}
System.out.printf("lastMaxNoInt=%d\n", lastMaxNoInt);
} // end while (noQueue.size() < TARGET_SIZE) {
}
private void fillQueueToTargetSizeWhenNoListIsEmpty() {
// 没有更多数据,使用后续自增数字补充
int remaining = TARGET_SIZE - noQueue.size();
for (int i = 0; i < remaining; i++) {
lastMaxNoInt++;
String newNo = String.format("%08d", lastMaxNoInt);
noQueue.offer(newNo);
}
System.out.printf("lastMaxNoInt=%d\n", lastMaxNoInt);
}
}