Spring Boot第一性原理:约定优于配置的威力

系列导航:本文是《Spring框架第一性原理》系列的第4篇 第1篇:为什么我们需要Spring框架? 第2篇:IoC容器:从手动new到自动装配的演进 第3篇:AOP:从代码重复到面向切面编程 第4篇:Spring Boot:约定优于配置的威力(本文) 引子:配置地狱的噩梦 场景重现:创建一个Spring Web应用(传统方式) 想象你是2013年的Java开发者,接到任务:创建一个简单的RESTful API服务。使用传统Spring框架,你需要经历以下"噩梦": 第一步:手动配置依赖(pom.xml,约50行) <!-- 1. Spring核心依赖 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>4.3.9.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.3.9.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>4.3.9.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>4.3.9.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>4.3.9.RELEASE</version> </dependency> <!-- 2. Jackson JSON依赖 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.8.9</version> </dependency> <!-- 3. Servlet API --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <!-- 4. 日志依赖 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency> <!-- 问题:--> <!-- ❌ 依赖繁多:手动引入10+个依赖 --> <!-- ❌ 版本冲突:spring-core必须和spring-webmvc版本一致 --> <!-- ❌ 依赖遗漏:忘记jackson导致JSON序列化失败 --> 第二步:配置web.xml(约30行) <!-- src/main/webapp/WEB-INF/web.xml --> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <!-- 1. 配置Spring上下文监听器 --> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> <!-- 2. 配置Spring上下文配置文件位置 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/applicationContext.xml</param-value> </context-param> <!-- 3. 配置DispatcherServlet --> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/dispatcher-servlet.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <!-- 4. 配置Servlet映射 --> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <!-- 5. 配置字符编码过滤器 --> <filter> <filter-name>encodingFilter</filter-name> <filter-class> org.springframework.web.filter.CharacterEncodingFilter </filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app> <!-- 问题:--> <!-- ❌ XML冗长:30行配置只为启动Web应用 --> <!-- ❌ 路径硬编码:配置文件路径写死 --> <!-- ❌ 易出错:一个标签写错就启动失败 --> 第三步:配置applicationContext.xml(约40行) <!-- src/main/webapp/WEB-INF/applicationContext.xml --> <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <!-- 1. 启用注解扫描 --> <context:component-scan base-package="com.example"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan> <!-- 2. 加载properties文件 --> <context:property-placeholder location="classpath:application.properties"/> <!-- 3. 配置数据源 --> <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="jdbcUrl" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <property name="maximumPoolSize" value="20"/> </bean> <!-- 4. 配置JdbcTemplate --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> <!-- 5. 配置事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <!-- 6. 启用事务注解 --> <tx:annotation-driven transaction-manager="transactionManager"/> </beans> <!-- 问题:--> <!-- ❌ 配置繁琐:每个Bean都要显式配置 --> <!-- ❌ 命名空间复杂:xsi:schemaLocation写错就无法启动 --> <!-- ❌ 重复配置:每个项目都要写相同的配置 --> 第四步:配置dispatcher-servlet.xml(约30行) <!-- src/main/webapp/WEB-INF/dispatcher-servlet.xml --> <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 1. 扫描Controller --> <context:component-scan base-package="com.example.controller"/> <!-- 2. 启用Spring MVC注解 --> <mvc:annotation-driven> <mvc:message-converters> <!-- 配置JSON转换器 --> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"> <property name="objectMapper"> <bean class="com.fasterxml.jackson.databind.ObjectMapper"> <property name="dateFormat"> <bean class="java.text.SimpleDateFormat"> <constructor-arg value="yyyy-MM-dd HH:mm:ss"/> </bean> </property> </bean> </property> </bean> </mvc:message-converters> </mvc:annotation-driven> <!-- 3. 配置静态资源处理 --> <mvc:default-servlet-handler/> <!-- 4. 配置拦截器 --> <mvc:interceptors> <bean class="com.example.interceptor.LogInterceptor"/> </mvc:interceptors> </beans> 第五步:配置应用(application.properties) # 数据库配置 jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=123456 # 日志配置 logging.level.root=INFO logging.level.com.example=DEBUG 第六步:编写业务代码 // Controller @RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; @GetMapping("/{id}") public User getUser(@PathVariable Long id) { return userService.getUser(id); } } // Service @Service public class UserService { @Autowired private JdbcTemplate jdbcTemplate; public User getUser(Long id) { return jdbcTemplate.queryForObject( "SELECT * FROM users WHERE id = ?", new Object[]{id}, new BeanPropertyRowMapper<>(User.class) ); } } 第七步:部署到Tomcat # 1. 打包成WAR文件 mvn clean package # 2. 将WAR文件复制到Tomcat webapps目录 cp target/myapp.war /usr/local/tomcat/webapps/ # 3. 启动Tomcat /usr/local/tomcat/bin/startup.sh # 4. 访问应用 curl http://localhost:8080/myapp/api/users/1 统计:传统Spring Web应用的复杂度 维度 数量/行数 说明 配置文件数量 5个 pom.xml、web.xml、applicationContext.xml、dispatcher-servlet.xml、application.properties 配置代码行数 约200行 纯配置代码,不含业务逻辑 依赖数量 15+个 需要手动管理版本 外部容器依赖 Tomcat 必须部署到Servlet容器 启动时间 约30秒 Tomcat启动 + Spring初始化 学习曲线 陡峭 XML配置、命名空间、Schema 问题总结 传统Spring Web应用的五大痛点: 1. 配置地狱(Configuration Hell) ├─ XML配置文件冗长(200+行) ├─ 命名空间复杂(记不住schemaLocation) ├─ 配置分散(5个配置文件) └─ 重复配置(每个项目都要写) 2. 依赖管理噩梦(Dependency Hell) ├─ 依赖繁多(15+个Maven依赖) ├─ 版本冲突(Spring各模块版本必须一致) ├─ 依赖遗漏(忘记引入导致运行时错误) └─ 传递依赖混乱(A依赖B,B依赖C的不同版本) 3. 容器部署复杂 ├─ 必须依赖外部容器(Tomcat、Jetty) ├─ 打包成WAR文件 ├─ 部署步骤繁琐 └─ 环境不一致(开发环境 vs 生产环境) 4. 启动速度慢 ├─ Tomcat启动耗时 ├─ Spring容器初始化耗时 └─ 总启动时间:30秒+ 5. 学习成本高 ├─ XML配置语法 ├─ Spring命名空间 ├─ 容器部署知识 └─ 调试困难(配置错误定位难) 对比:Spring Boot的"魔法" 同样的需求,Spring Boot只需要这样: 第一步:创建项目(使用Spring Initializr) 访问 https://start.spring.io/,选择: ...

2025-11-03 · maneng

线程池与异步编程:从Thread到CompletableFuture的演进

引子:一个Web服务器的性能优化之路 假设你正在开发一个Web服务器,每个HTTP请求需要启动一个新线程来处理。看似简单的设计,却隐藏着严重的性能问题。 场景A:为每个请求创建新线程 /** * 方案1:为每个请求创建新线程(性能差) */ public class ThreadPerRequestServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(8080); System.out.println("服务器启动,监听8080端口"); while (true) { Socket socket = serverSocket.accept(); // 为每个请求创建新线程 new Thread(() -> { try { handleRequest(socket); } catch (IOException e) { e.printStackTrace(); } finally { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } }).start(); } } private static void handleRequest(Socket socket) throws IOException { // 模拟请求处理(读取请求、业务处理、返回响应) InputStream in = socket.getInputStream(); OutputStream out = socket.getOutputStream(); // 简化处理 byte[] buffer = new byte[1024]; in.read(buffer); String response = "HTTP/1.1 200 OK\r\n\r\nHello World"; out.write(response.getBytes()); } } /* 性能测试(使用JMeter压测): 并发数:1000 请求总数:10000 结果: ├─ 吞吐量:500 req/s ├─ 平均响应时间:2000ms ├─ CPU使用率:60% └─ 内存使用:峰值2GB 问题分析: 1. 线程创建开销大 ├─ 每个线程需要1MB栈空间 ├─ 1000个线程 = 1GB内存 └─ 线程创建/销毁耗时(ms级别) 2. 上下文切换频繁 ├─ 1000个线程竞争CPU ├─ 大量时间花在线程切换 └─ CPU利用率低 3. 系统资源耗尽 ├─ 线程数无限制 ├─ 可能导致OOM └─ 系统崩溃 */ 场景B:使用线程池 /** * 方案2:使用线程池(性能好) */ public class ThreadPoolServer { // 创建固定大小的线程池 private static final ExecutorService executor = Executors.newFixedThreadPool(100); public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(8080); System.out.println("服务器启动,监听8080端口"); while (true) { Socket socket = serverSocket.accept(); // 提交任务到线程池 executor.submit(() -> { try { handleRequest(socket); } catch (IOException e) { e.printStackTrace(); } finally { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } }); } } private static void handleRequest(Socket socket) throws IOException { // 同上 InputStream in = socket.getInputStream(); OutputStream out = socket.getOutputStream(); byte[] buffer = new byte[1024]; in.read(buffer); String response = "HTTP/1.1 200 OK\r\n\r\nHello World"; out.write(response.getBytes()); } } /* 性能测试(同样的压测条件): 并发数:1000 请求总数:10000 结果: ├─ 吞吐量:5000 req/s ← 提升10倍! ├─ 平均响应时间:200ms ← 降低10倍! ├─ CPU使用率:85% ← 提升25% └─ 内存使用:峰值200MB ← 降低10倍! 优势分析: 1. 线程复用 ├─ 100个线程处理1000个请求 ├─ 无需频繁创建/销毁线程 └─ 节省大量时间和内存 2. 减少上下文切换 ├─ 线程数固定(100个) ├─ 上下文切换次数大幅减少 └─ CPU利用率提升 3. 资源可控 ├─ 线程数有上限 ├─ 内存使用可预测 └─ 系统稳定 */ 性能对比总结 对比维度 直接创建线程 线程池 提升 吞吐量 500 req/s 5000 req/s 10倍 响应时间 2000ms 200ms 10倍 CPU使用率 60% 85% +25% 内存峰值 2GB 200MB 10倍 线程数 1000+ 100 可控 核心洞察: ...

2025-11-03 · maneng

Redis持久化:RDB与AOF的权衡

一、引子:Redis宕机后数据去哪了? Redis是内存数据库,数据存储在内存中。那么问题来了:Redis宕机后,数据还在吗? 1.1 场景:双11大促数据丢失事故 场景:电商网站,双11大促 00:00:00 - 大促开始,流量暴增 00:00:01 - Redis缓存:10万个商品详情、50万个用户Session 00:05:00 - 服务器断电(机房故障) 00:05:01 - Redis进程被杀,内存数据全部丢失 00:10:00 - 服务器恢复,Redis重启 00:10:01 - Redis启动成功,但数据为空! 问题: 1. 所有商品详情缓存丢失 → 10万次数据库查询 2. 所有用户Session丢失 → 50万用户被强制登出 3. 缓存预热需要10分钟 → 数据库压力巨大 4. 用户体验极差 → 大量用户流失 损失: - 用户流失:50万用户 × 10% = 5万用户 - 订单损失:5万用户 × 20% = 1万单 - GMV损失:1万单 × 200元 = 200万元 核心问题:Redis是内存数据库,断电后数据丢失,需要持久化。 1.2 持久化的本质 持久化:将内存中的数据保存到磁盘,重启后可以恢复。 为什么需要持久化? 原因1:数据安全 - Redis宕机、服务器断电、进程被杀 - 内存数据丢失 - 持久化后可以恢复 原因2:快速恢复 - 没有持久化:需要从数据库重新加载(10分钟+) - 有持久化:直接加载持久化文件(1分钟) 原因3:数据备份 - 定期备份持久化文件 - 灾难恢复(机房火灾、磁盘损坏) Redis的两种持久化方式: ...

2025-11-03 · maneng

Spring Cloud第一性原理:从单体到微服务的架构演进

系列导航:本文是《Spring框架第一性原理》系列的第5篇 第1篇:为什么我们需要Spring框架? 第2篇:IoC容器:从手动new到自动装配的演进 第3篇:AOP:从代码重复到面向切面编程 第4篇:Spring Boot:约定优于配置的威力 第5篇:Spring Cloud:从单体到微服务的架构演进(本文) 引子:单体应用的困境 场景重现:一个电商系统的演进之路 让我们从一个真实的电商系统的成长历程说起。 第一阶段:创业初期(2015年) 团队规模:5人(2个后端、1个前端、1个产品、1个UI) 技术选型:单体架构 + Spring Boot 系统架构: ┌─────────────────────────────────────┐ │ 电商单体应用 │ │ (monolithic-ecommerce-app) │ ├─────────────────────────────────────┤ │ 用户模块 (UserModule) │ │ 商品模块 (ProductModule) │ │ 订单模块 (OrderModule) │ │ 库存模块 (InventoryModule) │ │ 支付模块 (PaymentModule) │ │ 物流模块 (LogisticsModule) │ │ 营销模块 (MarketingModule) │ ├─────────────────────────────────────┤ │ Spring Boot + MyBatis + MySQL │ └─────────────────────────────────────┘ ↓ MySQL数据库 代码结构: ecommerce-app/ ├── src/main/java/com/example/ │ ├── user/ # 用户模块 │ │ ├── UserController.java │ │ ├── UserService.java │ │ └── UserRepository.java │ ├── product/ # 商品模块 │ │ ├── ProductController.java │ │ ├── ProductService.java │ │ └── ProductRepository.java │ ├── order/ # 订单模块 │ │ ├── OrderController.java │ │ ├── OrderService.java │ │ └── OrderRepository.java │ ├── inventory/ # 库存模块 │ ├── payment/ # 支付模块 │ ├── logistics/ # 物流模块 │ └── marketing/ # 营销模块 └── pom.xml 典型业务流程(创建订单): ...

2025-11-03 · maneng

并发集合:从HashMap到ConcurrentHashMap的演进

引子:一个缓存系统的并发灾难 假设你正在开发一个高并发的缓存系统,使用HashMap存储缓存数据。看似简单的设计,却可能导致系统崩溃。 场景:本地缓存的并发问题 /** * 方案1:使用HashMap(线程不安全) */ public class UnsafeCache { private Map<String, String> cache = new HashMap<>(); public void put(String key, String value) { cache.put(key, value); } public String get(String key) { return cache.get(key); } public static void main(String[] args) throws InterruptedException { UnsafeCache cache = new UnsafeCache(); // 10个线程并发写入 Thread[] threads = new Thread[10]; for (int i = 0; i < 10; i++) { final int threadId = i; threads[i] = new Thread(() -> { for (int j = 0; j < 1000; j++) { cache.put("key-" + threadId + "-" + j, "value-" + j); } }); } for (Thread t : threads) t.start(); for (Thread t : threads) t.join(); System.out.println("缓存大小:" + cache.cache.size()); System.out.println("期望大小:10000"); } } /* 执行结果(多次运行): 第1次:缓存大小:9856 ❌ 数据丢失 第2次:缓存大小:9923 ❌ 数据丢失 第3次:程序卡死 ❌ 死循环(JDK 1.7) 第4次:抛出ConcurrentModificationException 问题列表: 1. 数据丢失(最常见) └─ 多个线程同时put,覆盖彼此的修改 2. 死循环(JDK 1.7的经典Bug) └─ 扩容时形成环形链表,get()陷入死循环 3. ConcurrentModificationException └─ 迭代时其他线程修改了HashMap 4. 数据不一致 └─ size()返回错误的值 真实案例: 某公司生产环境,使用HashMap作为本地缓存 高峰期CPU飙到100%,经排查是HashMap死循环 导致服务不可用,损失数百万 */ 解决方案演进 /** * 方案2:使用Hashtable(线程安全,但性能差) */ public class HashtableCache { private Map<String, String> cache = new Hashtable<>(); public void put(String key, String value) { cache.put(key, value); } public String get(String key) { return cache.get(key); } } /* Hashtable的问题: 1. 整个方法都加synchronized(粗粒度锁) 2. 读写都加锁(读也需要互斥) 3. 性能差(高并发场景) 性能测试: 并发读写10000次 ├─ HashMap(不安全):50ms ├─ Hashtable(安全):500ms └─ 性能下降10倍! */ /** * 方案3:使用Collections.synchronizedMap(性能同样差) */ public class SynchronizedMapCache { private Map<String, String> cache = Collections.synchronizedMap(new HashMap<>()); public void put(String key, String value) { cache.put(key, value); } public String get(String key) { return cache.get(key); } } /* Collections.synchronizedMap的实现: public V put(K key, V value) { synchronized (mutex) { // 全局锁 return m.put(key, value); } } 问题:与Hashtable相同,粗粒度锁 */ /** * 方案4:使用ConcurrentHashMap(最佳方案) */ public class ConcurrentHashMapCache { private Map<String, String> cache = new ConcurrentHashMap<>(); public void put(String key, String value) { cache.put(key, value); } public String get(String key) { return cache.get(key); } public static void main(String[] args) throws InterruptedException { ConcurrentHashMapCache cache = new ConcurrentHashMapCache(); // 10个线程并发写入 Thread[] threads = new Thread[10]; for (int i = 0; i < 10; i++) { final int threadId = i; threads[i] = new Thread(() -> { for (int j = 0; j < 1000; j++) { cache.put("key-" + threadId + "-" + j, "value-" + j); } }); } long start = System.currentTimeMillis(); for (Thread t : threads) t.start(); for (Thread t : threads) t.join(); long end = System.currentTimeMillis(); System.out.println("缓存大小:" + cache.cache.size()); System.out.println("期望大小:10000"); System.out.println("耗时:" + (end - start) + "ms"); } } /* 执行结果: 缓存大小:10000 ✅ 正确 期望大小:10000 耗时:65ms ConcurrentHashMap的优势: 1. 线程安全 2. 性能好(细粒度锁/无锁) 3. 读操作无锁(大部分情况) 4. 不会死循环 */ 性能对比总结 方案 线程安全 读性能 写性能 适用场景 HashMap ❌ 极快 极快 单线程 Hashtable ✅ 慢(加锁) 慢(加锁) 低并发 SynchronizedMap ✅ 慢(加锁) 慢(加锁) 低并发 ConcurrentHashMap ✅ 快(无锁) 较快(细粒度锁) 高并发 核心洞察: ...

2025-11-03 · maneng

Redis实战:分布式锁、消息队列、缓存设计

一、分布式锁:从SETNX到Redlock 1.1 为什么需要分布式锁? 场景:秒杀系统的超卖问题 // ❌ 错误:单机锁无法解决分布式超卖 @Service public class SeckillService { @Autowired private ProductRepository productRepository; /** * 秒杀下单(单机锁) */ public synchronized Order seckill(Long productId, Long userId) { // 1. 检查库存 Integer stock = productRepository.getStock(productId); if (stock <= 0) { throw new SoldOutException("商品已售罄"); } // 2. 扣减库存 productRepository.decrementStock(productId); // 3. 创建订单 return orderService.createOrder(productId, userId); } } // 问题: // 假设有3台服务器(Server A、B、C) // T1: Server A:检查库存=1(通过) // T2: Server B:检查库存=1(通过) // T3: Server A:扣减库存=0,创建订单1 // T4: Server B:扣减库存=-1(超卖!),创建订单2 // synchronized只能锁住单机JVM内的线程 // 无法锁住分布式环境的多个进程 核心问题:分布式环境下,单机锁无效,需要分布式锁。 ...

2025-11-03 · maneng

并发设计模式与最佳实践

一、不可变对象模式 1.1 不可变对象的设计 /** * 标准的不可变对象设计 */ public final class ImmutableUser { private final String name; private final int age; private final List<String> hobbies; public ImmutableUser(String name, int age, List<String> hobbies) { this.name = name; this.age = age; // 防御性复制 this.hobbies = new ArrayList<>(hobbies); } public String getName() { return name; } public int getAge() { return age; } public List<String> getHobbies() { // 返回不可变视图 return Collections.unmodifiableList(hobbies); } } /* 不可变对象的五大要求: 1. ✅ 类声明为final 2. ✅ 所有字段都是final 3. ✅ 所有字段都是private 4. ✅ 不提供setter方法 5. ✅ 可变字段防御性复制 优势: ├─ 天然线程安全 ├─ 可以安全共享 └─ 适合做缓存key 适用场景:String、Integer、LocalDate等 */ 二、ThreadLocal模式 2.1 基本使用 /** * ThreadLocal:线程本地存储 */ public class ThreadLocalExample { // 每个线程独立的SimpleDateFormat private static ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); public static String formatDate(Date date) { return dateFormat.get().format(date); // 线程安全 } public static void main(String[] args) { // 10个线程并发格式化日期 for (int i = 0; i < 10; i++) { new Thread(() -> { System.out.println(formatDate(new Date())); }).start(); } } } /* 使用场景: 1. SimpleDateFormat(线程不安全) 2. 数据库连接(每个线程独立连接) 3. 用户上下文(Spring Security) */ 2.2 内存泄漏问题 /** * ThreadLocal内存泄漏问题 */ public class ThreadLocalMemoryLeak { private static ThreadLocal<byte[]> threadLocal = new ThreadLocal<>(); public static void main(String[] args) throws InterruptedException { // 线程池场景 ExecutorService executor = Executors.newFixedThreadPool(10); for (int i = 0; i < 100; i++) { executor.submit(() -> { // 存储大对象 threadLocal.set(new byte[1024 * 1024]); // 1MB // 业务逻辑 // ... // ❌ 忘记清理,导致内存泄漏 // threadLocal.remove(); // 正确做法 }); } executor.shutdown(); } /* 内存泄漏原因: 1. ThreadLocal存储在ThreadLocalMap中 2. key是弱引用(ThreadLocal) 3. value是强引用(实际对象) 4. 线程池线程复用,ThreadLocalMap一直存在 5. value无法被GC,导致内存泄漏 解决方案: try { threadLocal.set(value); // 业务逻辑 } finally { threadLocal.remove(); // 必须清理 } */ } 三、两阶段终止模式 3.1 优雅停止线程 /** * 正确的线程停止方式 */ public class GracefulShutdown { private volatile boolean running = true; public void start() { Thread thread = new Thread(() -> { while (running) { // 检查标志位 try { // 业务逻辑 Thread.sleep(1000); } catch (InterruptedException e) { // 响应中断 Thread.currentThread().interrupt(); break; } } // 清理资源 cleanup(); }); thread.start(); } public void stop() { running = false; // 第一阶段:设置标志位 } private void cleanup() { // 第二阶段:清理资源 System.out.println("清理资源"); } /* 错误做法: thread.stop(); // ❌ 已废弃,不安全 正确做法: 1. volatile标志位 2. interrupt()中断 3. 两阶段:设置标志 + 清理资源 */ } 四、读写锁模式 4.1 ReentrantReadWriteLock /** * 读写锁:读多写少场景 */ public class ReadWriteLockExample { private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); private final Map<String, String> cache = new HashMap<>(); // 读操作:共享锁 public String get(String key) { rwLock.readLock().lock(); try { return cache.get(key); } finally { rwLock.readLock().unlock(); } } // 写操作:排他锁 public void put(String key, String value) { rwLock.writeLock().lock(); try { cache.put(key, value); } finally { rwLock.writeLock().unlock(); } } /* 读写锁特性: ├─ 读-读:可并发 ├─ 读-写:互斥 └─ 写-写:互斥 性能提升: 读多写少场景,性能比synchronized高3-5倍 */ } 4.2 StampedLock(JDK 8) /** * StampedLock:性能更好的读写锁 */ public class StampedLockExample { private final StampedLock lock = new StampedLock(); private double x, y; // 乐观读 public double distanceFromOrigin() { long stamp = lock.tryOptimisticRead(); // 乐观读 double currentX = x; double currentY = y; if (!lock.validate(stamp)) { // 验证是否被修改 stamp = lock.readLock(); // 升级为悲观读锁 try { currentX = x; currentY = y; } finally { lock.unlockRead(stamp); } } return Math.sqrt(currentX * currentX + currentY * currentY); } // 写锁 public void move(double deltaX, double deltaY) { long stamp = lock.writeLock(); try { x += deltaX; y += deltaY; } finally { lock.unlockWrite(stamp); } } /* StampedLock优势: 1. 乐观读不加锁(性能极高) 2. 读写性能比ReentrantReadWriteLock高 3. 适合读多写少场景 注意: ❌ 不支持重入 ❌ 不支持Condition */ } 五、并发编程最佳实践 5.1 核心原则 1. 优先使用不可变对象 └─ String、Integer、LocalDate 2. 减少锁的范围 └─ 只锁必要的代码 3. 避免锁嵌套 └─ 容易死锁 4. 使用并发工具类 ├─ AtomicInteger替代synchronized计数 ├─ ConcurrentHashMap替代Hashtable ├─ CountDownLatch/CyclicBarrier协调线程 └─ BlockingQueue实现生产者-消费者 5. 线程池管理 ├─ 自定义ThreadPoolExecutor(禁用Executors) ├─ 优雅关闭(shutdown + awaitTermination) └─ 线程命名(便于排查) 6. 异常处理 ├─ 捕获InterruptedException ├─ 设置UncaughtExceptionHandler └─ 避免异常导致线程死亡 5.2 常见陷阱 /** * 常见并发陷阱 */ public class ConcurrencyTraps { // ❌ 陷阱1:双重检查锁定错误 private static Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); // 需要volatile } } } return instance; } // ✅ 正确:private static volatile Singleton instance; // ❌ 陷阱2:SimpleDateFormat共享 private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); // ✅ 正确:使用ThreadLocal或DateTimeFormatter // ❌ 陷阱3:复合操作不原子 if (!map.containsKey(key)) { map.put(key, value); } // ✅ 正确:map.putIfAbsent(key, value); // ❌ 陷阱4:忘记释放锁 lock.lock(); // 业务逻辑 lock.unlock(); // 异常时不会执行 // ✅ 正确:try-finally // ❌ 陷阱5:使用Executors工厂方法 Executors.newFixedThreadPool(10); // 无界队列 // ✅ 正确:自定义ThreadPoolExecutor static class Singleton { private static volatile Singleton instance; public static Singleton getInstance() { return instance; } } } 5.3 性能调优 1. 减少锁竞争 ├─ 缩小锁范围 ├─ 降低锁粒度(分段锁) └─ 使用无锁算法(CAS) 2. 减少上下文切换 ├─ 控制线程数(CPU核心数 × 2) ├─ 使用协程/虚拟线程(JDK 19+) └─ 避免频繁阻塞 3. 内存优化 ├─ 对象池(避免频繁创建) ├─ ThreadLocal注意清理 └─ 合理设置JVM参数 4. 监控与调优 ├─ JProfiler监控线程状态 ├─ Arthas诊断死锁 └─ 压测验证性能 六、总结:并发编程知识体系 6.1 核心知识点回顾 第1篇:并发三大问题 ├─ 可见性:CPU缓存导致 ├─ 原子性:指令交错执行 ├─ 有序性:指令重排序 └─ JMM:happens-before规则 第2篇:同步机制 ├─ synchronized:锁升级(偏向锁→轻量级锁→重量级锁) ├─ Lock:ReentrantLock、公平锁/非公平锁 ├─ AQS:CLH队列、独占/共享模式 └─ 死锁:四个必要条件、预防策略 第3篇:无锁编程 ├─ CAS:CPU的CMPXCHG指令 ├─ Atomic:AtomicInteger、LongAdder ├─ 并发工具:CountDownLatch、Semaphore └─ 无锁数据结构:无锁栈、无锁队列 第4篇:线程池与异步 ├─ ThreadPoolExecutor:七参数、四拒绝策略 ├─ ForkJoinPool:工作窃取算法 ├─ CompletableFuture:组合式异步编程 └─ 最佳实践:配置、监控、优雅关闭 第5篇:并发集合 ├─ ConcurrentHashMap:分段锁→CAS+synchronized ├─ BlockingQueue:生产者-消费者模式 ├─ CopyOnWriteArrayList:写时复制 └─ 选择指南:性能对比、适用场景 第6篇:设计模式与实践 ├─ 不可变对象:天然线程安全 ├─ ThreadLocal:线程本地存储 ├─ 读写锁:读多写少优化 └─ 最佳实践:避坑指南、性能调优 6.2 学习路径建议 阶段1:理解原理(1-2周) ├─ JMM、happens-before ├─ 三大问题 └─ volatile、final 阶段2:掌握同步(2-3周) ├─ synchronized原理 ├─ Lock接口 └─ AQS源码 阶段3:无锁编程(2-3周) ├─ CAS原理 ├─ Atomic类 └─ 并发工具类 阶段4:工程实践(4-6周) ├─ 线程池配置 ├─ 并发集合使用 ├─ 实际项目应用 └─ 性能调优 总计:约3个月系统学习 6.3 推荐资源 经典书籍: 1. 《Java并发编程实战》 ⭐⭐⭐⭐⭐ 2. 《Java并发编程的艺术》 ⭐⭐⭐⭐ 3. 《深入理解Java虚拟机》 ⭐⭐⭐⭐⭐ 官方文档: 1. JDK源码(java.util.concurrent包) 2. Java Language Specification - Chapter 17 3. JSR-133: Java Memory Model 开源项目: 1. Netty(高性能网络框架) 2. Disruptor(高性能队列) 3. Spring(DI容器、事务管理) 结语 Java并发编程系列完结 ...

2025-11-03 · maneng

Redis第一性原理:为什么我们需要缓存?

一、引子:商品详情接口的性能进化之路 想象你正在开发一个电商平台的商品详情页面,每次用户访问都需要查询商品基本信息、品牌信息、类目信息、库存信息和商品图片。让我们看看三种不同的实现方式,以及它们各自的性能表现。 1.1 场景A:无缓存(直接查数据库) 这是最直接的实现方式:每次请求都查询数据库。 @Service public class ProductService { @Autowired private ProductRepository productRepository; @Autowired private BrandRepository brandRepository; @Autowired private CategoryRepository categoryRepository; @Autowired private InventoryRepository inventoryRepository; @Autowired private ProductImageRepository productImageRepository; /** * 查询商品详情(每次请求都查数据库) * 平均耗时:100ms * QPS上限:500 */ public ProductDetailVO getProductDetail(Long productId) { // 1. 查询商品基本信息(20ms) Product product = productRepository.findById(productId); if (product == null) { throw new ProductNotFoundException("商品不存在:" + productId); } // 2. 查询品牌信息(20ms) Brand brand = brandRepository.findById(product.getBrandId()); // 3. 查询类目信息(20ms) Category category = categoryRepository.findById(product.getCategoryId()); // 4. 查询库存信息(20ms) Inventory inventory = inventoryRepository.findByProductId(productId); // 5. 查询商品图片(20ms,可能有N+1查询问题) List<ProductImage> images = productImageRepository.findByProductId(productId); // 6. 组装返回对象 ProductDetailVO vo = new ProductDetailVO(); vo.setProductId(product.getId()); vo.setProductName(product.getName()); vo.setPrice(product.getPrice()); vo.setBrandName(brand.getName()); vo.setCategoryName(category.getName()); vo.setStock(inventory.getStock()); vo.setImages(images); return vo; } } 性能数据(压测工具:JMeter,1000并发): ...

2025-11-03 · maneng

从HashMap到Redis:分布式缓存的演进

一、引子:一个用户会话缓存的演进之路 假设你正在开发一个电商网站的用户会话管理功能。每次用户请求都需要验证身份,最初的实现是每次都查询数据库,但随着用户量增长,数据库压力越来越大。让我们看看这个功能如何一步步演进,从最简单的HashMap到最终的Redis分布式缓存。 1.1 场景0:无缓存(每次查数据库) 最直接的实现:每次请求都查询数据库验证用户身份。 @RestController public class UserController { @Autowired private UserRepository userRepository; /** * 获取用户信息(每次查数据库) * 问题:数据库压力大,响应慢 */ @GetMapping("/api/user/info") public UserVO getUserInfo(@RequestHeader("token") String token) { // 1. 根据token查询用户ID(查数据库) Long userId = tokenRepository.findUserIdByToken(token); if (userId == null) { throw new UnauthorizedException("未登录"); } // 2. 查询用户详细信息(查数据库) User user = userRepository.findById(userId); if (user == null) { throw new UserNotFoundException("用户不存在"); } return convertToVO(user); } } 性能数据: 指标 数值 说明 平均响应时间 50ms 2次SQL查询 QPS上限 1000 数据库连接池限制 数据库压力 100% 每次请求都查库 问题: ...

2025-11-03 · maneng

Redis五大数据结构:从场景到实现

一、引子:为什么Redis需要五大数据结构? 很多人的疑问:Memcached只有String一种数据结构,Redis为什么需要五种? 核心答案:不同的业务场景需要不同的数据结构。 1.1 如果只有String会怎样? 假设我们要实现一个排行榜功能,只有String的话: // ❌ 方案1:用String存储整个排行榜(JSON序列化) // 问题:每次更新一个用户分数,需要序列化/反序列化整个排行榜 public void updateScore(Long userId, int score) { // 1. 读取整个排行榜(反序列化) String json = redisTemplate.opsForValue().get("rank:list"); List<User> rankList = JSON.parseArray(json, User.class); // 10000个用户 // 2. 更新一个用户的分数 for (User user : rankList) { if (user.getId().equals(userId)) { user.setScore(score); break; } } // 3. 重新排序 rankList.sort((a, b) -> b.getScore() - a.getScore()); // 4. 写入Redis(序列化) String newJson = JSON.toJSONString(rankList); redisTemplate.opsForValue().set("rank:list", newJson); } // 性能问题: // - 读取:反序列化10000个用户,耗时100ms // - 排序:O(NlogN) = 10000*log(10000) ≈ 130000次比较 // - 写入:序列化10000个用户,耗时100ms // 总耗时:200ms+(单次更新) // ✅ 方案2:使用Redis ZSet(有序集合) // 优势:Redis内部维护排序,O(logN)复杂度 public void updateScore(Long userId, int score) { redisTemplate.opsForZSet().add("rank:zset", userId.toString(), score); } // 性能提升: // - 写入:O(logN) = log(10000) ≈ 13次比较 // - 总耗时:1ms // 性能提升:200倍 核心洞察: ...

2025-11-03 · maneng

如约数科科技工作室

浙ICP备2025203501号

👀 本站总访问量 ...| 👤 访客数 ...| 📅 今日访问 ...