• [技术干货] Java进阶 —— 多线程并发-转载
    一、基础概念进程         我们知道CPU是主机上的中央核心处理器,CPU的核数代表着主机能在一个瞬间同时并行处理的任务数,单核CPU只能在内存中并发处理任务。而在现有的操作系统中,几乎都支持进程这个概念。进程是程序的在内存中的一次执行过程,具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。线程        线程在程序中是独立的、并发的执行流,与分隔的进程相比隔离性会更小,线程之间共享内存、文件句柄和其它的进程应有的状态。线程比进程具有更高的性能,这是由于同一进程中的线程具有共性。简单理解,多线程是进程中并行执行的多个子程序。并发性和并行的区别        并行是指在同一时刻,有多条指令在多个处理器上同时执行;而并发是指在同一时刻只能执行,但是通过多进程快速轮换执行可以达到同时执行的效果。CPU主频就代表着这些进程之间频繁切换的速度。二、创建线程的三种方式2.1 通过继承Thread类来启用Java语言中JVM允许程序运行多个线程并通过java.lang.Thread类来实现。Thread类的特性        每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体,并通过该Thread对象的start()方法来启动线程。流程:定义子类继承Thread类;子类中重写Thread类中的run方法;创建Thread子类对象,即创建了线程对象;调用线程对象start方法:启动线程,调用run方法。 具体代码示例首先构建一个继承Thread类的子类//继承Thread类的方式实现多线程public class TestThread extends Thread{    @Override    public void run(){        System.out.println("多线程运行的代码");    }}AI写代码java运行 调用线程public class Test{    public static void main(String[]args){        Thread t = new TestThread();        t.start();   //启动线程    }}AI写代码java运行2.2 实现Runnable接口来实现流程定义子类,实现Runnable接口。子类中重写Runnable接口中的run方法。通过Thread类含参构造器创建线程对象。将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法中。调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。  实现Runnable接口public class TestRunnable implements Runnable{    @Override    public void run(){        System.out.println("实现Runnable接口运行多线程");    }}AI写代码java运行实现多线程public class Test{    public static void main(String[]args){        Thread t = new Thread(new TestRunnable);        //带有线程名称的实例化线程对象。可以通过Thread.currentThread().getName()获取        //Thread t = new Thread(new TestRunnable,"the FirstThread");        t.start();   //启动线程    }}AI写代码java运行与继承Thread类的区别继承Thread:线程代码存放Thread子类run方法中。重写run方法实现Runnable:线程代码存在接口的子类的run方法。实现run方法 实现Runnable接口方法的好处        实现Runnable接口方法通过继承Runnable接口避免了当继承的局限性,同时也使得多个线程可以同时共享一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。 2.3 实现Callable接口        在前面通过实现Runnable接口创建多线程时,Thread类的作用就是把run方法包装成线程的执行体。而从Java5以后,Java提供了一个Callable接口中的call()方法作为线程执行体,同时call()方法可以有返回值,也可以抛出异常。public class Test{    public static void main(String[]args){        //创建callable对象        ThirdThread tt = new ThirdThread();        //使用FutureTask来包装Callable对象        FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{            ...            ...        });        new Thread(task,"有返回值的线程").start();        try{            //获取线程返回值            System.out.println("子线程的返回值" + task.get());        }catch (EXception ex){            ex.printStackTrace();            }    }}AI写代码java运行Callable接口实现类和Runnable接口实现类的区别在于是否有参数返回! 三、Thread类的相关方法常用方法如下:void start():启动线程,并执行对象的run(0方法run():线程在被调度时执行的操作String getName():返回线程的名称void setName(String name):设置该线程名称static currentThread():返回当前线程 public class Test{    public static void main(String[]args){        TestRun r1 = new TestRun();        Thread t1 = new Thread(r1);        //为线程设置名称        t1.setName("线程t1");         t1.start();   //启动线程        System.out.println(t1.getName()); //若没指定,系统默认给出的线程名称是Thread-0....    }}public class TestRun implements Runnable{    @Override    public void run(){        System.out.println("实现Runnable接口运行多线程");    }}AI写代码java运行线程优先级线程的优先级设置增加了线程的执行顺序靠前的概率,是用一个数组1-10来表示的,默认的优先级是5。涉及的方法有:getPriority()和setPriority()//获取优先级t1.getPriority();//设置优先级t1.setPriority(10);AI写代码java运行线程让步static void yield()线程让步,即暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程,若队列中没有同优先级的线程,则跳过。Thread.yield();AI写代码java运行线程阻塞join():当某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到join()方法加入的join线程执行完为止。try{    //获取线程返回值    t1.join();}catch (EXception ex){    ex.printStackTrace();    }AI写代码java运行线程睡眠try{    Thread.sleep(1000);//当前线程睡眠1000毫秒}catch(InterruptedException e)(    e.printStackTrace();}AI写代码java运行线程生命结束t1.stop();AI写代码java运行判断当前线程是否存活t1.isAlive();AI写代码java运行四、生命周期线程从创建、启动到死亡经历了一个完整的生命周期,在线程的生命周期中一般要经历五种状态:新建——就绪——运行——阻塞——死亡。新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态;就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,也就是在执行.start()方法后;运行:当就绪的线程被调度并获得处理器资源时,便进入运行状态,run()方法定义了线程的操作和功能,此时run()方法的代码开始执行;阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态;死亡:线程完成了它的全部工作或线程被提前强制性地中止 。 ​​​​​线程可能以如下三种方法结束:run或call方法执行完成后线程抛出一个未捕获的Exception或Error直接调用了stop()方法五、同步锁和死锁5.1 同步锁         多线程模式的提出势必就会带来线程同步的问题,在保证数据一致性上,我们需要为线程加上同步锁。Java中对于多线程安全的问题提出了同步机制,即在方法声明的时候加入synchronized关键字来修饰或者直接使用synchronized来锁一个demo5.1.1 synchronized加锁的两种方式 synchronized同步锁关键字修饰 //使用synchronized同步锁关键字修饰需要同步执行的方法体public synchronized void drawing(int money){    需要同步执行的代码}AI写代码java运行注意:        在普通方法上加同步锁synchronized,锁的是整个对象,不是某一个方法。如果是不同对象的话那么就是不同的锁。静态的方法加synchronized对于所有的对象都是同一个锁!synchronized锁一段demo使用这种方法来锁指向this的代码块使用的都是同一个同步锁。如果改成方法对象的话比如Account对象的话就是不同的同步锁。synchronized(this){ //表示当前的对象的代码块被加了synchronized同步锁    demo...}AI写代码java运行5.1.2 Lock        相比于上面的synchronized相应的锁操作,Lock提供了更为广泛的锁操作。其中包括ReadWriteLock(读写锁)和ReentrantLock(可重入锁),ReadWriteLock提供了ReentrantReadWriteLock的实现类。在Java8中引入了一个新的StampedLock类替代了传统的ReentrantReadWriteLock并给出了三种锁模式:Write、ReadOptimistic和Reading。ReentrantLock 实现demo class x{    //定义锁对象    private final ReentrantLock lock = new ReentrantLock();    //...    //定义需要保证线程安全的方法    public void m(){        lock.lock();        try{            //需要保证线程安全的demo        }        finally{            lock.unlock();        }    }}AI写代码java运行5.2 死锁不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。解决方法专门的算法、原则,比如加锁顺序一致尽量减少同步资源的定义,尽量避免锁未释放的场景六、线程通信        当我们手动开启并在控制台中输出两个线程的运行过程的时候,程序并不能每次都准确的控制两个线程的轮换执行的先后次序,所以Java中也提供了一些机制来保证线程的协调运行。在传统的Java中,基于同步锁synchronized关键字提供了借助于Object类的wait()、notify()和notifyAll()方法来控制线程的阻塞情况,而之后也出现了基于Condition和阻塞队列BlockingQueue来控制线程阻塞的情况。6.1 传统的线程通信Object类中提供的wait()、notify()和notifyAll()方法必须由一个同步监视器对象来调用,所以这三种方法必须基于同步锁synchronized关键字。wait():该方法会导致当前线程进入等待状态,直到其它的线程调用notify()或notifyAll()方法来唤醒该线程,wait方法有三种形式:不带时间参数(等待唤醒)、带毫秒时间参数(时间到自动唤醒)和带毫微秒的时间参数(时间到自动唤醒)。调用wait方法当前线程会释放对同步监视器的锁定。notify():唤醒该同步监视器上等待的单个线程,这种选择是按照优先级最高的来唤醒结束其等待状态。notifyAll():唤醒等待的所有线程。//使用时直接调用方法就行,但必须是在有synchronized修饰的方法内去调用才可wait();notify();notifyAll();AI写代码6.2 使用Condition来控制线程通信        对于程序不使用synchronized关键字来保证同步锁,而是采用Lock对象来保证同步,Java中提供了Condition类来保证线程通信。Contidion类中提供了类似于synchronized关键字中的三种方法:await()、signal()和signalAll(),替代了同步监视器的功能。await():类似于wait方法,会使得当前线程进入等待状态,直到其它线程调用signal()或signalAll()来唤醒。signal():唤醒单个线程。signalAll():唤醒多个线程。//显示定义Lock对象Lock lock = new ReentrantLock();//获取ConditionCondition cond = lock.newCondition();//需要同步的方法中加锁public void fun(){    //加锁过程    lock.lock();    try{        if(条件) cond.await(); //线程进入等待        else{            //唤醒其他线程            cond.signalAll();        }    }catch(InterruptedException e){        e.printStrackTrace();    }finally{        //锁的释放        lock.unlock();    }}AI写代码java运行6.3 使用阻塞队列来控制线程通信        除了上述两种方法,Java5中还提供了BlockingQueue接口来作为线程同步的工具。它的工作原理是这样滴:当生产者往BlockingQueue接口中放入元素直至接口队列满了,线程阻塞;消费者从BlockingQueue接口队列中取元素直至队列空了,线程阻塞。BlockingQueue接口继承了Queue接口并提供了如下三组方法。 在队列尾部添加元素:add(E e)、offer(E e)、put(E e),当队列已满的时候,这三个方法分别会抛出异常、返回false和阻塞线程。在队列头部删除并返回删除元素:remove()、poll()、take()方法。当该队列已空时,这三个方法分别会抛出异常、返回false和阻塞线程。在队列头部取出但不删除元素:element()和peek(),当该队列已空时,分别会抛出异常和返回false在Java7之后,阻塞队列出现了新增,分别是:ArrayBlockingQueue、LinkedBlockingQueue、priorityBlockingQueue、SynchornizedQueue和DelayQueue这五个类。 七、线程池        系统启动一个新线程的成本是比较高的,尤其是当系统本身已经有大量的并发线程时,会导致系统性能急剧下降,甚至会导致JVM崩溃,因此我们通常采用线程池来维护系统的并发线程。与数据库连接池类似的时,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动后一个空闲的线程来执行它们的run()或call()方法,当运行结束后,该线程不会死亡而是返回线程池中进入空闲等待状态。        ExecutorService代表尽快执行线程的线程池,程序只需要将一个Runnable对象或Callable对象传给线程池,就会尽快执行线程任务;ScheduledExecutorService代表可在指定延迟后或周期性地执行线程任务的线程池。7.1 ExecutorService类使用示例使用线程池的步骤如下:调用Executors类的静态工厂方法调用创建一个ExecutorService对象,该对象就代表着一个线程池;创建Runnable实现类或Callable实现类的实例,作为线程执行的任务;调用ExecutorService对象的submit()方法来提交Runnable或者Callable对象实例;结束任务时,调用ExecutorService对象的shutdown()方法来关闭线程池;//开启6个线程的线程池ExecutorService pool = Executors.newFixedThreadPool(6);//创建Runnable实现类Runnable target = ()->{...} //提交线程任务到线程池pool.submit(); //关闭线程pool.shutdown();AI写代码java运行        用完一个线程池后,应该调用该线程池的shutdown()方法,该方法将启动线程池的关闭序列并不再接受新任务,线程池中的任务依次执行完毕后线程死亡;或者调用线程池的shutdownNow()方法来直接停止所有正在执行的活动任务。7.2 Java8中的ForkJoinPool        计算机发展到现在其实基本的硬件都支持多核CPU,为了更好地利用硬件设备的资源,Java中提供了一个ForkJoinPool来支持将一个任务拆分成多个小任务并行计算。ForkJoinPool是ExecutorService的实现类,是一个特殊的线程池。构造器的两种方法ForkJoinPool(int num):创建一个包含num个并行线程的ForkJoinPool;ForkJoinPool():以Runtime.availableProcessors()方法的返回值作为parallelism参数(上面我写成了num)来创建改线程池实现通用池的两个静态方法ForkJoinPool commonPool():改方法返回一个通用池,通用池的状态不会受到shutdown()等方法的影响,System.exit(0)除外。int getCommonPoolParallelism():该方法返回通用池的并行级别注意:        ForkJoinPool.submit(ForkJoinTask task) ,其中ForkJoinTask代表着一个可以并行和合并的任务,他有两个抽象的子类:RecursiveAction和RecursiveTask,分别代表着有返回值和无返回值的任务。class PrintTask extends RecursiveAction{    ...    @Override    protected void compute(){        ......        //分割任务        PrintTask t1 = new PrintTask(start,middle);        PrintTask t2 = new PrintTask(middle,end);        //并行执行子任务        t1.fork();        t2.fork();    }} public class Test{    public static void main(String[]args) throws Exception{        //实例化通用池对象        ForkJoinPool pool = new ForkJoinPool();        pool.submit(new PrintTask(0,1000));        //线程等待完成        pool.awaitTermination(2,TimeUnit.SECONDS);        //关闭线程池        pool.shutdown();    }}AI写代码java运行总结        现有的所有企业都采用的是多线程并发的方式来开发的,也要求我们能够应对在高并发场景下保证系统服务的高可用的要求,所以多线程和异步编程我们必须牢牢掌握。这几章可能会比较枯燥,难度也会比较大,荔枝也是啃了一段时间嘿嘿嘿,在学这部分之前一定要把面向对象学好,要不然会晕哈哈哈~~~今朝已然成为过去,明日依然向往未来!我是小荔枝,在技术成长的路上与你相伴,码文不易,麻烦举起小爪爪点个赞吧哈哈哈~~~ 比心心♥~~~————————————————版权声明:本文为CSDN博主「荔枝当大佬」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/qq_62706049/article/details/131848628
  • [互动交流] 如何安装SmartAssist Java 插件
    如何安装SmartAssist Java 插件CodeArts IDE for Python版本: 3.4.1提交: c3be4d08ef4b68e19b23b4136f5f8a7a960d5095日期: 2025-07-15T04:00:00.368Z (3个月前)OS: Windows_NT x64 10.0.19045
  • [技术干货] Java并发编程利器:从ConcurrentHashMap到Fork/Join的奇幻之旅
    篇讲了Lock锁、AQS相关的内容,本篇讲一下线程安全的类,拿来即用无需其他操作就能达到线程安全的效果,省力又省心 ~ ~你是否曾为多线程编程中的各种坑而头疼?本文将用生动比喻和实用代码,带你轻松掌握Java并发容器的精髓,让你的多线程程序既安全又高效!引言:为什么我们需要并发容器?想象一下传统的超市结账场景:只有一个收银台,所有人排成一队,效率低下。这就是传统集合在多线程环境下的写照。而现代并发容器就像拥有多个收银台的智能超市:多个收银台同时工作智能分配顾客到不同队列收银员之间互相协助在Java并发世界中,我们有三大法宝:ConcurrentHashMap - 智能分区的储物柜系统ConcurrentLinkedQueue - 无锁的快速通道阻塞队列 - 有协调员的等待区Fork/Join框架 - 团队协作的工作模式让我们一一探索它们的魔力!1. ConcurrentHashMap:智能分区的储物柜系统1.1 传统Map的问题:独木桥的困境// 传统HashMap在多线程环境下就像独木桥public class HashMapProblem { public static void main(String[] args) { Map<String, String> map = new HashMap<>(); // 多个线程同时操作HashMap,就像多人同时过独木桥 // 结果:有人掉水里(数据丢失),桥塌了(死循环) }}1.2 ConcurrentHashMap的解决方案:多车道高速公路分段锁设计:把整个Map分成多个小区域,每个区域独立加锁ConcurrentHashMap架构: ├── 区域1 (锁1) → 储物柜组1 ├── 区域2 (锁2) → 储物柜组2 ├── 区域3 (锁3) → 储物柜组3 └── ...核心优势:写操作只锁住对应的区域,其他区域仍可读写读操作基本不需要加锁大大提高了并发性能1.3 实战示例:高性能缓存系统/** * 基于ConcurrentHashMap的高性能缓存 * 像智能储物柜系统,支持高并发存取 */public class HighPerformanceCache<K, V> { private final ConcurrentHashMap<K, CacheEntry<V>> cache = new ConcurrentHashMap<>(); // 获取或计算缓存值(线程安全且高效) public V getOrCompute(K key, Supplier<V> supplier) { return cache.computeIfAbsent(key, k -> new CacheEntry<>(supplier.get())).getValue(); } // 批量获取,利用并发特性 public Map<K, V> getAll(Set<K> keys) { Map<K, V> result = new HashMap<>(); keys.forEach(key -> { CacheEntry<V> entry = cache.get(key); if (entry != null && !entry.isExpired()) { result.put(key, entry.getValue()); } }); return result; }}2. ConcurrentLinkedQueue:无锁的快速通道2.1 无锁队列的魔法传统队列就像只有一个入口的隧道,所有车辆必须排队。而ConcurrentLinkedQueue就像多入口的立体交通枢纽:// 无锁队列的生动理解public class LockFreeQueueAnalogy { public void trafficHubComparison() { // 传统阻塞队列:单入口隧道,经常堵车 // ConcurrentLinkedQueue:立体交通枢纽,多入口同时通行 // 秘密武器:CAS(Compare-And-Swap)算法 }}2.2 CAS:优雅的竞争解决CAS就像礼貌的询问:public class PoliteInquiry { public void casAnalogy() { // 传统加锁:像抢座位,谁先坐到就是谁的 // CAS无锁:像礼貌询问"这个座位有人吗?" // 如果没人就坐下,有人就找下一个座位 }}2.3 实战示例:高并发任务处理器/** * 基于ConcurrentLinkedQueue的高性能任务处理器 * 像高效的快递分拣中心 */public class HighPerformanceTaskProcessor { private final ConcurrentLinkedQueue<Runnable> taskQueue = new ConcurrentLinkedQueue<>(); // 提交任务 - 无锁操作,极高吞吐量 public void submit(Runnable task) { taskQueue.offer(task); // 像快递放入分拣流水线 startWorkerIfNeeded(); } // 工作线程 - 无锁获取任务 private class Worker implements Runnable { public void run() { while (!Thread.currentThread().isInterrupted()) { Runnable task = taskQueue.poll(); // 像从流水线取快递 if (task != null) { task.run(); // 处理任务 } } } }}3. 阻塞队列:有协调员的等待区3.1 阻塞队列的四种行为模式想象餐厅的四种接待方式:public class RestaurantReception { public void fourBehaviors() { // 1. 抛出异常 - 霸道的服务员 // "没位置了!走开!" // 2. 返回特殊值 - 礼貌的前台 // "抱歉现在没位置,您要不等会儿?" // 3. 一直阻塞 - 耐心的门童 // "请您在这稍等,有位置我马上叫您" // 4. 超时退出 - 体贴的经理 // "请您等待10分钟,如果还没位置我帮您安排其他餐厅" }}3.2 七种阻塞队列:不同的餐厅风格Java提供了7种阻塞队列,每种都有独特的"经营理念":ArrayBlockingQueue:传统固定座位餐厅// 有10个桌位的餐厅,公平模式ArrayBlockingQueue<String> restaurant = new ArrayBlockingQueue<>(10, true);LinkedBlockingQueue:可扩展的连锁餐厅// 最大容纳1000人的餐厅LinkedBlockingQueue<Order> orderQueue = new LinkedBlockingQueue<>(1000);PriorityBlockingQueue:VIP贵宾厅// 按客户等级服务的贵宾厅PriorityBlockingQueue<Customer> vipLounge = new PriorityBlockingQueue<>();DelayQueue:延时电影院// 电影到点才能入场DelayQueue<MovieScreening> schedule = new DelayQueue<>();SynchronousQueue:一对一传球游戏// 不存储元素,每个put必须等待一个takeSynchronousQueue<String> ballChannel = new SynchronousQueue<>(true);3.3 实战示例:生产者-消费者模式/** * 生产者-消费者模式的完美实现 * 像工厂的装配流水线 */public class ProducerConsumerPattern { private final BlockingQueue<Item> assemblyLine; public ProducerConsumerPattern(int lineCapacity) { this.assemblyLine = new ArrayBlockingQueue<>(lineCapacity); } // 生产者:原材料入库 public void startProducers(int count) { for (int i = 0; i < count; i++) { new Thread(() -> { while (true) { Item item = produceItem(); assemblyLine.put(item); // 流水线满时等待 } }).start(); } } // 消费者:产品出库 public void startConsumers(int count) { for (int i = 0; i < count; i++) { new Thread(() -> { while (true) { Item item = assemblyLine.take(); // 流水线空时等待 consumeItem(item); } }).start(); } }}4. Fork/Join框架:团队协作的智慧4.1 分而治之的哲学Fork/Join框架的核心理念:大事化小,小事并行,结果汇总就像编写一本巨著:传统方式:一个人从头写到尾Fork/Join方式:分给多个作者同时写不同章节,最后汇总4.2 工作窃取算法:聪明的互助团队public class TeamWorkExample { public void workStealingInAction() { // 初始:4个工人,每人25个任务 // 工人A先完成自己的任务 // 工人B还有10个任务没完成 // 工作窃取:工人A从工人B的任务列表"偷"任务帮忙 // 结果:整体效率最大化,没有人闲着 }}4.3 实战示例:并行数组求和/** * 使用Fork/Join并行计算数组和 * 像团队协作完成大项目 */public class ParallelArraySum { static class SumTask extends RecursiveTask<Long> { private static final int THRESHOLD = 1000; // 阈值 private final long[] array; private final int start, end; public SumTask(long[] array, int start, int end) { this.array = array; this.start = start; this.end = end; } @Override protected Long compute() { // 如果任务足够小,直接计算 if (end - start <= THRESHOLD) { long sum = 0; for (int i = start; i < end; i++) sum += array[i]; return sum; } // 拆分成两个子任务 int mid = (start + end) / 2; SumTask leftTask = new SumTask(array, start, mid); SumTask rightTask = new SumTask(array, mid, end); // 并行执行:一个fork,一个当前线程执行 leftTask.fork(); long rightResult = rightTask.compute(); long leftResult = leftTask.join(); return leftResult + rightResult; } } public static void main(String[] args) { long[] array = new long[1000000]; Arrays.fill(array, 1L); // 100万个1 ForkJoinPool pool = new ForkJoinPool(); long result = pool.invoke(new SumTask(array, 0, array.length)); System.out.println("计算结果: " + result); // 输出: 1000000 }}5. 性能对比与选择指南5.1 不同场景的工具选择使用场景推荐工具理由高并发缓存ConcurrentHashMap分段锁,读多写少优化任务队列ConcurrentLinkedQueue无锁,高吞吐量资源池管理LinkedBlockingQueue阻塞操作,流量控制优先级处理PriorityBlockingQueue按优先级排序延时任务DelayQueue支持延时执行直接传递SynchronousQueue零存储,直接传递并行计算Fork/Join框架分治算法,工作窃取5.2 性能优化要点public class PerformanceTips { public void optimizationGuidelines() { // 1. 合理设置容量:避免频繁扩容或内存浪费 // 2. 选择合适的队列:根据业务特性选择 // 3. 避免过度同步:能用无锁就不用有锁 // 4. 注意异常处理:并发环境下的异常传播 // 5. 监控资源使用:避免内存泄漏和资源耗尽 }}6. 最佳实践总结6.1 设计原则解耦生产消费:生产者专注生产,消费者专注消费合理设置边界:防止资源耗尽,保证系统稳定性优雅处理异常:不能让一个线程的异常影响整个系统监控与调优:根据实际负载调整参数6.2 常见陷阱与规避public class CommonPitfalls { public void avoidTheseMistakes() { // ❌ 错误:在并发容器中执行耗时操作 // ✅ 正确:快速完成容器操作,复杂逻辑异步处理 // ❌ 错误:忽略容量边界导致内存溢出 // ✅ 正确:合理设置容量,使用有界队列 // ❌ 错误:依赖size()做业务判断 // ✅ 正确:使用专门的状态变量 // ❌ 错误:在Fork/Join任务中执行IO // ✅ 正确:Fork/Join只用于计算密集型任务 }}结语:掌握并发编程的艺术Java并发容器就像精心设计的交通系统,每种工具都在特定场景下发挥独特价值:ConcurrentHashMap:智能的多车道高速公路ConcurrentLinkedQueue:无锁的立体交通枢纽阻塞队列:有协调员的智能等待区Fork/Join框架:团队协作的分布式工作模式掌握这些工具,你就能构建出既安全又高效的并发程序,真正发挥多核硬件的威力。记住:合适的工具用在合适的场景,这才是并发编程的真谛。现在,拿起这些利器,开始构建你的高性能并发应用吧! 转载请注明原文链接:https://www.cnblogs.com/sun-10387834/p/19171977
  • [技术干货] 高性能 Web 服务器与反向代理实战
    Nginx(发音为 "engine-x")已成为高并发、高性能 Web 服务的代名词。作为全球最受欢迎的 Web 服务器之一,Nginx 不仅被 Google、Facebook、Netflix、淘宝、京东等大型互联网公司广泛采用,更是微服务、负载均衡、API 网关等现代架构的核心组件。 一、什么是 Nginx?Nginx 是一个开源的 高性能 HTTP 服务器和反向代理服务器,同时也是一个 IMAP/POP3/SMTP 邮件代理服务器。它由俄罗斯程序员 Igor Sysoev 于 2004 年发布,最初为解决 C10K 问题(单机支持 1 万并发连接)而设计。核心特点:特性说明高性能异步非阻塞事件驱动架构,资源消耗低,支持高并发高可靠性进程模型稳定,即使高负载下也不会崩溃热部署支持不停机更新配置、升级版本模块化设计功能通过模块扩展,灵活可定制反向代理与负载均衡支持多种负载均衡算法静态资源服务高效处理 HTML、CSS、JS、图片等静态文件二、Nginx 架构原理:为什么这么快?1. 事件驱动 + 异步非阻塞与传统 Apache 的 多进程/多线程模型(每个连接占用一个进程/线程)不同,Nginx 采用 事件驱动的异步非阻塞 I/O 模型。Master 进程:管理进程,不处理请求。Worker 进程:每个 Worker 采用单线程 + 事件循环(Event Loop)处理成千上万个并发连接。I/O 多路复用:使用 epoll(Linux)、kqueue(BSD)等机制,一个进程可监听多个 socket。✅ 优势:内存占用少,上下文切换开销小,轻松应对数万并发连接。三、Nginx 的核心应用场景1. 静态 Web 服务器Nginx 是服务静态资源的绝佳选择,性能远超应用服务器(如 Tomcat)。server { listen 80; server_name www.example.com; location / { root /var/www/html; # 静态文件目录 index index.html; } # 缓存静态资源 location ~* \.(jpg|jpeg|png|css|js)$ { expires 1y; add_header Cache-Control "public, immutable"; }}2. 反向代理(Reverse Proxy)Nginx 作为“门面”,接收客户端请求,转发给后端应用服务器(如 Java、Python、Node.js),并返回响应。server { listen 80; server_name api.example.com; location / { proxy_pass http://127.0.0.1:8080; # 转发到本地 8080 端口 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }}✅ 优势:隐藏后端服务器真实 IP统一入口,便于管理提升安全性3. 负载均衡(Load Balancing)Nginx 可将请求分发到多个后端服务器,实现横向扩展与高可用。配置示例:upstream backend { # 负载均衡算法 least_conn; # 最少连接 # round-robin; # 轮询(默认) # ip_hash; # IP 哈希(会话保持) # hash $request_uri; # 一致性哈希 server 192.168.1.10:8080 weight=3; # 权重 3 server 192.168.1.11:8080; server 192.168.1.12:8080 backup; # 备用服务器}server { listen 80; location / { proxy_pass http://backend; }}4. SSL/TLS 加密(HTTPS)Nginx 可作为 SSL 终端,处理 HTTPS 请求并解密后转发给后端 HTTP 服务。server { listen 443 ssl http2; server_name www.example.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/private.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512; location / { proxy_pass http://backend; }}✅ 推荐:使用 Let's Encrypt 免费证书 + Certbot 自动续期。5. 缓存加速Nginx 支持反向代理缓存,减少后端压力,提升响应速度。# 定义缓存区proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;server { location / { proxy_cache my_cache; proxy_pass http://backend; proxy_cache_valid 200 302 10m; # 缓存 10 分钟 add_header X-Cache-Status $upstream_cache_status; }}缓存命中时,X-Cache-Status 返回 HIT,否则为 MISS。6. URL 重写与重定向# 301 永久重定向rewrite ^/old-page$ /new-page permanent;# 条件重写if ($http_user_agent ~* "bot|spider") { rewrite ^/.*$ /robots.txt break;}# 伪静态rewrite ^/article/(\d+)\.html$ /article.php?id=$1 last;四、常用配置指令详解指令作用listen监听端口和 IPserver_name匹配域名location定义 URL 路由规则root / alias文件路径映射proxy_pass反向代理目标upstream定义后端服务器组try_files尝试多个文件路径(常用于 SPA 路由)gzip启用 Gzip 压缩SPA 应用路由支持(如 Vue、React)location / { root /var/www/app; try_files $uri $uri/ /index.html;}确保前端路由刷新不 404。五、性能优化建议1. Worker 进程优化worker_processes auto; # 通常设置为 CPU 核心数worker_connections 1024; # 每个 Worker 最大连接数worker_rlimit_nofile 65535; # 提升文件描述符限制2. 开启 Gzip 压缩gzip on;gzip_types text/plain text/css application/json application/javascript text/xml application/xml;3. 启用 HTTP/2listen 443 ssl http2;减少延迟,提升加载速度。4. 静态资源缓存location ~* \.(css|js|jpg|png|gif)$ { expires 1y; add_header Cache-Control "public, immutable";}六、安全加固1. 隐藏 Nginx 版本号server_tokens off;2. 防止点击劫持add_header X-Frame-Options SAMEORIGIN;3. 防止 XSS 攻击add_header X-Content-Type-Options nosniff;add_header Content-Security-Policy "default-src 'self'";4. 限制请求频率limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;location /api/ { limit_req zone=api burst=20 nodelay;} 
  • [介绍/入门] Gin笔记二之gin.Engine和路由设置
    一篇笔记主要介绍 gin.Engine,设置路由等操作,以下是本篇笔记目录:gin.Default() 和 gin.New()HTTP 方法路由分组与中间件1、gin.Default() 和 gin.New()前面第一篇笔记介绍,创建一个 gin 的路由引擎使用的函数是 gin.Default(),返回的类型是 *gin.Engine,我们可以使用其创建路由和路由组。除了这个函数外,还有一个 gin.New(),其返回的也是 *gin.Engine,但是不一样的是 gin.Default() 会对 gin.Engine 添加默认的 Logger() 和 Recovery() 中间件。这两个函数大致内容如下:func New(opts ...OptionFunc) *Engine { ...}func Default(opts ...OptionFunc) *Engine { ... engine := New() engine.Use(Logger(), Recovery()) ...}我们使用第一篇笔记中使用 debug 模式运行系统后输出的信息可以再看一下:[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode)[GIN-debug] GET /test --> main.main.func1 (3 handlers)[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.[GIN-debug] Listening and serving HTTP on :9898我们使用的是 gin.Default() 运行的系统,所以在第一行告诉我们创建了一个带有 Logger and Recovery 中间件的 Engine。同时第三行输出路由信息的地方,标明了这个路由指向的处理函数,后面的括号里是 3 handlers,这个意思是除了我们处理路由的 handler,还有两个默认的中间件 handler,也就是这里的 Logger() 和 Recovery() 中间件。下面介绍一下 Logger() 和 Recovery() 这两个 handler 的作用。1. Logger()默认的 Logger() 会输出接口调用的信息,比如第一篇中我们定义了一个 /test 接口,当我们调用这个接口的时候,控制台会输出下面这条信息:[GIN] 2025/08/19 - 23:15:26 | 200 | 36.666µs | 127.0.0.1 | GET "/test"可以看到日志中会包含请求时间、返回的 HTTP 状态码、请求耗时、调用方 ip、请求方式和接口名称等。这条日志信息的输出就是 Logger() 这个中间件起的作用。在其内部,会调用一个 LoggerWithConfig() 函数,获取到请求的 ip、记录调用时间、调用方式等信息,然后进行输出,下面是部分源码信息:param.TimeStamp = time.Now()param.Latency = param.TimeStamp.Sub(start)param.ClientIP = c.ClientIP()param.Method = c.Request.Methodparam.StatusCode = c.Writer.Status()param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()2. Recovery()Recovery() 中间件则可以为我们捕获程序中未处理的 panic,记录错误信息并返回 500 状态码信息,比如我们在第一篇笔记中使用的 TestHandler 函数,我们在其中加一个除数为 0 的错误:func TestHandler(c *gin.Context) { response := TestResponse{ Code: 0, Message: "success", } a := 0 fmt.Println(1 / a) c.JSON(http.StatusOK, response)}在接口调用的时候,如果我们使用的是 gin.Default(),那么客户端不会报错,而是会收到一个 HTTP 状态码为 500 的报错信息,而如果使用的是 gin.New(),客户端则会直接发生错误。总的来说,Logger() 和 Recovery() 这两个的中间件是 gin 框架为我们默认添加的对于开发者来说较为友好的两个操作,在后面介绍中间件的时候,我们也可以手动实现这两个功能。2、HTTP 方法gin.Engine 支持配置 HTTP 多个方法,比如 GET、POST、PUT、DELETE 等。以第一篇笔记中的代码为例,其设置方法如下:r.GET("/test", TestHandler)r.POST("/test", TestHandler)r.PUT("/test", TestHandler)r.DELETE("/test", TestHandler)3、路由分组与中间件除了设置单个路由,我们还可以对路由进行分组设置,比如需要控制版本,或者模块设置需要统一的前缀,又或者是需要统一设置中间件功能的时候。其整体代码示例如下:package mainimport ( "fmt" "net/http" "github.com/gin-gonic/gin")type TestResponse struct { Code int `json:"code"` Message string `json:"message"`}func TestHandler(c *gin.Context) { response := TestResponse{ Code: 0, Message: "success", } c.JSON(http.StatusOK, response)}func main() { r := gin.Default() v1 := r.Group("/v1") { v1.GET("/test", TestHandler) } err := r.Run(":9898") if err != nil { fmt.Println("gin run in 9898 error:", err) }}这里,我们设置了一个路由名称以 v1 为前缀的路由组,其下每个路由的访问都需要带有 /v1,这样就实现了统一设置路由前缀的功能。而如果我们需要向其中添加中间件的时候,也可以不用挨个路由进行设置,而是在 v1 路由组的设置中就可以实现,比如:v1 := r.Group("/v1", Middleware1, Middleware2)这样,其下每个路由的 handler 函数在调用前就都会先调用 Middleware1 和 Middleware2 这两个中间件。以上就是本篇笔记关于 gin.Engine 的全部内容,其实中间件的相关操作也应该属于 gin.Engine 的内容,但是那部分需要介绍的知识点和想要用于介绍的代码示例略多,所以就单独开一篇笔记在后面再介绍。 转载于:https://www.cnblogs.com/hunterxiong/p/19175625
  • [介绍/入门] Java并发编程利器:深入解析13个原子操作类
    在多线程并发环境下,保证数据操作的原子性是个常见且关键的挑战。Java从JDK 1.5开始提供了java.util.concurrent.atomic包,其中包含13个强大的原子操作类,让我们能够以无锁的方式实现线程安全。本文将带你深入理解这些原子类的原理、API和使用场景。一、为什么需要原子操作类?1.1 问题的由来想象一下这样的场景:多个线程同时操作同一个银行账户进行取款,如果不加控制,可能会出现什么情况?// 不安全的计数器示例class UnsafeCounter { private int count = 0; public void increment() { count++; // 这不是原子操作! }}count++看似简单,实际上包含三个步骤:读取count的当前值将值加1将新值写回count在多线程环境下,这两个步骤可能被其他线程打断,导致数据不一致。1.2 传统的解决方案及其缺点传统做法是使用synchronized关键字:class SynchronizedCounter { private int count = 0; public synchronized void increment() { count++; }}synchronized确实能保证线程安全,但存在以下问题:性能开销:锁的获取和释放需要代价可能死锁:不正确的锁顺序可能导致死锁降低并发性:同一时刻只有一个线程能访问1.3 原子操作类的优势原子操作类基于CAS(Compare-And-Swap) 机制,提供了:无锁编程:避免传统锁的开销高性能:在低竞争环境下性能优异无死锁风险:基于硬件指令,不会产生死锁高并发:支持多个线程同时操作二、原子更新基本类型类2.1 AtomicBoolean - 原子更新布尔类型使用场景:状态标志位、开关控制、条件判断核心API详解方法参数返回值说明get()-boolean获取当前值set(boolean newValue)newValue: 新值void设置新值getAndSet(boolean newValue)newValue: 新值boolean原子性地设置为新值并返回旧值compareAndSet(boolean expect, boolean update)expect: 期望值update: 更新值boolean如果当前值等于期望值,则原子性地更新lazySet(boolean newValue)newValue: 新值void最终设置为新值,但不保证立即可见性weakCompareAndSet(boolean expect, boolean update)expect: 期望值update: 更新值boolean可能更弱的CAS操作,在某些平台上性能更好import java.util.concurrent.atomic.AtomicBoolean;/** * AtomicBoolean示例:用于原子性地更新布尔值 * 典型场景:系统开关、状态标志等 */public class AtomicBooleanDemo { public static void main(String[] args) { // 创建AtomicBoolean,初始值为false AtomicBoolean atomicBoolean = new AtomicBoolean(false); // get(): 获取当前值 System.out.println("初始值: " + atomicBoolean.get()); // getAndSet(): 原子性地设置为true,返回旧值 boolean oldValue = atomicBoolean.getAndSet(true); System.out.println("getAndSet旧值: " + oldValue + ", 新值: " + atomicBoolean.get()); // compareAndSet(): 比较并设置 boolean success = atomicBoolean.compareAndSet(true, false); System.out.println("CAS操作结果: " + success + ", 当前值: " + atomicBoolean.get()); // lazySet(): 最终会设置,但不保证立即可见性 atomicBoolean.lazySet(true); System.out.println("lazySet后的值: " + atomicBoolean.get()); // weakCompareAndSet(): 弱版本CAS boolean weakSuccess = atomicBoolean.weakCompareAndSet(true, false); System.out.println("弱CAS操作结果: " + weakSuccess + ", 当前值: " + atomicBoolean.get()); }}原理分析:AtomicBoolean内部实际上使用int类型来存储,0表示false,1表示true。通过compareAndSwapInt来实现原子操作。2.2 AtomicInteger - 原子更新整型使用场景:计数器、序列号生成、资源数量控制核心API详解方法参数返回值说明get()-int获取当前值set(int newValue)newValue: 新值void设置新值getAndSet(int newValue)newValue: 新值int原子性地设置为新值并返回旧值compareAndSet(int expect, int update)expect: 期望值update: 更新值booleanCAS操作getAndIncrement()-int原子递增,返回旧值getAndDecrement()-int原子递减,返回旧值getAndAdd(int delta)delta: 增量int原子加法,返回旧值incrementAndGet()-int原子递增,返回新值decrementAndGet()-int原子递减,返回新值addAndGet(int delta)delta: 增量int原子加法,返回新值updateAndGet(IntUnaryOperator)operator: 更新函数int函数式更新accumulateAndGet(int x, IntBinaryOperator)x: 参数operator: 操作函数int累积计算import java.util.concurrent.atomic.AtomicInteger;/** * AtomicInteger是最常用的原子类之一 * 适用于计数器、ID生成器等需要原子递增的场景 */public class AtomicIntegerDemo { public static void main(String[] args) { AtomicInteger atomicInt = new AtomicInteger(0); // 基础操作 System.out.println("初始值: " + atomicInt.get()); atomicInt.set(5); System.out.println("set(5)后: " + atomicInt.get()); // 原子递增并返回旧值 - 常用于计数 System.out.println("getAndIncrement: " + atomicInt.getAndIncrement()); // 返回5 System.out.println("当前值: " + atomicInt.get()); // 6 // 原子递减并返回旧值 System.out.println("getAndDecrement: " + atomicInt.getAndDecrement()); // 返回6 System.out.println("当前值: " + atomicInt.get()); // 5 // 原子加法并返回旧值 System.out.println("getAndAdd(10): " + atomicInt.getAndAdd(10)); // 返回5 System.out.println("当前值: " + atomicInt.get()); // 15 // 原子递增并返回新值 System.out.println("incrementAndGet: " + atomicInt.incrementAndGet()); // 16 // 原子加法并返回结果 - 适合批量增加 int result = atomicInt.addAndGet(10); System.out.println("addAndGet(10)结果: " + result); // 26 // 比较并设置 - 核心CAS操作 boolean updated = atomicInt.compareAndSet(26, 30); System.out.println("CAS操作结果: " + updated + ", 当前值: " + atomicInt.get()); // 获取并设置新值 - 适合重置操作 int previous = atomicInt.getAndSet(40); System.out.println("getAndSet旧值: " + previous + ", 新值: " + atomicInt.get()); // JDK8新增:函数式更新 - 更灵活的更新方式 atomicInt.updateAndGet(x -> x * 2); System.out.println("updateAndGet(*2)后的值: " + atomicInt.get()); // 80 // 累积计算 atomicInt.accumulateAndGet(10, (x, y) -> x + y * 2); System.out.println("accumulateAndGet后的值: " + atomicInt.get()); // 100 }}源码分析:public final int getAndIncrement() { // 自旋CAS:循环直到成功 for (;;) { int current = get(); // 步骤1:获取当前值 int next = current + 1; // 步骤2:计算新值 if (compareAndSet(current, next)) // 步骤3:CAS更新 return current; // 成功则返回旧值 } // 如果CAS失败,说明有其他线程修改了值,循环重试}2.3 AtomicLong - 原子更新长整型使用场景:大数值计数器、统计信息、唯一ID生成核心API详解方法参数返回值说明get()-long获取当前值set(long newValue)newValue: 新值void设置新值getAndSet(long newValue)newValue: 新值long原子性地设置为新值并返回旧值compareAndSet(long expect, long update)expect: 期望值update: 更新值booleanCAS操作getAndIncrement()-long原子递增,返回旧值getAndDecrement()-long原子递减,返回旧值getAndAdd(long delta)delta: 增量long原子加法,返回旧值incrementAndGet()-long原子递增,返回新值decrementAndGet()-long原子递减,返回新值addAndGet(long delta)delta: 增量long原子加法,返回新值updateAndGet(LongUnaryOperator)operator: 更新函数long函数式更新accumulateAndGet(long x, LongBinaryOperator)x: 参数operator: 操作函数long累积计算import java.util.concurrent.atomic.AtomicLong;/** * AtomicLong用于长整型的原子操作 * 在64位系统中性能与AtomicInteger相当 */public class AtomicLongDemo { public static void main(String[] args) { AtomicLong atomicLong = new AtomicLong(100L); System.out.println("初始值: " + atomicLong.get()); // 原子递增并返回旧值 - 适合序列号生成 System.out.println("getAndIncrement: " + atomicLong.getAndIncrement()); System.out.println("当前值: " + atomicLong.get()); // 原子递减并返回旧值 System.out.println("getAndDecrement: " + atomicLong.getAndDecrement()); System.out.println("当前值: " + atomicLong.get()); // 原子加法并返回旧值 System.out.println("getAndAdd(50): " + atomicLong.getAndAdd(50L)); System.out.println("当前值: " + atomicLong.get()); // 原子递增并返回新值 System.out.println("incrementAndGet: " + atomicLong.incrementAndGet()); // 原子加法并返回结果 - 适合统计累加 long newValue = atomicLong.addAndGet(50L); System.out.println("addAndGet(50)结果: " + newValue); // 比较并设置 boolean success = atomicLong.compareAndSet(250L, 300L); System.out.println("CAS操作结果: " + success + ", 当前值: " + atomicLong.get()); // JDK8新增:函数式更新 atomicLong.updateAndGet(x -> x / 2); System.out.println("updateAndGet(/2)后的值: " + atomicLong.get()); // JDK8新增:累积计算 - 适合复杂的原子计算 atomicLong.accumulateAndGet(100L, (x, y) -> x * y); System.out.println("accumulateAndGet后的值: " + atomicLong.get()); }}性能提示:在32位系统上,AtomicLong的CAS操作可能需要锁住总线,性能相对较差。Java 8提供了LongAdder作为高性能替代方案。三、原子更新数组类3.1 AtomicIntegerArray - 原子更新整型数组使用场景:并发计数器数组、桶统计、并行计算核心API详解方法参数返回值说明length()-int返回数组长度get(int i)i: 索引int获取指定索引的值set(int i, int newValue)i: 索引newValue: 新值void设置指定索引的值getAndSet(int i, int newValue)i: 索引newValue: 新值int原子设置并返回旧值compareAndSet(int i, int expect, int update)i: 索引expect: 期望值update: 更新值boolean对指定索引进行CAS操作getAndIncrement(int i)i: 索引int原子递增指定索引,返回旧值getAndDecrement(int i)i: 索引int原子递减指定索引,返回旧值getAndAdd(int i, int delta)i: 索引delta: 增量int原子加法,返回旧值incrementAndGet(int i)i: 索引int原子递增指定索引,返回新值addAndGet(int i, int delta)i: 索引delta: 增量int原子加法,返回新值import java.util.concurrent.atomic.AtomicIntegerArray;/** * AtomicIntegerArray允许原子地更新数组中的单个元素 * 注意:构造函数会复制传入的数组,不影响原数组 */public class AtomicIntegerArrayDemo { public static void main(String[] args) { int[] initialArray = {1, 2, 3, 4, 5}; // 创建原子整型数组,会复制传入的数组 AtomicIntegerArray atomicArray = new AtomicIntegerArray(initialArray); System.out.println("数组长度: " + atomicArray.length()); System.out.println("原始数组: " + atomicArray.toString()); // get(): 获取指定索引的值 System.out.println("索引0的值: " + atomicArray.get(0)); // set(): 设置指定索引的值 atomicArray.set(0, 10); System.out.println("set(0, 10)后的数组: " + atomicArray.toString()); // getAndSet(): 原子更新指定索引的元素并返回旧值 int oldValue = atomicArray.getAndSet(1, 20); System.out.println("索引1替换前的值: " + oldValue + ", 数组: " + atomicArray.toString()); // getAndIncrement(): 原子递增指定索引的元素 - 适合分桶计数 oldValue = atomicArray.getAndIncrement(2); System.out.println("索引2递增前值: " + oldValue + ", 数组: " + atomicArray.toString()); // compareAndSet(): 比较并设置特定位置的元素 boolean updated = atomicArray.compareAndSet(3, 4, 40); System.out.println("索引3 CAS结果: " + updated + ", 数组: " + atomicArray.toString()); // addAndGet(): 原子加法 - 适合累加统计 int newValue = atomicArray.addAndGet(4, 5); System.out.println("索引4加5后的值: " + newValue + ", 数组: " + atomicArray.toString()); // incrementAndGet(): 原子递增并返回新值 newValue = atomicArray.incrementAndGet(0); System.out.println("索引0递增后的值: " + newValue); // 重要:原始数组不会被修改 System.out.println("原始数组值未被修改: " + initialArray[0]); // 仍然是1 }}设计思想:AtomicIntegerArray通过复制数组来避免外部修改,每个数组元素的更新都是独立的原子操作。3.2 AtomicLongArray - 原子更新长整型数组使用场景:大数据统计、时间戳数组、大数值桶统计核心API详解方法参数返回值说明length()-int返回数组长度get(int i)i: 索引long获取指定索引的值set(int i, long newValue)i: 索引newValue: 新值void设置指定索引的值getAndSet(int i, long newValue)i: 索引newValue: 新值long原子设置并返回旧值compareAndSet(int i, long expect, long update)i: 索引expect: 期望值update: 更新值boolean对指定索引进行CAS操作getAndAdd(int i, long delta)i: 索引delta: 增量long原子加法,返回旧值addAndGet(int i, long delta)i: 索引delta: 增量long原子加法,返回新值import java.util.concurrent.atomic.AtomicLongArray;/** * AtomicLongArray提供长整型数组的原子操作 * 适用于需要大数值范围的并发统计 */public class AtomicLongArrayDemo { public static void main(String[] args) { long[] initialArray = {100L, 200L, 300L, 400L, 500L}; AtomicLongArray atomicLongArray = new AtomicLongArray(initialArray); System.out.println("数组长度: " + atomicLongArray.length()); System.out.println("初始数组: " + atomicLongArray.toString()); // 基础操作 System.out.println("索引0的值: " + atomicLongArray.get(0)); atomicLongArray.set(0, 150L); System.out.println("set(0, 150)后的数组: " + atomicLongArray.toString()); // 原子更新操作 long oldValue = atomicLongArray.getAndSet(1, 250L); System.out.println("索引1替换前的值: " + oldValue + ", 数组: " + atomicLongArray.toString()); // 原子加法操作 atomicLongArray.getAndAdd(2, 100L); System.out.println("索引2加100后的数组: " + atomicLongArray.toString()); // 比较并设置 atomicLongArray.compareAndSet(3, 400L, 450L); System.out.println("索引3 CAS后的数组: " + atomicLongArray.toString()); // 加法并获取新值 long newValue = atomicLongArray.addAndGet(4, 200L); System.out.println("索引4加200后的值: " + newValue); }}3.3 AtomicReferenceArray - 原子更新引用类型数组使用场景:对象池、缓存数组、并发数据结构核心API详解方法参数返回值说明length()-int返回数组长度get(int i)i: 索引E获取指定索引的引用set(int i, E newValue)i: 索引newValue: 新引用void设置指定索引的引用getAndSet(int i, E newValue)i: 索引newValue: 新引用E原子设置并返回旧引用compareAndSet(int i, E expect, E update)i: 索引expect: 期望引用update: 更新引用boolean对指定索引进行CAS操作lazySet(int i, E newValue)i: 索引newValue: 新引用void延迟设置引用import java.util.concurrent.atomic.AtomicReferenceArray;/** * AtomicReferenceArray用于原子更新引用类型数组 * 适用于对象引用需要原子更新的场景 */public class AtomicReferenceArrayDemo { static class Person { String name; int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return name + "(" + age + ")"; } } public static void main(String[] args) { Person[] persons = { new Person("Alice", 25), new Person("Bob", 30), new Person("Charlie", 35), new Person("David", 40) }; AtomicReferenceArray<Person> atomicArray = new AtomicReferenceArray<>(persons); System.out.println("数组长度: " + atomicArray.length()); System.out.println("初始数组: "); for (int i = 0; i < atomicArray.length(); i++) { System.out.println("索引 " + i + ": " + atomicArray.get(i)); } // 原子更新引用 - 适合对象替换 Person newPerson = new Person("Eve", 28); Person oldPerson = atomicArray.getAndSet(1, newPerson); System.out.println("索引1替换: " + oldPerson + " -> " + atomicArray.get(1)); // 比较并设置引用 boolean success = atomicArray.compareAndSet(2, persons[2], new Person("Frank", 45)); System.out.println("索引2 CAS结果: " + success + ", 新值: " + atomicArray.get(2)); // 延迟设置 atomicArray.lazySet(3, new Person("Grace", 50)); System.out.println("索引3延迟设置后的值: " + atomicArray.get(3)); // 遍历数组 System.out.println("最终数组状态:"); for (int i = 0; i < atomicArray.length(); i++) { System.out.println("索引 " + i + ": " + atomicArray.get(i)); } }}四、原子更新引用类型4.1 AtomicReference - 原子更新引用类型使用场景:单例模式、缓存更新、状态对象替换核心API详解方法参数返回值说明get()-V获取当前引用set(V newValue)newValue: 新引用void设置新引用getAndSet(V newValue)newValue: 新引用V原子设置并返回旧引用compareAndSet(V expect, V update)expect: 期望引用update: 更新引用booleanCAS操作weakCompareAndSet(V expect, V update)expect: 期望引用update: 更新引用boolean弱版本CASlazySet(V newValue)newValue: 新引用void延迟设置引用updateAndGet(UnaryOperator<V>)operator: 更新函数V函数式更新getAndUpdate(UnaryOperator<V>)operator: 更新函数V函数式更新并返回旧值accumulateAndGet(V x, BinaryOperator<V>)x: 参数operator: 操作函数V累积计算import java.util.concurrent.atomic.AtomicReference;/** * AtomicReference用于原子更新对象引用 * 解决"先检查后执行"的竞态条件 */public class AtomicReferenceDemo { static class User { private String name; private int age; public User(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } @Override public String toString() { return "User{name='" + name + "', age=" + age + "}"; } } public static void main(String[] args) { AtomicReference<User> atomicUser = new AtomicReference<>(); User initialUser = new User("张三", 25); atomicUser.set(initialUser); System.out.println("初始用户: " + atomicUser.get()); // getAndSet(): 原子更新引用 - 适合缓存更新 User newUser = new User("李四", 30); User oldUser = atomicUser.getAndSet(newUser); System.out.println("替换前的用户: " + oldUser); System.out.println("当前用户: " + atomicUser.get()); // compareAndSet(): 比较并设置 - 核心操作 boolean success = atomicUser.compareAndSet(newUser, new User("王五", 35)); System.out.println("CAS操作结果: " + success + ", 当前用户: " + atomicUser.get()); // weakCompareAndSet(): 弱版本CAS boolean weakSuccess = atomicUser.weakCompareAndSet( atomicUser.get(), new User("赵六", 40)); System.out.println("弱CAS操作结果: " + weakSuccess + ", 当前用户: " + atomicUser.get()); // lazySet(): 延迟设置 atomicUser.lazySet(new User("孙七", 45)); System.out.println("延迟设置后的用户: " + atomicUser.get()); // JDK8新增:函数式更新 atomicUser.updateAndGet(user -> new User(user.getName() + "_updated", user.getAge() + 1)); System.out.println("函数式更新后的用户: " + atomicUser.get()); // getAndUpdate(): 函数式更新并返回旧值 User previous = atomicUser.getAndUpdate(user -> new User("周八", 50)); System.out.println("更新前的用户: " + previous + ", 当前用户: " + atomicUser.get()); // accumulateAndGet(): 累积计算 atomicUser.accumulateAndGet(new User("吴九", 55), (old, param) -> new User(old.getName() + "&" + param.getName(), old.getAge() + param.getAge())); System.out.println("累积计算后的用户: " + atomicUser.get()); }}典型应用:单例模式的双重检查锁定class Singleton { private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>(); public static Singleton getInstance() { for (;;) { Singleton current = INSTANCE.get(); if (current != null) return current; current = new Singleton(); if (INSTANCE.compareAndSet(null, current)) { return current; } } }}4.2 AtomicMarkableReference - 带标记位的原子引用使用场景:带状态的缓存、ABA问题简单解决方案核心API详解方法参数返回值说明getReference()-V获取当前引用isMarked()-boolean获取当前标记位get(boolean[] markHolder)markHolder: 标记位容器V获取引用和标记位set(V newReference, boolean newMark)newReference: 新引用newMark: 新标记void设置引用和标记位compareAndSet(V expectedReference, V newReference, boolean expectedMark, boolean newMark)expectedReference: 期望引用newReference: 新引用expectedMark: 期望标记newMark: 新标记boolean同时比较引用和标记位attemptMark(V expectedReference, boolean newMark)expectedReference: 期望引用newMark: 新标记boolean尝试只更新标记位import java.util.concurrent.atomic.AtomicMarkableReference;/** * AtomicMarkableReference将引用与一个布尔标记位绑定 * 适用于需要同时更新引用和状态的场景 */public class AtomicMarkableReferenceDemo { public static void main(String[] args) { String initialRef = "初始数据"; boolean initialMark = false; // 创建带标记位的原子引用 AtomicMarkableReference<String> atomicMarkableRef = new AtomicMarkableReference<>(initialRef, initialMark); System.out.println("初始引用: " + atomicMarkableRef.getReference()); System.out.println("初始标记: " + atomicMarkableRef.isMarked()); // get(boolean[]): 同时获取引用和标记位 boolean[] markHolder = new boolean[1]; String currentRef = atomicMarkableRef.get(markHolder); System.out.println("当前引用: " + currentRef + ", 当前标记: " + markHolder[0]); // compareAndSet(): 尝试同时更新引用和标记位 String newRef = "新数据"; boolean newMark = true; boolean success = atomicMarkableRef.compareAndSet( initialRef, newRef, initialMark, newMark); System.out.println("CAS操作结果: " + success); System.out.println("新引用: " + atomicMarkableRef.getReference()); System.out.println("新标记: " + atomicMarkableRef.isMarked()); // attemptMark(): 只尝试更新标记位 boolean markUpdated = atomicMarkableRef.attemptMark(newRef, false); System.out.println("标记更新结果: " + markUpdated); System.out.println("最终标记: " + atomicMarkableRef.isMarked()); // set(): 直接设置引用和标记位 atomicMarkableRef.set("最终数据", true); System.out.println("直接设置后的引用: " + atomicMarkableRef.getReference()); System.out.println("直接设置后的标记: " + atomicMarkableRef.isMarked()); }}4.3 AtomicStampedReference - 带版本号的原子引用使用场景:解决ABA问题、乐观锁实现核心API详解方法参数返回值说明getReference()-V获取当前引用getStamp()-int获取当前版本号get(int[] stampHolder)stampHolder: 版本号容器V获取引用和版本号set(V newReference, int newStamp)newReference: 新引用newStamp: 新版本号void设置引用和版本号compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)expectedReference: 期望引用newReference: 新引用expectedStamp: 期望版本号newStamp: 新版本号boolean同时比较引用和版本号attemptStamp(V expectedReference, int newStamp)expectedReference: 期望引用newStamp: 新版本号boolean尝试只更新版本号import java.util.concurrent.atomic.AtomicStampedReference;/** * AtomicStampedReference通过版本号解决ABA问题 * 每次修改都会增加版本号,确保不会误判 */public class AtomicStampedReferenceDemo { public static void main(String[] args) { String initialRef = "数据A"; int initialStamp = 0; // 创建带版本号的原子引用 AtomicStampedReference<String> atomicStampedRef = new AtomicStampedReference<>(initialRef, initialStamp); System.out.println("初始引用: " + atomicStampedRef.getReference()); System.out.println("初始版本号: " + atomicStampedRef.getStamp()); // get(int[]): 同时获取引用和版本号 int[] stampHolder = new int[1]; String currentRef = atomicStampedRef.get(stampHolder); System.out.println("当前引用: " + currentRef + ", 当前版本号: " + stampHolder[0]); // 模拟ABA问题场景 String newRefB = "数据B"; String newRefA = "数据A"; // 又改回A,但版本号不同 // 第一次更新:A -> B,版本号 0 -> 1 boolean firstUpdate = atomicStampedRef.compareAndSet( initialRef, newRefB, initialStamp, initialStamp + 1); System.out.println("第一次更新(A->B)结果: " + firstUpdate); System.out.println("当前引用: " + atomicStampedRef.getReference()); System.out.println("当前版本号: " + atomicStampedRef.getStamp()); // 第二次更新:B -> A,版本号 1 -> 2 boolean secondUpdate = atomicStampedRef.compareAndSet( newRefB, newRefA, 1, 2); System.out.println("第二次更新(B->A)结果: " + secondUpdate); System.out.println("当前引用: " + atomicStampedRef.getReference()); System.out.println("当前版本号: " + atomicStampedRef.getStamp()); // 尝试用旧版本号更新(会失败)- 这就是解决ABA问题的关键! boolean failedUpdate = atomicStampedRef.compareAndSet( newRefA, "新数据", 0, 1); // 使用旧的版本号0 System.out.println("使用旧版本号更新结果: " + failedUpdate); System.out.println("引用未被修改: " + atomicStampedRef.getReference()); // attemptStamp(): 只更新版本号 boolean stampUpdated = atomicStampedRef.attemptStamp(newRefA, 3); System.out.println("版本号更新结果: " + stampUpdated); System.out.println("新版本号: " + atomicStampedRef.getStamp()); // 正确的方式:使用当前版本号 stampHolder = new int[1]; currentRef = atomicStampedRef.get(stampHolder); boolean correctUpdate = atomicStampedRef.compareAndSet( currentRef, "最终数据", stampHolder[0], stampHolder[0] + 1); System.out.println("使用正确版本号更新结果: " + correctUpdate); System.out.println("最终引用: " + atomicStampedRef.getReference()); System.out.println("最终版本号: " + atomicStampedRef.getStamp()); }}ABA问题详解:ABA问题是指:线程1读取值A线程2将值改为B,然后又改回A线程1进行CAS操作,发现当前值仍是A,于是操作成功虽然值看起来没变,但中间状态的变化可能对业务逻辑产生影响。AtomicStampedReference通过版本号完美解决了这个问题。五、原子更新字段类5.1 AtomicIntegerFieldUpdater - 原子更新整型字段使用场景:优化内存使用、大量对象需要原子字段更新核心API详解方法参数返回值说明newUpdater(Class<U> tclass, String fieldName)tclass: 目标类fieldName: 字段名AtomicIntegerFieldUpdater<U>静态方法创建更新器get(U obj)obj: 目标对象int获取字段值set(U obj, int newValue)obj: 目标对象newValue: 新值void设置字段值getAndSet(U obj, int newValue)obj: 目标对象newValue: 新值int原子设置并返回旧值compareAndSet(U obj, int expect, int update)obj: 目标对象expect: 期望值update: 更新值booleanCAS操作getAndIncrement(U obj)obj: 目标对象int原子递增,返回旧值getAndDecrement(U obj)obj: 目标对象int原子递减,返回旧值getAndAdd(U obj, int delta)obj: 目标对象delta: 增量int原子加法,返回旧值incrementAndGet(U obj)obj: 目标对象int原子递增,返回新值addAndGet(U obj, int delta)obj: 目标对象delta: 增量int原子加法,返回新值import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;/** * AtomicIntegerFieldUpdater以原子方式更新对象的volatile int字段 * 相比为每个对象创建AtomicInteger,可以节省大量内存 */public class AtomicIntegerFieldUpdaterDemo { static class Counter { // 必须用volatile修饰,保证可见性 public volatile int count; private String name; public Counter(String name, int initialCount) { this.name = name; this.count = initialCount; } public String getName() { return name; } public int getCount() { return count; } } public static void main(String[] args) { // 创建字段更新器,指定要更新的类和字段名 AtomicIntegerFieldUpdater<Counter> updater = AtomicIntegerFieldUpdater.newUpdater(Counter.class, "count"); Counter counter1 = new Counter("计数器1", 0); Counter counter2 = new Counter("计数器2", 10); System.out.println("计数器1初始计数: " + counter1.getCount()); System.out.println("计数器2初始计数: " + counter2.getCount()); // get(): 获取字段值 System.out.println("通过updater获取计数器1的值: " + updater.get(counter1)); // set(): 设置字段值 updater.set(counter1, 5); System.out.println("设置计数器1为5后的值: " + counter1.getCount()); // getAndIncrement(): 原子递增 - 相比synchronized性能更好 int oldCount = updater.getAndIncrement(counter1); System.out.println("计数器1递增前值: " + oldCount + ", 当前值: " + counter1.getCount()); // getAndAdd(): 原子加法 oldCount = updater.getAndAdd(counter1, 10); System.out.println("计数器1加10前值: " + oldCount + ", 当前值: " + counter1.getCount()); // incrementAndGet(): 原子递增并返回新值 int newCount = updater.incrementAndGet(counter1); System.out.println("计数器1递增后的值: " + newCount); // addAndGet(): 原子加法并返回新值 newCount = updater.addAndGet(counter1, 20); System.out.println("计数器1加20后的值: " + newCount); // compareAndSet(): 比较并设置 boolean updated = updater.compareAndSet(counter1, 36, 50); System.out.println("计数器1 CAS操作结果: " + updated + ", 当前值: " + counter1.getCount()); // 可以同时更新多个对象的相同字段 updater.incrementAndGet(counter2); System.out.println("计数器2递增后的值: " + counter2.getCount()); }}内存优化效果:AtomicInteger对象:16-24字节 overheadvolatile int + AtomicIntegerFieldUpdater:4字节 + 静态updater当有大量对象时,内存节省效果显著5.2 AtomicLongFieldUpdater - 原子更新长整型字段使用场景:大数值字段的原子更新、内存敏感场景核心API详解方法参数返回值说明newUpdater(Class<U> tclass, String fieldName)tclass: 目标类fieldName: 字段名AtomicLongFieldUpdater<U>静态方法创建更新器get(U obj)obj: 目标对象long获取字段值set(U obj, long newValue)obj: 目标对象newValue: 新值void设置字段值getAndSet(U obj, long newValue)obj: 目标对象newValue: 新值long原子设置并返回旧值compareAndSet(U obj, long expect, long update)obj: 目标对象expect: 期望值update: 更新值booleanCAS操作getAndAdd(U obj, long delta)obj: 目标对象delta: 增量long原子加法,返回旧值addAndGet(U obj, long delta)obj: 目标对象delta: 增量long原子加法,返回新值import java.util.concurrent.atomic.AtomicLongFieldUpdater;/** * AtomicLongFieldUpdater用于原子更新long字段 * 适用于需要大数值范围且内存敏感的场景 */public class AtomicLongFieldUpdaterDemo { static class Account { // 必须用volatile修饰 public volatile long balance; private final String owner; public Account(String owner, long initialBalance) { this.owner = owner; this.balance = initialBalance; } public String getOwner() { return owner; } public long getBalance() { return balance; } } public static void main(String[] args) { AtomicLongFieldUpdater<Account> balanceUpdater = AtomicLongFieldUpdater.newUpdater(Account.class, "balance"); Account account1 = new Account("张三", 1000L); Account account2 = new Account("李四", 2000L); System.out.println("张三账户初始余额: " + account1.getBalance()); System.out.println("李四账户初始余额: " + account2.getBalance()); // 基础操作 System.out.println("通过updater获取张三余额: " + balanceUpdater.get(account1)); balanceUpdater.set(account1, 1500L); System.out.println("设置张三余额为1500后的值: " + account1.getBalance()); // 原子存款 - 无锁线程安全 balanceUpdater.addAndGet(account1, 500L); System.out.println("张三存款500后余额: " + account1.getBalance()); // 原子取款 long oldBalance = balanceUpdater.getAndAdd(account1, -200L); System.out.println("张三取款200前余额: " + oldBalance + ", 取款后余额: " + account1.getBalance()); // 比较并设置 - 实现转账等业务 boolean transferSuccess = balanceUpdater.compareAndSet(account1, 1800L, 2000L); System.out.println("张三转账操作结果: " + transferSuccess + ", 当前余额: " + account1.getBalance()); // 同时操作多个账户 balanceUpdater.getAndAdd(account2, 1000L); System.out.println("李四存款1000后余额: " + account2.getBalance()); }}5.3 AtomicReferenceFieldUpdater - 原子更新引用字段使用场景:链表节点更新、树结构调整、对象关系维护核心API详解方法参数返回值说明newUpdater(Class<U> tclass, Class<W> vclass, String fieldName)tclass: 目标类vclass: 字段类型fieldName: 字段名AtomicReferenceFieldUpdater<U,W>静态方法创建更新器get(U obj)obj: 目标对象V获取字段引用set(U obj, V newValue)obj: 目标对象newValue: 新引用void设置字段引用getAndSet(U obj, V newValue)obj: 目标对象newValue: 新引用V原子设置并返回旧引用compareAndSet(U obj, V expect, V update)obj: 目标对象expect: 期望引用update: 更新引用booleanCAS操作lazySet(U obj, V newValue)obj: 目标对象newValue: 新引用void延迟设置引用import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;/** * AtomicReferenceFieldUpdater用于原子更新引用字段 * 常用于实现无锁数据结构 */public class AtomicReferenceFieldUpdaterDemo { static class Node<T> { // 必须用volatile修饰 public volatile Node<T> next; private final T value; public Node(T value) { this.value = value; } public T getValue() { return value; } public Node<T> getNext() { return next; } @Override public String toString() { return "Node{value=" + value + ", next=" + (next != null ? next.value : "null") + "}"; } } public static void main(String[] args) { // 创建引用字段更新器 AtomicReferenceFieldUpdater<Node, Node> nextUpdater = AtomicReferenceFieldUpdater.newUpdater(Node.class, Node.class, "next"); Node<String> first = new Node<>("第一个节点"); Node<String> second = new Node<>("第二个节点"); Node<String> third = new Node<>("第三个节点"); System.out.println("初始第一个节点的next: " + first.getNext()); // get(): 获取字段引用 System.out.println("通过updater获取第一个节点的next: " + nextUpdater.get(first)); // set(): 设置字段引用 nextUpdater.set(first, second); System.out.println("设置第一个节点的next为第二个节点: " + first); // compareAndSet(): 原子设置next字段 - 实现无锁链表 boolean setSuccess = nextUpdater.compareAndSet(first, second, third); System.out.println("CAS操作结果: " + setSuccess); System.out.println("第一个节点: " + first); // getAndSet(): 获取并设置引用 Node<String> oldNext = nextUpdater.getAndSet(second, third); System.out.println("第二个节点原来的next: " + oldNext); System.out.println("第二个节点: " + second); // lazySet(): 延迟设置 nextUpdater.lazySet(third, first); // 形成环状,仅作演示 System.out.println("第三个节点延迟设置后的next: " + third.getNext()); // 构建链表并展示 System.out.println("最终链表结构:"); Node<String> current = first; int count = 0; while (current != null && count < 5) { // 防止无限循环 System.out.println(current); current = current.getNext(); count++; } }}六、综合实战:构建线程安全计数器下面我们通过一个综合示例展示如何在实际项目中使用原子操作类:import java.util.concurrent.atomic.*;import java.util.concurrent.*;/** * 线程安全计数器综合示例 * 展示了多种原子类的实际应用 */public class ThreadSafeCounter { // 基本计数器 - 使用AtomicInteger private final AtomicInteger count = new AtomicInteger(0); // 大数值统计 - 使用AtomicLong private final AtomicLong total = new AtomicLong(0L); // 状态控制 - 使用AtomicReference private final AtomicReference<String> status = new AtomicReference<>("RUNNING"); // 统计数组 - 使用AtomicIntegerArray进行分桶统计 private final AtomicIntegerArray bucketStats = new AtomicIntegerArray(10); // 配置信息 - 使用AtomicReference支持动态更新 private final AtomicReference<Config> config = new AtomicReference<>(new Config(100, 60)); // 标记位控制 - 使用AtomicBoolean private final AtomicBoolean enabled = new AtomicBoolean(true); static class Config { final int maxConnections; final int timeoutSeconds; public Config(int maxConnections, int timeoutSeconds) { this.maxConnections = maxConnections; this.timeoutSeconds = timeoutSeconds; } @Override public String toString() { return "Config{maxConnections=" + maxConnections + ", timeoutSeconds=" + timeoutSeconds + "}"; } } // 核心API方法 public void increment() { if (!enabled.get()) { System.out.println("计数器已禁用,忽略操作"); return; } count.incrementAndGet(); total.addAndGet(1L); // 分桶统计:根据count值决定放入哪个桶 int bucket = count.get() % 10; bucketStats.getAndIncrement(bucket); } public void add(int value) { if (!enabled.get()) { System.out.println("计数器已禁用,忽略操作"); return; } count.addAndGet(value); total.addAndGet(value); } public boolean setStatus(String expected, String newStatus) { return status.compareAndSet(expected, newStatus); } public void updateConfig(Config newConfig) { Config oldConfig; do { oldConfig = config.get(); System.out.println("尝试更新配置: " + oldConfig + " -> " + newConfig); } while (!config.compareAndSet(oldConfig, newConfig)); System.out.println("配置更新成功"); } public boolean enable() { return enabled.compareAndSet(false, true); } public boolean disable() { return enabled.compareAndSet(true, false); } // 获取统计信息 public void printStats() { System.out.println("\n=== 统计信息 ==="); System.out.println("当前计数: " + count.get()); System.out.println("总数: " + total.get()); System.out.println("状态: " + status.get()); System.out.println("启用状态: " + enabled.get()); System.out.println("桶统计: " + bucketStats.toString()); Config currentConfig = config.get(); System.out.println("配置: " + currentConfig); // 验证数据一致性 long sum = 0; for (int i = 0; i < bucketStats.length(); i++) { sum += bucketStats.get(i); } System.out.println("桶统计总和: " + sum + ", 计数: " + count.get() + ", 一致性: " + (sum == count.get())); } public static void main(String[] args) throws InterruptedException { ThreadSafeCounter counter = new ThreadSafeCounter(); // 创建多个线程同时操作计数器 int threadCount = 10; int operationsPerThread = 1000; ExecutorService executor = Executors.newFixedThreadPool(threadCount); CountDownLatch latch = new CountDownLatch(threadCount); System.out.println("开始并发测试..."); for (int i = 0; i < threadCount; i++) { final int threadId = i; executor.execute(() -> { try { for (int j = 0; j < operationsPerThread; j++) { counter.increment(); // 每隔一定操作数更新配置 if (j % 200 == 0) { counter.updateConfig(new Config(100 + j, 60)); } // 模拟随机禁用/启用 if (j == 500 && threadId == 0) { System.out.println("线程" + threadId + "尝试禁用计数器"); counter.disable(); Thread.sleep(10); // 短暂休眠 System.out.println("线程" + threadId + "尝试启用计数器"); counter.enable(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { latch.countDown(); } }); } // 等待所有线程完成 latch.await(); executor.shutdown(); // 打印最终统计 counter.printStats(); int expectedCount = threadCount * operationsPerThread; System.out.println("\n=== 测试结果 ==="); System.out.println("期望计数: " + expectedCount); System.out.println("实际计数: " + counter.count.get()); System.out.println("计数正确: " + (counter.count.get() == expectedCount)); System.out.println("测试" + (counter.count.get() == expectedCount ? "通过" : "失败")); }}七、原子操作类的工作原理7.1 CAS机制详解CAS(Compare-And-Swap)是原子操作类的核心,包含三个操作数:内存位置(V)期望原值(A)新值(B)CAS的语义是:"我认为V的值应该是A,如果是,那么将V的值更新为B,否则不修改并告诉我现在的值是多少"CAS操作是硬件级别的原子操作,在现代CPU中通常通过以下方式实现:x86架构:CMPXCHG指令ARM架构:LDREX/STREX指令对7.2 Unsafe类的作用所有原子操作类底层都依赖sun.misc.Unsafe类,它提供了硬件级别的原子操作:public final class Unsafe { // 对象字段操作 public native long objectFieldOffset(Field f); // 数组基础偏移 public native int arrayBaseOffset(Class arrayClass); // 数组索引缩放 public native int arrayIndexScale(Class arrayClass); // CAS操作 public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x); public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x); public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x); // 获取和设置值 public native int getIntVolatile(Object o, long offset); public native void putIntVolatile(Object o, long offset, int x); // 延迟设置(有更弱的可见性保证) public native void putOrderedInt(Object o, long offset, int x);}7.3 内存屏障与可见性原子操作类通过内存屏障保证可见性:写操作:在写入后插入写屏障,保证写入对其他线程可见读操作:在读取前插入读屏障,保证读取到最新值Java内存模型中的屏障类型:LoadLoad屏障:保证该屏障前的读操作先于屏障后的读操作完成StoreStore屏障:保证该屏障前的写操作先于屏障后的写操作完成LoadStore屏障:保证该屏障前的读操作先于屏障后的写操作完成StoreLoad屏障:保证该屏障前的所有写操作对其他处理器可见八、性能对比与选型建议8.1 性能对比场景synchronized原子操作类性能提升低竞争慢快2-10倍中等竞争中等中等相当高竞争快慢(自旋)可能更差8.2 不同原子类的性能特点原子类适用场景性能特点AtomicInteger普通计数器性能优秀,适用大部分场景AtomicLong大数值计数在32位系统上性能较差LongAdder高并发统计高竞争环境下性能最优AtomicReference对象引用更新性能与对象大小相关字段更新器内存敏感场景节省内存,性能稍差8.3 选型指南计数器场景简单计数:AtomicInteger大数值计数:AtomicLong 或 LongAdder分桶统计:AtomicIntegerArray状态控制布尔标志:AtomicBoolean对象状态:AtomicReference带版本状态:AtomicStampedReference内存敏感场景大量对象:字段更新器(AtomicXXXFieldUpdater)缓存系统:AtomicReference数据结构无锁队列:AtomicReference无锁栈:AtomicReference无锁链表:AtomicReferenceFieldUpdater8.4 最佳实践避免过度使用:不是所有场景都需要原子类注意ABA问题:必要时使用带版本号的原子类考虑高竞争:高竞争环境下考虑LongAdder等替代方案内存布局:字段更新器可以优化内存使用JDK8+特性:利用新的函数式更新方法性能测试:在实际环境中进行性能测试九、总结Java原子操作类为我们提供了强大的无锁并发编程工具:9.1 核心价值13个原子类覆盖了基本类型、数组、引用和字段更新CAS机制基于硬件指令,性能优异无锁设计避免了死锁和锁开销丰富的API支持各种并发场景9.2 使用场景总结类别主要类核心用途基本类型AtomicInteger, AtomicLong, AtomicBoolean计数器、状态标志数组AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray并发数组、分桶统计引用AtomicReference, AtomicStampedReference, AtomicMarkableReference对象缓存、状态管理字段更新AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater内存优化、大量对象9.3 学习建议从简单开始:先掌握AtomicInteger和AtomicReference理解原理:深入理解CAS机制和内存模型实践应用:在真实项目中尝试使用原子类性能调优:根据实际场景选择合适的原子类持续学习:关注JDK新版本中的并发工具改进掌握这些原子操作类,能够让我们在适当的场景下写出更高效、更安全的并发代码。记住,工具虽好,但要因地制宜,根据具体场景选择最合适的并发控制方案。希望本文能帮助你深入理解Java原子操作类,在实际项目中游刃有余地处理并发问题! 转载于:https://www.cnblogs.com/sun-10387834/p/19172186
  • 快速构建现代化 Java 应用
    Spring Boot 目前已成为构建企业级应用的事实标准。它以“约定优于配置”(Convention over Configuration)为核心理念,极大地简化了 Spring 应用的初始搭建和开发过程,让开发者能够专注于业务逻辑,而非繁琐的配置。 一、什么是 Spring Boot?Spring Boot 是由 Pivotal 团队提供的一个开源框架,它基于 Spring 框架 构建,旨在:简化 Spring 应用的创建和部署。提供开箱即用的默认配置,减少样板代码。内嵌服务器(如 Tomcat、Jetty),无需打包成 WAR 部署到外部容器。提供生产级特性,如健康检查、指标监控、外部化配置等。一句话总结:Spring Boot 让 Spring 应用的开发像“搭积木”一样简单,开箱即用,快速启动。二、为什么选择 Spring Boot?1. 自动配置(Auto-configuration)Spring Boot 能根据你添加的依赖(如 spring-boot-starter-web)自动配置 Spring 应用。例如:添加了 Web 依赖,它会自动配置:内嵌 Tomcat 服务器Spring MVC默认的视图解析器错误页面处理你无需手动编写大量 XML 或 Java 配置。2. 起步依赖(er Dependencies)Spring Boot 提供了一系列“starter”依赖,将常用的依赖组合在一起,避免版本冲突。Starter功能spring-boot-starter-webWeb + REST + Tomcatspring-boot-starter-data-jpaJPA + Hibernatespring-boot-starter-data-redisRedis 客户端spring-boot-starter-securitySpring Securityspring-boot-starter-test测试支持只需引入一个依赖,即可获得一整套功能。3. 内嵌服务器无需将应用打包成 WAR 文件部署到 Tomcat、Jetty 等外部服务器。Spring Boot 应用自带服务器,打包成 JAR 即可运行:java -jar myapp.jar4. 生产就绪(Production Ready)Spring Boot 提供了 Spring Boot Actuator 模块,开箱即用的监控和管理功能:/actuator/health:应用健康状态/actuator/metrics:性能指标/actuator/env:当前环境变量/actuator/info:自定义应用信息5. 外部化配置支持多种方式配置应用参数:application.propertiesapplication.yml环境变量命令行参数并支持多环境配置(如 application-dev.yml, application-prod.yml)。6. 强大的 CLI 和代码生成工具Spring Initializr:在线生成项目骨架。Spring Boot CLI:命令行工具,快速运行 Groovy 脚本。三、快速创建一个 Spring Boot 应用方法1:使用 Spring Initializr访问 官方网站选择:Project: Maven / GradleLanguage: JavaSpring Boot Version: 最新稳定版Group: com.exampleArtifact: demo添加依赖:Spring Web, Spring Boot DevTools, Lombok点击 “Generate” 下载项目压缩包。方法2:使用 IDE(如 IntelliJ IDEA)File → New → Project选择 “Spring Initializr”填写项目信息并选择依赖完成创建四、项目结构解析src/├── main/│ ├── java/│ │ └── com/example/demo/│ │ ├── DemoApplication.java # 主启动类│ │ └── controller/│ │ └── HelloController.java # 控制器示例│ └── resources/│ ├── application.yml # 配置文件│ ├── static/ # 静态资源│ └── templates/ # 模板文件(如 Thymeleaf)└── test/ # 测试代码五、编写第一个 REST API// HelloController.java@RestController // = @Controller + @ResponseBody@RequestMapping("/api")public class HelloController { @GetMapping("/hello") public String sayHello() { return "Hello, Spring Boot!"; } @GetMapping("/user") public Map<String, Object> getUser() { Map<String, Object> user = new HashMap<>(); user.put("id", 1); user.put("name", "Alice"); user.put("email", "alice@example.com"); return user; }}启动应用后访问:http://localhost:8080/api/hello → 输出 Hello, Spring Boot!http://localhost:8080/api/user → 返回 JSON 数据六、核心配置文件(application.yml)server: port: 8080 servlet: context-path: /appspring: datasource: url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true# 自定义属性app: name: My Awesome App version: 1.0.0可通过 @Value("${app.name}") 或 @ConfigurationProperties 注入。七、Spring Boot 的核心注解注解说明@SpringBootApplication主类注解,包含 @SpringBootConfiguration, @EnableAutoConfiguration, @ComponentScan@RestController创建 RESTful 控制器@RequestMapping映射 HTTP 请求@Autowired自动注入 Bean@Component, @Service, @Repository组件注册@ConfigurationProperties绑定配置文件属性八、开发与部署开发阶段使用 spring-boot-devtools 实现热部署(代码修改自动重启)。使用 @Profile 切换开发/测试/生产环境。打包与运行# Maven 打包mvn clean package# 运行 JARjava -jar target/demo-0.0.1-SNAPSHOT.jar# 指定配置文件java -jar app.jar --spring.profiles.active=prod Spring Boot 已成为 Java 开发的标准工具链,无论是构建简单的 Web 应用,还是复杂的微服务系统,它都能提供强大的支持。其“约定优于配置”的理念,让开发者从繁琐的配置中解放出来,真正实现“快速开发、快速交付”。
  • [技术干货] 健康体检项目之运营数据统计
    Apache POI 是用Java编写的免费开源的跨平台的Java API,Apache POI提供API给Java程序对Microsoft Office格式档案读和写的功能,其中使用最多的就是使用POI操作Excel文件。POI为“Poor Obfuscation Implementation”的首字母缩写,意为“简洁版的模糊实现”。一、运营数据统计1.1 需求分析通过运营数据统计可以展示出体检机构的运营情况,包括会员数据、预约到诊数据、热门套餐等信息。本章节就是要通过一个表格的形式来展示这些运营数据。效果如下图:  1.2 完善页面运营数据统计对应的页面为/pages/report_business.html。1.2.1 定义模型数据定义数据模型,通过VUE的数据绑定展示数据<script> var vue = new Vue({ el: '#app', data:{ reportData:{ reportDate:null, todayNewMember :0, totalMember :0, thisWeekNewMember :0, thisMonthNewMember :0, todayOrderNumber :0, todayVisitsNumber :0, thisWeekOrderNumber :0, thisWeekVisitsNumber :0, thisMonthOrderNumber :0, thisMonthVisitsNumber :0, hotSetmeal :[] } } }) </script><div class="box" style="height: 900px"> <div class="excelTitle" > <el-button @click="exportExcel">导出Excel</el-button>运营数据统计 </div> <div class="excelTime">日期:{{reportData.reportDate}}</div> <table class="exceTable" cellspacing="0" cellpadding="0"> <tr> <td colspan="4" class="headBody">会员数据统计</td> </tr> <tr> <td width='20%' class="tabletrBg">新增会员数</td> <td width='30%'>{{reportData.todayNewMember}}</td> <td width='20%' class="tabletrBg">总会员数</td> <td width='30%'>{{reportData.totalMember}}</td> </tr> <tr> <td class="tabletrBg">本周新增会员数</td> <td>{{reportData.thisWeekNewMember}}</td> <td class="tabletrBg">本月新增会员数</td> <td>{{reportData.thisMonthNewMember}}</td> </tr> <tr> <td colspan="4" class="headBody">预约到诊数据统计</td> </tr> <tr> <td class="tabletrBg">今日预约数</td> <td>{{reportData.todayOrderNumber}}</td> <td class="tabletrBg">今日到诊数</td> <td>{{reportData.todayVisitsNumber}}</td> </tr> <tr> <td class="tabletrBg">本周预约数</td> <td>{{reportData.thisWeekOrderNumber}}</td> <td class="tabletrBg">本周到诊数</td> <td>{{reportData.thisWeekVisitsNumber}}</td> </tr> <tr> <td class="tabletrBg">本月预约数</td> <td>{{reportData.thisMonthOrderNumber}}</td> <td class="tabletrBg">本月到诊数</td> <td>{{reportData.thisMonthVisitsNumber}}</td> </tr> <tr> <td colspan="4" class="headBody">热门套餐</td> </tr> <tr class="tabletrBg textCenter"> <td>套餐名称</td> <td>预约数量</td> <td>占比</td> <td>备注</td> </tr> <tr v-for="s in reportData.hotSetmeal"> <td>{{s.name}}</td> <td>{{s.setmeal_count}}</td> <td>{{s.proportion}}</td> <td></td> </tr> </table> </div>1.2.2 发送请求获取动态数据在VUE的钩子函数中发送ajax请求获取动态数据,通过VUE的数据绑定将数据展示到页面<script> var vue = new Vue({ el: '#app', data:{ reportData:{ reportDate:null, todayNewMember :0, totalMember :0, thisWeekNewMember :0, thisMonthNewMember :0, todayOrderNumber :0, todayVisitsNumber :0, thisWeekOrderNumber :0, thisWeekVisitsNumber :0, thisMonthOrderNumber :0, thisMonthVisitsNumber :0, hotSetmeal :[] } }, created() { //发送ajax请求获取动态数据 axios.get("/report/getBusinessReportData.do").then((res)=>{ this.reportData = res.data.data; }); } }) </script>根据页面对数据格式的要求,我们发送ajax请求,服务端需要返回如下格式的数据:{ "data":{ "todayVisitsNumber":0, "reportDate":"2019-04-25", "todayNewMember":0, "thisWeekVisitsNumber":0, "thisMonthNewMember":2, "thisWeekNewMember":0, "totalMember":10, "thisMonthOrderNumber":2, "thisMonthVisitsNumber":0, "todayOrderNumber":0, "thisWeekOrderNumber":0, "hotSetmeal":[ {"proportion":0.4545,"name":"粉红珍爱(女)升级TM12项筛查体检套餐","setmeal_count":5}, {"proportion":0.1818,"name":"阳光爸妈升级肿瘤12项筛查体检套餐","setmeal_count":2}, {"proportion":0.1818,"name":"珍爱高端升级肿瘤12项筛查","setmeal_count":2}, {"proportion":0.0909,"name":"孕前检查套餐","setmeal_count":1} ], }, "flag":true, "message":"获取运营统计数据成功" }1.3 后台代码1.3.1 Controller在ReportController中提供getBusinessReportData方法@Reference private ReportService reportService; ​ /** * 获取运营统计数据 * @return */ @RequestMapping("/getBusinessReportData") public Result getBusinessReportData(){ try { Map<String, Object> result = reportService.getBusinessReport(); return new Result(true,MessageConstant.GET_BUSINESS_REPORT_SUCCESS,result); } catch (Exception e) { e.printStackTrace(); return new Result(true,MessageConstant.GET_BUSINESS_REPORT_FAIL); } }1.3.2 服务接口在health_interface工程中创建ReportService服务接口并声明getBusinessReport方法package com.yunhe.service; import java.util.Map; public interface ReportService { /** * 获得运营统计数据 * Map数据格式: * todayNewMember -> number * totalMember -> number * thisWeekNewMember -> number * thisMonthNewMember -> number * todayOrderNumber -> number * todayVisitsNumber -> number * thisWeekOrderNumber -> number * thisWeekVisitsNumber -> number * thisMonthOrderNumber -> number * thisMonthVisitsNumber -> number * hotSetmeals -> List<Setmeal> */ public Map<String,Object> getBusinessReport() throws Exception; }1.3.3 服务实现类在health_service_provider工程中创建服务实现类ReportServiceImpl并实现ReportService接口package com.yunhe.service; import com.alibaba.dubbo.config.annotation.Service; import com.yunhe.dao.MemberDao; import com.yunhe.dao.OrderDao; import com.yunhe.utils.DateUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 统计报表服务 */ @Service(interfaceClass = ReportService.class) @Transactional public class ReportServiceImpl implements ReportService { @Autowired private MemberDao memberDao; @Autowired private OrderDao orderDao; /** * 获得运营统计数据 * Map数据格式: * todayNewMember -> number * totalMember -> number * thisWeekNewMember -> number * thisMonthNewMember -> number * todayOrderNumber -> number * todayVisitsNumber -> number * thisWeekOrderNumber -> number * thisWeekVisitsNumber -> number * thisMonthOrderNumber -> number * thisMonthVisitsNumber -> number * hotSetmeal -> List<Setmeal> */ public Map<String, Object> getBusinessReport() throws Exception{ //获得当前日期 String today = DateUtils.parseDate2String(DateUtils.getToday()); //获得本周一的日期 String thisWeekMonday = DateUtils.parseDate2String(DateUtils.getThisWeekMonday()); //获得本月第一天的日期 String firstDay4ThisMonth = DateUtils.parseDate2String(DateUtils.getFirstDay4ThisMonth()); //今日新增会员数 Integer todayNewMember = memberDao.findMemberCountByDate(today); //总会员数 Integer totalMember = memberDao.findMemberTotalCount(); //本周新增会员数 Integer thisWeekNewMember = memberDao.findMemberCountAfterDate(thisWeekMonday); //本月新增会员数 Integer thisMonthNewMember = memberDao.findMemberCountAfterDate(firstDay4ThisMonth); //今日预约数 Integer todayOrderNumber = orderDao.findOrderCountByDate(today); //本周预约数 Integer thisWeekOrderNumber = orderDao.findOrderCountAfterDate(thisWeekMonday); //本月预约数 Integer thisMonthOrderNumber = orderDao.findOrderCountAfterDate(firstDay4ThisMonth); //今日到诊数 Integer todayVisitsNumber = orderDao.findVisitsCountByDate(today); //本周到诊数 Integer thisWeekVisitsNumber = orderDao.findVisitsCountAfterDate(thisWeekMonday); //本月到诊数 Integer thisMonthVisitsNumber = orderDao.findVisitsCountAfterDate(firstDay4ThisMonth); //热门套餐(取前4) List<Map> hotSetmeal = orderDao.findHotSetmeal(); Map<String,Object> result = new HashMap<>(); result.put("reportDate",today); result.put("todayNewMember",todayNewMember); result.put("totalMember",totalMember); result.put("thisWeekNewMember",thisWeekNewMember); result.put("thisMonthNewMember",thisMonthNewMember); result.put("todayOrderNumber",todayOrderNumber); result.put("thisWeekOrderNumber",thisWeekOrderNumber); result.put("thisMonthOrderNumber",thisMonthOrderNumber); result.put("todayVisitsNumber",todayVisitsNumber); result.put("thisWeekVisitsNumber",thisWeekVisitsNumber); result.put("thisMonthVisitsNumber",thisMonthVisitsNumber); result.put("hotSetmeal",hotSetmeal); return result; } }1.3.4 Dao接口在OrderDao和MemberDao中声明相关统计查询方法package com.yunhe.dao; import com.yunhe.pojo.Order; import java.util.List; import java.util.Map; public interface OrderDao { public void add(Order order); public List<Order> findByCondition(Order order); public Map findById4Detail(Integer id); public Integer findOrderCountByDate(String date); public Integer findOrderCountAfterDate(String date); public Integer findVisitsCountByDate(String date); public Integer findVisitsCountAfterDate(String date); public List<Map> findHotSetmeal(); }package com.yunhe.dao; import com.github.pagehelper.Page; import com.yunhe.pojo.Member; import java.util.List; public interface MemberDao { public List<Member> findAll(); public Page<Member> selectByCondition(String queryString); public void add(Member member); public void deleteById(Integer id); public Member findById(Integer id); public Member findByTelephone(String telephone); public void edit(Member member); public Integer findMemberCountBeforeDate(String date); public Integer findMemberCountByDate(String date); public Integer findMemberCountAfterDate(String date); public Integer findMemberTotalCount(); }1.3.5 Mapper映射文件在OrderDao.xml和MemberDao.xml中定义SQL语句OrderDao.xml:<!--根据日期统计预约数--> <select id="findOrderCountByDate" parameterType="string" resultType="int"> select count(id) from t_order where orderDate = #{value} </select> ​ <!--根据日期统计预约数,统计指定日期之后的预约数--> <select id="findOrderCountAfterDate" parameterType="string" resultType="int"> select count(id) from t_order where orderDate &gt;= #{value} </select> ​ <!--根据日期统计到诊数--> <select id="findVisitsCountByDate" parameterType="string" resultType="int"> select count(id) from t_order where orderDate = #{value} and orderStatus = '已到诊' </select> ​ <!--根据日期统计到诊数,统计指定日期之后的到诊数--> <select id="findVisitsCountAfterDate" parameterType="string" resultType="int"> select count(id) from t_order where orderDate &gt;= #{value} and orderStatus = '已到诊' </select> ​ <!--热门套餐,查询前4条--> <select id="findHotSetmeal" resultType="map"> select s.name, count(o.id) setmeal_count , count(o.id)/(select count(id) from t_order) proportion from t_order o inner join t_setmeal s on s.id = o.setmeal_id group by o.setmeal_id order by setmeal_count desc limit 0,4 </select>MemberDao.xml:<!--根据日期统计会员数,统计指定日期之前的会员数--> <select id="findMemberCountBeforeDate" parameterType="string" resultType="int"> select count(id) from t_member where regTime &lt;= #{value} </select> ​ <!--根据日期统计会员数--> <select id="findMemberCountByDate" parameterType="string" resultType="int"> select count(id) from t_member where regTime = #{value} </select> ​ <!--根据日期统计会员数,统计指定日期之后的会员数--> <select id="findMemberCountAfterDate" parameterType="string" resultType="int"> select count(id) from t_member where regTime &gt;= #{value} </select> ​ <!--总会员数--> <select id="findMemberTotalCount" resultType="int"> select count(id) from t_member </select>二、. 运营数据统计报表导出2.1 需求分析运营数据统计报表导出就是将统计数据写入到Excel并提供给客户端浏览器进行下载,以便体检机构管理人员对运营数据的查看和存档。2.2 提供模板文件本节我们需要将运营统计数据通过POI写入到Excel文件,对应的Excel效果如下:  通过上面的Excel效果可以看到,表格比较复杂,涉及到合并单元格、字体、字号、字体加粗、对齐方式等的设置。如果我们通过POI编程的方式来设置这些效果代码会非常繁琐。在企业实际开发中,对于这种比较复杂的表格导出一般我们会提前设计一个Excel模板文件,在这个模板文件中提前将表格的结构和样式设置好,我们的程序只需要读取这个文件并在文件中的相应位置写入具体的值就可以了。在本章节资料中已经提供了一个名为report_template.xlsx的模板文件,需要将这个文件复制到health_backend工程的template目录中2.3 完善页面在report_business.html页面提供导出按钮并绑定事件<div class="excelTitle" > <el-button @click="exportExcel">导出Excel</el-button>运营数据统计 </div>methods:{ //导出Excel报表 exportExcel(){ window.location.href = '/report/exportBusinessReport.do'; } }2.4 后台代码在ReportController中提供exportBusinessReport方法,基于POI将数据写入到Excel中并通过输出流下载到客户端。/** * 导出Excel报表 * @return */ @RequestMapping("/exportBusinessReport") public Result exportBusinessReport(HttpServletRequest request, HttpServletResponse response){ try{ //远程调用报表服务获取报表数据 Map<String, Object> result = reportService.getBusinessReport(); //取出返回结果数据,准备将报表数据写入到Excel文件中 String reportDate = (String) result.get("reportDate"); Integer todayNewMember = (Integer) result.get("todayNewMember"); Integer totalMember = (Integer) result.get("totalMember"); Integer thisWeekNewMember = (Integer) result.get("thisWeekNewMember"); Integer thisMonthNewMember = (Integer) result.get("thisMonthNewMember"); Integer todayOrderNumber = (Integer) result.get("todayOrderNumber"); Integer thisWeekOrderNumber = (Integer) result.get("thisWeekOrderNumber"); Integer thisMonthOrderNumber = (Integer) result.get("thisMonthOrderNumber"); Integer todayVisitsNumber = (Integer) result.get("todayVisitsNumber"); Integer thisWeekVisitsNumber = (Integer) result.get("thisWeekVisitsNumber"); Integer thisMonthVisitsNumber = (Integer) result.get("thisMonthVisitsNumber"); List<Map> hotSetmeal = (List<Map>) result.get("hotSetmeal"); //获得Excel模板文件绝对路径 String temlateRealPath = request.getSession().getServletContext().getRealPath("template") + File.separator + "report_template.xlsx"; //读取模板文件创建Excel表格对象 XSSFWorkbook workbook = new XSSFWorkbook(new FileInputStream(new File(temlateRealPath))); XSSFSheet sheet = workbook.getSheetAt(0); XSSFRow row = sheet.getRow(2); row.getCell(5).setCellValue(reportDate);//日期 ​ row = sheet.getRow(4); row.getCell(5).setCellValue(todayNewMember);//新增会员数(本日) row.getCell(7).setCellValue(totalMember);//总会员数 ​ row = sheet.getRow(5); row.getCell(5).setCellValue(thisWeekNewMember);//本周新增会员数 row.getCell(7).setCellValue(thisMonthNewMember);//本月新增会员数 ​ row = sheet.getRow(7); row.getCell(5).setCellValue(todayOrderNumber);//今日预约数 row.getCell(7).setCellValue(todayVisitsNumber);//今日到诊数 ​ row = sheet.getRow(8); row.getCell(5).setCellValue(thisWeekOrderNumber);//本周预约数 row.getCell(7).setCellValue(thisWeekVisitsNumber);//本周到诊数 ​ row = sheet.getRow(9); row.getCell(5).setCellValue(thisMonthOrderNumber);//本月预约数 row.getCell(7).setCellValue(thisMonthVisitsNumber);//本月到诊数 ​ int rowNum = 12; for(Map map : hotSetmeal){//热门套餐 String name = (String) map.get("name"); Long setmeal_count = (Long) map.get("setmeal_count"); BigDecimal proportion = (BigDecimal) map.get("proportion"); row = sheet.getRow(rowNum ++); row.getCell(4).setCellValue(name);//套餐名称 row.getCell(5).setCellValue(setmeal_count);//预约数量 row.getCell(6).setCellValue(proportion.doubleValue());//占比 } ​ //通过输出流进行文件下载 ServletOutputStream out = response.getOutputStream(); response.setContentType("application/vnd.ms-excel"); response.setHeader("content-Disposition", "attachment;filename=report.xlsx"); workbook.write(out); out.flush(); out.close(); workbook.close(); return null; }catch (Exception e){ return new Result(false, MessageConstant.GET_BUSINESS_REPORT_FAIL,null); } } 
  • [技术干货] Java 8 Stream 实战采用优雅的方式处理集合数据
    在 Java 开发中,我们经常需要对集合数据进行过滤、映射、统计等操作。传统的 for 循环和 Iterator 虽然可行,但代码冗长且不易维护。自 Java 8 引入 Stream API 后,集合操作变得更加简洁、函数式和可读性强。一、基于一个实际的业务背景假设我们正在开发一个电商后台系统,需要对商品(Product)进行如下分析:查询价格大于 100 元的商品按分类统计商品数量获取销量最高的前 3 个商品将商品名称转为大写并去重计算所有商品的平均价格我们将使用 List<Product> 模拟数据源,并通过 Stream 实现上述需求。二、实体类定义public class Product { private Long id; private String name; private String category; // 分类:如 "Electronics", "Clothing" private Double price; private Integer sales; // 销量 // 构造方法、getter、setter 省略 public Product(Long id, String name, String category, Double price, Integer sales) { this.id = id; this.name = name; this.category = category; this.price = price; this.sales = sales; } @Override public String toString() { return "Product{id=" + id + ", name='" + name + "', category='" + category + "', price=" + price + ", sales=" + sales + '}'; }}三、数据准备import java.util.Arrays;import java.util.List;List<Product> products = Arrays.asList( new Product(1L, "iPhone 15", "Electronics", 999.99, 120), new Product(2L, "MacBook Pro", "Electronics", 1999.99, 85), new Product(3L, "T-Shirt", "Clothing", 29.99, 200), new Product(4L, "Jeans", "Clothing", 79.99, 150), new Product(5L, "Watch", "Electronics", 299.99, 90), new Product(6L, "Dress", "Clothing", 149.99, 60), new Product(7L, "AirPods", "Electronics", 179.99, 300));四、Stream 实战案例查询价格大于 100 元的商品List<Product> expensiveProducts = products.stream() .filter(p -> p.getPrice() > 100) .collect(Collectors.toList());expensiveProducts.forEach(System.out::println);输出:Product{id=1, name='iPhone 15', category='Electronics', price=999.99, sales=120}Product{id=2, name='MacBook Pro', category='Electronics', price=1999.99, sales=85}Product{id=5, name='Watch', category='Electronics', price=299.99, sales=90}Product{id=6, name='Dress', category='Clothing', price=149.99, sales=60}Product{id=7, name='AirPods', category='Electronics', price=179.99, sales=300}说明:filter() 用于条件筛选,collect(Collectors.toList()) 将结果收集为 List。按分类统计商品数量Map<String, Long> countByCategory = products.stream() .collect(Collectors.groupingBy( Product::getCategory, Collectors.counting() ));System.out.println(countByCategory);// 输出:{Clothing=3, Electronics=4}说明:groupingBy 实现分组,counting() 统计每组数量。获取销量最高的前 3 个商品List<Product> top3Sales = products.stream() .sorted(Comparator.comparing(Product::getSales).reversed()) .limit(3) .collect(Collectors.toList());top3Sales.forEach(p -> System.out.println(p.getName() + " - 销量: " + p.getSales()));输出:AirPods - 销量: 300T-Shirt - 销量: 200Jeans - 销量: 150说明:sorted() 排序,reversed() 降序,limit(3) 取前3条。商品名称转大写并去重Set<String> upperCaseNames = products.stream() .map(p -> p.getName().toUpperCase()) .distinct() .collect(Collectors.toSet());System.out.println(upperCaseNames);// 输出:[MACBOOK PRO, DRESS, AIRPODS, T-SHIRT, WATCH, IPHONE 15, JEANS]说明:map() 转换数据,distinct() 去重。计算所有商品的平均价格Double averagePrice = products.stream() .mapToDouble(Product::getPrice) .average() .orElse(0.0);System.out.printf("平均价格: %.2f%n", averagePrice);// 输出:平均价格: 661.42说明:mapToDouble() 转换为原始类型流,避免装箱开销;average() 返回 OptionalDouble。 通过以上案例可以看出,Java 8 的 Stream API 极大地提升了集合操作的表达力和简洁性。它将“做什么”(what to do)与“怎么做”(how to do)分离,让我们在实际的开发过程中,可以很便捷的处理数据流。 附:常用 Stream 方法速查表类型方法说明中间操作filter, map, flatMap, distinct, sorted, peek, limit, skip返回 Stream,可链式调用终端操作collect, forEach, count, anyMatch, allMatch, noneMatch, findFirst, reduce触发执行,返回结果或 void 
  • [技术干货] JAVA结合JasperReports输出报表
    JasperReport是一个强大、灵活的报表生成工具,能够展示丰富的页面内容,并将之转换成PDF,HTML,或者XML格式。该库完全由Java写成,可以用于在各种Java应用程序,包括J2EE,Web应用程序中生成动态内容。下载JasperReports的JAR包和iReport设计器,并将其添加到项目中。接着,创建JasperReport报表模板和数据源,编写JRXML文件。在文件中定义数据源、Query语句、数据列和其他相关信息。然后,使用iReport设计器来修改和设置报表模板。在iReport中,可以添加图像、文本、表格和其他控件,并对其进行格式化和布局。同时,可以设置条件格式、样式和表格特性等。接下来,编写Java代码来调用并生成报表。要使用JasperReports内置的工具,只需几行代码即可完成报表的生成、导出和打印。例如,可以使用JasperPrint进行数据填充,并使用JasperExportManager导出PDF、Excel、HTML或其他格式。进行测试和调试。调试时,应该特别注意数据源、参数和生成结果是否符合预期。如果出现错误,可以查看错误日志并逐一排除错误。结合JasperReports输出报表前面我们已经使用Jaspersoft Studio设计了两个模板文件:demo1.jrxml和demo2.jrxml。其中demo1.jrxml的动态列表数据是基于JDBC数据源方式进行数据填充,demo2.jrxml的动态列表数据是基于JavaBean数据源方式进行数据填充。本小节我们就结合JasperReports的Java API来完成pdf报表输出。一、JDBC数据源方式填充数据第一步:创建maven工程,导入相关maven坐标<dependency> <groupId>net.sf.jasperreports</groupId> <artifactId>jasperreports</artifactId> <version>6.8.0</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency>第二步:将设计好的demo1.jrxml文件复制到当前工程的resources目录下  第三步:编写单元测试@Test public void testReport_JDBC() throws Exception{ Class.forName("com.mysql.jdbc.Driver"); Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/health", "root", "root"); ​ String jrxmlPath = "D:\\ideaProjects\\projects111\\jasperreports_test\\src\\main\\resources\\demo1.jrxml"; String jasperPath = "D:\\ideaProjects\\projects111\\jasperreports_test\\src\\main\\resources\\demo1.jasper"; ​ //编译模板 JasperCompileManager.compileReportToFile(jrxmlPath,jasperPath); ​ //构造数据 Map paramters = new HashMap(); paramters.put("company","xx公司"); ​ //填充数据---使用JDBC数据源方式填充 JasperPrint jasperPrint = JasperFillManager.fillReport(jasperPath, paramters, connection); //输出文件 String pdfPath = "D:\\test.pdf"; JasperExportManager.exportReportToPdfFile(jasperPrint,pdfPath); }通过上面的操作步骤可以输出pdf文件,但是中文的地方无法正常显示。这是因为JasperReports默认情况下对中文支持并不友好,需要我们自己进行修复。具体操作步骤如下:1、在Jaspersoft Studio中打开demo1.jrxml文件,选中中文相关元素,统一将字体设置为“华文宋体”并将修改后的demo1.jrxml重新复制到maven工程中2、将本章资源/解决中文无法显示问题目录下的文件复制到maven工程的resources目录中  按照上面步骤操作后重新执行单元测试导出PDF文件:  二、 JavaBean数据源方式填充数据第一步:为了能够避免中文无法显示问题,首先需要将demo2.jrxml文件相关元素字体改为“华文宋体”并将demo2.jrxml文件复制到maven工程的resources目录下  第二步:编写单元测试方法输出PDF文件@Test public void testReport_JavaBean() throws Exception{ String jrxmlPath = "D:\\ideaProjects\\projects111\\jasperreports_test\\src\\main\\resources\\demo2.jrxml"; String jasperPath = "D:\\ideaProjects\\projects111\\jasperreports_test\\src\\main\\resources\\demo2.jasper"; ​ //编译模板 JasperCompileManager.compileReportToFile(jrxmlPath,jasperPath); ​ //构造数据 Map paramters = new HashMap(); paramters.put("company","xx公司"); ​ List<Map> list = new ArrayList(); Map map1 = new HashMap(); map1.put("tName","入职体检套餐"); map1.put("tCode","RZTJ"); map1.put("tAge","18-60"); map1.put("tPrice","500"); ​ Map map2 = new HashMap(); map2.put("tName","阳光爸妈老年健康体检"); map2.put("tCode","YGBM"); map2.put("tAge","55-60"); map2.put("tPrice","500"); list.add(map1); list.add(map2); ​ //填充数据---使用JavaBean数据源方式填充 JasperPrint jasperPrint = JasperFillManager.fillReport(jasperPath, paramters, new JRBeanCollectionDataSource(list)); //输出文件 String pdfPath = "D:\\test.pdf"; JasperExportManager.exportReportToPdfFile(jasperPrint,pdfPath); }查看输出效果:  三、 在项目中输出运营数据PDF报表本小节我们将在项目中实现运营数据的PDF报表导出功能。3.1 设计PDF模板文件使用Jaspersoft Studio设计运营数据PDF报表模板文件health_business3.jrxml,设计后的效果如下:  在资源中已经提供好了此文件,直接使用即可。3.2 搭建环境第一步:在health_common工程的pom.xml中导入JasperReports的maven坐标<dependency> <groupId>net.sf.jasperreports</groupId> <artifactId>jasperreports</artifactId> <version>6.8.0</version> </dependency>第二步:将资源中提供的模板文件health_business3.jrxml复制到health_backend工程的template目录下  第三步:将解决中问题的相关资源文件复制到项目中  3.3 修改页面修改health_backend工程的report_business.html页面,添加导出PDF的按钮并绑定事件  3.4 Java代码实现在health_backend工程的ReportController中提供exportBusinessReport4PDF方法//导出运营数据到pdf并提供客户端下载 @RequestMapping("/exportBusinessReport4PDF") public Result exportBusinessReport4PDF(HttpServletRequest request, HttpServletResponse response) { try { Map<String, Object> result = reportService.getBusinessReportData(); ​ //取出返回结果数据,准备将报表数据写入到PDF文件中 List<Map> hotSetmeal = (List<Map>) result.get("hotSetmeal"); ​ //动态获取模板文件绝对磁盘路径 String jrxmlPath = request.getSession().getServletContext().getRealPath("template") + File.separator + "health_business3.jrxml"; String jasperPath = request.getSession().getServletContext().getRealPath("template") + File.separator + "health_business3.jasper"; //编译模板 JasperCompileManager.compileReportToFile(jrxmlPath, jasperPath); ​ //填充数据---使用JavaBean数据源方式填充 JasperPrint jasperPrint = JasperFillManager.fillReport(jasperPath,result, new JRBeanCollectionDataSource(hotSetmeal)); ​ ServletOutputStream out = response.getOutputStream(); response.setContentType("application/pdf"); response.setHeader("content-Disposition", "attachment;filename=report.pdf"); ​ //输出文件 JasperExportManager.exportReportToPdfStream(jasperPrint,out); ​ return null; } catch (Exception e) { e.printStackTrace(); return new Result(false, MessageConstant.GET_BUSINESS_REPORT_FAIL); } }
  • 【转载】springboot~将一个Integer类型序列为k/v对象
    对于一些带着固定标签的字段来说,我们通常把它们配置到字段中,而在数据库中存它们的字典code,或者是字典主键,不是一个整型的数字,而在前端显示时,有时需要将它们翻译成名称,这时后端可以帮他们进行翻译,或者前端通过code自己使用字典翻译;下面说一下第一种,后端在View model中将integer类型的字典字典翻译成一个k/v的对象。JsonSerializer一个json序列化的基类,我们可以继承它,并实现自己的原因,在springboot框架中,你返回的json对象事实上是jackson帮我们做了一次序列化工作,而我们的字段如果希望在序列化时行加工,可以利用这个环节,下面定义一下DictionarySerializer,来实现字典字段的序列化。/** * 自定义序列化器,将一个Integer类型的字段序列化成一个name/code的对象 */ public class DictionarySerializer extends JsonSerializer<Integer> { @Autowired DictionaryMapper dictionaryMapper; @Override public void serialize(Integer value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { //获取当前字段的名称 String type = jsonGenerator.getOutputContext().getCurrentName(); Integer code = (Integer) value; jsonGenerator.writeStartObject(); Dictionary dictionary = dictionaryMapper.selectOne( new QueryWrapper<Dictionary>().lambda() .eq(Dictionary::getCode, code) .eq(Dictionary::getType, type)); if (dictionary == null) throw new IllegalArgumentException(String.format("字典数据未配置,类型:%s,值:%s", type, code)); jsonGenerator.writeStringField("name", dictionary.getName()); jsonGenerator.writeNumberField("code", code); jsonGenerator.writeEndObject(); } }在实体中gender字段会进行声明 @ApiModelProperty("性别") @JsonSerialize(using= DictionarySerializer.class) private Integer gender; 在接口中返回一个对象,对象中包含了gender字段,而这个字段已经被序列化成对象,本例通过查询数据库实现,实际工作中,应该通过缓存来实现。 { "id": "ab9a48d4f49d93237f7090d340d9fa07", "username": "123", "email": "123@qq.com", "phone": "13754911028", "realName": null, "roleList": [ { "id": "1", "name": "管理员1" } ], "createTime": "2022-04-12T10:04:14", "updateTime": "2022-04-12T10:04:14", "createBy": "admin", "updateBy": "admin", "status": 1, "organization": null, "job": null, "gender": { "name": "男", "value": 0 } }转载自https://www.cnblogs.com/lori/p/16162883.html
  • [技术干货] 【转载】统一管理第三方包的依赖包版本号
    起因在java项目中,我在maven的pom.xml中引用了io.github.officiallysingh:spring-boot-starter-spark:1.3包,然后这个包里又有org.apache.spark:spark-core_2.13:3.5.5包的引用,而在spark-core_2.13包中又引用了org.apache.avro:avro-mapred:1.11.4包,这个包的版本0.10.0修改为0.9.0,我们如何实现呢?推荐方法通过在dependencyManagement中声明三方包的版本,来在自己项目中,将所有指定包的版本进行统一,并且包版本不同产生的冲突在当前项目的pom.xml中添加代码<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot-dependencies.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.apache.avro</groupId> <artifactId>avro-mapred</artifactId> <version>1.11.3</version> </dependency> </dependencyManagement>刷新依赖之后,可以看到三方包里的依赖包avro-mapred版本已经改变了其它方法下面这个表格总结了你可以在项目中使用的三种主要策略。方法操作方式适用场景与说明💡 直接声明依赖在 <dependencies> 中直接声明你想要的 jersey-client 版本。最简洁直接,适用于单模块项目,快速覆盖传递依赖的版本。📦 依赖管理在 <dependencyManagement> 中统一管理 jersey-client 的版本。推荐用于多模块项目,可以保证所有模块使用的版本一致,避免冲突。🗑️ 排除+引入先通过 <exclusions> 排除旧版本,再显式引入新版本。最严格的控制,确保构建时不会引入冲突的旧版本,但配置稍显繁琐。 转载自https://www.cnblogs.com/lori/p/19142696
  • [技术干货] Redis 解锁:C++ 实战深度探索 Set 数据类型
    前言欢迎来到 Redis Set 的终极指南。如果您曾需要管理一组独一无二的元素集合——无论是用户 ID、文章标签还是邮件地址——并希望以闪电般的速度对其执行强大的集合运算,那么您来对地方了。Redis Set 绝不是一个简单的列表,它是一种精妙的数据结构,将数学中强大的集合理论直接带入您的高性能数据库中。在本文中,我们将从最基础的概念讲起,逐步深入到高级的实际应用。我们将使用优秀的 C++ 库 redis-plus-plus 来演示所有示例,并逐行剖析代码。无论您是 C++ 开发者、后端工程师,还是仅仅对 Redis 感到好奇,读完本文,您都将深刻理解是什么让 Set 成为 Redis 中功能最丰富的工具之一。Redis Set 究竟是什么?在我们深入代码之前,先来建立一个清晰的思维模型。想象你有一个魔力袋,你可以往里面扔东西,但这个袋子有两条非常特殊的规则:强制保持唯一:这个袋子会自动拒绝重复的物品。如果你想把一个标有“A”的弹珠放进一个已经有“A”弹珠的袋子里,它会阻止你,确保袋子里每样东西都只有一个。顺序毫不在意:当你从袋子里往外取东西时,它们的顺序是完全随机的。袋子不记得到底是按什么顺序把东西放进去的。这个“魔力袋”正是 Redis Set 的精准比喻:一个无序的、元素唯一的字符串集合。这个简单的定义是其强大功能的基石,使其能够以惊人的速度进行成员资格检查、数量统计以及诸如交集、并集等复杂的服务器端运算。第一章:基础入门 - 创建和查看你的第一个 Set让我们从最基本的操作开始:如何向一个 Set 添加元素,以及如何查看它的全部内容。为此,我们将使用 SADD 和 SMEMBERS 这两个命令。SADD:向集合中添加成员SADD 是您向 Set 中添加一个或多个元素的主要工具。如果某个元素已经存在,Redis 会优雅地忽略它。该命令的返回值是新成功添加的元素的数量。SMEMBERS:获取所有成员SMEMBERS 的功能正如其名:返回指定 Set 中的所有成员。这对于获取整个集合非常有用,但请注意:在拥有数百万元素的超大 Set 上使用此命令可能会暂时阻塞您的 Redis 服务器,因为它需要时间来准备所有数据。我们将在后续章节中讨论更安全的替代方案 SSCAN。C++ 实战:sadd 与 smembers现在,让我们来分析一段代码,它演示了这些基础操作。 // 引入必要的头文件...#include <iostream>#include <set>#include <string>#include <vector>#include <iterator>#include <sw/redis++/redis.h>// 一个辅助函数,用于打印容器内容template<typename T>void PrintContainer(const T& container) {    for (const auto& elem : container) {        std::cout << elem << " ";    }    std::cout << std::endl;}void test1(sw::redis::Redis& redis){    std::cout << "sadd 和 smembers" << std::endl;    // 清空数据库,确保一个干净的测试环境    redis.flushall();    // 1. 一次添加一个元素    redis.sadd("key", "111");    // 2. 使用初始化列表,一次添加多个元素    redis.sadd("key", {"222", "333", "444"});    // 3. 使用迭代器,从另一个容器中添加多个元素    std::set<std::string> elems = {"555", "666", "777"};    // 返回值是成功插入了多少个元素    redis.sadd("key", elems.begin(), elems.end());    // --- 现在,让我们获取所有元素 ---    std::set<std::string> result;    // 为我们的 C++ set 构建一个插入迭代器    auto it = std::inserter(result, result.end());        // 从 Redis set 中获取所有成员,并插入到我们的 C++ set 中    redis.smembers("key", it);    PrintContainer(result);}一键获取完整项目代码cpp代码剖析:redis.flushall():我们首先清空整个 Redis 数据库,以确保测试环境的纯净。单个元素 sadd:redis.sadd("key", "111"); 将字符串 “111” 添加到名为 key 的 Set 中。由于 Set 原本是空的,此命令返回 1。初始化列表 sadd:redis.sadd("key", {"222", "333", "444"}); 展示了 redis-plus-plus 库的一个便捷特性,允许您一次性添加多个元素。这比发送三个独立的命令效率更高。此调用将返回 3。基于迭代器的 sadd:在这里,我们先填充了一个 C++ 的 std::set,然后使用它的迭代器(elems.begin(), elems.end())将其所有元素添加到 Redis 的 Set 中。这对于将现有 C++ 容器中的数据同步到 Redis 非常有用。使用 smembers 获取数据:我们创建了一个 std::set<string> result; 来存放从 Redis 返回的数据。在客户端使用 std::set 是一个绝佳选择,因为它不仅 mirroring(镜像)了 Redis Set 的唯一性,还能自动对元素进行排序,便于我们进行可预测的展示。auto it = std::inserter(result, result.end()); 是至关重要的一行。我们需要一种方式告诉 redis-plus-plus 应该把接收到的元素放在哪里。inserter 是一种特殊的迭代器,当你给它赋值时,它会调用其关联容器的 insert() 方法。redis.smembers("key", it); 执行命令。redis-plus-plus 获取 key 中的所有成员,并使用我们的迭代器 it 将它们逐一插入到 result 集合中。C++ 关键概念:inserter vs back_inserter在原始笔记中,有一个关键的区别被强调了出来:std::back_inserter 创建一个调用 push_back() 的迭代器。它适用于 std::vector, std::list, std::deque 等容器。std::set 没有 push_back() 方法,因为它需要维护内部的排序。因此,对于 std::set,我们必须使用 std::inserter,它会调用 insert() 方法。预测输出:PrintContainer 函数将打印 result 集合的内容。由于 std::set 会对其元素进行排序,输出将是按字母/数字顺序排列的。sadd 和 smembers111 222 333 444 555 666 777一键获取完整项目代码第二章:深入探索 - 检查与修改你的 Set既然我们知道了如何构建一个 Set,接下来让我们学习如何查询它的属性并执行基本的修改。这些命令是 Set 日常操作的核心,并且它们都快得令人难以置信。SISMEMBER:这个元素存在吗? (时间复杂度 O(1))这是 Set 命令库中最强大的命令之一。SISMEMBER 检查一个特定元素是否是 Set 的成员。如果存在,返回 1 (true);如果不存在,返回 0 (false)。它的性能是 O(1),这意味着其速度是恒定的,不依赖于 Set 的大小。无论是在一个有10个元素的 Set 还是在一个有1000万个元素的 Set 中检查成员资格,花费的时间都是相同的。C++ 实战:sismembervoid test2(sw::redis::Redis& redis){    std::cout << "sismember" << std::endl;    redis.flushall();    redis.sadd("key", {"111", "222", "333", "444"});    // 检查 "111" 是否是集合的成员    bool result = redis.sismember("key", "111");    std::cout << "result:" << result << std::endl;}一键获取完整项目代码cpp剖析:我们创建一个 Set,然后使用 sismember 检查 “111” 是否存在。redis-plus-plus 库非常方便地将 Redis 返回的 1 或 0 直接映射为了 C++ 的 bool 类型。因为 “111” 确实在 Set 中,result 将为 true。应用场景:标签系统:检查一篇博客文章是否已经被标记为 “DevOps”。权限控制:检查一个 userID 是否在 admin_users 这个 Set 中。唯一性事件:检查用户是否已经执行了某个一次性操作(例如,“voted_on_poll_123”)。预测输出:当 bool true 被输出到 cout 时,通常会显示为 1。sismemberresult:1一键获取完整项目代码SCARD:集合里有多少元素? (时间复杂度 O(1))SCARD 代表 “Set Cardinality”(集合基数),它简单地返回一个 Set 中元素的数量。与 SISMEMBER 一样,这也是一个 O(1) 操作。Redis 内部维护了一个计数器,所以它不需要遍历所有元素就能告诉你总数。C++ 实战:scardvoid test3(sw::redis::Redis& redis){    std::cout << "scard" << std::endl;    redis.flushall();    // 向集合中添加4个唯一元素    redis.sadd("key", {"111", "222", "333", "444"});    // 获取集合中的元素个数    long long result = redis.scard("key"); // 返回 4    std::cout << "result:" << result << std::endl;}一键获取完整项目代码cpp剖析:我们添加了四个元素,然后调用 scard。命令返回了计数 4。应用场景:在线用户:跟踪已登录的独立用户数量。点赞计数:快速显示一张照片获得的独立点赞数。数据分析:统计今天访问网站的独立 IP 地址数量。预测输出:scardresult:4一键获取完整项目代码SPOP:随机移除并返回一个元素SPOP 是一个既有趣又实用的命令。它会从 Set 中随机选择一个元素,将其移除,然后返回给你。这是一种“破坏性读取”,因为元素在被读取后就从集合中消失了。C++ 实战:spopvoid test4(sw::redis::Redis& redis){    std::cout << "spop" << std::endl;    redis.flushall();    redis.sadd("key", {"111", "222", "333", "444"});    // 随机弹出一个元素,spop 的返回值是 Optional<string>    auto result = redis.spop("key");    if (result)    {        // 因为返回值是 Optional,我们通过 .value() 来获取原始的 string 内容        std::cout << "result:" << result.value() << std::endl;    }    else    {        std::cout << "result is empty" << std::endl;    }}一键获取完整项目代码cpp剖析:auto result = redis.spop("key"); 执行命令。redis-plus-plus 将返回值包装在 sw::redis::Optional<std::string> 中。这是因为如果你对一个空 Set 执行 spop,Redis 会返回 nil(空)。Optional 类型可以优雅地处理这种情况,避免空指针等问题。if (result) 检查 Optional 对象是否真的包含一个值。在我们的例子中,由于 Set 非空,它肯定会弹出一个元素,所以条件为真。result.value() 从 Optional 中提取出实际的 std::string 值。核心特性:随机性:SPOP 最大的特点就是随机。这意味着每次运行这段代码,得到的结果都可能不同。它非常适合需要随机处理任务的场景。应用场景:抽奖系统:从参与用户 Set 中随机抽取一名中奖者。任务队列:从待处理任务池中随机分配一个任务给工作进程。在线匹配:从等待匹配的玩家池中随机抽取一个进行游戏。预测输出:输出是不确定的,可能是以下四种情况之一:// 可能的输出 1spopresult:111// 可能的输出 2spopresult:333一键获取完整项目代码第三章:集合的威力 - 集合运算这才是 Redis Set 真正大放异彩的地方。Redis 能够在服务器端以极高的效率执行集合的交集 (intersection)、并集 (union) 和差集 (difference) 运算,避免了将大量数据传输到客户端再进行计算的开销。交集运算:SINTER & SINTERSTORE交集运算会找出所有给定的 Set 中共同存在的元素。SINTER: 计算交集并直接返回给客户端。SINTERSTORE: 计算交集,但不返回,而是将结果存储在一个新的目标 Set 中。C++ 实战:sinter (求交集并返回)void test5(sw::redis::Redis& redis){    // 这里的 cout 应该是 "sinter",一个小笔误    std::cout << "sinter" << std::endl;    redis.flushall();    redis.sadd("key1", {"111", "222", "333", "444"});    redis.sadd("key2", {"111", "222", "444"});    std::set<std::string> result;    auto it = std::inserter(result, result.end());    // 求交集涉及多个 key,我们使用初始化列表来描述    // 将 "key1" 和 "key2" 的交集插入到 result 中    redis.sinter({"key1", "key2"}, it);    PrintContainer(result);}一键获取完整项目代码cpp剖析:key1 包含 {"111", "222", "333", "444"}。key2 包含 {"111", "222", "444"}。redis.sinter({"key1", "key2"}, it); 命令计算出两个集合的共同成员是 {"111", "222", "444"},并通过迭代器将它们存入 C++ 的 result 集合中。应用场景:共同好友:计算用户A的好友列表和用户B的好友列表的交集。内容推荐:找出同时对 “科幻” 和 “悬疑” 标签感兴趣的用户。预测输出:sinter111 222 444一键获取完整项目代码12C++ 实战:sinterstore (求交集并存储)void test6(sw::redis::Redis& redis){    std::cout << "sinterstore" << std::endl;    redis.flushall();    redis.sadd("key1", {"111", "222", "333"});    redis.sadd("key2", {"111", "222", "444"});    // 指定一个 destination ("key3"),将交集结果存储到其中    long long len = redis.sinterstore("key3", {"key1", "key2"});    std::cout << "len:" << len << std::endl;    // 检查 "key3" 中的元素以验证结果    std::set<std::string> result;    auto it = std::inserter(result, result.end());    redis.smembers("key3", it);    PrintContainer(result);}一键获取完整项目代码cpp剖析:redis.sinterstore("key3", {"key1", "key2"}); 计算出交集 {"111", "222"},然后将这个结果存入一个全新的 Set key3 中。如果 key3 已存在,它将被覆盖。该命令返回新生成的 key3 集合的元素数量,即 2。所以 len 的值为 2。后续的 smembers 验证了 key3 的内容确实是正确的交集结果。应用场景:当你需要缓存或复用交集计算结果时,SINTERSTORE 非常有用。例如,为一组用户预先计算出他们共同喜欢的商品列表。预测输出:sinterstorelen:2111 222一键获取完整项目代码第四章:超越基础 - 更多强大的 Set 命令我们已经覆盖了所提供代码中的所有命令,但 Redis Set 的能力远不止于此。为了成为真正的 Set 大师,让我们来了解一下其他一些极其有用的命令。并集运算:SUNION & SUNIONSTORE并集运算返回所有给定集合的全部不重复的元素。命令:SUNION key [key ...] 和 SUNIONSTORE destination key [key ...]应用场景:好友圈:获取用户A的好友、用户B的好友和用户C的好友的完整、不重复的列表。权限合并:一个用户属于 “editor” 角色组和 “publisher” 角色组,通过并集可以得到该用户拥有的所有权限的集合。差集运算:SDIFF & SDIFFSTORE差集运算返回那些只存在于第一个集合中,但不在任何后续集合中的元素。命令:SDIFF key [key ...] 和 SDIFFSTORE destination key [key ...]应用场景:好友推荐:找出我的好友中,有哪些还不是我朋友A的好友,从而可以向我推荐。内容去重:向用户展示新闻时,从“今日热点”中排除掉他“已读新闻”Set 中的内容。安全迭代:SSCAN正如前文提到的,SMEMBERS 对于大集合是危险的。SSCAN 提供了安全的替代方案。它使用一个游标 (cursor) 来分批次地返回集合中的元素,每次只返回一小部分,绝不会阻塞服务器。命令:SSCAN key cursor [MATCH pattern] [COUNT count]工作方式:你用一个初始为 0 的游标开始第一次调用。Redis 返回下一批元素和一个新的游标。你用这个新的游标进行下一次调用,如此往复,直到返回的游标为 0,表示迭代完成。适用场景:任何需要遍历生产环境中大集合的操作,例如数据迁移、离线分析等。总结Redis Set 是一种看似简单却异常强大的数据结构。让我们回顾一下它的核心优势:唯一性:自动处理数据去重,简化了应用逻辑。极速性能:绝大多数核心操作(增、删、查、计数)的时间复杂度都是 O(1),性能与集合大小无关。强大的集合运算:能够在服务器端原子性地、高效地执行交、并、差集运算,极大地减少了网络开销和客户端的计算压力。从简单的在线用户统计,到复杂的社交网络好友关系分析,再到智能推荐系统,Redis Set 都能以其优雅和高效提供坚实的解决方案。希望通过本文的深度解析和 C++ 代码示例,您已经准备好在自己的项目中发挥 Redis Set 的真正威力了。————————————————原文链接:https://blog.csdn.net/2301_80863610/article/details/152178781
  • [技术干货] 如何理解Java中的并发?
    Java 中的并发(Concurrency) 指多个任务在同一时间段内交替执行(宏观上同时进行,微观上可能是 CPU 快速切换调度),目的是提高程序效率,充分利用系统资源(如 CPU、内存、I/O 等)。一、为什么需要并发?资源利用率最大化当程序执行 I/O 操作(如读写文件、网络请求)时,CPU 通常处于空闲状态。通过并发,可在等待 I/O 时让 CPU 处理其他任务,避免资源浪费。例如:一个下载文件的程序,在等待网络数据时,可同时解析已下载的部分数据。响应速度提升对于交互式程序(如 GUI 应用、服务器),并发能避免单任务阻塞导致的界面卡顿或请求超时。例如:Web 服务器同时处理多个用户的请求,而非逐个排队处理。二、并发的核心概念1. 线程(Thread)与进程(Process)进程:程序的一次执行过程,是系统资源分配的基本单位(有独立的内存空间)。线程:进程内的执行单元,是 CPU 调度的基本单位(共享进程的内存空间)。关系:一个进程可包含多个线程(多线程),线程间切换成本远低于进程切换。2. 并行(Parallelism)与并发(Concurrency)的区别并发:多个任务“交替执行”(CPU 切换速度快,看起来同时进行),适用于单 CPU 或多 CPU。并行:多个任务“同时执行”(需多 CPU 核心,每个核心处理一个任务)。例如:4 核 CPU 同时运行 4 个线程是并行,1 核 CPU 快速切换 4 个线程是并发。三、Java 实现并发的方式Java 提供了多种并发编程工具,核心是通过线程实现:1. 基础方式继承 Thread 类:重写 run() 方法定义任务,调用 start() 启动线程。实现 Runnable 接口:定义任务逻辑,通过 Thread 类包装并启动(推荐,避免单继承限制)。实现 Callable 接口:与 Runnable 类似,但可返回结果并抛出异常,配合 Future 获取结果。// Callable 示例import java.util.concurrent.Callable;import java.util.concurrent.ExecutionException;import java.util.concurrent.FutureTask;public class CallableDemo {    public static void main(String[] args) throws ExecutionException, InterruptedException {        // 1. 定义任务(有返回值)        Callable<Integer> task = () -> {            int sum = 0;            for (int i = 0; i <= 100; i++) {                sum += i;            }            return sum;        };        // 2. 包装任务        FutureTask<Integer> futureTask = new FutureTask<>(task);        // 3. 启动线程        new Thread(futureTask).start();        // 4. 获取结果(会阻塞直到任务完成)        System.out.println("1-100的和:" + futureTask.get()); // 输出5050    }}一键获取完整项目代码java2. 线程池(ThreadPoolExecutor)频繁创建/销毁线程会消耗资源,线程池通过复用线程提高效率,是生产环境的首选。Java 提供 Executors 工具类快速创建线程池:import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class ThreadPoolDemo {    public static void main(String[] args) {        // 创建固定大小的线程池(3个线程)        ExecutorService pool = Executors.newFixedThreadPool(3);        // 提交5个任务(线程池会复用3个线程处理)        for (int i = 0; i < 5; i++) {            int taskId = i;            pool.submit(() -> {                System.out.println("处理任务" + taskId + ",线程:" + Thread.currentThread().getName());            });        }        // 关闭线程池        pool.shutdown();    }}一键获取完整项目代码java四、并发带来的问题及解决方案并发虽提高效率,但多线程共享资源时会引发问题:1. 线程安全问题当多个线程同时操作共享数据(如全局变量、集合),可能导致数据不一致。示例:两个线程同时对变量 count 做 ++ 操作,预期结果为 2,实际可能为 1(因 ++ 是多步操作,可能被打断)。2. 解决方案synchronized 关键字:通过“锁”保证同一时间只有一个线程执行临界区代码(修饰方法或代码块)。public class SynchronizedDemo {    private static int count = 0;    private static final Object lock = new Object(); // 锁对象    public static void main(String[] args) throws InterruptedException {        Thread t1 = new Thread(() -> {            for (int i = 0; i < 10000; i++) {                synchronized (lock) { // 同步代码块:同一时间只有一个线程进入                    count++;                }            }        });        Thread t2 = new Thread(() -> {            for (int i = 0; i < 10000; i++) {                synchronized (lock) {                    count++;                }            }        });        t1.start();        t2.start();        t1.join(); // 等待线程执行完毕        t2.join();        System.out.println("count最终值:" + count); // 正确输出20000    }}一键获取完整项目代码javajava.util.concurrent 工具类:提供线程安全的集合(如 ConcurrentHashMap)、原子类(如 AtomicInteger)、锁机制(如 ReentrantLock)等,比 synchronized 更灵活。五、并发编程的核心挑战可见性:一个线程修改的共享变量,其他线程可能无法立即看到(因 CPU 缓存导致)。解决方案:使用 volatile 关键字(保证变量修改后立即刷新到主内存)。原子性:一个操作不可被中断(如 count++ 实际是“读-改-写”三步,非原子操作)。解决方案:synchronized、原子类(AtomicInteger)。有序性:CPU 可能对指令重排序优化,导致代码执行顺序与预期不一致。解决方案:volatile、synchronized 或显式内存屏障。六、总结并发的本质:通过多线程交替执行,提高资源利用率和程序响应速度。核心问题:线程安全(数据不一致),需通过锁机制或并发工具解决。实践建议:优先使用线程池管理线程,避免手动创建;复杂场景下借助 java.util.concurrent 包的工具类(如 CountDownLatch、Semaphore)简化开发。理解并发是 Java 进阶的关键,尤其在高并发场景(如分布式系统、高流量服务器)中,合理设计并发模型能显著提升系统性能。————————————————原文链接:https://blog.csdn.net/2508_93307008/article/details/153339720
  • [技术干货] 【Java 开发日记】我们来说一说动态代理
    什么是动态代理首先,动态代理是代理模式的一种实现方式,代理模式除了动态代理还有 静态代理,只不过静态代理能够在编译时期确定类的执行对象,而动态代理只有在运行时才能够确定执行对象是谁。代理可以看作是对最终调用目标的一个封装,能够通过操作代理对象来调用目标类,这样就可以实现调用者和目标对象的解耦合。动态代理的应用场景有很多,最常见的就是 AOP 的实现、RPC 远程调用、Java 注解对象获取、日志框架、全局性异常处理、事务处理等。动态代理的实现有很多,但是 JDK 动态代理是很重要的一种,下面就 JDK 动态代理来深入理解一波。 JDK 动态代理首先先来看一下动态代理的执行过程在 JDK 动态代理中,实现了 InvocationHandler 的类可以看作是 代理类(因为类也是一种对象,所以上面为了描述关系,把代理类形容成了代理对象)。JDK 动态代理就是围绕实现了 InvocationHandler 的代理类进行的,比如下面就是一个 InvocationHandler 的实现类,同时它也是一个代理类。public class UserHandler implements InvocationHandler {    private UserDao userDao;    public UserHandler(UserDao userDao){        this.userDao = userDao;    }    @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        saveUserStart();        Object obj = method.invoke(userDao, args);        saveUserDone();        return obj;    }    public void saveUserStart(){        System.out.println("---- 开始插入 ----");    }    public void saveUserDone(){        System.out.println("---- 插入完成 ----");    }}一键获取完整项目代码代理类一个最最最重要的方法就是 invoke 方法,它有三个参数Object proxy: 动态代理对象,关于这个方法后面会说。Method method: 表示最终要执行的方法,method.invoke 用于执行被代理的方法,也就是真正的目标方法Object[] args: 这个参数就是向目标方法传递的参数。这里构造好了代理类,现在就要使用它来实现对目标对象的调用,那么如何操作呢?请看下面代码public static void dynamicProxy(){    UserDao userDao = new UserDaoImpl();    InvocationHandler handler = new UserHandler(userDao);    ClassLoader loader = userDao.getClass().getClassLoader();    Class<?>[] interfaces = userDao.getClass().getInterfaces();    UserDao proxy = (UserDao)Proxy.newProxyInstance(loader, interfaces, handler);    proxy.saveUser();}一键获取完整项目代码如果要用 JDK 动态代理的话,就需要知道目标对象的类加载器、目标对象的接口,当然还要知道目标对象是谁。构造完成后,就可以调用 Proxy.newProxyInstance方法,然后把类加载器、目标对象的接口、目标对象绑定上去就完事儿了。这里需要注意一下 Proxy 类,它就是动态代理实现所用到的代理类。Proxy 位于java.lang.reflect 包下,这同时也旁敲侧击的表明动态代理的本质就是反射。下面就围绕 JDK 动态代理,来深入理解一下它的原理,以及搞懂为什么动态代理的本质就是反射。 动态代理的实现原理在了解动态代理的实现原理之前,先来了解一下 InvocationHandler 接口 InvocationHandler 接口JavaDoc 告诉我们,InvocationHandler 是一个接口,实现这个接口的类就表示该类是一个代理实现类,也就是代理类。InvocationHandler 接口中只有一个 invoke 方法。动态代理的优势在于能够很方便的对代理类中方法进行集中处理,而不用修改每个被代理的方法。因为所有被代理的方法(真正执行的方法)都是通过在 InvocationHandler 中的 invoke 方法调用的。所以只需要对 invoke 方法进行集中处理。invoke 方法只有三个参数public Object invoke(Object proxy, Method method, Object[] args)        throws Throwable;一键获取完整项目代码proxy:代理对象method: 代理对象调用的方法args:调用方法中的参数。动态代理的整个代理过程不像静态代理那样一目了然,清晰易懂,因为在动态代理的过程中,没有看到代理类的真正代理过程,也不明白其具体操作,所以要分析动态代理的实现原理,必须借助源码。那么问题来了,首先第一步应该从哪分析?如果不知道如何分析的话,干脆就使用倒推法,从后往前找,直接先从 _Proxy.newProxyInstance_入手,看看是否能略知一二。 Proxy.newInstance 方法分析Proxy 提供了创建动态代理类和实例的静态方法,它也是由这些方法创建的所有动态代理类的超类。Proxy.newProxyInstance 源码(java.lang.reflect.Proxy)public static Object newProxyInstance(ClassLoader loader,                                      Class<?>[] interfaces,                                      InvocationHandler h)    throws IllegalArgumentException    {    Objects.requireNonNull(h);    final Class<?>[] intfs = interfaces.clone();    final SecurityManager sm = System.getSecurityManager();    if (sm != null) {    checkProxyAccess(Reflection.getCallerClass(), loader, intfs);    }    Class<?> cl = getProxyClass0(loader, intfs);    try {    if (sm != null) {    checkNewProxyPermission(Reflection.getCallerClass(), cl);    }    final Constructor<?> cons = cl.getConstructor(constructorParams);    final InvocationHandler ih = h;    if (!Modifier.isPublic(cl.getModifiers())) {    AccessController.doPrivileged(new PrivilegedAction<Void>() {    public Void run() {    cons.setAccessible(true);    return null;    }    });    }    return cons.newInstance(new Object[]{h});    } catch (Exception e) {    ...}一键获取完整项目代码乍一看起来有点麻烦,其实源码都是这样,看起来非常复杂,但是慢慢分析、厘清条理过后就好,最重要的是分析源码不能着急。上面这个 Proxy.newProxyInstsance 其实就做了下面几件事,这里画了一个流程图作为参考。从上图中也可以看出,newProxyInstsance 方法最重要的几个环节就是获得代理类、获得构造器,然后构造新实例。对反射有一些了解的同学,应该会知道获得构造器和构造新实例是怎么回事。所以重点就放在了获得代理类,这是最关键的一步,对应源码中的 _Class<?> cl = getProxyClass0(loader, intfs);_ 进入这个方法一探究竟private static Class<?> getProxyClass0(ClassLoader loader,                                       Class<?>... interfaces) {    if (interfaces.length > 65535) {        throw new IllegalArgumentException("interface limit exceeded");    }    return proxyClassCache.get(loader, interfaces);}一键获取完整项目代码这个方法比较简单,首先会直接判断接口长度是否大于 65535(刚开始看到这里是有点不明白的,这个判断是要判断什么?interfaces 这不是一个 class 类型吗,从 length 点进去也看不到这个属性,细看一下才明白,这居然是可变参数,Class … 中的 … 就是可变参数,所以这个判断应该是判断接口数量是否大于 65535。)然后会直接从 proxyClassCache 中根据 loader 和 interfaces 获取代理对象实例。如果能够根据 loader 和 interfaces 找到代理对象,将会返回缓存中的对象副本;否则,它将通过 ProxyClassFactory 创建代理类。proxyClassCache.get 就是一系列从缓存中的查询操作,注意这里的 proxyClassCache 其实是一个 WeakCache,WeakCahe 也是位于 java.lang.reflect 包下的一个缓存映射 map,它的主要特点是一个弱引用的 map,但是它内部有一个 SubKey ,这个子键却是强引用的。这里不用去追究这个 proxyClassCache 是如何进行缓存的,只需要知道它的缓存时机就可以了:即在类加载的时候进行缓存。如果无法找到代理对象,就会通过 ProxyClassFactory 创建代理,ProxyClassFactory 继承于 BiFunctionprivate static final class ProxyClassFactory        implements BiFunction<ClassLoader, Class<?>[], Class<?>>    {...}一键获取完整项目代码ProxyClassFactory 里面有两个属性一个方法。proxyClassNamePrefix:这个属性表明使用 ProxyClassFactory 创建出来的代理实例的命名是以 “$Proxy” 为前缀的。nextUniqueNumber:这个属性表明 ProxyClassFactory 的后缀是使用 AtomicLong 生成的数字所以代理实例的命名一般是 Proxy1这种。这个 apply 方法是一个根据接口和类加载器进行代理实例创建的工厂方法,下面是这段代码的核心。@Overridepublic Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {    ...    long num = nextUniqueNumber.getAndIncrement();    String proxyName = proxyPkg + proxyClassNamePrefix + num;    byte[] proxyClassFile = ProxyGenerator.generateProxyClass(        proxyName, interfaces, accessFlags);    try {        return defineClass0(loader, proxyName,                            proxyClassFile, 0, proxyClassFile.length);    } catch (ClassFormatError e) {        throw new IllegalArgumentException(e.toString());    }}一键获取完整项目代码可以看到,代理实例的命名就是上面所描述的那种命名方式,只不过它这里加上了 proxyPkg 包名的路径。然后下面就是生成代理实例的关键代码。ProxyGenerator.generateProxyClass 跟进去是只能看到 .class 文件的,class 文件是虚拟机编译之后的结果,所以要看一下 .java 文件源码。.java 源码位于 OpenJDK中的 sun.misc 包中的 ProxyGenerator 下。此类的 generateProxyClass() 静态方法的核心内容就是去调用 generateClassFile() 实例方法来生成 Class 文件。方法太长了就不贴了,这里就大致解释以下其作用:第一步:收集所有要生成的代理方法,将其包装成 ProxyMethod 对象并注册到 Map 集合中。第二步:收集所有要为 Class 文件生成的字段信息和方法信息。第三步:完成了上面的工作后,开始组装 Class 文件。而 defineClass0 这个方法点进去是 native ,底层是 C/C++ 实现的,于是去看了一下 C/C++ 源码,路径在点开之后的 C/C++ 源码还是挺让人绝望的。不过再回头看一下这个 defineClass0 方法,它实际上就是根据上面生成的 proxyClassFile 字节数组来生成对应的实例罢了,所以不必再深究 C/C++ 对于代理对象的合成过程了。所以总结一下可以看出,JDK 生成了一个叫 $Proxy0 的代理类,这个类文件放在内存中的,在创建代理对象时,就是通过反射获得这个类的构造方法,然后创建的代理实例。所以最开始的 dynamicProxy 方法反编译后的代码就是这样的public final class $Proxy0 extends java.lang.reflect.Proxy implements com.cxuan.dynamic.UserDao {  public $Proxy0(java.lang.reflect.InvocationHandler) throws ;    Code:       0: aload_0       1: aload_1       2: invokespecial #8                  // Method java/lang/reflect/Proxy."<init>":(Ljava/lang/reflect/InvocationHandler;)V       5: return一键获取完整项目代码可以看到代理类继承了 Proxy 类,所以也就决定了 Java 动态代理只能对接口进行代理。 于是,上面这个图应该就可以看懂了。 invoke 方法中第一个参数 proxy 的作用细心的小伙伴们可能都发现了,invoke 方法中第一个 proxy 的作用是啥?代码里面好像 proxy 也没用到,这个参数的意义是啥呢?它运行时的类型是啥啊?为什么不使用 this 代替呢?Stackoverflow 给出了一个回答 https://stackoverflow.com/questions/22930195/understanding-proxy-arguments-of-the-invoke-method-of-java-lang-reflect-invoca什么意思呢?就是说这个 proxy ,它是真正的代理对象,invoke 方法可以返回调用代理对象方法的返回结果,也可以返回对象的真实代理对象,也就是 $Proxy0,这也是它运行时的类型。至于为什么不用 this 来代替 proxy,因为实现了 InvocationHandler 的对象中的 this ,指代的还是 InvocationHandler 接口实现类本身,而不是真实的代理对象————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/152166279
总条数:732 到第
上滑加载中