java高並發系列 - 第19天:JUC中的Executor框架詳解1,全面掌握java並發核心技術


這是java高並發系列第19篇文章。

本文主要內容

  1. 介紹Executor框架相關內容
  2. 介紹Executor
  3. 介紹ExecutorService
  4. 介紹線程池ThreadPoolExecutor及案例
  5. 介紹定時器ScheduledExecutorService及案例
  6. 介紹Excecutors類的使用
  7. 介紹Future接口
  8. 介紹Callable接口
  9. 介紹FutureTask的使用
  10. 獲取異步任務的執行結果的幾種方法

Executors框架介紹

Executors框架是Doug Lea的神作,通過這個框架,可以很容易的使用線程池高效地處理並行任務。

Excecutor框架主要包含3部分的內容:

  1. 任務相關的:包含被執行的任務要實現的接口:Runnable接口或Callable接口
  2. 任務的執行相關的:包含任務執行機制的核心接口Executor,以及繼承自ExecutorExecutorService接口。Executor框架中有兩個關鍵的類實現了ExecutorService接口(ThreadPoolExecutorScheduleThreadPoolExecutor
  3. 異步計算結果相關的:包含接口Future實現Future接口的FutureTask類

Executors框架包括:

  • Executor
  • ExecutorService
  • ThreadPoolExecutor
  • Executors
  • Future
  • Callable
  • FutureTask
  • CompletableFuture
  • CompletionService
  • ExecutorCompletionService

下面我們來一個個介紹其用途和使用方法。

Executor接口

Executor接口中定義了方法execute(Runable able)接口,該方法接受一個Runable實例,他來執行一個任務,任務即實現一個Runable接口的類。

ExecutorService接口

ExecutorService繼承於Executor接口,他提供了更為豐富的線程實現方法,比如ExecutorService提供關閉自己的方法,以及為跟蹤一個或多個異步任務執行狀況而生成Future的方法。

ExecutorService有三種狀態:運行、關閉、終止。創建后便進入運行狀態,當調用了shutdown()方法時,便進入了關閉狀態,此時意味着ExecutorService不再接受新的任務,但是他還是會執行已經提交的任務,當所有已經提交了的任務執行完后,便達到終止狀態。如果不調用shutdown方法,ExecutorService方法會一直運行下去,系統一般不會主動關閉。

ThreadPoolExecutor類

線程池類,實現了ExecutorService接口中所有方法,該類也是我們經常要用到的,非常重要,關於此類有詳細的介紹,可以移步:玩轉java中的線程池

ScheduleThreadPoolExecutor定時器

ScheduleThreadPoolExecutor繼承自ScheduleThreadPoolExecutor,他主要用來延遲執行任務,或者定時執行任務。功能和Timer類似,但是ScheduleThreadPoolExecutor更強大、更靈活一些。Timer后台是單個線程,而ScheduleThreadPoolExecutor可以在創建的時候指定多個線程。

常用方法介紹:

schedule:延遲執行任務1次

使用ScheduleThreadPoolExecutor的schedule方法,看一下這個方法的聲明:

public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)

3個參數:

command:需要執行的任務

delay:需要延遲的時間

unit:參數2的時間單位,是個枚舉,可以是天、小時、分鍾、秒、毫秒、納秒等

示例代碼:

package com.itsoku.chat18;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 跟着阿里p7學並發,微信公眾號:javacode2018
 */
public class Demo1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println(System.currentTimeMillis());
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
        scheduledExecutorService.schedule(() -> {
            System.out.println(System.currentTimeMillis() + "開始執行");
            //模擬任務耗時
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(System.currentTimeMillis() + "執行結束");
        }, 2, TimeUnit.SECONDS);
    }
}

輸出:

1564575180457
1564575185525開始執行
1564575188530執行結束

scheduleAtFixedRate:固定的頻率執行任務

使用ScheduleThreadPoolExecutor的scheduleAtFixedRate方法,該方法設置了執行周期,下一次執行時間相當於是上一次的執行時間加上period,任務每次執行完畢之后才會計算下次的執行時間。

看一下這個方法的聲明:

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);

4個參數:

command:表示要執行的任務

initialDelay:表示延遲多久執行第一次

period:連續執行之間的時間間隔

unit:參數2和參數3的時間單位,是個枚舉,可以是天、小時、分鍾、秒、毫秒、納秒等

假設系統調用scheduleAtFixedRate的時間是T1,那么執行時間如下:

第1次:T1+initialDelay

第2次:T1+initialDelay+period

第3次:T1+initialDelay+2*period

第n次:T1+initialDelay+(n-1)*period

示例代碼:

package com.itsoku.chat18;

import java.sql.Time;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 跟着阿里p7學並發,微信公眾號:javacode2018
 */
public class Demo2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println(System.currentTimeMillis());
        //任務執行計數器
        AtomicInteger count = new AtomicInteger(1);
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            int currCount = count.getAndIncrement();
            System.out.println(Thread.currentThread().getName());
            System.out.println(System.currentTimeMillis() + "第" + currCount + "次" + "開始執行");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(System.currentTimeMillis() + "第" + currCount + "次" + "執行結束");
        }, 1, 1, TimeUnit.SECONDS);
    }
}

前面6次輸出結果:

1564576404181
pool-1-thread-1
1564576405247第1次開始執行
1564576407251第1次執行結束
pool-1-thread-1
1564576407251第2次開始執行
1564576409252第2次執行結束
pool-1-thread-2
1564576409252第3次開始執行
1564576411255第3次執行結束
pool-1-thread-1
1564576411256第4次開始執行
1564576413260第4次執行結束
pool-1-thread-3
1564576413260第5次開始執行
1564576415265第5次執行結束
pool-1-thread-2
1564576415266第6次開始執行
1564576417269第6次執行結束

代碼中設置的任務第一次執行時間是系統啟動之后延遲一秒執行。后面每次時間間隔1秒,從輸出中可以看出系統啟動之后過了1秒任務第一次執行(1、3行輸出),輸出的結果中可以看到任務第一次執行結束時間和第二次的結束時間一樣,為什么會這樣?前面有介紹,任務當前執行完畢之后會計算下次執行時間,下次執行時間為上次執行的開始時間+period,第一次開始執行時間是1564576405247,加1秒為1564576406247,這個時間小於第一次結束的時間了,說明小於系統當前時間了,會立即執行。

scheduleWithFixedDelay:固定的間隔執行任務

使用ScheduleThreadPoolExecutor的scheduleWithFixedDelay方法,該方法設置了執行周期,與scheduleAtFixedRate方法不同的是,下一次執行時間是上一次任務執行完的系統時間加上period,因而具體執行時間不是固定的,但周期是固定的,是采用相對固定的延遲來執行任務。看一下這個方法的聲明:

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);

4個參數:

command:表示要執行的任務

initialDelay:表示延遲多久執行第一次

period:表示下次執行時間和上次執行結束時間之間的間隔時間

unit:參數2和參數3的時間單位,是個枚舉,可以是天、小時、分鍾、秒、毫秒、納秒等

假設系統調用scheduleAtFixedRate的時間是T1,那么執行時間如下:

第1次:T1+initialDelay,執行結束時間:E1

第2次:E1+period,執行結束時間:E2

第3次:E2+period,執行結束時間:E3

第4次:E3+period,執行結束時間:E4

第n次:上次執行結束時間+period

示例代碼:

package com.itsoku.chat18;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 跟着阿里p7學並發,微信公眾號:javacode2018
 */
public class Demo3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println(System.currentTimeMillis());
        //任務執行計數器
        AtomicInteger count = new AtomicInteger(1);
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
        scheduledExecutorService.scheduleWithFixedDelay(() -> {
            int currCount = count.getAndIncrement();
            System.out.println(Thread.currentThread().getName());
            System.out.println(System.currentTimeMillis() + "第" + currCount + "次" + "開始執行");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(System.currentTimeMillis() + "第" + currCount + "次" + "執行結束");
        }, 1, 3, TimeUnit.SECONDS);
    }
}

前幾次輸出如下:

1564578510983
pool-1-thread-1
1564578512087第1次開始執行
1564578514091第1次執行結束
pool-1-thread-1
1564578517096第2次開始執行
1564578519100第2次執行結束
pool-1-thread-2
1564578522103第3次開始執行
1564578524105第3次執行結束
pool-1-thread-1
1564578527106第4次開始執行
1564578529106第4次執行結束

延遲1秒之后執行第1次,后面每次的執行時間和上次執行結束時間間隔3秒。

scheduleAtFixedRatescheduleWithFixedDelay示例建議多看2遍。

定時任務有異常會怎么樣?

示例代碼:

package com.itsoku.chat18;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 跟着阿里p7學並發,微信公眾號:javacode2018
 */
public class Demo4 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println(System.currentTimeMillis());
        //任務執行計數器
        AtomicInteger count = new AtomicInteger(1);
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
        ScheduledFuture<?> scheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> {
            int currCount = count.getAndIncrement();
            System.out.println(Thread.currentThread().getName());
            System.out.println(System.currentTimeMillis() + "第" + currCount + "次" + "開始執行");
            System.out.println(10 / 0);
            System.out.println(System.currentTimeMillis() + "第" + currCount + "次" + "執行結束");
        }, 1, 1, TimeUnit.SECONDS);

        TimeUnit.SECONDS.sleep(5);
        System.out.println(scheduledFuture.isCancelled());
        System.out.println(scheduledFuture.isDone());

    }
}

系統輸出如下內容就再也沒有輸出了:

1564578848143
pool-1-thread-1
1564578849226第1次開始執行
false
true

先說補充點知識:schedule、scheduleAtFixedRate、scheduleWithFixedDelay這幾個方法有個返回值ScheduledFuture,通過ScheduledFuture可以對執行的任務做一些操作,如判斷任務是否被取消、是否執行完成。

再回到上面代碼,任務中有個10/0的操作,會觸發異常,發生異常之后沒有任何現象,被ScheduledExecutorService內部給吞掉了,然后這個任務再也不會執行了,scheduledFuture.isDone()輸出true,表示這個任務已經結束了,再也不會被執行了。所以如果程序有異常,開發者自己注意處理一下,不然跑着跑着發現任務怎么不跑了,也沒有異常輸出。

取消定時任務的執行

可能任務執行一會,想取消執行,可以調用ScheduledFuturecancel方法,參數表示是否給任務發送中斷信號。

package com.itsoku.chat18;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 跟着阿里p7學並發,微信公眾號:javacode2018
 */
public class Demo5 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println(System.currentTimeMillis());
        //任務執行計數器
        AtomicInteger count = new AtomicInteger(1);
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        ScheduledFuture<?> scheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> {
            int currCount = count.getAndIncrement();
            System.out.println(Thread.currentThread().getName());
            System.out.println(System.currentTimeMillis() + "第" + currCount + "次" + "開始執行");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(System.currentTimeMillis() + "第" + currCount + "次" + "執行結束");
        }, 1, 1, TimeUnit.SECONDS);

        TimeUnit.SECONDS.sleep(5);
        scheduledFuture.cancel(false);
        TimeUnit.SECONDS.sleep(1);
        System.out.println("任務是否被取消:"+scheduledFuture.isCancelled());
        System.out.println("任務是否已完成:"+scheduledFuture.isDone());
    }
}

輸出:

1564579843190
pool-1-thread-1
1564579844255第1次開始執行
1564579846260第1次執行結束
pool-1-thread-1
1564579847263第2次開始執行
任務是否被取消:true
任務是否已完成:true
1564579849267第2次執行結束

輸出中可以看到任務被取消成功了。

Executors類

Executors類,提供了一系列工廠方法用於創建線程池,返回的線程池都實現了ExecutorService接口。常用的方法有:

newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory)

創建一個單線程的線程池。這個線程池只有一個線程在工作,也就是相當於單線程串行執行所有任務。如果這個唯一的線程因為異常結束,那么會有一個新的線程來替代它。此線程池保證所有任務的執行順序按照任務的提交順序執行。內部使用了無限容量的LinkedBlockingQueue阻塞隊列來緩存任務,任務如果比較多,單線程如果處理不過來,會導致隊列堆滿,引發OOM。

newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)

創建固定大小的線程池。每次提交一個任務就創建一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,在提交新任務,任務將會進入等待隊列中等待。如果某個線程因為執行異常而結束,那么線程池會補充一個新線程。內部使用了無限容量的LinkedBlockingQueue阻塞隊列來緩存任務,任務如果比較多,如果處理不過來,會導致隊列堆滿,引發OOM。

newCachedThreadPool

public static ExecutorService newCachedThreadPool()
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)

創建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程,

那么就會回收部分空閑(60秒處於等待任務到來)的線程,當任務數增加時,此線程池又可以智能的添加新線程來處理任務。此線程池的最大值是Integer的最大值(2^31-1)。內部使用了SynchronousQueue同步隊列來緩存任務,此隊列的特性是放入任務時必須要有對應的線程獲取任務,任務才可以放入成功。如果處理的任務比較耗時,任務來的速度也比較快,會創建太多的線程引發OOM。

newScheduledThreadPool

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)

創建一個大小無限的線程池。此線程池支持定時以及周期性執行任務的需求。

在《阿里巴巴java開發手冊》中指出了線程資源必須通過線程池提供,不允許在應用中自行顯示的創建線程,這樣一方面是線程的創建更加規范,可以合理控制開辟線程的數量;另一方面線程的細節管理交給線程池處理,優化了資源的開銷。而線程池不允許使用Executors去創建,而要通過ThreadPoolExecutor方式,這一方面是由於jdk中Executor框架雖然提供了如newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()等創建線程池的方法,但都有其局限性,不夠靈活;另外由於前面幾種方法內部也是通過ThreadPoolExecutor方式實現,使用ThreadPoolExecutor有助於大家明確線程池的運行規則,創建符合自己的業務場景需要的線程池,避免資源耗盡的風險。

Future、Callable接口

Future接口定義了操作異步異步任務執行一些方法,如獲取異步任務的執行結果、取消任務的執行、判斷任務是否被取消、判斷任務執行是否完畢等。

Callable接口中定義了需要有返回的任務需要實現的方法。

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

比如主線程讓一個子線程去執行任務,子線程可能比較耗時,啟動子線程開始執行任務后,主線程就去做其他事情了,過了一會才去獲取子任務的執行結果。

獲取異步任務執行結果

示例代碼:

package com.itsoku.chat18;

import java.util.concurrent.*;

/**
 * 跟着阿里p7學並發,微信公眾號:javacode2018
 */
public class Demo6 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        Future<Integer> result = executorService.submit(() -> {
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName()+",start!");
            TimeUnit.SECONDS.sleep(5);
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName()+",end!");
            return 10;
        });
        System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName());
        System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",結果:" + result.get());
    }
}

輸出:

1564581941442,main
1564581941442,pool-1-thread-1,start!
1564581946447,pool-1-thread-1,end!
1564581941442,main,結果:10

代碼中創建了一個線程池,調用線程池的submit方法執行任務,submit參數為Callable接口:表示需要執行的任務有返回值,submit方法返回一個Future對象,Future相當於一個憑證,可以在任意時間拿着這個憑證去獲取對應任務的執行結果(調用其get方法),代碼中調用了result.get()方法之后,此方法會阻塞當前線程直到任務執行結束。

超時獲取異步任務執行結果

可能任務執行比較耗時,比如耗時1分鍾,我最多只能等待10秒,如果10秒還沒返回,我就去做其他事情了。

剛好get有個超時的方法,聲明如下:

V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;

示例代碼:

package com.itsoku.chat18;

import java.util.concurrent.*;

/**
 * 跟着阿里p7學並發,微信公眾號:javacode2018
 */
public class Demo8 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        Future<Integer> result = executorService.submit(() -> {
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName()+",start!");
            TimeUnit.SECONDS.sleep(5);
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName()+",end!");
            return 10;
        });
        System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName());
        try {
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",結果:" + result.get(3,TimeUnit.SECONDS));
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
        executorService.shutdown();
    }
}

輸出:

1564583177139,main
1564583177139,pool-1-thread-1,start!
java.util.concurrent.TimeoutException
	at java.util.concurrent.FutureTask.get(FutureTask.java:205)
	at com.itsoku.chat18.Demo8.main(Demo8.java:19)
1564583182142,pool-1-thread-1,end!

任務執行中休眠了5秒,get方法獲取執行結果,超時時間是3秒,3秒還未獲取到結果,get觸發了TimeoutException異常,當前線程從阻塞狀態蘇醒了。

Future其他方法介紹一下

cancel:取消在執行的任務,參數表示是否對執行的任務發送中斷信號,方法聲明如下:

boolean cancel(boolean mayInterruptIfRunning);

isCancelled:用來判斷任務是否被取消

isDone:判斷任務是否執行完畢。

cancel方法來個示例:

package com.itsoku.chat18;

import java.util.concurrent.*;

/**
 * 跟着阿里p7學並發,微信公眾號:javacode2018
 */
public class Demo7 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        Future<Integer> result = executorService.submit(() -> {
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName()+",start!");
            TimeUnit.SECONDS.sleep(5);
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName()+",end!");
            return 10;
        });

        executorService.shutdown();
        
        TimeUnit.SECONDS.sleep(1);
        result.cancel(false);
        System.out.println(result.isCancelled());
        System.out.println(result.isDone());

        TimeUnit.SECONDS.sleep(5);
        System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName());
        System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",結果:" + result.get());
        executorService.shutdown();
    }
}

輸出:

1564583031646,pool-1-thread-1,start!
true
true
1564583036649,pool-1-thread-1,end!
1564583037653,main
Exception in thread "main" java.util.concurrent.CancellationException
	at java.util.concurrent.FutureTask.report(FutureTask.java:121)
	at java.util.concurrent.FutureTask.get(FutureTask.java:192)
	at com.itsoku.chat18.Demo7.main(Demo7.java:24)

輸出2個true,表示任務已被取消,已完成,最后調用get方法會觸發CancellationException異常。

總結:從上面可以看出Future、Callable接口需要結合ExecutorService來使用,需要有線程池的支持。

FutureTask類

FutureTask除了實現Future接口,還實現了Runnable接口,因此FutureTask可以交給Executor執行,也可以交給線程執行執行(Thread有個Runnable的構造方法),FutureTask表示帶返回值結果的任務。

上面我們演示的是通過線程池執行任務然后獲取執行結果。

這次我們通過FutureTask類,自己啟動一個線程來獲取執行結果,示例如下:

package com.itsoku.chat18;

import java.util.concurrent.*;

/**
 * 跟着阿里p7學並發,微信公眾號:javacode2018
 */
public class Demo9 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask<Integer>(()->{
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName()+",start!");
            TimeUnit.SECONDS.sleep(5);
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName()+",end!");
            return 10;
        });
        System.out.println(System.currentTimeMillis()+","+Thread.currentThread().getName());
        new Thread(futureTask).start();
        System.out.println(System.currentTimeMillis()+","+Thread.currentThread().getName());
        System.out.println(System.currentTimeMillis()+","+Thread.currentThread().getName()+",結果:"+futureTask.get());
    }
}

輸出:

1564585122547,main
1564585122547,main
1564585122547,Thread-0,start!
1564585127549,Thread-0,end!
1564585122547,main,結果:10

大家可以回過頭去看一下上面用線程池的submit方法返回的Future實際類型正是FutureTask對象,有興趣的可以設置個斷點去看看。

FutureTask類還是相當重要的,標記一下。

下面3個類,下一篇文章進行詳解

  1. 介紹CompletableFuture
  2. 介紹CompletionService
  3. 介紹ExecutorCompletionService

java高並發系列

高並發系列連載中,感興趣的加我微信itsoku,一起交流,關注公眾號:路人甲Java,每天獲取最新連載文章!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM