news 2026/4/25 7:03:37

Java 并发编程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java 并发编程

一、为什么我们必须搞懂并发编程?

很多人会问:"我就是个写业务 CRUD 的,平时很少写多线程代码,学并发有什么用?" 我给你三个无法拒绝的理由:

1. 解决线上核心故障

Java 服务线上 80% 的诡异问题,都和并发相关。超卖、重复数据、脏读、服务卡死、CPU 飙升,这些问题的根因几乎都在并发层面。不懂并发,你连问题出在哪都找不到,更别说解决了。

2. 充分利用服务器性能

现在的服务器都是多核 CPU,单线程程序只能利用一个核心,多核的性能完全被浪费了。而合理的并发编程,能让你的服务吞吐量提升数倍。

3. 主流框架的底层基础

你每天用的 Spring、MyBatis、Tomcat、Redis 客户端、消息队列,底层全是并发编程。不懂并发,你永远只能停留在 "用框架" 的层面,出了问题根本不知道怎么排查。

4. 面试晋升的硬门槛

不管是初级升中级,还是中级升高级,并发编程都是 Java 面试必问的核心内容。不懂并发,你永远只能停留在 "业务开发" 的舒适区,很难突破职业瓶颈。

二、线程安全的本质:三大特性与 JMM 内存模型

所有的并发问题,根源都来自于 Java 内存模型(JMM)的三大特性:原子性、可见性、有序性。线程安全的代码,必须同时保证这三个特性。

2.1 什么是 JMM 内存模型?

Java 内存模型(Java Memory Model)是 Java 虚拟机规范中定义的,用来解决多线程环境下,CPU 缓存、寄存器和主内存之间的数据同步问题。

简单来说,每个线程都有自己的工作内存(对应 CPU 的缓存和寄存器),所有的变量都存储在主内存中。线程对变量的所有操作,都必须在工作内存中进行,不能直接读写主内存。不同线程之间也无法直接访问对方的工作内存,线程间的变量传递必须通过主内存来完成。

plaintext

┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 线程A工作内存 │ │ 线程B工作内存 │ │ 线程C工作内存 │ └───────┬─────┘ └───────┬─────┘ └───────┬─────┘ │ │ │ └────────────────────┼────────────────────┘ ▼ ┌─────────────────┐ │ 主内存 │ └─────────────────┘

这个模型带来了三个核心问题,也就是并发编程的三大特性。

2.2 原子性

原子性:一个操作是不可分割的,要么全部执行成功,要么全部不执行,执行过程中不会被其他线程打断。

比如经典的count++操作,看起来是一行代码,实际上分为 3 步:

  1. 从主内存读取 count 的值到工作内存
  2. 在工作内存中对 count 进行 + 1 操作
  3. 将计算后的结果写回主内存

在多线程环境下,这三个步骤随时可能被其他线程打断,导致最终的结果不符合预期。这就是原子性问题,也是最常见的并发问题。

2.3 可见性

可见性:一个线程修改了共享变量的值,其他线程能够立即看到这个修改。

在 JMM 模型中,线程修改的是自己工作内存中的变量副本,什么时候把这个修改同步到主内存,是不确定的。这就会导致一个线程修改了变量,另一个线程看不到,最终引发线程安全问题。

举个最简单的例子:

java

运行

public class VisibilityDemo { private static boolean flag = true; public static void main(String[] args) throws InterruptedException { new Thread(() -> { while (flag) { // 空循环 } System.out.println("线程感知到flag变化,退出循环"); }).start(); Thread.sleep(1000); flag = false; System.out.println("主线程已经将flag设置为false"); } }

这段代码在大多数情况下,子线程永远不会退出循环。因为主线程修改的flag值,没有被及时同步到子线程的工作内存中,子线程看不到这个变化。这就是典型的可见性问题。

2.4 有序性

有序性:程序执行的顺序,按照代码的先后顺序执行。

为了提升性能,编译器和 CPU 会对指令进行重排序。重排序不会影响单线程的执行结果,但在多线程环境下,会导致意想不到的问题。

最经典的例子就是单例模式的双重检查锁(DCL)问题:

java

运行

public class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 第二次检查 instance = new Singleton(); // 问题出在这里 } } } return instance; } }

instance = new Singleton()这行代码,实际上分为 3 步:

  1. 为 Singleton 对象分配内存空间
  2. 初始化 Singleton 对象
  3. 将 instance 引用指向分配的内存地址

编译器和 CPU 可能会对这 3 步进行重排序,变成 1→3→2。这就会导致一个线程执行了 1 和 3,还没执行 2 的时候,另一个线程进来,发现 instance 已经不为 null 了,直接返回一个未初始化完成的对象,最终引发空指针异常。这就是有序性问题。

三、Java 并发编程的核心解决方案

Java 提供了一套完整的工具,来解决三大特性带来的线程安全问题。下面我从基础到进阶,逐一讲解最常用的解决方案,以及它们的适用场景和优缺点。

3.1 基础关键字:volatile 与 synchronized

1. volatile 关键字

volatile是 Java 提供的最轻量级的同步机制,它能解决可见性有序性问题,但不能解决原子性问题

volatile的核心作用:

  • 保证可见性:对 volatile 变量的修改,会立即同步到主内存;每次读取 volatile 变量,都会从主内存重新加载
  • 禁止指令重排序:通过内存屏障,禁止编译器和 CPU 对指令进行重排序

适用场景

  • 状态标记变量:比如上面例子中的flag,用 volatile 修饰就能解决可见性问题
  • 双重检查锁单例模式:给 instance 变量加上 volatile,就能禁止指令重排序,解决 DCL 问题

正确示例

java

运行

// 状态标记 private volatile boolean flag = true; // 安全的DCL单例 public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }

注意:volatile 不能解决原子性问题,比如volatile int count; count++依然是线程不安全的。

2. synchronized 关键字

synchronized是 Java 最基础的锁机制,它能同时保证原子性、可见性、有序性,是解决线程安全问题的 "万能钥匙"。

synchronized的使用方式:

java

运行

// 1. 修饰实例方法:锁的是当前对象this public synchronized void add() { count++; } // 2. 修饰静态方法:锁的是当前类的Class对象 public static synchronized void staticAdd() { staticCount++; } // 3. 修饰代码块:锁的是括号里的对象 public void add() { synchronized (this) { count++; } }

锁的升级过程:很多人觉得 synchronized 性能差,其实从 JDK1.6 开始,JVM 对 synchronized 做了大量优化,引入了锁升级机制,性能已经和 ReentrantLock 相差无几。

锁的升级流程:无锁 → 偏向锁 → 轻量级锁 → 重量级锁

  • 偏向锁:只有一个线程访问同步块时,锁会偏向这个线程,避免多次加锁解锁的开销
  • 轻量级锁:多个线程交替访问同步块时,使用 CAS 操作加锁,不会阻塞线程
  • 重量级锁:多个线程同时竞争锁时,锁升级为重量级锁,未抢到锁的线程会被阻塞

优点

  • 使用简单,不需要手动释放锁,JVM 会自动释放
  • 不会出现死锁问题(发生异常时 JVM 会自动释放锁)
  • JVM 内置优化,性能优秀

缺点

  • 不支持手动中断锁等待
  • 不支持尝试非阻塞获取锁
  • 不支持公平锁
  • 是独占锁,读多写少的场景下性能不佳

3.2 JUC 显式锁:Lock 体系

JDK1.5 引入了 java.util.concurrent(简称 JUC)包,提供了更灵活的显式锁体系,核心接口是Lock,最常用的实现类是ReentrantLockReentrantReadWriteLock

1. ReentrantLock 可重入锁

ReentrantLocksynchronized一样,是可重入的独占锁,但它提供了更灵活的功能。

核心用法

java

运行

private final Lock lock = new ReentrantLock(); private int count; public void add() { lock.lock(); // 加锁 try { count++; } finally { lock.unlock(); // 必须在finally中释放锁,否则会发生死锁 } }

ReentrantLock 的高级功能

  • 支持公平锁:new ReentrantLock(true)可以创建公平锁,先等待的线程先获取锁
  • 支持非阻塞尝试获取锁:tryLock()方法,获取不到锁立即返回,不会阻塞
  • 支持可中断的锁等待:lockInterruptibly()方法,等待过程中可以被中断
  • 支持多个等待条件:newCondition()方法,可以创建多个条件队列,实现更精细的线程协作

和 synchronized 的对比

表格

特性synchronizedReentrantLock
可重入支持支持
公平锁不支持支持
非阻塞获取锁不支持支持
可中断不支持支持
多个条件队列不支持支持
自动释放锁支持不支持,必须手动释放
异常处理自动释放锁必须在 finally 中释放

适用场景:需要更灵活的锁控制,比如尝试获取锁、超时获取锁、公平锁、多条件队列的场景。

2. ReentrantReadWriteLock 读写锁

读写锁是为了解决读多写少的场景而设计的。它维护了两个锁:读锁(共享锁)和写锁(独占锁)。

  • 读锁:多个线程可以同时获取读锁,读读不互斥
  • 写锁:只有一个线程能获取写锁,读写、写写都互斥

核心用法

java

运行

private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); private final Lock readLock = rwLock.readLock(); private final Lock writeLock = rwLock.writeLock(); private Map<String, Object> data = new HashMap<>(); // 读操作,加读锁 public Object get(String key) { readLock.lock(); try { return data.get(key); } finally { readLock.unlock(); } } // 写操作,加写锁 public void put(String key, Object value) { writeLock.lock(); try { data.put(key, value); } finally { writeLock.unlock(); } }

适用场景:读多写少的场景,比如缓存、配置管理、元数据存储等,能大幅提升并发性能。

3.3 原子类:无锁编程解决原子性问题

对于简单的数值递增、递减操作,用锁会带来额外的开销。JUC 包提供了一系列原子类,基于 CAS(Compare And Swap)操作实现无锁的原子性保证,性能比锁高很多。

常用的原子类

  • 基本类型原子类:AtomicIntegerAtomicLongAtomicBoolean
  • 数组原子类:AtomicIntegerArrayAtomicLongArray
  • 引用原子类:AtomicReferenceAtomicStampedReference
  • 高并发累加器:LongAdderDoubleAdder

核心用法

java

运行

// AtomicInteger示例 private AtomicInteger count = new AtomicInteger(0); public void add() { count.incrementAndGet(); // 原子性+1,相当于count++ } // LongAdder示例,高并发下性能比AtomicLong更好 private LongAdder sum = new LongAdder(); public void add(long x) { sum.add(x); }

CAS 的原理:CAS 操作包含三个参数:内存地址 V、旧的预期值 A、新值 B。只有当 V 的值等于 A 时,才会将 V 的值更新为 B,否则什么都不做。整个操作是原子性的,由 CPU 指令保证。

CAS 是无锁编程的核心,它不会阻塞线程,在并发量不高的场景下,性能远高于锁。

CAS 的缺点

  • ABA 问题:一个值从 A 变成 B,又变回 A,CAS 会认为它没有变化。可以用AtomicStampedReference加版本号解决
  • 循环时间长开销大:高并发下,大量线程同时 CAS,会导致多次重试,CPU 开销大。可以用LongAdder解决
  • 只能保证一个变量的原子性:多个变量的原子操作,还是需要用锁

3.4 线程池:解决多线程的资源管理问题

很多人写多线程代码,喜欢直接new Thread(),这是非常错误的做法。线程的创建和销毁都有很大的开销,无限制的创建线程会导致内存溢出、CPU 占用过高。

线程池的核心作用就是统一管理线程,实现线程的复用,控制最大并发数,避免资源耗尽。

线程池的核心参数

java

运行

public ThreadPoolExecutor( int corePoolSize, // 核心线程数 int maximumPoolSize, // 最大线程数 long keepAliveTime, // 非核心线程的空闲存活时间 TimeUnit unit, // 时间单位 BlockingQueue<Runnable> workQueue, // 任务等待队列 ThreadFactory threadFactory, // 线程创建工厂 RejectedExecutionHandler handler // 拒绝策略 )

核心参数说明

  1. 核心线程数:线程池中一直保持存活的线程数,即使空闲也不会被销毁
  2. 最大线程数:线程池允许的最大线程数量
  3. 等待队列:当核心线程都在忙碌时,新任务会进入等待队列排队
  4. 拒绝策略:当等待队列满了,且达到最大线程数时,新任务会被拒绝,有 4 种内置策略:
    • AbortPolicy:直接抛出异常,默认策略
    • CallerRunsPolicy:用调用者所在的线程来执行任务
    • DiscardOldestPolicy:丢弃队列中最老的任务,执行当前任务
    • DiscardPolicy:直接丢弃当前任务,不抛出异常
线程池的正确创建方式

绝对不要用 Executors 创建线程池,阿里开发规范明确禁止这种做法。因为 Executors 创建的线程池,要么无限制的创建线程,要么无限制的添加任务,最终都会导致 OOM。

正确的创建方式:手动创建 ThreadPoolExecutor,根据业务场景设置合理的参数。

java

运行

@Configuration public class ThreadPoolConfig { @Bean public ExecutorService businessExecutor() { // 核心线程数:CPU密集型任务设置为CPU核心数,IO密集型设置为CPU核心数*2 int corePoolSize = Runtime.getRuntime().availableProcessors() * 2; return new ThreadPoolExecutor( corePoolSize, corePoolSize * 2, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(200), // 有界队列,避免OOM new ThreadFactory() { // 自定义线程工厂,给线程起名字,方便排查问题 private final AtomicInteger threadNumber = new AtomicInteger(1); @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r, "business-pool-" + threadNumber.getAndIncrement()); thread.setDaemon(false); return thread; } }, new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略,避免任务丢失 ); } }
线程池的常见坑
  1. 使用无界队列:比如new LinkedBlockingQueue(),会导致任务无限堆积,最终 OOM
  2. 核心线程数设置不合理:设置太大导致 CPU 切换频繁,设置太小导致并发量上不去
  3. 不自定义线程工厂:线程没有名字,出了问题根本不知道是哪个线程池的线程出了问题
  4. 所有任务共用一个线程池:核心任务和非核心任务共用一个线程池,非核心任务阻塞会影响核心任务
  5. 在任务中捕获了所有异常:导致任务执行失败了也看不到异常信息,排查问题困难

3.5 并发容器:替代同步容器,提升并发性能

很多人会用Collections.synchronizedList(new ArrayList())这种同步容器,它的实现是给所有方法都加上 synchronized 锁,并发性能极差。

JUC 包提供了一系列高性能的并发容器,专门为多线程场景设计,性能远高于同步容器。

常用的并发容器

表格

普通容器同步容器并发容器适用场景
ArrayListVector/SynchronizedListCopyOnWriteArrayList读多写少的列表场景
HashMapHashtable/SynchronizedMapConcurrentHashMap高并发的键值对存储
TreeMapSynchronizedSortedMapConcurrentSkipListMap高并发的有序键值对存储
LinkedListSynchronizedListLinkedBlockingQueue高并发的生产者消费者队列

核心用法示例

java

运行

// ConcurrentHashMap,高并发场景下替代HashMap ConcurrentHashMap<String, Object> map = new ConcurrentHashMap<>(); map.put("key", "value"); map.get("key"); // CopyOnWriteArrayList,读多写少场景下替代ArrayList CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); list.add("a"); list.get(0); // LinkedBlockingQueue,生产者消费者模型 BlockingQueue<String> queue = new LinkedBlockingQueue<>(100); // 生产者 queue.put("task"); // 消费者 String task = queue.take();

四、生产环境高频并发问题与解决方案

4.1 秒杀超卖问题

问题描述:秒杀活动中,库存扣减出现负数,商品超卖。这是最经典的并发问题。

错误代码

java

运行

// 错误示例:并发下会出现超卖 public void seckill(Long productId, Integer quantity) { // 查询库存 Integer stock = stockMapper.selectStock(productId); // 判断库存是否充足 if (stock < quantity) { throw new BusinessException("库存不足"); } // 扣减库存 stockMapper.deductStock(productId, quantity); // 生成订单 orderMapper.insert(order); }

解决方案

  1. 数据库乐观锁:给库存表加 version 字段,更新时判断 version 是否一致

sql

UPDATE product_stock SET stock = stock - #{quantity}, version = version + 1 WHERE product_id = #{productId} AND version = #{version} AND stock >= #{quantity}
  1. 分布式锁:用 Redis 或 Zookeeper 分布式锁,保证同一时间只有一个线程能扣减库存

java

运行

public void seckill(Long productId, Integer quantity) { String lockKey = "stock:lock:" + productId; try { // 获取分布式锁 if (!redisLock.tryLock(lockKey, 3, TimeUnit.SECONDS)) { throw new BusinessException("系统繁忙,请稍后再试"); } // 扣减库存逻辑 Integer stock = stockMapper.selectStock(productId); if (stock < quantity) { throw new BusinessException("库存不足"); } stockMapper.deductStock(productId, quantity); orderMapper.insert(order); } finally { // 释放锁 redisLock.unlock(lockKey); } }
  1. 数据库行锁:用 SQL 自带的行锁,先锁行再扣减

sql

SELECT stock FROM product_stock WHERE product_id = #{productId} FOR UPDATE; UPDATE product_stock SET stock = stock - #{quantity} WHERE product_id = #{productId} AND stock >= #{quantity};

4.2 死锁问题

问题描述:两个线程互相等待对方持有的锁,导致线程永远阻塞,服务卡死。

死锁产生的四个必要条件

  1. 互斥条件:一个资源只能被一个线程持有
  2. 持有并等待:线程持有一个资源,同时等待另一个资源
  3. 不可剥夺:线程持有的资源,只能自己释放,不能被其他线程剥夺
  4. 循环等待:多个线程之间形成循环等待资源的关系

死锁示例

java

运行

public class DeadLockDemo { private static final Object lockA = new Object(); private static final Object lockB = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (lockA) { System.out.println("线程1持有lockA,等待lockB"); try { Thread.sleep(1000); } catch (InterruptedException e) {} synchronized (lockB) { System.out.println("线程1持有lockA和lockB"); } } }).start(); new Thread(() -> { synchronized (lockB) { System.out.println("线程2持有lockB,等待lockA"); try { Thread.sleep(1000); } catch (InterruptedException e) {} synchronized (lockA) { System.out.println("线程2持有lockB和lockA"); } } }).start(); } }

解决方案

  1. 预防死锁:破坏四个必要条件中的一个,最常用的是破坏循环等待条件,让所有线程按照相同的顺序获取锁
  2. 排查死锁:用jstack命令查看线程堆栈,找到死锁的线程和锁
  3. 使用 tryLock:用 ReentrantLock 的 tryLock 方法设置超时时间,避免无限等待
  4. 减少锁的嵌套:尽量避免在一个锁里面获取另一个锁

4.3 线程池导致的 OOM 问题

问题描述:服务运行一段时间后,出现OutOfMemoryError: unable to create new native thread或者OutOfMemoryError: Java heap space

常见原因

  1. 使用无界队列,任务无限堆积,占用大量内存
  2. 最大线程数设置过大,创建了上千个线程,导致内存耗尽
  3. 任务执行时间过长,队列不断堆积新任务
  4. 线程池没有关闭,比如每次请求都创建一个新的线程池

解决方案

  1. 必须使用有界队列,设置合理的队列长度
  2. 根据业务场景设置合理的最大线程数,一般不超过 CPU 核心数 * 10
  3. 监控线程池的状态:队列长度、活跃线程数、任务执行时间
  4. 任务设置超时时间,避免长时间阻塞
  5. 核心业务和非核心业务使用不同的线程池,互相隔离

五、生产环境最佳实践

  1. 无锁优先:能用原子类解决的,就不用锁;能用读写锁解决的,就不用独占锁。锁的粒度越小越好,持有锁的时间越短越好。
  2. 优先使用 JDK 内置工具:不要自己手写锁和线程同步逻辑,JDK 内置的并发工具已经经过了严格的测试,比你自己写的更可靠。
  3. 合理设置线程池参数:核心线程数根据任务类型设置,CPU 密集型任务设置为 CPU 核心数,IO 密集型设置为 CPU 核心数 * 2;必须使用有界队列;自定义线程工厂给线程命名。
  4. 避免共享可变状态:这是解决并发问题的根本方法。尽量减少共享变量,能用局部变量就不用成员变量;能使用不可变对象就不用可变对象。
  5. 并发代码必须做压测:并发问题在低并发下很难复现,必须在测试环境做高并发压测,验证代码的线程安全性。
  6. 监控线程池和锁的状态:生产环境必须监控线程池的队列长度、活跃线程数、任务拒绝数;监控锁的竞争情况,及时发现性能瓶颈。
  7. 避免在锁中执行耗时操作:持有锁的时间越长,锁的竞争越激烈,性能越差。不要在锁中执行数据库查询、RPC 调用等耗时操作。
  8. 正确处理线程中的异常:线程池中的任务如果抛出异常,不会打印到日志中,必须在任务中捕获异常并记录日志,否则任务失败了都不知道。
  9. 优先使用并发容器,不要自己实现同步:ConcurrentHashMap、CopyOnWriteArrayList 这些并发容器,比你自己用 synchronized 包装的容器性能好得多。
  10. 不要滥用 volatile:volatile 只能解决可见性和有序性问题,不能解决原子性问题。不要指望用 volatile 来解决计数类的原子性问题。

总结

并发编程不是玄学,也不是只有架构师才需要懂的底层知识。它是每个 Java 全栈工程师必须掌握的核心技能,是解决线上核心故障、突破职业瓶颈的关键。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/25 7:02:07

NotaGen快速部署:一键启动WebUI,5分钟开始音乐创作之旅

NotaGen快速部署&#xff1a;一键启动WebUI&#xff0c;5分钟开始音乐创作之旅 1. 准备工作与环境检查 1.1 系统要求 在开始部署NotaGen之前&#xff0c;请确保您的系统满足以下最低要求&#xff1a; 操作系统&#xff1a;Linux&#xff08;推荐Ubuntu 20.04&#xff09;或…

作者头像 李华
网站建设 2026/4/25 6:59:25

AI与机器学习:概念差异与技术应用解析

1. 概念辨析&#xff1a;AI与机器学习的本质差异第一次接触这两个术语时&#xff0c;我也曾困惑——为什么新闻报道时而说"AI突破"&#xff0c;时而提"机器学习进展"&#xff1f;直到参与实际项目后才明白&#xff0c;这就像区分"汽车"和"内…

作者头像 李华
网站建设 2026/4/25 6:57:46

保姆级教程:用Anaconda为QMT创建Python 3.6.8虚拟环境,避免版本冲突

量化交易必备&#xff1a;Anaconda虚拟环境精准配置Python 3.6.8全攻略 当你在深夜调试QMT策略时&#xff0c;突然发现因为Python版本冲突导致整个开发环境崩溃——这种经历足以让任何量化开发者抓狂。本文将带你彻底解决这个痛点&#xff0c;不仅教你如何创建完美的Python 3.6…

作者头像 李华
网站建设 2026/4/25 6:55:35

ToastFish:Windows通知栏背单词神器完整使用指南

ToastFish&#xff1a;Windows通知栏背单词神器完整使用指南 【免费下载链接】ToastFish 一个利用摸鱼时间背单词的软件。 项目地址: https://gitcode.com/GitHub_Trending/to/ToastFish ToastFish是一款巧妙利用Windows通知栏的智能背单词软件&#xff0c;让你在上班、…

作者头像 李华
网站建设 2026/4/25 6:47:22

别让一个超时的第三方http接口拖垮所有接口

大多数Java项目在调第三方HTTP接口时&#xff0c;会封装一个工具类&#xff0c;里面用懒加载或者静态变量持有一个全局的OkHttpClient实例。所有业务方调第三方接口&#xff0c;都走这一个实例。 平时这么用没什么问题。OkHttpClient本身是线程安全的&#xff0c;官方也推荐复…

作者头像 李华