zoukankan      html  css  js  c++  java
  • Java之多线程

    一、线程的引入:
    定义:同时对多项任务加以控制

    我们上下代码:

    1.未使用线程

     1 package com.learn.chap08.sec01;
     2 /**
     3  * 未使用线程--同步执行
     4  * @author Administrator
     5  *
     6  */
     7 public class Demo1 {
     8     
     9     /**
    10      * 听音乐
    11      */
    12     public static void music(){
    13         for (int i = 0; i < 5; i++) {
    14             System.out.println("听音乐");
    15         }
    16     }
    17     
    18     /**
    19      * 吃饭
    20      */
    21     public static void eat(){
    22         for (int i = 0; i < 5; i++) {
    23             System.out.println("吃饭");
    24         }
    25     }
    26     
    27     public static void main(String[] args) {
    28         music();
    29         eat();
    30     }
    31 }

    2. 使用线程

     1 package com.learn.chap08.sec01;
     2 /**
     3  * 使用多线程--异步执行
     4  * @author Administrator
     5  *
     6  */
     7 public class Eat extends Thread{
     8 
     9     @Override
    10     public void run() {
    11         for (int i = 0; i < 5; i++) {
    12             try {
    13                 Thread.sleep(100); // 100毫秒
    14             } catch (InterruptedException e) {
    15                 // TODO Auto-generated catch block
    16                 e.printStackTrace();
    17             }
    18             System.out.println("吃饭");
    19         }
    20     }
    21     
    22 }
     1 package com.learn.chap08.sec01;
     2 /**
     3  * 使用多线程--异步执行
     4  * @author Administrator
     5  *
     6  */
     7 public class Music extends Thread {
     8 
     9     @Override
    10     public void run() {
    11         // TODO Auto-generated method stub
    12         for (int i = 0; i < 5; i++) {
    13             try {
    14                 Thread.sleep(100);
    15             } catch (InterruptedException e) {
    16                 // TODO Auto-generated catch block
    17                 e.printStackTrace();
    18             }
    19             System.out.println("听音乐");
    20         }
    21     }
    22     
    23 }
     1 package com.learn.chap08.sec01;
     2 /**
     3  * 使用多线程--异步执行
     4  * @author Administrator
     5  *
     6  */
     7 public class Demo2 {
     8     public static void main(String[] args) {
     9         /**
    10          * 利用多线程--实现一边听音乐 一边吃饭
    11          */
    12         Music musicThread = new Music();
    13         Eat eatThread     = new Eat();
    14         musicThread.start();
    15         eatThread.start();
    16     }
    17 }

    二、使用多线程

    1. 继承Thread类

    代码如下:

     1 package com.learn.chap08.sec02;
     2 
     3 public class Thread1 extends Thread {
     4     private String threadName;
     5     private int baoZi = 1;
     6 
     7     public Thread1(String threadName) {
     8         super();
     9         this.threadName = threadName;
    10     }
    11 
    12     @Override
    13     public void run() {
    14         // TODO Auto-generated method stub
    15         while(baoZi<=10){
    16             System.out.println(this.threadName+" 吃第"+baoZi+"个包子");
    17             baoZi++;
    18         }
    19     }
    20     
    21     public static void main(String[] args) {
    22         System.out.println("张三、李四各自吃10个包子");
    23         Thread1 t1=new Thread1("张三线程");
    24         Thread1 t2=new Thread1("李四线程");
    25         t1.start();
    26         t2.start();
    27     }
    28     
    29 }

    2. 实现Runnable接口

    代码如下:

     1 package com.learn.chap08.sec02;
     2 
     3 public class Thread2 implements Runnable{
     4 
     5     private String threadName;
     6     private int baoZi = 1;
     7 
     8     public Thread2(String threadName) {
     9         super();
    10         this.threadName = threadName;
    11     }
    12 
    13     @Override
    14     public void run() {
    15         // TODO Auto-generated method stub
    16         while(baoZi<=10){
    17             System.out.println(this.threadName+" 吃第"+baoZi+"个包子");
    18             baoZi++;
    19         }
    20     }
    21     
    22     public static void main(String[] args) {
    23         System.out.println("张三、李四各自吃10个包子");
    24         Thread1 t1=new Thread1("张三线程");
    25         Thread1 t2=new Thread1("李四线程");
    26         Thread t11=new Thread(t1);
    27         Thread t12=new Thread(t2);
    28         t11.start();
    29         t12.start();
    30     }
    31     
    32 }
     1 package com.learn.chap08.sec02;
     2 
     3 public class Thread3 implements Runnable{
     4 
     5     private String threadName;
     6     private int baoZi = 1;
     7 
     8     public Thread3(String threadName) {
     9         super();
    10         this.threadName = threadName;
    11     }
    12 
    13     @Override
    14     public synchronized void run() {
    15         // TODO Auto-generated method stub
    16         while(baoZi<=10){
    17             System.out.println(this.threadName+" 吃第"+baoZi+"个包子");
    18             baoZi++;
    19         }
    20     }
    21     
    22     public static void main(String[] args) {
    23         
    24         Thread3 t1=new Thread3("超级张三线程");
    25         
    26         Thread t11=new Thread(t1);
    27         Thread t12=new Thread(t1);
    28         Thread t13=new Thread(t1);
    29         // 实现资源共享
    30         t11.start();
    31         t12.start();
    32         t13.start();
    33     }
    34     
    35 }

    总结: Runnable接口 可以实现资源共享  而Thread不能。

    三、线程状态

    四、多线程常用的方法

    举例代码如下:

     1 package com.learn.chap08.sec04;
     2 
     3 public class Demo1 implements Runnable{
     4 
     5     @Override
     6     public void run() {
     7         // TODO Auto-generated method stub
     8         for (int i = 0; i < 10; i++) {
     9             // 获取当前线程
    10             Thread t=Thread.currentThread();
    11             System.out.println(t.getName()+":"+i);
    12         }
    13         
    14     }
    15     
    16     public static void main(String[] args) {
    17         Demo1 demo1 = new Demo1();
    18         new Thread(demo1).start();
    19         new Thread(demo1,"线程一").start();
    20         new Thread(demo1,"线程二").start();
    21         
    22         Thread it = new Thread(demo1);
    23         System.out.println(it.isAlive());
    24         it.start();
    25         System.out.println(it.isAlive());
    26     }
    27 
    28 }

    运行结果

    false
    true
    Thread-1:0
    Thread-1:1
    Thread-1:2
    Thread-1:3
    Thread-1:4
    Thread-1:5
    Thread-1:6
    Thread-1:7
    Thread-1:8
    Thread-1:9
    线程二:0
    线程二:1
    线程二:2
    线程二:3
    线程二:4
    线程二:5
    线程二:6
    线程二:7
    线程二:8
    线程二:9
    Thread-0:0
    Thread-0:1
    Thread-0:2
    Thread-0:3
    Thread-0:4
    Thread-0:5
    Thread-0:6
    Thread-0:7
    Thread-0:8
    Thread-0:9
    线程一:0
    线程一:1
    线程一:2
    线程一:3
    线程一:4
    线程一:5
    线程一:6
    线程一:7
    线程一:8
    线程一:9

     1 package com.learn.chap08.sec04;
     2 
     3 public class Demo2 implements Runnable{
     4 
     5     @Override
     6     public void run() {
     7         // TODO Auto-generated method stub
     8         for (int i = 0; i < 10; i++) {
     9             try {
    10                 Thread.sleep(1000); // 线程休眠
    11                 // 获取当前线程
    12                 Thread t=Thread.currentThread();
    13                 System.out.println(t.getName()+":"+i);
    14             } catch (InterruptedException e) {
    15                 // TODO Auto-generated catch block
    16                 e.printStackTrace();
    17             }
    18             
    19         }
    20         
    21     }
    22     
    23     public static void main(String[] args) {
    24         Demo2 demo1 = new Demo2();
    25         new Thread(demo1).start();
    26         new Thread(demo1,"线程一").start();
    27     }
    28 
    29 }

    运行结果

    Thread-0:0
    线程一:0
    Thread-0:1
    线程一:1

    .

    .

    .

     1 package com.learn.chap08.sec04;
     2 
     3 public class Demo3 implements Runnable{
     4 
     5     @Override
     6     public void run() {
     7         // TODO Auto-generated method stub
     8         for (int i = 0; i < 10; i++) {
     9             // 获取当前线程
    10             Thread t=Thread.currentThread();
    11             System.out.println(t.getName()+":"+i);
    12             
    13         }
    14         
    15     }
    16     
    17     public static void main(String[] args) {
    18         Demo3 demo1 = new Demo3();
    19         Thread t1 = new Thread(demo1,"线程一");
    20         Thread t2 = new Thread(demo1,"线程二");
    21         Thread t3 = new Thread(demo1,"线程三");
    22         t1.setPriority(Thread.MAX_PRIORITY); // 更改线程的优先级
    23         t2.setPriority(Thread.NORM_PRIORITY);
    24         t3.setPriority(Thread.MIN_PRIORITY);
    25         t1.start();
    26         t2.start();
    27         t3.start();
    28     }
    29 
    30 }
     1 package com.learn.chap08.sec04;
     2 
     3 public class Demo4 implements Runnable{
     4 
     5     @SuppressWarnings("static-access")
     6     @Override
     7     public void run() {
     8         // TODO Auto-generated method stub
     9         for (int i = 0; i < 10; i++) {
    10             try {
    11                 Thread.sleep(100);
    12                 // 获取当前线程
    13                 Thread t=Thread.currentThread();
    14                 System.out.println(t.getName()+":"+i);
    15                 if(i==5){
    16                     System.out.println("线程礼让:");
    17                     Thread.currentThread().yield();// 出现@SuppressWarnings("static-access")
    18                 }
    19             } catch (InterruptedException e) {
    20                 // TODO Auto-generated catch block
    21                 e.printStackTrace();
    22             }
    23         }
    24     }
    25     
    26     public static void main(String[] args) {
    27         Demo4 demo1 = new Demo4();
    28         Thread t1 = new Thread(demo1,"线程一");
    29         Thread t2 = new Thread(demo1,"线程二");
    30         t1.start();
    31         t2.start();
    32     }
    33 
    34 }

    运行结果

    线程一:0
    线程二:0
    线程一:1
    线程二:1
    线程一:2
    线程二:2
    线程二:3
    线程一:3
    线程一:4
    线程二:4
    线程一:5
    线程礼让:
    线程二:5
    线程礼让:
    线程一:6
    线程二:6
    线程一:7
    线程二:7
    线程一:8
    线程二:8
    线程一:9
    线程二:9

    五、线程同步
    1. 同步方法
    2. 同步锁(锁机制)

     上下代码:

     1 package com.learn.chap08.sec05;
     2 
     3 public class Demo2 implements Runnable{
     4 
     5     private int baoZi=10;
     6     
     7     @Override
     8     /**
     9      * 同步方法
    10      */
    11     public synchronized void run() { // 用synchronized标识 的方法为同步方法  不加锁的话,下面的张三、李四、王五可能会同时进入run()方法
    12         // TODO Auto-generated method stub
    13         while(baoZi>0){
    14             System.out.println(Thread.currentThread().getName()+"吃了第"+baoZi+"个包子");
    15             baoZi--;
    16         }
    17     }
    18     
    19     public static void main(String[] args) {
    20         Demo2 demo1=new Demo2();
    21         new Thread(demo1,"张三").start();
    22         new Thread(demo1,"李四").start();
    23         new Thread(demo1,"王五").start();
    24     }
    25 
    26 }
     1 package com.learn.chap08.sec05;
     2 
     3 public class Demo3 implements Runnable{
     4 
     5     private int baoZi=10;
     6     
     7     @Override
     8     public void run() {
     9         /**
    10          * 同步块
    11          */
    12         synchronized (this) { // 同步块
    13             while(baoZi>0){
    14                 System.out.println(Thread.currentThread().getName()+"吃了第"+baoZi+"个包子");
    15                 baoZi--;
    16             }
    17         }
    18     }
    19     
    20     public static void main(String[] args) {
    21         Demo3 demo1=new Demo3();
    22         new Thread(demo1,"张三").start();
    23         new Thread(demo1,"李四").start();
    24         new Thread(demo1,"王五").start();
    25     }
    26 
    27 }

    下面举例:java synchronized同步方法调用另一个同步方法,锁机制问题

    1 public synchronized void methodA(int a, int b);
    2 
    3 public synchronized void methodB(int a){
    4     methodA(a, 0);
    5 }

    要明白两个问题,1.锁的对象是谁,2.谁持有了锁。
    假设方法A和B是在同一个类Test中的两个方法。
    Test t=new Test();
    t.methodB();
    这个时候,methodB方法被调用时,因为加了synchronized ,需要先获得一个锁,这个锁的对象应该是t,也就是当前的这个Test类的实例,而获得锁的东西是线程,也就是说当前线程拿到了t的锁(而不是你说的B方法获得锁),这个时候B方法内调用methodA,因为A也加了synchronized,也需要获得一个锁,因为A和B都是Test类中的方法,所以当前线程要获得的锁的对象也是t。由于当前线程在执行B方法时已经持有了t对象的锁,因此这时候调用methodA是没有任何影响的,相当于方法A上没有加synchronized。

    另一种情况:假设现在有两个Test类
    Test t1=new Test();
    Test t2=new Test();
    t1.methodB();//此时当前线程持有了t1对象的锁
    t2.methodB();//此时当前线程也持有了t2对象的锁
    当前线程持有了两把锁,锁的对象分别是两个不同的Test类的实例t1和t2,互相没有影响。

    再一种情况:假设在多线程环境下,两个线程都可以访问Test t=new Test();
    此时假设thread1里调用t.methodB();同时thread2里调用t.methodB()

    这时假设thread1先抢到t对象的锁,那么thread2需要等待thread1释放t对象的锁才可以执行B方法。
    结果像这样:
    thread1获得t的锁--thread1执行methodB--thread1执行methodA--释放t的锁---thread2获得t的锁--thread2执行methodB--thread2执行methodA--释放t的锁。

    synchronized还有很多种使用方法,但只有明白是那条线程获得哪个对象的锁,就很容易明白了。

    2016年10月31号偶然看到了crossoverjie整理的多线程知识点(原文链接:http://www.jianshu.com/p/72d53d4f833a),感觉不错,于是在这顺便记录下,补充下知识,呵呵!

    进程与线程的区别

    进程

    进程简单的来说就是在内存中运行的应用程序,一个进程可以启动多个线程。
    比如在windows中一个运行EXE文件就是一个进程。

    线程

    同一进程内的线程共享此进程的地址空间,同时共享进程所拥有的内存和其他资源。

    --------------------------------------------------------------------------------------------------

    线程Demo-继承Thread类

    首先我们我们继承java.lang.Thread类来创建线程。

     1 package top.crosssoverjie.study.Thread;
     2 
     3 public class TestThread {
     4     public static void main(String[] args) {
     5         System.out.println("主线程ID是:" + Thread.currentThread().getId());
     6         MyThread my = new MyThread("线程1");
     7         my.start() ;
     8 
     9         MyThread my2 = new MyThread("线程2") ;
    10         /**
    11          * 这里直接调用my2的run()方法。
    12          */
    13         my2.run() ;
    14     }
    15 
    16 }
    17 
    18 class MyThread extends Thread {
    19     private String name;
    20 
    21     public MyThread(String name) {
    22         this.name = name;
    23     }
    24 
    25     @Override
    26     public void run() {
    27         System.out.println("名字:" + name + "的线程ID是="
    28                 + Thread.currentThread().getId());
    29     }
    30 
    31 }

    输出结果:

    主线程ID是:1
    名字:线程2的线程ID是=1
    名字:线程1的线程ID是=9

    由输出结果我们可以得出以下结论:

    • my和my2的线程ID不相同,my2和主线程ID相同。说明直接调用run()方法不会创建新的线程,而是在主线程中直接调用的run()方法,和普通的方法调用没有区别。
    • 虽然my的start()方法是在my2的run()方法之前调用,但是却是后输出内容,说明新建的线程并不会影响主线程的执行。

    ----------------------------------------------------------------------------------------------------

    线程Demo-实现Runnable接口

    除了继承java.lang.Thread类之外,我们还可以实现java.lang.Runnable接口来创建线程。

     1 package top.crosssoverjie.study.Thread;
     2 
     3 public class TestRunnable {
     4     public static void main(String[] args) {
     5         System.out.println("主线程的线程ID是"+Thread.currentThread().getId());
     6         MyThread2 my = new MyThread2("线程1") ;
     7         Thread t = new Thread(my) ;
     8         t.start() ;
     9 
    10         MyThread2 my2 = new MyThread2("线程2") ;
    11         Thread t2 = new Thread(my2) ;
    12         /**
    13          * 方法调用,并不会创建线程,依然是主线程
    14          */
    15         t2.run() ;
    16     }
    17 }
    18 
    19 class MyThread2 implements Runnable{
    20     private String name ;
    21     public MyThread2(String name){
    22         this.name = name ;
    23     }
    24 
    25     @Override
    26     public void run() {
    27         System.out.println("线程"+name+"的线程ID是"+Thread.currentThread().getId());
    28     }
    29 
    30 
    31 }

    输出结果:

    主线程的线程ID是1
    线程线程2的线程ID是1
    线程线程1的线程ID是9

    notes:

    • 实现Runnable的方式需要将实现Runnable接口的类作为参数传递给Thread,然后通过Thread类调用Start()方法来创建线程。
    • 这两种方式都可以来创建线程,至于选择哪一种要看自己的需求。直接继承Thread类的话代码要简洁一些,但是由于java只支持单继承,所以如果要继承其他类的同时需要实现线程那就只能实现Runnable接口了,这里更推荐实现Runnable接口。

    实际上如果我们查看Thread类的源码我们会发现Thread是实现了Runnable接口的:


    线程中常用的方法:

    方法详解- public static void sleep(long mills)

     1 package top.crosssoverjie.study.Thread;
     2 
     3 public class TestSleep {
     4 
     5     private int i = 10 ;
     6     private Object ob = new Object() ;
     7 
     8     public static void main(String[] args) {
     9         TestSleep t = new TestSleep() ;
    10         MyThread3 thread1 = t.new MyThread3() ;
    11         MyThread3 thread2 = t.new MyThread3() ;
    12         thread1.start() ;
    13         thread2.start() ;
    14     }
    15 
    16     class MyThread3 extends Thread{
    17         @Override
    18         public void run() {
    19             synchronized (ob) {
    20                 i++ ;
    21                 System.out.println("i的值:"+i);
    22                 System.out.println("线程:"+Thread.currentThread().getName()+"进入休眠状态");
    23                 try {
    24                     Thread.currentThread().sleep(1000) ;
    25                 } catch (Exception e) {
    26                     e.printStackTrace();
    27                 }
    28                 System.out.println("线程:"+Thread.currentThread().getName()+"休眠结束");
    29                 i++;
    30                 System.out.println("i的值>:"+i);
    31             }
    32         }
    33     }
    34 
    35 }

    输出结果:

    i的值:11
    线程:Thread-0进入休眠状态
    线程:Thread-0休眠结束
    i的值>:12
    i的值:13
    线程:Thread-1进入休眠状态
    线程:Thread-1休眠结束
    i的值>:14
     

    由输出结果我们可以得出:

         当Thread0进入休眠状态时,Thread1并没有继续执行,而是等待Thread0休眠结束释放了对象锁,Thread1才继续执行。
         当调用sleep()方法时,必须捕获异常或者向上层抛出异常。当线程休眠时间满时,并不一定会马上执行,因为此时有可能CPU正在执行其他的任务,所以调用了sleep()方法相当于线程进入了阻塞状态。

    方法详解- public static void yield()

     1 package top.crosssoverjie.study.Thread;
     2 
     3 public class Testyield {
     4     public static void main(String[] args) {
     5         MyThread4 my = new MyThread4() ;
     6         my.start() ;
     7     }
     8 }
     9 class MyThread4 extends Thread{
    10     @Override
    11     public void run() {
    12         long open = System.currentTimeMillis();
    13         int count= 0 ;
    14         for(int i=0 ;i<1000000;i++){
    15             count= count+(i+1);
    16 //            Thread.yield() ;
    17         }
    18         long end = System.currentTimeMillis();
    19         System.out.println("用时:"+(end-open)+"毫秒");
    20     }
    21 }

    输出结果:
    用时:1毫秒
    如果将 Thread.yield()注释取消掉,输出结果:
    用时:116毫秒

    总结:

    • 调用yield()方法是为了让当前线程交出CPU权限,让CPU去执行其他线程。它和sleep()方法类似同样是不会释放锁。但是yield()不能控制具体的交出CUP的时间。并且它只能让相同优先级的线程获得CPU执行时间的机会。
    • 调用yield()方法不会让线程进入阻塞状态,而是进入就绪状态,它只需要等待重新获取CPU的时间,这一点和sleep()方法是不一样的。

    方法详解- public final void join()

    在很多情况下我们需要在子线程中执行大量的耗时任务,但是我们主线程又必须得等待子线程执行完毕之后才能结束,这就需要用到 join()方法了。join()方法的作用是等待线程对象销毁,如果子线程执行了这个方法,那么主线程就要等待子线程执行完毕之后才会销毁,请看下面这个例子:

     1 package top.crosssoverjie.study.Thread;
     2 
     3 public class Testjoin {
     4     public static void main(String[] args) throws InterruptedException {
     5         new MyThread5("t1").start() ;
     6         for (int i = 0; i < 10; i++) {
     7             if(i == 5){
     8                 MyThread5 my =new MyThread5("t2") ;
     9                 my.start() ;
    10                 my.join() ;
    11             }
    12             System.out.println("main当前线程:"+Thread.currentThread().getName()+" "+i);
    13         }
    14     }
    15 }
    16 class MyThread5 extends Thread{
    17 
    18     public MyThread5(String name){
    19         super(name) ;
    20     }
    21     @Override
    22     public void run() {
    23         for (int i = 0; i < 5; i++) {
    24             System.out.println("当前线程:"+Thread.currentThread().getName()+" "+i);
    25         }
    26     }
    27 }

    输出结果:

    main当前线程:main 0
    当前线程:t1 0
    当前线程:t1 1
    main当前线程:main 1
    当前线程:t1 2
    main当前线程:main 2
    当前线程:t1 3
    main当前线程:main 3
    当前线程:t1 4
    main当前线程:main 4
    当前线程:t2 0
    当前线程:t2 1
    当前线程:t2 2
    当前线程:t2 3
    当前线程:t2 4
    main当前线程:main 5
    main当前线程:main 6
    main当前线程:main 7
    main当前线程:main 8
    main当前线程:main 9


    如果我们把join()方法注释掉之后:
    main当前线程:main 0
    当前线程:t1 0
    main当前线程:main 1
    当前线程:t1 1
    main当前线程:main 2
    当前线程:t1 2
    main当前线程:main 3
    当前线程:t1 3
    main当前线程:main 4
    当前线程:t1 4
    main当前线程:main 5
    main当前线程:main 6
    main当前线程:main 7
    main当前线程:main 8
    main当前线程:main 9
    当前线程:t2 0
    当前线程:t2 1
    当前线程:t2 2
    当前线程:t2 3
    当前线程:t2 4


    由上我们可以得出以下结论:

    • 在使用了join()方法之后主线程会等待子线程结束之后才会结束。

    方法详解- setDaemon(boolean on),getDaemon()

    用来设置是否为守护线程和判断是否为守护线程。
    notes:

    • 守护线程依赖于创建他的线程,而用户线程则不需要。如果在main()方法中创建了一个守护线程,那么当main方法执行完毕之后守护线程也会关闭。而用户线程则不会,在JVM中垃圾收集器的线程就是守护线程。

    优雅的终止线程

    有三种方法可以终止线程,如下:

    1. 使用退出标识,使线程正常的退出,也就是当run()方法完成后线程终止。
    2. 使用stop()方法强行关闭,这个方法现在已经被废弃,不推荐使用
    3. 使用interrupt()方法终止线程。

    具体的实现代码我将在下一篇博文中将到。。

    线程的优先级

    在操作系统中线程是分优先级的,优先级高的线程CPU将会提供更多的资源,在java中我们可以通过setPriority(int newPriority)方法来更改线程的优先级。
    在java中分为1~10这个十个优先级,设置不在这个范围内的优先级将会抛出IllegalArgumentException异常。
    java中有三个预设好的优先级:

    • public final static int MIN_PRIORITY = 1;
    • public final static int NORM_PRIORITY = 5;
    • public final static int MAX_PRIORITY = 10;
     

    java多线程思维图:






  • 相关阅读:
    HTTP断点续传 规格严格
    Java Shutdown 规格严格
    linux 命令源码 规格严格
    JTable调整列宽 规格严格
    linux 多CPU 规格严格
    Hello can not find git path 规格严格
    Kill 规格严格
    拜拜牛人 规格严格
    Swing 规格严格
    Debugging hangs in JVM (on AIX but methodology applicable to other platforms) 规格严格
  • 原文地址:https://www.cnblogs.com/eaglezb/p/6011361.html
Copyright © 2011-2022 走看看