程序员最近都爱上了这个网站  程序员们快来瞅瞅吧!  it98k网:it98k.com

本站消息

站长简介/公众号

  出租广告位,需要合作请联系站长


+关注
已关注

分类  

暂无分类

标签  

暂无标签

日期归档  

Java多线程

发布于2021-05-29 21:20     阅读(910)     评论(0)     点赞(10)     收藏(1)


进程与线程

进程

进程是程序的一次动态执行过程,它需要经历从代码加载,代码执行到执行完毕的一个完整的过程,这个过程也是进程本身从产生,发展到最终消亡的过程。多进程操作系统能同时达运行多个进程(程序),由于 CPU 具备分时机制,所以每个进程都能循环获得自己的CPU 时间片。由于 CPU 执行速度非常快,使得所有程序好像是在同时运行一样。

线程

线程是比进程更小的执行单位,线程是进程的基础之上进行进一步的划分。所谓多线程是指一个进程在执行过程中可以产生多个更小的程序单元,这些更小的单元称为线程,这些线程可以同时存在,同时运行,一个进程可能包含多个同时执行的线程。多线程是实现并发机制的一种有效手段。进程和线程一样,都是实现并发的一个基本单位。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IxWRaiON-1621858419431)(C:\Users\kaico\Documents\WXWork\1688851869174369\WeDrive\room19\我的文件\学习文件\md学习笔记\Java基础\images\进程与线程的关系图.png)]

Java多线程的基本概念

程序

被存储在磁盘或其他的数据存储设备中的可执行文件,也就是一堆静态的代码。

进程

是程序的一次动态的执行过程,操作系统同时管理一个计算机中的多个进程。(有独立的内存空间和系统资源),每一个进程的内部数据和状态都是完全独立的。

线程

是进程中执行运算的最小单位,客完成一个独立的顺序控制流程

多线程

指一个进程在执行过程中可以产生多个线程,这些线程可以同时存在、同时运行,一个进程可能包含了多个同时执行的线程。多个线程交替占用CPU资源,而非真正的并行执行

主线程
  1. main()方法为主线程的入口
  2. 产生其他子线程的线程
  3. 必须是最后完成执行,因为它执行各种关闭动作
并发与并行

并发:两个队列交替使用一台咖啡机(同一时刻只有一台在使用)

并行:两个队列分别使用(同时)两个咖啡机

同步与异步

同步:就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。
换句话说,就是由调用者主动等待这个调用的结果。

异步:调用*在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用*发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

为什么要用同步?

Java中当多个线程同时操作一个可共享的资源变量时,可能会因为多个线程操作资源变量不同步而导致数据冲突,因此加入同步锁以避免在该线程没有完成操作之前被其他线程调用, 从而保证了该变量的唯一性。

线程调度
  1. 分时调度:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。
  2. 抢占式调度:优先让优先级高的程序使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性)。Java使用的为抢占式调度。
阻塞与非阻塞
  1. 阻塞调用是指在调用返回结果之前,当前线程会被挂起。调用线程只要在返回结果之后才会返回。
  2. 非阻塞是指在不能立刻得到结果之前,该调用不会阻塞该线程。

锁在代码中指的是在同一时刻、同一JVM中同一段代码(或者同一个数据)只能被一个线程执行(操作)。

作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等 ) 。

死锁

指两个或以上个线程都在等待其他线程先完成,造成了程序的停滞。

Java中创建线程的几种方式

Thread类

java.lang.Thread类代表线程,任何线程都是Thread类(子类)的实例。查看JDK文档

主要方法
  1. run():使用Runnable引用构造线程对象,调用方法时最终调用接口中的版本;没有使用Runnable引用构造线程对象,调用该方法时则啥也不做;
  2. start():用于启动线程,Java虚拟机会自动调用该线程的run()方法;
创建线程方式

自定义类继承Thread类并根据自己的需求重写run方法,然后在主类中创建该类的对象调用start方法,这样就启动了一个线程。使用方式就是自定义一个类继承Thread类,作为一个可以备用的线程类。

public class SubThreadRun extends Thread {
    @Override
    public void run() {
        //打印1~20的整数值
        for (int i = 0; i < 20 ; i ++) {
            System.out.println("SubThreadRun线程中:" + i);
        }
    }
}

这里启动线程有两个方法:run()方法和start()方法

区别如下:

  • 调用run方法测试时,本质上就是相当于对普通成员方法的调用,因此执行流程就是run方法的代码执行完毕后才能继续向下执行。
  • 调用start方法测试时,相当于又启动了一个线程,加上执行main方法的线程,一共有两个线程,这两个线程同时运行,所以输出结果是交错的。
使用缺陷

假如SubThreadRun类已经继承了一个父类,这个时候我们又要把该类作为自定义线程类,如果还是用继承Thread类的方法来实现的话就违背了Java不可多继承的概念。所以使用接口的方式就可以避免这种问题。

Runnable接口

自定义类实现Runnable接口并重写run方法,创建该类的对象作为实参来构造Thread类型的对象,然后使用Thread类型的对象调用start方法。查看JDK文档

//创建一个自定义类SubRunnableRun实现Runnable接口
public class SubRunnableRun implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 20 ; i ++) {
            System.out.println("SubRunnableRun线程中:" + i);
        }
    }
}
使用方式

创建线程调用的是Thread的有参构造方法,参数是Runnable类型的。

 //1.创建自定义类型的对象
 SubRunnableRun srr = new SubRunnableRun();
 //2.使用该对象作为实参构造Thread类型的对象
 Thread t1 = new Thread(srr);
 //3.使用Thread类型的对象调用start方法
 t1.start();

内部类的方式实现创建

上面两种创建线程的方式都需要单独创建一个类来继承Thread类或者实现Runnable接口,并重写run方法。而匿名内部类可以不创建单独的类而实现自定义线程的创建。这种方式只是前两种创建线程方式的简写。

代码案例
public static void main(String[] args) {

        //匿名内部类的语法格式:父类/接口类型 引用变量 = new 父类/接口类型 {方法的重写};
        //1.使用继承加匿名内部类的方式创建并启动线程
        Thread t1 = new Thread() {
            @Override
            public void run() {
                System.out.println("继承Thread类方式创建线程...");
            }
        };
        t1.start();
       
        //2.使用接口加匿名内部类的方式创建并启动线程
        Runnable ra = new Runnable() {
            @Override
            public void run() {
                System.out.println("实现Runnable接口方式实现线程...");
            }
        };
        Thread t2 = new Thread(ra);
        t2.start();
    }

这两个利用匿名内部类创建线程的方式还能继续简化代码,尤其是使用Runnable接口创建线程的方式,可以使用Lambda表达式进行简化。

通过实现Callable接口创建

前两种方式创建线程存在一个问题,就是run方法是没有返回值的,所以如果我们希望在线程结束之后给出一个结果,那就需要用到实现Callable接口创建线程。

Callable接口

从Java5开始新增创建线程的第三中方式为实现java.util.concurrent.Callable接口。查看JDK文档

常用方法:
V call() :计算结果,如果无法计算结果,则抛出一个异常;

启动线程只有创建一个Thread类并调用start方法,如果想让线程启动时调用到Callable接口中的call方法,就得用到FutureTask类。

FutureTask类

java.util.concurrent.FutureTask类实现了RunnableFuture接口,RunnableFuture接口是Runnable和Future的综合体,作为一个Future,FutureTask可以执行异步计算,可以查看异步程序是否执行完毕,并且可以开始和取消程序,并取得程序最终的执行结果,也可以用于获取调用方法后的返回结果。查看JDK文档

常用方法:

  1. 构造方法:FutureTask​(Callable callable),创建一个FutureTask,它将在运行时执行给定的Callable;
  2. 成员方法:V get(),获取call()方法计算的结果;
使用方式

从上面的概念可以了解到FutureTask类的一个构造方法是以Callable为参数的,然后FutureTask类是Runnable的子类,所以FutureTask类可以作为Thread类的参数。这样的话我们就可以创建一个Thread线程并调用Callable接口中的call方法。

public class ThreadCallableTest implements Callable {
    @Override
    public Object call() throws Exception {
        //计算1 ~ 10000之间的累加和并打印返回
        int sum = 0;
        for (int i = 0; i <= 10000; i ++) {
            sum += i;
        }
        System.out.println("call方法中的结果:" + sum);
        return sum;
    }

    public static void main(String[] args) {

        ThreadCallableTest tct = new ThreadCallableTest();
        FutureTask ft = new FutureTask(tct);
        Thread t1 = new Thread(ft);
        t1.start();
        Object ob = null;
        try {
            ob = ft.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("main方法中的结果:" + ob);
    }
}

线程池

线程池的由来

比如说服务器编程中,如果为每一个客户都分配一个新的工作线程,并且当工作线程与客户通信结束时,这个线程被销毁,这就需要频繁的创建和销毁工作线程。如果访问服务器的客户端过多,那么会严重影响服务器的性能。

线程池的概念

首先创建一些线程,他们的集合称为线程池,当服务器接收到一个客户请求后,就从线程池中取出一个空余的线程为之服务,服务完后不关闭线程,而是将线程放回到线程池中。

线程池的创建

线程池的创建方法总共有 7 种,但总体来说可分为 2 类:
第一类:Executors是一个工具类和线程池的工厂类,用于创建并返回不同类型的线程池,常用的方法如下:查看JDK文档
在这里插入图片描述
第二类:HreadPoolExecutor通过构造方法创建线程池,最多可以设置7个参数,创建线程池的构造方法如下:查看JDK文档
在这里插入图片描述
通过上面两类方法创建完线程池后都可以用ExecutorService接口进行接收,它是真正的线程池接口,主要实现类是ThreadPoolExecutor,常用方法如下:
在这里插入图片描述

代码实例

1、使用newFixedThreadPool方法创建线程池:

public class FixedThreadPool {
    public static void main(String[] args) {
        
        // 创建含有两个线程的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(2);
        // 创建任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("线程:" + Thread.currentThread().getName() + "执行了任务!");
            }
        };
        // 线程池执行任务
        threadPool.execute(runnable);
        threadPool.execute(runnable);
        threadPool.execute(runnable);
        threadPool.execute(runnable);
    }
}

输出结果:
    线程:pool-1-thread-2执行了任务!
    线程:pool-1-thread-1执行了任务!
    线程:pool-1-thread-2执行了任务!
    线程:pool-1-thread-1执行了任务!

从结果上可以看出,这四个任务分别被线程池中的固定的两个线程所执行,线程池也不会创建新的线程来执行任务。

2、使用newCachedThreadPool方法创建线程池

public class cachedThreadPool {
    public static void main(String[] args) {

        //1.创建线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //2.设置任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("线程:" + Thread.currentThread().getName() + "执行了任务!");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                }
            }
        };
        //3.执行任务
        for (int i = 0; i < 100; i ++) {
            executorService.execute(runnable);
        }
    }
}

输出结果:
    线程:pool-1-thread-1执行了任务!
    线程:pool-1-thread-4执行了任务!
    线程:pool-1-thread-3执行了任务!
    线程:pool-1-thread-2执行了任务!
    线程:pool-1-thread-5执行了任务!
    线程:pool-1-thread-7执行了任务!
    线程:pool-1-thread-6执行了任务!
    线程:pool-1-thread-8执行了任务!
    线程:pool-1-thread-9执行了任务!
    线程:pool-1-thread-10执行了任务!

从结果上可以看出,线程池根据任务的数量来创建对应的线程数量。

3、使用newSingleThreadExecutor的方法创建线程池

public class singleThreadExecutor {

    public static void main(String[] args) {

        //创建线程池
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        //执行任务
        for (int i = 0; i < 10; i ++) {
            final int task = i + 1;
            executorService.execute(()->{
                System.out.println("线程:" + Thread.currentThread().getName() + "执行了第" + task +"任务!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

输出结果:
    线程:pool-1-thread-1执行了第1任务!
    线程:pool-1-thread-1执行了第2任务!
    线程:pool-1-thread-1执行了第3任务!
    线程:pool-1-thread-1执行了第4任务!
    线程:pool-1-thread-1执行了第5任务!
    线程:pool-1-thread-1执行了第6任务!
    线程:pool-1-thread-1执行了第7任务!
    线程:pool-1-thread-1执行了第8任务!
    线程:pool-1-thread-1执行了第9任务!
    线程:pool-1-thread-1执行了第10任务!

从结果可以看出,该方法创建的线程可以保证任务执行的顺序。一般保证任务能够顺序执行的方法为同一时刻只有一个线程来执行任务,和消息队列的顺序消费原理一致。

4、使用newScheduledThreadPool的方法创建线程池

public class ScheduledThreadPool {

    public static void main(String[] args) {

        //创建包含2个线程的线程池
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
        //记录创建任务时的当前时间
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date startTime = new Date();
        String start = formatter.format(startTime);
        System.out.println("创建任务时的时间:" + start);
        //创建任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                Date endTime = new Date();
                String end = formatter.format(endTime);
                System.out.println("线程:" + Thread.currentThread().getName() + "任务执行的时间为:" + end);
            }
        };
        //执行任务(参数:runnable-要执行的任务,2-从现在开始延迟执行的时间,TimeUnit.SECONDS-延迟参数的时间单位)
        for(int i = 0; i < 2; i ++) {
            scheduledExecutorService.schedule(runnable,2, TimeUnit.SECONDS);
        }
    }
}

输出结果:
    创建任务的时间:2021-04-19 19:26:18
    线程:pool-1-thread-1任务执行的时间为:2021-04-19 19:26:20
    线程:pool-1-thread-2任务执行的时间为:2021-04-19 19:26:20

从结果可以看出,该方法创建的线程池可以分配已有的线程执行一些需要延迟的任务。

5、使用newSingleThreadScheduledExecutor方法创建线程池

public class SingleThreadScheduledExecutor {

    public static void main(String[] args) {

        //创建线程池
        ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        //创建任务
        Date startTime = new Date();
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String start = formatter.format(startTime);
        System.out.println("创建任务的时间:" + start);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                Date endTime = new Date();
                String end = formatter.format(endTime);
                System.out.println("线程:" + Thread.currentThread().getName() + "任务执行的时间为:" + end);
            }
        };
        //执行任务
        for(int i = 0; i < 2; i ++) {
            scheduledExecutorService.schedule(runnable,2, TimeUnit.SECONDS);
        }
    }
}

输出结果:
    创建任务的时间:2021-04-19 19:51:58
    线程:pool-1-thread-1任务执行的时间为:2021-04-19 19:52:00
    线程:pool-1-thread-1任务执行的时间为:2021-04-19 19:52:00

从结果可以看出,该方法创建的线程池只有一个线程,该线程去执行一些需要延迟的任务。

6、使用newWorkStealingPool方法创建线程池

public class newWorkStealingPool {

    public static void main(String[] args) {

        //创建线程池
        ExecutorService executorService = Executors.newWorkStealingPool();
        //执行任务
        for (int i = 0; i < 4; i ++) {
            final int task = i + 1;
            executorService.execute(()->{
                System.out.println("线程:" + Thread.currentThread().getName() + "执行了第" + task +"任务!");
            });
        }
        //确保任务被执行
        while (!executorService.isTerminated()) {
        }
    }
}

输出结果:
    线程:ForkJoinPool-1-worker-9执行了第1任务!
    线程:ForkJoinPool-1-worker-4执行了第4任务!
    线程:ForkJoinPool-1-worker-11执行了第3任务!
    线程:ForkJoinPool-1-worker-2执行了第2任务!

从结果可以看出,该方法会创建一个含有足够多线程的线程池,来维持相应的并行级别,任务会被抢占式执行。(任务执行顺序不确定)

7、使用ThreadPoolExecutor创建线程池
ThreadPoolExecutor构造方法,该构造方法最多可以设置7个参数:

ThreadPoolExecutor(int corePoolSize, 
                   int maximumPoolSize, 
                   long keepAliveTime, 
                   TimeUnit unit, 
                   BlockingQueue<Runnable> workQueue, 
                   ThreadFactory threadFactory, 
                   RejectedExecutionHandler handler)
  1. corePoolSize:核心线程数,在线程池中一直存在的线程(对应银行办理业务模型:一开始就开放的窗口)

  2. maximumPoolSize:最大线程数,线程池中能创建最多的线程数,除了核心线程数以外的几个线程会在线程池的任务队列满了之后创建(对应银行办理业务模型:所有窗口)

  3. keepAliveTime:最大线程数的存活时间,当长时间没有任务时,线程池会销毁一部分线程,保留核心线程

  4. unit:时间单位,是第三个参数的单位,这两个参数组合成最大线程数的存活时间

    TimeUnit.DAYS:天
    TimeUnit.HOURS:小时
    TimeUnit.MINUTES:分
    TimeUnit.SECONDS:秒
    TimeUnit.MILLISECONDS:毫秒
    TimeUnit.MICROSECONDS:微秒
    TimeUnit.NANOSECONDS:纳秒

  5. workQueue:等待队列,用于保存在线程池等待的任务(对应银行办理业务模型:等待区)
    ArrayBlockingQueue:一个由数组支持的有界阻塞队列。
    LinkedBlockingQueue:一个由链表组成的有界阻塞队列。
    SynchronousQueue:该阻塞队列不储存任务,直接提交给线程,这样就会形成对于提交的任务,如果有空闲线程,则使用空闲线程来处理,否则新建一个线程来处理任务。
    PriorityBlockingQueue:一个带优先级的无界阻塞队列,每次出队都返回优先级最高或者最低的元素
    DelayQueue:一个使用优先级队列实现支持延时获取元素的无界阻塞队列,只有在延迟期满时才能从中提取元素,现实中的使用: 淘宝订单业务:下单之后如果三十分钟之内没有付款就自动取消订单。
    LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
    LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

  6. threadFactory:线程工厂,用于创建线程。

  7. handler:拒绝策略,任务超出线程池可接受范围时,拒绝处理任务时的策略。
    ThreadPoolExecutor.AbortPolicy:当任务添加到线程池中被拒绝时,它将抛出 RejectedExecutionException 异常(默认使用该策略)
    ThreadPoolExecutor.CallerRunsPolicy:当任务添加到线程池中被拒绝时,会调用当前线程池的所在的线程去执行被拒绝的任务
    ThreadPoolExecutor.DiscardOldestPolicy:当任务添加到线程池中被拒绝时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务添加进去
    ThreadPoolExecutor.DiscardPolicy:如果该任务被拒绝,这直接忽略或者抛弃该任务

当任务数小于等于核心线程数+等待队列数量的总和时:

public class ThreadPoolExecutorTest {

    public static void main(String[] args) {

        // 创建线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        //创建任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "==>执行任务");
            }
        };
        // 执行任务
        for (int i = 0; i < 5; i++) {
            threadPool.execute(runnable);
        }
        //关闭线程池
        threadPool.shutdown();
    }
}


输出结果:
    pool-1-thread-2==>执行任务
    pool-1-thread-1==>执行任务
    pool-1-thread-2==>执行任务
    pool-1-thread-1==>执行任务
    pool-1-thread-2==>执行任务

从结果中可以看出,只有两个核心线程在执行任务。

当任务数大于核心线程数+等待队列数量的总和,但是小于等于最大线程数时:

public class ThreadPoolExecutorTest {

    public static void main(String[] args) {

        // 创建线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        //创建任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "==>执行任务");
            }
        };
        // 执行任务
        for (int i = 0; i < 7; i++) {
            threadPool.execute(runnable);
        }
        //关闭线程池
        threadPool.shutdown();
    }
}

输出结果:
    pool-1-thread-1==>执行任务
    pool-1-thread-4==>执行任务
    pool-1-thread-2==>执行任务
    pool-1-thread-2==>执行任务
    pool-1-thread-3==>执行任务
    pool-1-thread-4==>执行任务
    pool-1-thread-1==>执行任务

从结果中可以看出,启动了最大线程来执行任务。

当任务数大于最大线程数时:

public class ThreadPoolExecutorTest {

    public static void main(String[] args) {

        // 创建线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        //创建任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "==>执行任务");
            }
        };
        // 执行任务
        for (int i = 0; i < 8; i++) {
            threadPool.execute(runnable);
        }
        //关闭线程池
        threadPool.shutdown();
    }
}

输出结果:
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.zck.task18.ThreadPool.ThreadPoolExecutorTest$1@7f31245a rejected from java.util.concurrent.ThreadPoolExecutor@6d6f6e28[Running, pool size = 4, active threads = 0, queued tasks = 0, completed tasks = 7]
    at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
    at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
    at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
    at com.zck.task18.ThreadPool.ThreadPoolExecutorTest.main(ThreadPoolExecutorTest.java:25)
    pool-1-thread-1==>执行任务
    pool-1-thread-4==>执行任务
    pool-1-thread-4==>执行任务
    pool-1-thread-4==>执行任务
    pool-1-thread-2==>执行任务
    pool-1-thread-3==>执行任务
    pool-1-thread-1==>执行任务

从结果中可以看出,任务大于最大线程数,使用拒绝策略直接抛出异常。

原文链接:https://blog.csdn.net/weixin_44044929/article/details/117231319



所属网站分类: 技术文章 > 博客

作者:天花灯

链接:http://www.javaheidong.com/blog/article/207569/4612b17591a9721acb58/

来源:java黑洞网

任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任

10 0
收藏该文
已收藏

评论内容:(最多支持255个字符)