当前位置:   article > 正文

Java 线程池源码解析_java线程池用法

java线程池用法

线程池

池化思想:线程池、数据连接池等,比如我们 Spark 的 Executor 就是典型的线程池,用户在启动 Spark 作业的同时启动线程池,这样 Spark 的 Task 就可以直接获取资源,而不用像 MR 程序那样等待容器上的进程开启了。

如果不使用线程池的话,我们需要:

  1. 手动创建线程对象
  2. 执行任务
  3. 执行完毕,回收资源

优点

  1. 提高线程的利用率
  2. 提高程序的响应速度(线程对象提前创建好的,节省了创建和销毁的开销)
  3. 便于统一管理线程对象
  4. 可以控制最大的并发数

1、Java 创建线程池示例

1.1、构造参数解释

我们先看看 ThreadPoolExecutor 的构造器的源码:

  1. public ThreadPoolExecutor(int corePoolSize,
  2. int maximumPoolSize,
  3. long keepAliveTime,
  4. TimeUnit unit,
  5. BlockingQueue<Runnable> workQueue,
  6. ThreadFactory threadFactory,
  7. RejectedExecutionHandler handler) {
  8. if (corePoolSize < 0 ||
  9. maximumPoolSize <= 0 ||
  10. maximumPoolSize < corePoolSize ||
  11. keepAliveTime < 0)
  12. throw new IllegalArgumentException();
  13. if (workQueue == null || threadFactory == null || handler == null)
  14. throw new NullPointerException();
  15. this.corePoolSize = corePoolSize;
  16. this.maximumPoolSize = maximumPoolSize;
  17. this.workQueue = workQueue;
  18. this.keepAliveTime = unit.toNanos(keepAliveTime); // 统一转为亚秒格式
  19. this.threadFactory = threadFactory;
  20. this.handler = handler;
  21. }

参数大概意思就是:核心线程数、最大线程数、存活时间、存活时间单位、阻塞队列(存放任务)、线程创建工厂、拒绝策略。我们也可以通过下面的例子理解一下:

        线程池就好比我们的银行,它有很多个接待客人的柜台(一个柜台就是一个线程,用来执行任务)和用来供客人等待的座位(等待的执行的任务)。上面的图中,我们有 5 个柜台(对应第2个参数:maximumPoolSize) ;而其中绿色的三个柜台代表常驻柜台(也就是一直都有人,对应第1个参数:corePoolSize);红色的柜台代表预备柜台,也就是当业务繁忙的时候,最多还可以打电话摇两个人来(maximumPoolSize - corePoolSize);但是摇人来帮忙是有时效性的,如果帮完忙一段时间(取决于第3个和第4个参数)没有活干,这些线程就会被释放;摇人的方式取决于第6个参数(下面我们是通过默认的线程工厂来再创建线程的);蓝色的等候区区域代表座位(可允许等待的任务数量),当柜台前和座位都满了的时候,如果再有任务进来就会被拒绝,具体的拒绝方式取决于第7个参数,下面我们给出的拒绝策略是直接报错:

  1. import java.util.concurrent.ArrayBlockingQueue;
  2. import java.util.concurrent.ExecutorService;
  3. import java.util.concurrent.Executors;
  4. import java.util.concurrent.ThreadPoolExecutor;
  5. import java.util.concurrent.TimeUnit;
  6. public class TestThreadPool {
  7. public static void main(String[] args) {
  8. // 1.核心线程数 2.最大线程数 3.存活时间 4.时间单位 5.等待队列 6.线程工厂 7.拒绝策略(直接抛异常)
  9. ExecutorService executorService = new ThreadPoolExecutor(3, 5, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
  10. for (int i = 0; i < 9; i++) {
  11. executorService.execute(()->{
  12. System.out.println(Thread.currentThread().getName()+"正在工作");
  13. });
  14. }
  15. // 关闭线程池
  16. executorService.shutdown();
  17. }
  18. }
  • 使用线程池中执行任务很简单,我们不需要关心具体是哪个线程执行的,只需要把任务丢给它即可(通过 lambda 表达式来告诉线程池我们的任务执行逻辑)

        上面的案例中,我们的线程池的最大接收的任务量是 8 (最大线程数:5 + 等待队列容量:3),但并不是说只能跑8个任务,如果有任务释放资源仍然可以继续执行任务。

        所以,上面的案例同一时刻最多只能共存8个任务(其中最多只有五个任务会同时执行),如果,当我们的任务超过8时,会直接报错(因为我们设置了拒绝策略就是直接抛异常)。

2、ThreadExecutorPool 源码解析

创建一个包含 10 个核心线程、总线程数为 20、存活时间为 0 s、拒绝策略为直接抛异常的一个线程池对象:

  1. public static void main(String[] args) {
  2. ThreadPoolExecutor executor = new ThreadPoolExecutor(
  3. 10, 20,
  4. 0L, TimeUnit.SECONDS,
  5. new LinkedBlockingDeque<>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()
  6. );
  7. executor.execute(new Runnable() {
  8. @Override
  9. public void run() {
  10. }
  11. });
  12. }

2.1、线程池保活与回收源码分析

2.1.1、execute 方法源码
  1. public void execute(Runnable command) {
  2. if (command == null)
  3. throw new NullPointerException();
  4. /*
  5. * Proceed in 3 steps:
  6. *
  7. * 1. If fewer than corePoolSize threads are running, try to
  8. * start a new thread with the given command as its first
  9. * task. The call to addWorker atomically checks runState and
  10. * workerCount, and so prevents false alarms that would add
  11. * threads when it shouldn't, by returning false.
  12. *
  13. * 2. If a task can be successfully queued, then we still need
  14. * to double-check whether we should have added a thread
  15. * (because existing ones died since last checking) or that
  16. * the pool shut down since entry into this method. So we
  17. * recheck state and if necessary roll back the enqueuing if
  18. * stopped, or start a new thread if there are none.
  19. *
  20. * 3. If we cannot queue task, then we try to add a new
  21. * thread. If it fails, we know we are shut down or saturated
  22. * and so reject the task.
  23. */
  24. int c = ctl.get();
  25. if (workerCountOf(c) < corePoolSize) { // 如果总线程数小于核心线程数
  26. if (addWorker(command, true)) // 添加一个核心线程
  27. return;
  28. c = ctl.get();
  29. }
  30. if (isRunning(c) && workQueue.offer(command)) { // 如果当前全部线程都在工作就把任务添加到阻塞任务队列当中
  31. int recheck = ctl.get();
  32. if (! isRunning(recheck) && remove(command))
  33. reject(command);
  34. else if (workerCountOf(recheck) == 0)
  35. addWorker(null, false);
  36. }
  37. else if (!addWorker(command, false)) // 如果上面没有成功把任务加到队列就新加一个线程
  38. reject(command); // 如果新加线程失败(比如超过最大线程数)就拒绝任务
  39. }

这里的注释已经说的很明白了:

  1. 如果正在运行的线程数少于核心线程数(corePoolSize),则尝试启动一个新线程,并将给定的命令参数作为其第一个任务。调用addWorker方法可以原子性地检查运行状态和工作线程数,从而防止在不应该添加线程时发出错误的警报,通过返回false来实现。
  2. 如果任务能够成功入队,我们仍然需要再次检查是否应该添加一个线程(因为自上次检查以来已有线程死亡)或者线程池在进入此方法后已经关闭。所以我们重新检查状态,如果有必要回滚入队操作(如果已停止),或者如果没有线程,则启动一个新线程。

  3. 如果我们无法将任务入队,则尝试添加一个新的线程。如果添加失败,我们知道线程池已经关闭或饱和,因此拒绝该任务。

        所以在上面我们创建线程池的代码中,并不是线程池创建好之后就会立马创建 10 个核心线程,而是真正有任务来的时候才会去新创建一个线程。

思考:如果线程的任务结束了,线程对象会怎么样呢?

        从创建线程池的构造器就不难想到,构造器中有一个阻塞队列的参数,其实当线程没有任务的时候,线程并不会关闭,而是一直阻塞,也叫保活

        保活线程的关键在于阻塞队列,即LinkedBlockingDeque。当队列为空时,如果线程尝试从队列中取元素,线程会被阻塞,直到队列中有元素可供取出。这样,线程就会在等待任务的过程中保持活跃状态。

2.1.2、getTask 方法源码

        getTask 方法是 runWorker 方法里的调用的,而 runWoker 又是 Worker 对象的方法,这个 Worker 实例又是上面的 execute 方法中的 addWorker 方法中实例化出来的。

        getTask 方法是所有线程池中的线程一直在不断调用检查的(在 runTask 方法中的 while 循环中被调用):

  1. private Runnable getTask() {
  2. boolean timedOut = false; // Did the last poll() time out?
  3. for (;;) {
  4. int c = ctl.get();
  5. // Check if queue empty only if necessary.
  6. if (runStateAtLeast(c, SHUTDOWN)
  7. && (runStateAtLeast(c, STOP) || workQueue.isEmpty())) {
  8. decrementWorkerCount();
  9. return null;
  10. }
  11. // 总线程数
  12. int wc = workerCountOf(c);
  13. // 判断是否需要超时回收线程
  14. boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
  15. if ((wc > maximumPoolSize || (timed && timedOut))
  16. && (wc > 1 || workQueue.isEmpty())) {
  17. if (compareAndDecrementWorkerCount(c))
  18. return null;
  19. continue;
  20. }
  21. try {
  22. Runnable r = timed ?
  23. workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : // 回收超时的线程
  24. workQueue.take(); // 阻塞
  25. if (r != null)
  26. return r;
  27. timedOut = true;
  28. } catch (InterruptedException retry) {
  29. timedOut = false;
  30. }
  31. }
  32. }

方法解释

如果满足以下任一条件,该工作线程将返回null并退出:

  1. 当前线程数超过了最大线程数(由于调用了setMaximumPoolSize方法)。
  2. 线程池已停止。
  3. 线程池已关闭且队列为空。
  4. 该工作线程在等待任务时超时,并且超时的工作线程会受到终止处理(即allowCoreThreadTimeOut || workerCount > corePoolSize),无论是在超时等待之前还是之后。如果队列非空,则该工作线程不是线程池中的最后一个线程。

返回值

        如果满足上述条件之一,返回null,表示工作线程必须退出,此时workerCount会减1。
否则,返回task,表示成功获取到任务。

        从源码中可以看到,线程池中的工作线程会执行getTask()方法来获取任务。在这个方法中,线程会调用workQueue.poll()或workQueue.take()方法来尝试从LinkedBlockingDeque中获取任务。如果队列为空,这些方法会使线程阻塞,直到有新的任务添加到队列中。这就是线程在没有任务执行时仍然保持活跃(保活)的机制。

  • workQueue.take():当队列为空时,该方法会无限期地等待,直到有新的任务被添加到队列中。这意味着,如果所有核心线程都在执行任务并且队列为空,那么调用 take() 方法的线程会一直阻塞,直到其他线程向队列中添加了新的任务。
  • workQueue.poll():此方法在队列为空时会立即返回null,而不是等待。这通常用于非核心线程(也称为工作线程),当没有任务可做时,这些线程可以选择终止自己以减少资源占用。

思考:现在的核心线程数是 10,如果此时正在工作的线程有 11 个(10个核心线程,一个其它线程),那如果所有线程的任务都完成了,那么线程池又会执行怎样的逻辑呢?

2.1.3、runWorker 方法源码

在上面的 getTask 方法中,线程池中的每个线程在调用这个方法的时候都会判断是否需要回收线程:

  1. // 判断是否需要超时回收线程
  2. boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

现在我们有 11 个线程,所以明显总线程数 wc(11) > corePoolSize(10),所以自然会执行下面的逻辑:

  1. Runnable r = timed ?
  2. workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
  3. workQueue.take();
  4. if (r != null)
  5. return r;
  6. timedOut = true;

也就是 workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS):当前调用该方法的线程会阻塞一段时间(keppAliveTime 个单位),如果这段时间过后依然访问不到任务,那么下面的 timeOut = true ,getTask 方法返回 null。

  1. final void runWorker(Worker w) {
  2. Thread wt = Thread.currentThread();
  3. Runnable task = w.firstTask;
  4. w.firstTask = null;
  5. w.unlock(); // allow interrupts
  6. boolean completedAbruptly = true;
  7. try {
  8. while (task != null || (task = getTask()) != null) {
  9. w.lock();
  10. // If pool is stopping, ensure thread is interrupted;
  11. // if not, ensure thread is not interrupted. This
  12. // requires a recheck in second case to deal with
  13. // shutdownNow race while clearing interrupt
  14. if ((runStateAtLeast(ctl.get(), STOP) ||
  15. (Thread.interrupted() &&
  16. runStateAtLeast(ctl.get(), STOP))) &&
  17. !wt.isInterrupted())
  18. wt.interrupt();
  19. try {
  20. beforeExecute(wt, task);
  21. try {
  22. task.run();
  23. afterExecute(task, null);
  24. } catch (Throwable ex) {
  25. afterExecute(task, ex);
  26. throw ex;
  27. }
  28. } finally {
  29. task = null;
  30. w.completedTasks++;
  31. w.unlock();
  32. }
  33. }
  34. completedAbruptly = false;
  35. } finally {
  36. processWorkerExit(w, completedAbruptly);
  37. }
  38. }

因为 while 中的条件均为 false,所以 runWorker 会先执行下面的 completedAbruptly = false; 然后执行 finally 中的 processWorkerExit(w, completedAbruptly); 这里的 processWorkerExit 方法正是回收线程的关键所在,如果没有它,我们的所有空闲线程就都将被回收,所以这个方法中就定义了回收的逻辑:

  1. private void processWorkerExit(Worker w, boolean completedAbruptly) {
  2. if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
  3. decrementWorkerCount();
  4. final ReentrantLock mainLock = this.mainLock;
  5. mainLock.lock();
  6. try {
  7. completedTaskCount += w.completedTasks;
  8. workers.remove(w);
  9. } finally {
  10. mainLock.unlock();
  11. }
  12. tryTerminate();
  13. int c = ctl.get();
  14. if (runStateLessThan(c, STOP)) {
  15. if (!completedAbruptly) {
  16. int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
  17. if (min == 0 && ! workQueue.isEmpty())
  18. min = 1;
  19. if (workerCountOf(c) >= min)
  20. return; // 线程直接结束
  21. }
  22. addWorker(null, false); // 本线程结束,再新建一个线程
  23. }
  24. }

比如现在我们一共有 11 个线程(10个核心线程),那么 int min = allowCoreThreadTimeOut ? 0 : corePoolSize; (暂不考虑 allowCoreThreadTimeOut)的结果就是 10,所以这个 11 个线程在执行 if (workerCountOf(c) >= min) 的时候,第一个执行该语句的线程会直接 return,之后执行该判断语句的 10 个线程由于不满足条件,但是自己又存活不了,所以在自己结束之前,会再创建一个没有初始任务的新线程,这样就保证了线程池中的线程数总是不低于核心线程数的。

2.2、线程池五大状态变化源码

下面是 ThreadPoolExecutor 源码中对于常量 ctl 的解释:

        主池控制状态ctl是一个原子整数,包含两个概念字段workerCount,表示线程的有效数量runState,表示是否正在运行、关闭等。

        为了将它们打包为一个整数,我们将workerCounts限制为(2^29)-1(约5亿)个线程,而不是(2^31)-1(20亿)个其他可表示的线程。如果这在未来是一个问题,可以将变量更改为AtomicLong,并调整下面的移位/掩码常数。但是,在需要之前,使用int,此代码会更快、更简单。workerCount是允许启动但不允许停止的工人数量。该值可能与实际活动线程数暂时不同,例如,当ThreadFactory在被请求时未能创建线程,以及当退出的线程在终止前仍在执行记账时。用户可见的池大小报告为工作者集的当前大小。

        runState提供了主要的生命周期控制,取值为:

  • RUNNING:正常接受新任务和处理排队的任务
  • SHUTDOWN:不接受新任务,但处理排队任务
  • STOP:不接受新建任务,不处理排队任务,并中断正在进行的任务
  • TIDING:所有任务都已终止,workerCount为零,转换到状态的线程TIDING将运行terminated()钩子方法terminated:terminated。

        runState随时间单调增加,但不需要达到每个状态。转换为:RUNNING->SHUTDOWN On invocation of SHUTDOWN()(RUNNING或SHUTDOWN)->STOP On invocationof shutdownNow()SHUTDOWN->TIDING当队列和池都为空时STOP->TIDIING当池为空时TIDIING->TERMINATED当TERMINATED()钩子方法完成时等待awaitTermination()的线程将在状态达到TERMINATED时返回。检测从SHUTDOWN到TIDING的转换并不像您希望的那样简单,因为队列在非空之后可能会变空,反之亦然,但我们只能在看到它是空的之后,看到workerCount为0时终止(这有时需要重新检查——见下文)

线程池的五大状态从源码中可以看到:

这里的 COUNT_MASK 代表的是线程池的最大数量,也就是 2 的 29 次方 -1 个。 

 Integer.SIZE为32,所以COUNT_BITS为29,最终各个状态对应的二级制为:

  1. RUNNING:11100000 00000000 00000000 00000000
  2. SHUTDOWN:00000000 00000000 00000000 00000000
  3. STOP:00100000 00000000 00000000 00000000
  4. TIDYING:01000000 00000000 00000000 00000000
  5. TERMINATED:01100000 00000000 00000000 00000000

        所以,只需要使用一个Integer数字的最高三个bit,就可以表示5种线程池的状态,而剩下的29个bit就可以用来表示工作线程数,比如,假如 ctl 为:11100000 00000000 00000000 00001010,就表示线程池的状态为RUNNING,线程池中目前在工作的线程有10个,这里说的“在工作”意思是线程活着,要么在执行任务,要么在阻塞等待任务。

        把线程状态放到高3位,把线程数量放到剩下的29位的好处就是方便之后频繁修改线程个数。

 下面是线程状态的转换关系:

2.2.1、Thread.interrupt 线程中断
  1. public static void main(String[] args) throws InterruptedException {
  2. Thread t1 = new Thread(new Runnable() {
  3. @Override
  4. public void run() {
  5. while (true){
  6. System.out.println("线程 t1 在执行任务");
  7. }
  8. }
  9. });
  10. t1.start();
  11. Thread.sleep(3000);
  12. t1.interrupt();
  13. System.out.println("线程 t1 被阻塞了");
  14. }

        事实上,上面的代码并不会真正中断线程 t1 ,因为 interrupt 只是一个信号,线程要不要停掉还是取决于它自己。所以要想真正打断线程,需要线程自己判断:

  1. Thread t1 = new Thread(new Runnable() {
  2. @Override
  3. public void run() {
  4. while (!Thread.interrupted()){ // 如果线程未被中断
  5. System.out.println("线程 t1 在执行任务");
  6. }
  7. }
  8. });

这样才能真正的关闭线程,否则 interrupt 方法没有意义。

2.2.2、线程池的状态转换 

        线程池的 shutdown 方法会把线程池状态设置为 SHUTDOWN ,但是 SHUTDOWN 状态并不会立即关闭线程池中的所有线程,而是会等任务执行完毕再关闭线程,毕竟直接关闭线程可能会出现一些安全问题。

        如果希望立即关闭线程,需要调用 shutdownNow方法,这个方法会把线程池的状态设置为 STOP

这里的 onShutdown 方法是个空的(它是提供给子类用的),这里不需要关心。,而下面的 tryTerminate 才会对线程池的状态进行修改,这才是我们需要关心的:

 同样,这里的 terminate 方法其实也只是一个空方法,它也是留给子类在需要时使用的,就相当于一个扩展机制,以后要是再 TIDYING 状态之后希望对线程池进行什么操作,就重写它就可以了。

上面代码中的 isRunning 方法也特别简单:

只要当前的线程池状态小于 SHUTDOWN 就是 RUNNING 状态。

2.2.3、addWorker 再解析

首先了解乐观锁:

  • Java提供了如AtomicInteger、AtomicLong、AtomicReference等原子类来支持CAS操作,这些类位于java.util.concurrent.atomic包下。
  • 下面的源码中就是在循环体内部使用CAS操作或版本号机制来实现了乐观锁的行为。
  1. private boolean addWorker(Runnable firstTask, boolean core) {
  2. retry:
  3. for (int c = ctl.get();;) {
  4. // Check if queue empty only if necessary.
  5. if (runStateAtLeast(c, SHUTDOWN) // 如果当前线程池的状态 >= SHUTDOWN
  6. && (runStateAtLeast(c, STOP) // 并且当前线程池的状态 >= STOP
  7. || firstTask != null // execute(command) 的command为null代表不携带任务
  8. || workQueue.isEmpty())) // 任务队列为空
  9. return false; // 返回false代表创建线程失败
  10. for (;;) {
  11. if (workerCountOf(c) // 线程池的总线程数
  12. // 在execute方法源码中,当当前线程数<核心线程数是addWorker的core=true
  13. >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK)) // 是否大于核心线程数或者总线程数
  14. return false;
  15. // 当有多个线程同时执行compareAndIncrementWorkerCount方法时,只会有一个返回true
  16. if (compareAndIncrementWorkerCount(c)) // 先把总线程数+1再去创建线程,ctl是原子类型的,所以这样不会出现线程安全的问题
  17. break retry; // 如果新建线程成功,就会跳出去取真正新建线程
  18. c = ctl.get(); // 新建线程失败的话刷新总线程数
  19. if (runStateAtLeast(c, SHUTDOWN)) // 判断线程池状态是否>=SHUTDOWN
  20. continue retry;
  21. // else CAS failed due to workerCount change; retry inner loop
  22. // 否则继续执行该循环
  23. }
  24. }
  25. boolean workerStarted = false;
  26. boolean workerAdded = false;
  27. Worker w = null;
  28. try {
  29. w = new Worker(firstTask);
  30. final Thread t = w.thread;
  31. if (t != null) {
  32. final ReentrantLock mainLock = this.mainLock;
  33. mainLock.lock();
  34. try {
  35. // Recheck while holding lock.
  36. // Back out on ThreadFactory failure or if
  37. // shut down before lock acquired.
  38. int c = ctl.get();
  39. if (isRunning(c) ||
  40. (runStateLessThan(c, STOP) && firstTask == null)) {
  41. if (t.getState() != Thread.State.NEW)
  42. throw new IllegalThreadStateException();
  43. workers.add(w);
  44. workerAdded = true;
  45. int s = workers.size();
  46. if (s > largestPoolSize)
  47. largestPoolSize = s;
  48. }
  49. } finally {
  50. mainLock.unlock();
  51. }
  52. if (workerAdded) {
  53. t.start();
  54. workerStarted = true;
  55. }
  56. }
  57. } finally {
  58. if (! workerStarted)
  59. addWorkerFailed(w);
  60. }
  61. return workerStarted;
  62. }

        比如我们设置核心线程数为10,但是现在只创建了9个核心线程,如果此时有 2 个以上线程同时执行了 executor.execute(task) 方法,那么在 execute 中会先判断当前线程总数是否大于核心线程数,因为这些线程是同时执行的,所以都返回 9 ,那么 addWorker 的 core 参数就是 true,进入 addWorker 方法,这些线程会都先给总线程数 +1 再去创建线程,而不是先去创建线程再给总线程数 + 1,因为 addWorker 的参数 core = true(代表创建一个核心线程),那么如果都去创建核心线程就会使得核心线程数超过设置的值。

        所以第一个抢先给 ctl + 1 的线程会真正创建一个核心线程(ctl 是原子性的),而其它线程只能被跳出循环重新执行,这样当第二次执行循环时,就会发现此时的核心线程数已经达到了,就会返回 false,代表创建核心线程失败。

总结

        所以我们需要知道,在 Java 的线程池中,它并不是上来就把所有核心线程都开启,而是需要的时候才会开启;此外,当核心线程都忙碌的时候,新来的任务并不会去创建非核心线程,而是优先放到一个任务队列当中去,只有当任务队列满了(这里用的 LinkedBlockingQueue 是链表结构,所以不会满)才会去创建一个新的非核心队列。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/秋刀鱼在做梦/article/detail/944548
推荐阅读
相关标签
  

闽ICP备14008679号