zoukankan      html  css  js  c++  java
  • Java并发编程笔记之基础总结(一)

    一.线程概念

    说到线程就必须要提一下进程,因为线程是进程中的一个实体,线程本身是不会独立存在的。进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程至少有一个线程,进程中的多个线程是共享进程的资源的。操作系统在分配资源时候是把资源分配给进程的,但是 CPU 资源就比较特殊,它是分派到线程的,因为真正要占用 CPU 运行的是线程,所以也说线程是 CPU 分配的基本单位。

    Java 中当我们启动 main 函数时候其实就启动了一个 JVM 的进程,而 main 函数所在线程就是这个进程中的一个线程,也叫做主线程。

    如上图一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器,栈区域。、

    其中程序计数器是一块内存区域,用来记录线程当前要执行的指令地址,那么程序计数器为何要设计为线程私有的呢?

    前面说了线程是占用 CPU 执行的基本单位,而 CPU 一般是使用时间片轮转方式让线程轮询占用的,所以当前线程 CPU 时间片用完后,要让出 CPU,等下次轮到自己时候在执行。

    那么如何知道之前程序执行到哪里了?

    其实程序计数器就是为了记录该线程让出 CPU 时候的执行地址,待再次分配到时间片时候就可以从自己私有的计数器指定地址继续执行了。

    另外每个线程有自己的栈资源,用于存储该线程的局部变量,这些局部变量是该线程私有的,其它线程是访问不了的,另外栈还用来存放线程的调用栈帧。

    堆是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建时候分配的,堆里面主要存放使用 new 操作创建的对象实例。

    方法区则是用来存放进程中的代码片段的,是线程共享的。

    二.线程创建方式与运行

    Java 中有三种线程创建方法,分别为实现 Runnable 接口的run方法、继承 Thread 类并重写 run 方法、使用 FutureTask 方式。

      1.继承 Thread 方法的实现,如下所示:

    /**
     * Created by cong on 2018/7/17.
     */
    public class ThreadTest {
        //继承Thread类并重写run方法
        public static class MyThread extends Thread {
    
            @Override
            public void run() {
    
                System.out.println("-----子线程-----");
    
            }
        }
    
        public static void main(String[] args) {
    
            // 创建线程
            MyThread thread = new MyThread();
    
            // 启动线程
            thread.start();
        }
    }

    运行结果如下:

    如上代码 MyThread 类继承了 Thread 类,并重写了 run 方法,然后调用了线程的 start 方法启动了线程,当创建完 thread 对象后该线程并没有被启动执行.当调用了 start 方法后才是真正启动了线程。其实当调用了 start 方法后线程并没有马上执行而是处于就绪状态,这个就绪状态是指该线程已经获取了除 CPU 资源外的其它资源,等获取 CPU 资源后才会真正处于运行状态。

    当 run 方法执行完毕,该线程就处于终止状态了。使用继承方式好处是 run 方法内获取当前线程直接使用 this 就可以,无须使用 Thread.currentThread() 方法,不好的地方是 Java 不支持多继承,如果继承了 Thread 类那么就不能再继承其它类,另外任务与代码没有分离,当多个线程执行一样的任务时候需要多份任务代码,而 Runable 则没有这个限制。

      2.实现 Runnable 接口的 run 方法方式,例子如下所示:

    /**
     * Created by cong on 2018/7/17.
     */
    public class RunableTest implements Runnable {
        @Override
        public void run() {
            System.out.println("----子线程----");
        }
    
        public static void main(String[] args) throws InterruptedException{
    
            RunableTest runableTest = new RunableTest();
            new Thread(runableTest).start();
            new Thread(runableTest).start();
        }
    }

    运行结果如下:

    如上面代码,两个线程公用一个 task 代码逻辑,需要的话 RunableTask 可以添加参数进行任务区分,另外 RunableTask 可以继承其他类,但是上面两种方法都有一个缺点就是任务没有返回值,

      3.使用 FutureTask方式,例子如下所示:

    /**
     * Created by cong on 2018/7/17.
     */
    public class FutureTaskTest implements Callable<String> {
        @Override
        public String call() throws Exception {
    
            return "hello";
        }
    
        public static void main(String[] args) throws InterruptedException {
            // 创建异步任务
            FutureTask<String> futureTask = new FutureTask<>(new FutureTaskTest());
            //启动线程
            new Thread(futureTask).start();
            try {
                //等待任务执行完毕,并返回结果
                String result = futureTask.get();
                System.out.println(result);
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }

    运行结果如下:

    总结:每种方式都有自己的优缺点,应该根据实际场景进行选择

    三.线程通知与等待

    Java 中 Object 类是所有类的父类,鉴于继承机制,Java 把所有类都需要的方法放到了 Object 类里面,其中就包含本节要讲的通知等待系列函数,这些通知等待函数是组成并发包中线程同步组件的基础。

    下面讲解下 Object 中关于线程同步的通知等待函数。如下所示:

      1.void wait() 方法:首先谈下什么是共享资源,所谓共享资源是说该资源被多个线程共享,多个线程都可以去访问或者修改的资源。当一个线程调用一个共享对象的 wait() 方法时候,调用线程会被阻塞挂起,直到下面几个事情之一发生才返回:

    1. 其它线程调用了该共享对象的 notify() 或者 notifyAll() 方法;
    2. 其它线程调用了该线程的 interrupt() 方法设置了该线程的中断标志,该线程会抛出 InterruptedException 异常返回

    另外需要注意的是如果调用 wait() 方法的线程没有事先获取到该对象的监视器锁,则调用 wait() 方法时候调用线程会抛出 IllegalMonitorStateException 异常。

    那么一个线程如何获取到一个共享变量的监视器呢?

      1.执行使用 synchronized 同步代码块时候,使用该共享变量作为参数,例子如下:

    synchronized(共享变量){
       //doSomething
    }

      2.调用该共享变量的方法,并且该方法使用了 synchronized 修饰,代码如下:

    synchronized void add(int a,int b){
       //doSomething
    }

    另外需要注意的是一个线程可以从挂起状态变为可以运行状态(也就是被唤醒)即使该线程没有被其它线程调用 notify(),notifyAll() 进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒

    虽然虚假唤醒在应用实践中很少发生,但是还是需要防范于未然的,做法就是不停的去测试该线程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个循环中去调用 wait() 方法进行防范,退出循环的条件是条件满足了唤醒该线程。代码如下:

     synchronized (obj) {
         while (条件不满足){
             obj.wait();  
         }
     }

    如上代码是经典的调用共享变量 wait() 方法的实例,首先通过同步块获取 obj 上面的监视器锁,然后通过 while 循环内调用 obj 的 wait() 方法。

    下面从生产者消费者例子来加深理解,例子如下:

      生产者:

    //生产线程
    synchronized (queue) { 
    
        //消费队列满,则等待队列空闲
        while (queue.size() == MAX_SIZE) { 
            try { 
                //挂起当前线程,并释放通过同步块获取的queue上面的锁,让消费线程可以获取该锁,然后获取队列里面元素
                queue.wait(); 
            } catch (Exception ex) { 
                ex.printStackTrace(); 
            } 
        }
    
        //空闲则生成元素,并通知消费线程
        queue.add(ele); 
        queue.notifyAll(); 
    
        } 
    } 

      消费者:

    //消费线程
    synchronized (queue) { 
    
        //消费队列为空
        while (queue.size() == 0) { 
            try {
                //挂起当前线程,并释放通过同步块获取的queue上面的锁,让生产线程可以获取该锁,生产元素放入队列
                queue.wait(); 
            } catch (Exception ex) { 
                ex.printStackTrace(); 
            } 
        }
    
        //消费元素,并通知唤醒生产线程
        queue.take(); 
        queue.notifyAll();  
    } 

    如上面代码所示是一个生产者的例子,其中 queue 为共享变量,生产者线程在调用 queue 的 wait 方法前,通过使用 synchronized 关键字拿到了该共享变量 queue 的监视器,所以调用 wait() 方法才不会抛出 IllegalMonitorStateException 异常,如果当前队列没有空闲容量则会调用 queued 的 wait() 挂起当前线程,这里使用循环就是为了避免上面说的虚假唤醒问题,这里假如当前线程虚假唤醒了,但是队列还是没有空余容量的话,当前线程还是会调用 wait() 把自己挂起。

    另外当一个线程调用了共享变量的 wait() 方法后该线程会被挂起,同时该线程会暂时释放对该共享变量监视器的持有,直到另外一个线程调用了共享变量的 notify() 或者 notifyAll() 方法才有可能会重新获取到该共享变量的监视器的持有权(这里说有可能,是因为考虑到多个线程第一次都调用了 wait() 方法,所以多个线程会竞争持有该共享变量的监视器)。、

    接下来讲解下调用共享变量 wait() 方法后当前线程会释放持有的共享变量的锁的理解。

    如上代码假如生产线程 A 首先通过 synchronized 获取到了 queue 上的锁,那么其它生产线程和所有消费线程都会被阻塞,线程 A 获取锁后发现当前队列已满会调用 queue.wait() 方法阻塞自己,然后会释放获取的 queue 上面的锁,这里考虑下为何要释放该锁?如果不释放,由于其它生产线程和所有消费线程已经被阻塞挂起,而线程 A 也被挂起,这就处于了死锁状态。这里线程 A 挂起自己后释放共享变量上面的锁就是为了打破死锁必要条件之一的持有并等待原则。关于死锁下面章节会有讲到,线程 A 释放锁后其它生产线程和所有消费线程中会有一个线程获取 queue 上的锁进而进入同步块,这就打破了死锁。

    最后再举一个例子说明当一个线程调用共享对象的 wait() 方法被阻塞挂起后,如果其它线程中断了该线程,则该线程会抛出 InterruptedException 异常后返回,代码如下:

    /**
     * Created by cong on 2018/7/17.
     */
    public class WaitNotifyInteruptTest {
        static Object obj = new Object();
    
        public static void main(String[] args) throws InterruptedException {
    
            //创建线程
            Thread threadA = new Thread(new Runnable() {
                public void run() {
                    try {
                        System.out.println("---开始---");
                        //阻塞当前线程
                        obj.wait();
                        System.out.println("---结束---");
    
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
    
            threadA.start();
    
            Thread.sleep(1000);
    
            System.out.println("---开始打断线程A---");
            threadA.interrupt();
            System.out.println("---线程A已经被打断---");
        }
    }

    运行结果如下:

    如上代码 threadA 调用了共享对 obj 的 wait() 方法后阻塞挂起了自己,然后主线程在休眠1s后中断了 threadA 线程,可知中断后 threadA 在 obj.wait() 处抛出了 java.lang.IllegalMonitorStateException 异常后返回后终止。

      2.void wait(long timeout) 方法:该方法相比 wait() 方法多一个超时参数,不同在于如果一个线程调用了共享对象的该方法挂起后,如果没有在指定的 timeout ms 时间内被其它线程调用该共享变量的 notify() 或者 notifyAll() 方法唤醒,那么该函数还是会因为超时而返回。需要注意的是如果在调用该函数时候 timeout 传递了负数会抛出 IllegalArgumentException 异常。

      3.void wait(long timeout, int nanos) 方法:内部是调用 wait(long timeout),如下代码:只是当 nanos>0 时候让参数一递增1。源码如下:

    public final void wait(long timeout, int nanos) throws InterruptedException {
            if (timeout < 0) {
                throw new IllegalArgumentException("timeout value is negative");
            }
    
            if (nanos < 0 || nanos > 999999) {
                throw new IllegalArgumentException(
                                    "nanosecond timeout value out of range");
            }
    
            if (nanos > 0) {
                timeout++;
            }
    
            wait(timeout);
    }

      4.void notify() 方法:一个线程调用共享对象的 notify() 方法后,会唤醒一个在该共享变量上调用 wait 系列方法后被挂起的线程,一个共享变量上可能会有多个线程在等待,具体唤醒哪一个等待的线程是随机的。另外被唤醒的线程不能马上从 wait 返回继续执行,它必须获取了共享对象的监视器后才可以返回,也就是唤醒它的线程释放了共享变量上面的监视器锁后,被唤醒它的线程也不一定会获取到共享对象的监视器,这是因为该线程还需要和其它线程一块竞争该锁,只有该线程竞争到了该共享变量的监视器后才可以继续执行。

    类似 wait 系列方法,只有当前线程已经获取到了该共享变量的监视器锁后,才可以调用该共享变量的 notify() 方法,否者会抛出 IllegalMonitorStateException 异常。

      5.void notifyAll() 方法:不同于 nofity() 方法在共享变量上调用一次就会唤醒在该共享变量上调用 wait 系列方法被挂起的一个线程,notifyAll() 则会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程。

    最后讲一个例子来说明 notify() 和 notifyAll() 的具体含义和一些需要注意的地方,代码实例如下:

    /**
     * Created by cong on 2018/7/17.
     */
    public class Test1 {
        private static volatile Object resourceA = new Object();
    
        public static void main(String[] args) throws InterruptedException {
            // 创建线程
            Thread threadA = new Thread(new Runnable() {
                public void run() {
                    // 获取resourceA共享资源的监视器锁
                    synchronized (resourceA) {
                        System.out.println("threadA get resourceA lock");
                        try {
                            System.out.println("threadA begin wait");
                            resourceA.wait();
                            System.out.println("threadA end wait");
    
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                }
            });
    
            // 创建线程
            Thread threadB = new Thread(new Runnable() {
                public void run() {
                    synchronized (resourceA) {
                        System.out.println("threadB get resourceA lock");
                        try {
                            System.out.println("threadB begin wait");
                            resourceA.wait();
                            System.out.println("threadB end wait");
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                }
    
            });
    
            // 创建线程
            Thread threadC = new Thread(new Runnable() {
                public void run() {
                    synchronized (resourceA) {
                        System.out.println("threadC begin notify");
                        resourceA.notifyAll();
                    }
                }
            });
    
            // 启动线程
            threadA.start();
            threadB.start();
    
            Thread.sleep(1000);
            threadC.start();
    
            // 等待线程结束
            threadA.join();
            threadB.join();
            threadC.join();
            System.out.println("main over");
        }
    }

    运行结果如下:

    如上代码开启了三个线程,其中线程 A 和 B 分别调用了共享资源 resourceA 的 wait() 方法,线程 C 则调用了 nofity() 方法。

    这里启动线程 C 前首先调用 sleep 方法让主线程休眠 1s,目的是让线程 A 和 B 全部执行到调用 wait 方法后在调用线程 C 的 notify 方法。

    这个例子企图希望在线程 A 和线程 B 都因调用共享资源 resourceA 的 wait() 方法而被阻塞后,线程 C 在调用 resourceA 的 notify() 方法,希望可以唤醒线程 A 和线程 B,但是从执行结果看只有一个线程 A 被唤醒了,线程 B 没有被唤醒,

    从结果看线程调度器这次先调度了线程 A 占用 CPU 来运行,线程 A 首先获取 resourceA 上面的锁,然后调用 resourceA 的 wait() 方法挂起当前线程并释放获取到的锁,然后线程 B 获取到 resourceA 上面的锁并调用了 resourceA 的 wait(),此时线程 B 也被阻塞挂起并释放了 resourceA 上的锁。

    线程 C 休眠结束后在共享资源 resourceA 上调用了 notify() 方法,则会激活 resourceA 的阻塞集合里面的一个线程,这里激活了线程 A,所以线程 A 调用的 wait() 方法返回了,线程 A 执行完毕。而线程 B 还处于阻塞状态。

    如果把线程 C 里面调用的 notify() 改为调用 notifyAll() 而执行结果如下:

    可知线程 A 和线程 B 被挂起后,线程 C 调用 notifyAll() 函数会唤醒在 resourceA 等待的所有线程,这里线程 A 和线程 B 都会被唤醒,只是线程 B 先获取到 resourceA 上面的锁然后从 wait() 方法返回,等线程 B 执行完毕后,线程 A 又获取了 resourceA 上面的锁,然后从 wait() 方返回,当线程 A 执行完毕,主线程就返回后,然后打印输出。

    总结:在调用具体共享对象的 wait 或者 notify 系列函数前要先获取共享对象的锁;另外通知和等待是实现线程同步的原生方法,理解它们的协作功能很有必要;最后由于线程虚假唤醒的存在,一定要使用循环检查的方式。

      6.等待线程执行终止的 join 方法:在项目实践时候经常会遇到一个场景,就是需要等待某几件事情完成后才能继续往下执行,比如多个线程去加载资源,当多个线程全部加载完毕后在汇总处理,Thread 类中有个静态的 join 方法就可以做这个事情,前面介绍的等待通知方法是属于 Object 类的,而 join 方法则是直接在 Thread 类里面提供的,join 是无参,返回值为 void 的方法。下面看一个简单的例子来介绍 join 的使用:

    /**
     * Created by cong on 2018/7/17.
     */
    public class Test3 {
        public static void main(String[] args) throws InterruptedException {
            Thread threadOne = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("child threadOne over!");
                }
            });
    
            Thread threadTwo = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("child threadTwo over!");
                }
            });
            //启动子线程
            threadOne.start();
            threadTwo.start();
            System.out.println("wait all child thread over!");
            //等待子线程执行完毕,返回
            threadOne.join();
            threadTwo.join();
            System.out.println("all child thread over!");
        }
    }

    运行结果如下:

    如代码主线程里面启动了两个子线程,然后在分别调用了它们的 join() 方法,那么主线程首先会阻塞到 threadOne.join() 方法,等 threadOne 执行完毕后返回,threadOne 执行完毕后 threadOne.join() 就会返回,然后主线程调用 threadTwo.join() 后再次被阻塞,等 threadTwo 执行完毕后主线程也就返回了。这里只是为了演示 join 的作用,对应这类需求后面会讲的 CountDownLatch 是不错选择。

    另外线程 A 调用线程 B 的 join 方法后会被阻塞,当其它线程调用了线程 B 的 interrupt() 方法中断了线程 B 时候,线程 B 会抛出 InterruptedException 异常而返回,下面通过一个例子来加深理解:

    /**
     * Created by cong on 2018/7/17.
     */
    public class Test4 {
        public static void main(String[] args) throws InterruptedException {
            //线程one
            Thread threadOne = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("threadOne begin run!");
                    for (;;) {
                    }
                }
            });
            //获取主线程
            final Thread mainThread = Thread.currentThread();
            //线程two
            Thread threadTwo = new Thread(new Runnable() {
                @Override
                public void run() {
                    //休眠1s
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //中断主线程
                    mainThread.interrupt();
                }
            });
            // 启动子线程
            threadOne.start();
            //延迟1s启动线程
            threadTwo.start();
            try{//等待线程one执行结束
                threadOne.join();
    
            }catch(InterruptedException e){
                System.out.println("main thread:" + e);
            }
        }
    }

    运行结果如下:

    如上代码 threadOne 线程里面执行死循环,主线程调用 threadOne 的 join 方法阻塞自己等待线程 threadOne 执行完毕,待 threadTwo 休眠 1s 后会调用主线程的 interrupt() 方法设置主线程的中断标志。

    从结果看主线程中 threadOne.join() 处会抛出 InterruptedException 异常而返回。这里需要注意的是 threadTwo 里面调用的是主线程的 interrupt(),而不是线程 threadOne 的。

    总结:由于 CountDownLatch 功能比 join 更丰富,所以项目实践中一般使用 CountDownLatch。

      7.让线程睡眠的 sleep 方法:Thread 类中有一个静态的 sleep 方法,当一个执行中的线程调用了 Thread 的 sleep 方法后,调用线程会暂时让出指定时间的执行权,也就是这期间不参与 CPU 的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。当指定的睡眠时间到了该函数会正常返回,线程就处于就绪状态,然后参与 CPU 的调度,当获取到了 CPU 资源就可以继续运行了。如果在睡眠期间其它线程调用了该线程的 interrupt() 方法中断了该线程,该线程会在调用 sleep 的地方抛出 InterruptedException 异常返回。

    用一个例子来说明线程在睡眠时候拥有的监视器资源不会被释放是什么意思,例子如下:

    /**
     * Created by cong on 2018/7/17.
     */
    public class SleepTest2 {
        // 创建一个独占锁
        private static final Lock lock = new ReentrantLock();
        
        public static void main(String[] args) throws InterruptedException {
            // 创建线程A
            Thread threadA = new Thread(new Runnable() {
                public void run() {
                    // 获取独占锁
                    lock.lock();
                    try {
                        System.out.println("child threadA is in sleep");
    
                        Thread.sleep(10000);
    
                        System.out.println("child threadA is in awaked");
    
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        // 释放锁
                        lock.unlock();
                    }
                }
            });
            // 创建线程B
            Thread threadB = new Thread(new Runnable() {
                public void run() {
                    // 获取独占锁
                    lock.lock();
                    try {
                        System.out.println("child threadB is in sleep");
                        Thread.sleep(10000);
                        System.out.println("child threadB is in awaked");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        // 释放锁
                        lock.unlock();
                    }
                }
            });
            // 启动线程
            threadA.start();
            threadB.start();
        }
    }

    运行结果如下:

    如上代码首先创建了一个独占锁,然后创建了两个线程,每个线程内部先获取锁,然后睡眠,睡眠结束后会释放锁。

    首先无论你执行多少遍上面的代码都是先输出线程 A 的打印或者先输出线程 B 的打印,不会存在线程 A 和线程 B 交叉打印的情况。

    从执行结果看线程 B 先获取了锁,那么线程 B 会先打印一行,然后调用 sleep 让自己沉睡 10s,在线程 B 沉睡的这 10s 内那个独占锁 lock 还是线程 B 自己持有的,线程 A 会一直阻塞直到线程 B 醒过来后执行 unlock 释放锁。

    下面在来看下当一个线程处于睡眠时候如果另外一个线程中断了它,会不会在调用 sleep 处抛出异常。代码如下:

    /**
     * Created by cong on 2018/7/17.
     */
    public class SleepInterruptTest {
        public static void main(String[] args) throws InterruptedException {
            //创建线程
            Thread thread = new Thread(new  Runnable() {
                public void run() {
                    try {
                        System.out.println("child thread is in sleep");
                        Thread.sleep(10000);
                        System.out.println("child thread is in awaked");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            //启动线程
            thread.start();
    
            //主线程休眠2s
            Thread.sleep(2000);
    
            //主线程中断子线程
            thread.interrupt();
        }
    }

    如上代码在子线程睡眠期间主线程中断了它,所以子线程在调用 sleep 处抛出了 InterruptedException 异常。

    总结:sleep 方法只是会让调用线程暂时让出指定时间的 CPU 执行权,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。

  • 相关阅读:
    Python在程序中进行多任务操作-协程
    Python-异常处理
    Python多任务-协程
    【每日一具4】TikTok 抖音国际版(网站)使用起来非常简单,无需FQ随便看
    【每日一具3】优美APP一款好用的短视频软件,优美APP专注于各种小姐姐短视频
    Python在程序中进行多任务操作-线程
    Python在程序中进行多任务操作-进程
    Python多任务-线程
    Python多任务-进程
    【每日一具3】优美APP一款好用的短视频软件,优美APP专注于各种小姐姐短视频
  • 原文地址:https://www.cnblogs.com/huangjuncong/p/9323717.html
Copyright © 2011-2022 走看看