zoukankan      html  css  js  c++  java
  • java-多线程(下)

    多线程简单入门(Java)(下篇:多线程Java中的使用)

    目录

    一、创建多线程

    二、线程的安全

    三、线程的通信

    一、创建多线程

    在Java中,多线程的创建有4种方式。

    方式一:继承于Thread类;

    方式二:实现Runnable接口;

    方式三:实现Callable()接口;

    方式四:使用线程池。

    方式一:继承于Thread类。步骤如下:

    1、创建一个继承于Thread类的子类;

    2、重写Thread类的子类的run()方法;

    3、创建一个Thread类的对象;

    4、通过对象调用start()方法开启线程。

    代码:

      //1、创建一个继承于Thread类的子类
      class Mythread extends Thread{ 
         //2、重写Thread类的run()
         @Override
      public void run() {
        super.run();
        for (int i = 0; i < 100; i++) {
                if(i%2==0){
                    System.out.println(Thread.currentThread().getName()+":"+i);//Thread.currentThread().getName()是获取当前线程名的方法
                }
        }
      }
      }
      public class ThreadTest{
          public static void main(String[] args){
         //3、创建Thread类的子类的对象
      Mythread t1=new  Mythread();
       //4、通过此对象调用start():启动当前线程,调用当前线程的run()方法
      t1.start();
      }
      }
    

    运行结果(打印了0到100的偶数):

    方式二:实现Runnable接口。步骤如下:

    (1)、创建一个实现Runnable接口的类;

    (2)、(1)中的实现类去实现Runnable接口中的抽象方法run();

    3)、创建实现类的对象;

    (4)、创建Thread类的对象,并将(3)中的对象作为参数传递到Thread类的构造器中。

    (5)、通过Thread类的对象调用start()方法。

    代码:

      //1、创建一个实现了Runnable接口的类
      class Mthread implements  Runnable{
      //2、实现类去实现Runnable中的抽象方法:run()
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if(i%2==0){
                System.out.println(Thread.currentThread().getName()+i);
            }
        }
    }
         }
    public  class ThreadTest1 {
       public static void main(String[] args) {
        //3、创建实现类的对象
        Mthread mthread = new Mthread();
        // 4、此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1 = new Thread(mthread);
        //5、通过Thread类的对象调用start()://此时的run()是调用Mthread重写的run()
        t1.setName("线程一");//为线程设置名字
        t1.start();
        //再启动一个线程,也是遍历100以内的偶数
        Thread t2 = new Thread(mthread);
        t2.setName("线程二");
        t2.start();
    
    } 
      }
    

    运行结果(线程一和线程二都会打印0-100的偶数):

    方式一和方式二的比较

    开发中如何选择?优先选择Runnable接口的方式。原因有两个:1、实现Runnable接口的方式突破了Thread类的单继承性的局限性。2、实现Runnable接口的方式更适合处理有多个线程共享数据的情况。

    二者的联系:其实Thread类也实现了Runnable接口:public class Thread implements Runnable。

    二者的相同点:都需要实现run()方法,run()方法中声明了线程需要执行的逻辑。

    下面我们举个例子简单说明一下Thread类中常用的方法。
    1、start():启动当前线程并调用当前线程的run()方法。

    2、run():通常需要重写Thread类中的此方法,将创建的线程执行的操作声明在此方法中。

    3、currentThread():静态方法,返回当前代码执行的线程。

    4、getName():获取当前线程的名字。

    5、setName():设置当前线程的名字。

    6、yield():释放当前CPU的执行权。也叫做线程让步。暂停当前正在执行的线程,把执行机会让给优先级相同的或者更高优先级的线程。若等待队列中没有同优先级的线程,忽略此
    方法。

    7、join():当某个程序执行流中调用了其他线程的join()方法时,调用线程将被阻塞,直到join()方法加入的join线程执行完为止。举个例子:

    在线程A中调用线程B的join()方法,此时线程A进入阻塞状态。直到线程B完全执行完以后,线程A才结束阻塞状态。

    8、stop():已过时。当执行此方法时,强制结束当前线程。

    9、sleep(long millitime):让当前线程睡眠指定的毫秒数。此段时间内,当前线程是阻塞状态。在必要的时候执行sleep()方法会让线程执行地慢一些。

    10、isAlive():判断当前线程是否还存活。

    11、线程的优先级:

    (1)、线程的优先级分为三个等级:最大优先级,最小优先级,默认优先级

    MAX_PRIORITY:10

    MIN_PRIORITY:1

    NORM_PRIORITY:5 默认优先级

    2)、获取和设置当前线程的优先级

    getPriority()

    setPriority()

    (3)、说明

    高优先级的线程要抢占低优先级的线程,但是也只是从概率上这么说。高优先级的线程高概率地被执行,并不意味着”一定是高优先级先执行,结束后再执行低优先级“。

    代码:

      class HelloThread  extends  Thread{
    @Override
    public void run() {
        super.run();
        for (int i = 0; i < 100; i++) {
            //sleep()方法的测试:这个时候线程会执行地慢一些。
            if (i % 2 == 0) {
                try {
                    sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ":" + i + "," + getPriority());//线程优先级默认是5
            //yield()方法的测试:在线程执行到20的时候,必定会交出CPU执行权,让其他线程先执行一次。
      //            if (i % 20 == 0) {
      //                Thread.currentThread().yield();//Thread.currentThread()相当于this.
      //           }
              }
          }
          public HelloThread(String  name) {//构造器
             super(name);
         }
      
    
      public class ThreadMethodTest {
          public static void main(String[] args) throws InterruptedException {
        HelloThread  h1=new HelloThread("Thread1");//本代码选择在构造器中设置线程的名字。
        HelloThread  h2=new HelloThread("Thread2");//本代码选择在构造器中设置线程的名字。
        //设置名字是主线程所做的事。主线程这里指的是main()主线程 。
        //h1.setName("线程一");
        //设置线程的优先级。优先级的测试:主线程优先级最低,但是实际中,主线程也可能先执行。尤其说明,优先级高低,只是个概率事件。
        h1.setPriority(Thread.MAX_PRIORITY);
        h1.start();
        h2.setPriority(Thread.MAX_PRIORITY);
        h2.start();
        //为主main()线程命名
        Thread.currentThread().setName("主线程");//当前线程就是主线程
        Thread.currentThread().setPriority(Thread.MIN_PRIORITY);//设置优先级
        for (int i = 0; i <100 ; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i+","+Thread.currentThread().getPriority());
            //join()方法的测试:在主线程执行到20的时候,被阻塞,线程执行完了,主线程才继续执行。
      //          if(i==20){
      //              h1.join();//强行加入线程h1.
      //          }
        }
        System.out.println(h1.isAlive());//测试线程是否执行完,执行完就是false.未执行完:true.
          }
      }
    

    运行结果(每个人可能不一样,我这里是主线程先运行完,线程一线程二才运行的。主线程运行完,线程1的状态为true.):

    方式三:实现Callable()接口(JDK5.0新增。该方法获取call()方法的返回值时需要借助FutureTask类)

    1、创建一个实现Callable()接口的实现类。

    2、实现Call方法,将此线程需要执行的操作声明在call()中。

    3、创建实现Callable实现类的对象。

    4、创建FutureTask类的对象,并将3中创建的实现类的对象作为参数传递到FutureTask的参数中。

    5、将4中FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start();

    6、获取Call()方法的返回值。

    代码:

      //1、创建一个实现Callable的实现类
      class NumberThread implements Callable<Integer> {
    //2、实现Call方法,将此线程需要执行的操作声明在call()中。
    @Override
    public Integer call() throws Exception {
        int sum=0;
        for (int i = 1; i <=100; i++) {
            if(i%2==0){
                System.out.println(i);
                sum+=i;
            }
        }
                
        return sum;//自动装箱
          }
      }
      public class ThreadNew {
    public static void main(String[] args) {
        //3、创建Callable接口实现类的对象
        NumberThread  numberThread=new NumberThread();
        //4、将此Callable接口实现类的对象作为参数传递到FutureTask的构造器中。创建FutureTask的对象。
        FutureTask<Integer> futureTask = new FutureTask<Integer>(numberThread);
        //5、将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread 对象,并调用start();
        new Thread(futureTask).start();//futureTask实现了Runnable接口
        //6、获取call方法的返回值。
        Object sum = null;
        try {//get()方法的返回值即为futureTask构造器参数Callable实现类重写的call()的返回值。
            sum = futureTask.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("总和为:"+sum);
    
    }
      }
    

    运行结果(打印0-100的偶数,并返回0-100的所有偶数的和):

    方式二和方式三的比较

    相比于run()方法,Callable()更加强大。call()方法可以有返回值;可以抛出异常;并支持泛型的返回值。

    方式四:使用线程池

    思路:经常创建和销毁,使用特别大的资源,比如并发情况下的线程,对性能影响很大。如果能提前创建好多个线程放入线程池中,在需要的时候直接获取,使用完再放回池中就很方

    便了。这样能避免频繁地创建销毁线程,实现重复利用。

    好处:1、能提高响应速度(减少了创建新线程的时间);

    2、降低资源消耗(重复利用线程池,不需每次创建);

    3、便于线程管理。

    1、提供指定线程数量的线程池。

    2、执行指定的线程操作,需要提供实现Runnable接口或Callable接口的实现类的对象。Runnable()用service.execute()调用;Callable接口用service.submit()调用。

    3、关闭连接池。

    方法:ExecutorService,真正的线程池API.常见的子类:ThreadPoolExecutor。

    void execute(Runnable command):执行任务/命令,没有返回值。一般用来执行Runnable。

    Future submit(Callable task):执行任务,有返回值,一般用来执行Callable.

    void shutdown:关闭连接池。

    Executors:工具类,线程池的工厂类。用于创建并返回不同类型的线程池。

    Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程的线程池。

    代码:

      class  NumberThreadPool  implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if(i%2==0){
                System.out.println(Thread.currentThread().getName()+"-"+i);
            }
        }
    }
      }
      class  NumberThreadPool1  implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if(i%2==1){
                System.out.println(Thread.currentThread().getName()+"-"+i);
            }
        }
    }
      }
      public class ThreadPool {
    public static void main(String[] args) {
       //1、提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        //设置线程池的属性
        ThreadPoolExecutor service1=(ThreadPoolExecutor) service;//强转
        System.out.println(service.getClass());
        service1.setCorePoolSize(15);
       // service1.setKeepAliveTime();
        //2、执行指定的线程操作,需要提供实现Runnable接口或Callable接口的实现类的对象。
        //service.submit(new NumberThreadPool());//适合使用于Callable()
        service.execute(new NumberThreadPool());//适合使用于Runnable()//打印偶数
        service.execute(new NumberThreadPool1());//适合使用Runnable()//打印奇数
        //关闭连接池
        service.shutdown();
    }
      }
    

    运行结果:

    二、线程的同步

    先看一个窗口售票的例子:创建三个窗口卖票。总票数为100张

    代码如下:

      class Window extends Thread{
    private  static int ticket=100;//三个线程共享同一个ticket——总票数,
    // 三个线程卖100张票
    
    
    @Override
    public void run() {
        super.run();
        while(true){
            if(ticket>0){
                System.out.println(getName()+":卖票,票号为"+ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
      }
    
      public class WindowTest {
    public static void main(String[] args) {
        Window t1=new Window();
        Window t2=new Window();
        Window t3=new Window();
    
        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");
    
    
        t1.start();
        t2.start();
        t3.start();
    
    }
      }
    

    运行结果:出现了重票,存在线程安全问题。

    分析一下,为何存在线程安全问题?---因为存在共享数据ticket,三个线程操作一个变量。

    1、问题一:出现了重票错票问题。

    2、问题二:出现的原因:当某个线程来操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作了车票

    3、问题三:如何解决?当一个线程在操作ticket的时候,其他线程不能参与进来,直到线程A操作完以后,其他线程才可以操作。即使线程A出现了阻塞也不能被改变。

    在Java中,我们通过同步机制来解决线程的安全问题。有如下三种方式。分别是同步代码块,同步方法,LOCK锁。有了同步的方式,操作代码时,只能有一个线程参与,其他线程等

    待。相当于是一个单线程的过程。同步解决了线程的安全问题。
    方式一:同步代码块 ,使用synchronized关键字。

    synchronized(同步监视器){
    //需要被同步的代码
    }

    说明:1、操作共享数据的代码,即为需要被同步的代码。不能包含多了,也不能包含少了。

    2、共享数据:多个线程共同操作的变量,比如:ticket就是共享数据。

    3、同步监视器:俗称:锁。任何一个类的对象都可以来充当这个锁。

    注意:多个线程必须共用一把锁。在实现Runnable时,可以考虑用this充当这个锁。

    代码:

      //继承类的方法,同步监视器****慎用this
      class Window2 extends Thread {
    private static int ticket = 100;//三个线程共享同一个ticket——总票数,
    // 三个线程卖100张票
    private  static Object obj = new Object();//保证三个共享同一个数据
    
    @Override
    public void run() {
        super.run();
        while (true) {//Class class =Window2.class->
            //synchronized (Window2.class) {
            synchronized (obj) {//此时不能用this,因为this代表着t1,t2,t3.此时锁不唯一。
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(getName() + ":卖票,票号为" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
      }
      public class WindowTest2 {
          public static void main(String[] args) {
        Window2 t1=new Window2();
        Window2 t2=new Window2();
        Window2 t3=new Window2();
    
        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");
    
    
        t1.start();
        t2.start();
        t3.start();
    
    }
      }
      //实现Runnable接口的方法
      class Window1 implements  Runnable {
    private int ticket = 100;//不用加static
    Object obj = new Object();//obj就是一个锁。
    
    @Override
    public void run() {
        //Object obj = new Object();//obj只能是一个锁。唯一性
        while (true) {
            synchronized (this) {//此时this代表的就是唯一的window1对象。
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "卖票:票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
      }
        public  class WindowTest1 {
        public static void main(String[] args) {
            Window1 w = new Window1();//共用一个window1对象
            Thread t1 = new Thread(w);//w作为参数传递给线程Thread
            Thread t2 = new Thread(w);
            Thread t3 = new Thread(w);
            t1.setName("窗口一");
            t2.setName("窗口二");
            t3.setName("窗口三");
            t1.start();
            t2.start();
            t3.start();
        }
    }
    

    运行结果(线程安全):

    方式二:同步方法

    如果操作共享数据的代码完整地声明在一个方法中,我们不妨将此方法声明为同步的(使用synchronized关键字)。

    关于同步方法的总结:

    1、同步方法仍然涉及到同步监视器,只是不需要我们显式地声明。

    2、非静态的同步方法,同步监视器是:this。

    静态的同步方法,同步监视器是:当前类本身。

    代码:

      //使用同步方法来处理继承Thread类的方式中线程安全问题
      class Window4 extends Thread {
    private static int ticket = 100;//三个线程共享同一个ticket——总票数,
    @Override
    public  void run() {
        super.run();
        while (true) {
           show();
    
            }
        }
    private   static synchronized void show(){//同步监视器:t1,t2,t2
            if (ticket > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":卖票,票号为" + ticket);
                ticket--;
            }
        }
    
    }
    
      public class WindowTest4 {
          public static void main(String[] args) {
        Window4 t1=new Window4();
        Window4 t2=new Window4();
        Window4 t3=new Window4();
    
    
        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");
    
    
        t1.start();
        t2.start();
        t3.start();
    
    }
      }
      //使用同步方法解决实现Runnable接口的线程安全问题
      class Window3 implements  Runnable {
          private int ticket = 100;//不用加static
    
    
    @Override
    public void run() {
        while (true) {
            show();
        }
    }
    //同步方法
    private  synchronized void show(){
    //synchronized(this){
            if (ticket > 0) {
    
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "卖票:票号为:" + ticket);
                ticket--;
            }
            }
      }
      public  class WindowTest3 {
          public static void main(String[] args) {
        Window3 w = new Window3();//共用一个window1对象
        Thread t1 = new Thread(w);//w作为参数传递给线程Thread
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");
        t1.start();
        t2.start();
        t3.start();
    }
      }
    

    方式三:LOCK锁(JDK5.0新增)

    方式一和方式二都使用到了synchronized关键字。那么,synchronized与LOCK有何异同?

    相同:二者都可以解决线程安全的问题。

    不同之处:synchronized机制在执行完相应的代码逻辑以后,自动地释放同步监视器。

    另外需要注意的是LOCK需要手动地启动同步:lock(),同时结束也是需要手动的实现:unlock()。

    实现LOCK锁步骤:

    1、创建锁的对象;

    2、上锁:调用lock()方法;

    3、解锁:调用unlock()方法。

    代码:

      class Window  implements Runnable{
         private int ticket=100;
         //实例化lock
        private ReentrantLock  lock=new ReentrantLock(true);//1、创建锁的对象
    
    
    @Override
    public void run() {
        while(true){
            try{
                //2、调用lock方法
                lock.lock();
                if (ticket>0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"卖票:"+"票号为"+ticket);
                    ticket--;
                }else{
                    break;
                }
            }finally{
                //3、解锁的方法:unlock
    
                lock.unlock();
            }
    
        }
    }
      }
    
      public class LockTest {
          public static void main(String[] args) {
        Window  w=new Window();
        Thread t1=new Thread(w);
        Thread t2=new Thread(w);
        Thread t3=new Thread(w);
    
        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");
    
        t1.start();
        t2.start();
        t3.start();
    }
    

    三、线程通信

    下面以一个简单的例子来理解一下线程通信的方法。

      //两个线程交替打印1到100
      class  Number implements Runnable{
          private  int number =1;//共享数据,线程安全问题
    
    @Override
    public void run() {
        while(true){
            synchronized (this) {
                //唤醒
                notify();//notifyAll():唤醒多个。
                if(number<=100){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":"+number);
                    number++;
                    //使得调用如下wait()方法的线程进入阻塞状态
                    try {
                       wait();//wait完了以后会释放锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                else {
                    break;
                }
            }
        }
    }
      }
      public class CommunicationTest {
    public static void main(String[] args) {
        Number number = new Number();
        Thread t1 = new Thread(number);
        Thread t2= new Thread(number);
        t1.setName("线程一");
        t2.setName("线程二");
        t1.start();
        t2.start();
      }
    }
    

    运行结果:

    线程通信有三个方法:wait(),notify(),notifyAll()。

    wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。wait()与sleep()不同,在被运用同步代码块中时,sleep()休眠以后,不会释放锁。但是wait()执

    行完后会释放锁。

    notify():一旦执行此方法,就会唤醒被wait()的一个线程。如果有多个线程,就会唤醒优先级高的线程。

    notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。

    说明:

    1、wait(),notify(),notifyAll()只能出现在同步块或同步方法中。

    2、wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则会出现MonitorStateException异常。

    3、三个方法定义在Object类中。方便任何一个对象充当同步监视器的调用。

    线程通信的应用:生产者消费者问题

    分析:是多线程问题。生产者线程,消费者线程。

    是否有共享数据?是,店员或产品的数量。

    如何解决线程的安全问题?同步机制,三种方式。

    代码:

      class Clerk {
          private int productCount = 0;//生产的产品数量
    
    //以下两个方法需要同步,否则存在线程安全问题
    //生产产品(同步方法)
    public synchronized void produceProduct() {
        if (productCount < 20) {
            productCount++;
            System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");
            notify();
        } else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
        }
    }
    
    //消费产品
    public synchronized void consumerProduct() {
        if (productCount > 0) {
            System.out.println(Thread.currentThread().getName() + "开始消费第" + productCount + "个产品");
            productCount--;
            notify();
        } else {//等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
      }
    
      class Producer extends Thread {
          private Clerk clerk;
    
          public Producer(Clerk clerk) {
        this.clerk = clerk;
    
          }
    
    @Override
    public void run() {
        super.run();
        System.out.println(Thread.currentThread().getName() + ":开始生产产品.......");
        while (true) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.produceProduct();//生产
    
        }
    }
      }
    
      class Consumer extends Thread {
          private Clerk clerk;
    
          public Consumer(Clerk clerk) {
          this.clerk=clerk;
    }
    
    @Override
    public void run() {
        super.run();
        System.out.println(Thread.currentThread().getName() + ":开始消费产品.......");
        while (true) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            clerk.consumerProduct();//消费
    
        }
    }
      }
    
      public class ProduceTest {
          public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Producer p1 = new Producer(clerk);
        p1.setName("生产者1");
    
        Consumer c1 = new Consumer(clerk);
        c1.setName("消费者1");
        Consumer c2= new Consumer(clerk);
        c2.setName("消费者2");
        p1.start();
        c1.start();
        c2.start();
    }
      }
    

    运行结果:

    学习让我快乐,工作让我快乐。学习和工作都是为了更好的生活!
  • 相关阅读:
    [BZOJ1385][Baltic2000]Division expression
    [BZOJ1412/Luogu2598][ZJOI2009]狼和羊的故事
    iPhone SlideShow
    替换一个文件中的内容BAT
    用指定字符串替换指定内容
    修改注册表
    如何在单独的窗口中打开 Excel 文件
    IBatis和Hibernate区别
    c# 常用的面试题
    在线编译器
  • 原文地址:https://www.cnblogs.com/xyuanzi/p/13970626.html
Copyright © 2011-2022 走看看