NoProvider

来自姬鸿昌的知识库
跳到导航 跳到搜索

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);

    }

}