尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

并发编程核心:线程安全与性能优化实战

并发编程核心:线程安全与性能优化实战 1. 为什么我们需要并发编程在单核CPU时代程序执行是顺序的就像一个人在厨房里做饭——切完菜才能开火炒菜。但现代计算机都是多核处理器就像有了多个厨师可以同时工作。如果还坚持顺序执行就相当于让其他厨师闲着看一个人忙活这显然是对计算资源的巨大浪费。我曾在电商大促期间遇到过这样的案例一个商品详情页接口需要串行调用库存服务、价格服务和评价服务每个服务耗时约100ms。当QPS达到1000时系统直接崩溃。改为并发调用后接口耗时从300ms降到120ms吞吐量提升了2.5倍。这就是并发编程的威力。注意并发(Concurrency)和并行(Parallelism)是不同的概念。并发是逻辑上的同时发生单核时间片轮转并行是物理上的同时执行多核真正同步。本文主要讨论并发场景。2. 线程并发的基本执行单元2.1 线程的本质与实现线程是操作系统能够进行运算调度的最小单位它被包含在进程之中。用公司架构类比进程 一家公司拥有独立办公空间和资金线程 公司员工共享办公室资源但独立工作Java中创建线程的三种典型方式// 方式1继承Thread类 class MyThread extends Thread { public void run() { System.out.println(Thread running); } } // 方式2实现Runnable接口 class MyRunnable implements Runnable { public void run() { System.out.println(Runnable running); } } // 方式3使用Lambda表达式 new Thread(() - { System.out.println(Lambda thread running); }).start();实际项目中更推荐方式2和3因为Java不支持多重继承继承Thread类会占用继承名额Runnable接口更符合面向对象的设计原则线程池只能接收Runnable/Callable任务2.2 线程的生命周期与状态转换线程从创建到销毁会经历多个状态以Java为例NEW刚创建未启动RUNNABLE可运行状态可能在执行也可能在等待CPU时间片BLOCKED等待监视器锁同步代码块WAITING无限期等待wait()/join()TIMED_WAITING限期等待sleep()/wait(timeout)TERMINATED执行结束状态转换示意图NEW → RUNNABLE ↔ BLOCKED ↓ ↓ TERMINATED ← WAITING ↑ TIMED_WAITING我在排查一个线上问题时发现线程大量处于BLOCKED状态。经查是因为一个同步方法执行时间过长涉及数据库操作改为更细粒度的锁后性能提升40%。这说明理解线程状态对性能调优至关重要。3. 线程安全与同步机制3.1 竞态条件与临界区问题当多个线程同时访问共享资源时如果没有正确同步就会出现竞态条件(Race Condition)。举个转账的例子class Account { private int balance; // 不安全的实现 void transfer(Account target, int amount) { this.balance - amount; target.balance amount; } }如果两个线程同时执行A向B转账可能出现线程1读取A.balance100线程2读取A.balance100线程1计算A.balance100-5050线程2计算A.balance100-3070最终A.balance可能是50或70而不是预期的203.2 同步解决方案对比方案实现方式优点缺点适用场景synchronized方法/代码块加锁简单易用性能较差简单的同步需求ReentrantLock显式锁API可中断、可定时、公平锁需手动释放复杂锁需求volatile变量可见性轻量级不保证原子性状态标志位Atomic类CAS操作高性能只能保护单个变量计数器等场景实际项目中我曾用AtomicInteger替代synchronized实现计数器QPS从8000提升到12000。但要注意ABA问题必要时使用AtomicStampedReference。4. 等待通知机制深度解析4.1 wait/notify的工作原理等待通知机制是线程间协作的核心方式其正确使用需要理解几个关键点必须在同步代码块中调用持有对象监视器wait()会释放锁notify()不会立即释放锁经典的生产者-消费者模式实现class Buffer { private QueueInteger queue new LinkedList(); private int capacity; public Buffer(int capacity) { this.capacity capacity; } public synchronized void produce(int item) throws InterruptedException { while (queue.size() capacity) { wait(); // 缓冲区满等待 } queue.offer(item); notifyAll(); // 通知消费者 } public synchronized int consume() throws InterruptedException { while (queue.isEmpty()) { wait(); // 缓冲区空等待 } int item queue.poll(); notifyAll(); // 通知生产者 return item; } }4.2 常见陷阱与最佳实践虚假唤醒问题wait()返回后必须重新检查条件用while而不是ifnotify vs notifyAllnotify随机唤醒一个notifyAll唤醒所有。在大多数情况下应该使用notifyAll丢失唤醒问题如果notify先于wait调用通知会丢失。这解释了为什么条件检查要用while循环我在消息队列实现中就遇到过虚假唤醒问题消费者线程被唤醒后直接操作队列导致NPE。改为while循环检查后问题解决。5. 线程池并发编程的工业级解决方案5.1 为什么需要线程池直接创建线程的问题创建/销毁线程开销大涉及系统调用无限制创建会导致资源耗尽缺乏统一管理难以监控、统计线程池的优势重用已有线程降低开销控制并发数量避免资源竞争提供定时执行、定期执行等功能5.2 ThreadPoolExecutor核心参数ThreadPoolExecutor( int corePoolSize, // 核心线程数 int maximumPoolSize, // 最大线程数 long keepAliveTime, // 空闲线程存活时间 TimeUnit unit, // 时间单位 BlockingQueueRunnable workQueue, // 任务队列 RejectedExecutionHandler handler // 拒绝策略 )四种拒绝策略AbortPolicy默认抛出RejectedExecutionExceptionCallerRunsPolicy由调用线程执行该任务DiscardPolicy直接丢弃任务DiscardOldestPolicy丢弃队列最前面的任务5.3 线程池配置实践建议CPU密集型任务核心线程数 CPU核数 1IO密集型任务核心线程数 CPU核数 * 2混合型任务拆分不同线程池处理队列选择需要控制并发量ArrayBlockingQueue大量短时任务SynchronousQueue优先级任务PriorityBlockingQueue在电商系统中我们将订单创建IO密集和库存扣减CPU密集拆分到不同线程池配合合适的队列大小和拒绝策略在大促期间保持了系统稳定。6. 高级并发模式与应用6.1 Fork/Join框架适用于可分解的递归型任务采用工作窃取算法提高CPU利用率。典型实现class FibonacciTask extends RecursiveTaskInteger { final int n; FibonacciTask(int n) { this.n n; } protected Integer compute() { if (n 1) return n; FibonacciTask f1 new FibonacciTask(n - 1); f1.fork(); FibonacciTask f2 new FibonacciTask(n - 2); return f2.compute() f1.join(); } }6.2 CompletableFuture异步编程Java 8引入的函数式异步编程工具CompletableFuture.supplyAsync(() - { // 异步获取商品信息 return getProductInfo(productId); }).thenApplyAsync(product - { // 异步计算折扣 return calculateDiscount(product); }).thenAcceptAsync(result - { // 异步保存结果 saveToDatabase(result); }).exceptionally(ex - { // 异常处理 log.error(Process failed, ex); return null; });在实际项目中用CompletableFuture重构串行调用链后接口响应时间从450ms降至180ms。7. 并发调试与性能优化7.1 常见并发问题定位死锁检测jstack查看线程dump查找BLOCKED状态和持有锁的信息使用jConsole或VisualVM的可视化工具线程泄漏排查监控线程数增长趋势检查线程池配置特别是非核心线程超时时间分析线程栈确定泄漏点性能瓶颈分析使用Arthas的monitor命令统计方法调用耗时用async-profiler进行CPU热点分析关注锁竞争情况JFR的lock视图7.2 优化实战经验减少锁粒度从方法级锁改为代码块锁读写分离用ReadWriteLock替代独占锁无锁化设计使用ConcurrentHashMap等并发容器线程本地存储ThreadLocal避免共享变量异步化改造将同步调用改为异步消息在最近一次性能优化中我们将用户会话管理从HashMapsynchronized改为ConcurrentHashMapTPS从1200提升到3500。但要注意ConcurrentHashMap的size()方法不是精确值需要精确计数时可以用AtomicLong配合。
返回列表