-
在 Spring Boot 开发中,数组(Array)作为一种基础而高效的数据结构,广泛应用于参数接收、配置管理、批量处理等场景。尽管 Java 生态中更常使用 List、Set 等集合类型,但在特定场合(如性能敏感、固定长度、与前端/接口协议对齐等),原生数组仍有不可替代的价值。一、控制器中接收数组参数1.1 接收 URL 查询参数中的数组前端可通过重复参数名传递数组,Spring Boot 自动绑定为 Java 数组或 List。@RestControllerpublic class UserController { // 方式1:使用数组接收 @GetMapping("/users") public String getUsersByIds(@RequestParam("ids") Long[] ids) { return "Received IDs: " + Arrays.toString(ids); } // 方式2:使用 List 接收(推荐) @GetMapping("/users/list") public String getUsersByIdsList(@RequestParam("ids") List<Long> ids) { return "Received IDs: " + ids; }}请求示例:GET /users?ids=1&ids=2&ids=3 支持:Long[]、String[]、int[] 等基本类型及包装类数组 注意:若参数为空(如 /users),ids 将为 null,建议加 required = false 或设默认值@RequestParam(value = "ids", required = false, defaultValue = "") String[] ids1.2 接收 JSON 请求体中的数组当请求体为 JSON 格式时,可直接映射到对象字段中的数组。public class BatchUpdateRequest { private Long[] userIds; // 或 List<Long> userIds private String status; // getter/setter}@PostMapping("/batch-update")public ResponseEntity<String> batchUpdate(@RequestBody BatchUpdateRequest request) { System.out.println("User IDs: " + Arrays.toString(request.getUserIds())); return ResponseEntity.ok("Success");}请求体示例:{ "userIds": [101, 102, 103], "status": "ACTIVE"}Jackson 默认支持数组反序列化若需自定义行为,可配置 ObjectMapper二、在配置文件中使用数组2.1 application.yml 中定义数组app: supported-languages: [zh-CN, en-US, ja-JP] max-retry-attempts: 3 admin-ids: - 1001 - 1002 - 10032.2 使用 @ConfigurationProperties 绑定@Component@ConfigurationProperties(prefix = "app")@Data // Lombok 注解,自动生成 getter/setterpublic class AppProperties { private String[] supportedLanguages; private int maxRetryAttempts; private Long[] adminIds;} 支持 String[]、int[]、Long[] 等 不支持泛型数组(如 List<String> 需用 List)2.3 使用 @Value 注入数组(不推荐复杂场景)@Value("${app.supported-languages}")private String[] supportedLanguages;// 或使用 SpEL 表达式(适用于简单字符串数组)@Value("#{'${app.admin-ids}'.split(',')}")private Long[] adminIds; // 需确保配置为逗号分隔 缺点:类型转换弱、错误提示不友好,建议优先使用 @ConfigurationProperties三、数组与 JSON 序列化Spring Boot 默认使用 Jackson 处理 JSON。3.1 返回数组作为响应体@GetMapping("/languages")public String[] getSupportedLanguages() { return new String[]{"zh-CN", "en-US", "ja-JP"};}响应结果:["zh-CN","en-US","ja-JP"]3.2 自定义序列化行为(可选)若需控制空数组、null 值等行为,可在字段或类上添加注解:public class ApiResponse { @JsonInclude(JsonInclude.Include.NON_EMPTY) private String[] tags = {}; // 空数组不返回 // getter/setter}
-
一、基础概念进程 我们知道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
-
【问题来源】 内部测试环境功能测试 【问题简要】 麻烦咨询下,产生通话详单记录之后,会分别向tbilllog1-12和trecordinfo1-12表中写入数据,有种数据场景在tbilllog表中存在多条的情况,只取deveicetype=2(业务代表)的数据是能和trecordinfo表中的数据条数对应起来,但是只通过callid关联的话无法准确匹配到对应的录音数据,我在论坛上查看之前的问题求助,有看到回复过类似的问题,如下面的截图1,如果能通过callid和tbilllog表的callbegin和trecordinfo表的begintime一起关联能准确匹配到同一条数据,但是实际去查表发现有些数据的时间不是完全一样的,一条数据一样,另一条数据一个是14:07:23,另外一个是14:07:22,相差了1秒,如下面的截图2,所以想问下,这两个时间存在有些不一样的是否是写入过慢情况导致?那是否就无法通过tbilllog表的callbegin和trecordinfo表的begintime一起关联呢?【问题类别】 话单数据【AICC解决方案版本】 AICC 版本:AICC 25_300.0【期望解决时间】 尽快 【日志或错误截图】1、 2
-
如何安装SmartAssist Java 插件CodeArts IDE for Python版本: 3.4.1提交: c3be4d08ef4b68e19b23b4136f5f8a7a960d5095日期: 2025-07-15T04:00:00.368Z (3个月前)OS: Windows_NT x64 10.0.19045
-
【问题来源】 内部测试环境功能测试 【问题简要】 麻烦咨询下,媒体呼叫话单写入tbilllog1-12表中时效是实时写入吗?比如说通话挂断之后数据即可写入到对应月份的tbilllog表中,数据写入话单表这中间可能会存在延迟时间吗?【问题类别】 话单数据【AICC解决方案版本】 AICC 版本:AICC 25_300.0【期望解决时间】 尽快 【日志或错误截图】无
-
【问题来源】 内部测试环境功能测试 【问题简要】 在AICC产品文档_25.300.0版本的文档中查看AICC CC-CMS接口参考相关接口说明,没有找到有根据开始时间和结束时间作为查询条件对应的通话详单数据信息所对应的接口,截图上所标记的查询指定VDN下的呼叫信息接口有开始时间和结束时间,不过看下来返回的出参中有很多需要的字段不包括,比如保持时长,保持次数,通话状态等字段,咨询了解话单数据都记录在了tbilllog表中,目前有个场景对于客户在ivr挂断或排队挂断等数据需要做记录进行回呼以及这张表中没有相关保持时长等字段信息,对于我所说的这种场景目前最新的版本中是否有好的解决方案呢?谢谢!【问题类别】 CMS话单数据【AICC解决方案版本】 AICC 版本:AICC 25_300.0【期望解决时间】 尽快 【日志或错误截图】
-
篇讲了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
-
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.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从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
-
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 >= #{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 >= #{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 <= #{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 >= #{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 开发中,我们经常需要对集合数据进行过滤、映射、统计等操作。传统的 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
-
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); } }
-
对于一些带着固定标签的字段来说,我们通常把它们配置到字段中,而在数据库中存它们的字典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
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签