最代码官方的gravatar头像
最代码官方 2017-04-18 21:42:54
java多线程实例代码详解

java多线程实例代码详解

原文:http://blog.csdn.net/paranoidyang/article/details/70184523

作者:Paranoidyang

线程与进程的区别

(1)程序是一段静态的代码,进程是程序的一次动态执行过程,它是操作系统资源调度的基本单位。线程是比进程更小的执行单位,一个进程在其执行过程中,可以产生多个线程,所以又称线程为“轻型进程”。虽然说可以并发运行多个线程,但在任何时刻cpu只运行一个线程,只是宏观上看好像是同时运行,其实微观上它们只是快速交替执行的。这就是Java中的多线程机制。
(2)不同进程的代码、内部数据和状态都是完全独立的,而一个程序内的多线程是共享同一块内存空间和同一组系统资源的,有可能互相影响。
(3)线程切换比进程切换的负担要小。

线程的创建

java提供了类java.lang.Thread来支持多线程编程,创建线程主要有两种方法:

(1)继承Thread类

Thread类中的run 方法是空的,直接通过 Thread类实例化的线程对象不能完成任何事,所以可以通过继承Thread 类,重写run 方法,实现具有各种不同功能的线程类。
run()又称为线程体,不能直接调用run(),而是通过调用start(),让线程自动调用run(),因为start()会首先进行与多线程相关的初始化(即让start()做准备工作)。

class ThreadType extends Thread{   
     public void run(){   //重写Thread类中的run 方法
         ……   
     }   
}  

(2)实现Runnable接口

java只允许单继承,如果类已经继承了其他类,就不能再继承Thread类了,所以提供了实现Runnable接口来创建线程的方式。
该接口只定义了一个run方法,在新类中实现它即可。Runnable接口并没有任何对线程的支持,还必须通过创建Thread类的实例,将Rnnable接口对象作为Thread类构造方法的参数传递进去,从而创建一个线程。如:

 class ThreadDemo3 implements Runnable {  
        // 重载run函数  
        public void run() {  
            for (int count = 1, row = 1; row < 10; row++, count++){ // 循环计算输出的*数目  
                for (int i = 0; i < count; i++){ // 循环输出指定的count数目的*  
                    System.out.print('*');   
                }  
                System.out.println();   
            }  
        }  

        public static void main(String argv[]) {  
            Runnable rb = new ThreadDemo3(); // 创建,并初始化ThreadDemo3对象rb  
            Thread td = new Thread(rb); // 通过Thread创建线程  
            td.start(); // 启动线程td  
        }  
    }  

注意:如果当前线程是通过继承Thread类创建的,则访问当前线程可以直接使用this,如果当前线程是通过实现Runnable接口创建的,则通过调用Thread.currentThread()方法来获取当前线程。

线程的生命周期

按照线程体在计算机系统内存中状态的不同,可以将线程分为以下5种状态:
(1)创建状态
新建一个线程对象,仅仅作为一个实例存在,JVM没有为其分配运行资源。
(2)就绪状态
创建状态的线程调用start方法后,转换为就绪状态,此时线程已得到除CPU时间之外的其他系统资源,一旦获得CPU,就进入运行状态。注意的是,线程没有结束run()方法之前,不能再调用start()方法,否则将发生IllegalThreadStateException异常,即启动的线程不能再启动。
(3)运行状态
就绪状态的线程获取了CPU,执行程序代码。
(4)阻塞状态
阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种: (一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。 (二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。 (三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
(5)死亡状态
线程死亡的原因有两个:一是执行完了线程体(run方法),二是因为异常run方法被强制性终止。如果线程进入死亡状态,JVM会收回线程占用的资源(释放分配给线程对象的内存)。
注意:调用stop()可以使线程立即进入死亡状态,不过该方法现在已经不推荐使用了,线程的退出通常采用自然终止的方法,不建议人工强制停止,容易引起“死锁”。

转换图如下:

从图中,可以看出,比较复杂的是就绪状态和阻塞状态转换的过程,java提供了大量的方法来支持阻塞,下面一 一说明:
sleep():可以以毫秒为单位,指定休眠一段时间(作为参数),时间一过,又进入就绪状态。
wait()和notify():wait使得线程进入阻塞状态,它有两种形式,一种是允许指定以毫秒为单位的一段时间作为参数的,另一种是无参数的。前者当对应的notify方法被调用或超出指定时间时线程重新进入就绪状态,后者则必须调用notify方法才能重新进入就绪状态。
注意:此外,还有suspend方法(对应的恢复则用resume方法)也能使线程进入阻塞状态,不过这个方法现在已经不提倡使用了,会引起“死锁”,因为调用该方法会释放占用的所有资源,由JVM调度转入临时存储空间。

线程调度和优先级

java采用抢占式调度,即优先级高线程的先运行,优先级相同的交替运行
java将线程的优先级分为10个等级,1-10,数字越大表明线程的级别超高,可以通过setPriority方法设置线程优先级。
在java中有一个比较特殊的线程称为守护线程,它具有最低的优先级,用于为系统中的其他线程对象提供服务。典型的就是JVM中的系统资源自动回收线程。

线程互斥(银行取款问题)

线程互斥是什么?什么时候要用到线程互斥呢?

发现问题

举个例子,假设你的银行账户有100元,并且你和你的妻子两人都知道账户密码,如果某一天,你去取100元,银行系统会先查看你的账户够不够100元,明显你是满足条件的,但是,如果此时你的妻子也需要去取100元,并且你的取钱线程刚好因为某些状况被打断了(这时系统还来不及修改你的账户余额),所以你的妻子去取钱时也满足条件,所以她完成了取钱动作,而你取钱线程恢复之后,你也将完成取钱动作。大家可以发现共享数据(账户余额)的完整性被破坏了,两人都从银行里取出了一百元,而账户明明只有一百元,如果现实中真发生这种情况,估计银行就要哭晕在厕所了。代码及运行结果如下:

//Account.java
public class Acount{
  double balance;
  public Acount(double money){
    balance = money;
    System.out.println("Totle Money: "+balance);
  }
}


//AccountThread.java
class Account
{
    double balance;

    public Account(double money)
    {
        balance = money;
        System.out.println("Totle Money: " + balance);
    }
}

public class AccountThread extends Thread
{
    Account Account;

    int delay;

    public AccountThread(Account Account, int delay)
    {
        this.Account = Account;
        this.delay = delay;
    }

    public void run()
    {
        if (Account.balance >= 100) {
            try {
                sleep(delay);
                Account.balance = Account.balance - 100;
                System.out.println("withdraw  100 successful!");
            } catch (InterruptedException e) {
            }
        } else
            System.out.println("withdraw failed!");
    }

    public static void main(String[] args)
    {
        Account Account = new Account(100);
        AccountThread AccountThread1 = new AccountThread(Account, 1000);
        AccountThread AccountThread2 = new AccountThread(Account, 0);
        AccountThread1.start();
        AccountThread2.start();
    }
}

解决问题

为了解决这个问题,java提供了线程互斥,通过synchronized关键字为共享的资源或数据加锁,避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。在java语言中,每一个对象都有一把内置锁。线程进入同步代码块或方法的时候会通过synchronized关键字自动获取该对象上的内置锁,其他需要获取该锁的线程,必须等待当前拥有该锁的线程将其释放,从而保证任一时刻,只有一个线程访问共享资源。
为了接下来更好地理解synchronized用法的一些区别,我们先引入两个概念:对象锁类锁
java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。 但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的

synchronized详解

synchronized的用法:修饰方法和修饰代码块。
下面分析synchronized这两种用法在对象锁和类锁上有什么区别

(1)对象锁——synchronized修饰方法和代码块

    public class TestSynchronized   
    {    
        public void test1()   
        {    
        /*
       synchronized修饰代码块。传入的对象实例是this,表明是当前对象,当然,如果需要同步其他对象实例,也可传入其他对象的实例 
       */
             synchronized(this)  
             {    
                  int i = 5;    
                  while( i-- > 0)   
                  {    
                       System.out.println(Thread.currentThread().getName() + " : " + i);    
                       try   
                       {    
                            Thread.sleep(500);    
                       }   
                       catch (InterruptedException ie)   
                       {    
                       }    
                  }    
             }    
        }    
          /*
          synchronized修饰方法。因为前面同步代码块中传入参数是this,所以两个公共资源代码所需要获得的对象锁都是同一个对象锁
          */
        public synchronized void test2()   
        {    
             int i = 5;    
             while( i-- > 0)   
             {    
                  System.out.println(Thread.currentThread().getName() + " : " + i);    
                  try   
                  {    
                       Thread.sleep(500);    
                  }   
                  catch (InterruptedException ie)   
                  {    
                  }    
             }    
        }    

        public static void main(String[] args)   
        {    
             final TestSynchronized myt2 = new TestSynchronized();  
             /*
             main方法中分别开启两个线程(这两个线程的run()方法分别调用test1和test2方法),因为两个公共资源代码所需要获得的对象锁都是同一个对象锁,所以当有一个线程获得锁时,另一个线程必须等待。上面也给出了运行的结果可以看到:直到test1线程执行完毕,释放掉锁,test2线程才开始执行。
             */  
             Thread test1 = new Thread(  new Runnable() {  public void run() {  myt2.test1();  }  }, "test1"  );    
             Thread test2 = new Thread(  new Runnable() {  public void run() { myt2.test2();   }  }, "test2"  );    
             test1.start();;    
             test2.start();    
    //         TestRunnable tr=new TestRunnable();  
    //         Thread test3=new Thread(tr);  
    //         test3.start();  
        }   

    }  

运行结果:

如果我们把test2方法的synchronized关键字去掉,执行结果会如何呢?

我们可以看到,结果输出是交替着进行输出的,这是因为,虽然某个线程得到了对象的内置锁(即可以访问同步的方法或代码),但是另一个线程还是可以访问该对象的,即访问没有进行加锁的方法或者代码,所以加锁方法和没加锁方法之间是互不影响的。
(这里说一个题外话,代码里面明明是先开启test1线程,为什么先执行的是test2呢?这是因为java编译器在编译成字节码的时候,会根据实际情况对代码进行一个重排序,编译前代码写在前面,在编译后的字节码不一定排在前面,所以这种运行结果是正常的)

(2)类锁——synchronized修饰(静态)方法和代码块:

    public class TestSynchronized   
    {    
        public void test1()   
        {    
             synchronized(TestSynchronized.class)   
             {    
                  int i = 5;    
                  while( i-- > 0)   
                  {    
                       System.out.println(Thread.currentThread().getName() + " : " + i);    
                       try   
                       {    
                            Thread.sleep(500);    
                       }   
                       catch (InterruptedException ie)   
                       {    
                       }    
                  }    
             }    
        }    

        public static synchronized void test2()   
        {    
             int i = 5;    
             while( i-- > 0)   
             {    
                  System.out.println(Thread.currentThread().getName() + " : " + i);    
                  try   
                  {    
                       Thread.sleep(500);    
                  }   
                  catch (InterruptedException ie)   
                  {    
                  }    
             }    
        }    

        public static void main(String[] args)   
        {    
             final TestSynchronized myt2 = new TestSynchronized();    
             Thread test1 = new Thread(  new Runnable() {  public void run() {  myt2.test1();  }  }, "test1"  );    
             Thread test2 = new Thread(  new Runnable() {  public void run() { TestSynchronized.test2();   }  }, "test2"  );    
             test1.start();    
             test2.start();    
    //         TestRunnable tr=new TestRunnable();  
    //         Thread test3=new Thread(tr);  
    //         test3.start();  
        }   

    }  

执行结果如下:

从中可以看出,两个同步代码所需要获得的对象锁都是同一个对象锁,即synchronized修饰静态方法所对应的锁为类锁(即TestSynchronized.class),注意喔,类锁只是我们为了方便区别静态方法的特点而抽象出来的一个概念,因为静态方法是所有对象实例共用的,所以对应着synchronized修饰的静态方法的锁也是唯一的,所以抽象出来个类锁。
为了更好地这证明类锁和对象锁是两个不一样的锁,我们同时用synchronized修饰静态方法和普通的方法,看看运行结果如何

    public class TestSynchronized   
    {    
        public synchronized void test1()   //修饰普通方法
        {    
                  int i = 5;    
                  while( i-- > 0)   
                  {    
                       System.out.println(Thread.currentThread().getName() + " : " + i);    
                       try   
                       {    
                            Thread.sleep(500);    
                       }   
                       catch (InterruptedException ie)   
                       {    
                       }    
                  }    
        }    

        public static synchronized void test2()  //修饰静态方法 
        {    
             int i = 5;    
             while( i-- > 0)   
             {    
                  System.out.println(Thread.currentThread().getName() + " : " + i);    
                  try   
                  {    
                       Thread.sleep(500);    
                  }   
                  catch (InterruptedException ie)   
                  {    
                  }    
             }    
        }    

        public static void main(String[] args)   
        {    
             final TestSynchronized myt2 = new TestSynchronized();    
             Thread test1 = new Thread(  new Runnable() {  public void run() {  myt2.test1();  }  }, "test1"  );    
             Thread test2 = new Thread(  new Runnable() {  public void run() { TestSynchronized.test2();   }  }, "test2"  );    
             test1.start();    
             test2.start();    
    //         TestRunnable tr=new TestRunnable();  
    //         Thread test3=new Thread(tr);  
    //         test3.start();  
        }   

    }  

运行结果:

可见,线程是交替执行的,这就验证了类锁和对象锁是两个不一样的锁,控制着不同的区域,它们是互不干扰的。而且,线程获得对象锁的同时,也可以获得该类锁,即同时获得两个锁,这是允许的。

总结:
1、无论是同步代码块还是同步方法,必须获得对象锁才能够进入同步代码块或者同步方法进行操作。
2、同步是一种高开销的操作,因此应该尽量减少同步的内容。 通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
3、如果采用方法级别的同步,对象锁为方法所在的对象;如果是静态同步方法,对象锁为方法所在的类(唯一)。
4、对于代码块,对象锁即指synchronized(object)中的object。

此处参考了博客:http://langgufu.iteye.com/blog/2152608

线程同步(生产-消费者模型)

线程互斥和线程同步都是指,某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。不同的是,同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问(有序交替执行),而线程互斥无法限制访问者对资源的访问顺序,即访问是无序的(一个线程释放锁之后,不能保证什么时候再次获得锁)。
一言蔽之,同步是一种更复杂的互斥
一个典型的线程同步的应用是生产-消费者模型。其约束条件为:
(1)生产者生产产品,并将其保存到仓库中。
(2)消费者从仓库中取得产品。
(3)由于库房容量有限,因此只有当库房还有空间时,生产者才可以将产品放入库房;否则只能等待。
(4)只有库房中存在满足数量的产品时,消费者才能取走产品,否则只能等待。
实际应用中,很多例子都可以归结为该模型。这里举个例子,还是之前存款和取款的问题。假设存在一个账户对象(仓库)及两个线程:存款线程(生产者)和取款线程(消费者),并对其进行如下的限制:

  • 只有当账户上的余额balance=0时,存款线程才可以存进100元;否则只能等待。
  • 只有当账户上的余额balance=100时,取款线程才可以取走100元;否则只能等待。

    根据生产-消费者模型,应该得到一个交替执行的运行序列:存款100元、取款100元、存款100元、取款100元……很明显,使用前面的互斥对象是无法完成这两个线程的同步问题的。为了实现线程同步,java为互斥对象提供了两个方法:一个是wait();另一个是notify()。(可见,同步确实是在互斥的基础上加上某些机制实现次序访问的)
    要注意的是,这两个方法是作为互斥对象的方法来实现的,而不是作为Thread类的方法实现,并且,必须将这两个方法放在临界代码段中(synchronized修饰的代码),也就是说执行该方法的线程必须已获得了互斥对象的互斥锁,因为这两个方法实际上也是在操作互斥对象的互斥锁。
    wait():阻塞线程,释放互斥对象的互斥锁。(而sleep方法阻塞线程后,并不释放互斥锁)
    notify():当另一个线程调用互斥对象的notify()方法时,该互斥对象等待队列中的第一个线程才能进入就绪状态。
    例子代码及运行结果如下:

//Account4.java
public class Account4 {
  double balance;
  public Account4(){
     balance = 0;
     System.out.println("Totle Money: "+balance);
   }
   /*
   取款
   */
   public synchronized void withdraw(double money){
       if(balance == 0)
         try{
              wait();  //使取款线程进入阻塞状态,并释放互斥对象的互斥锁    
         }catch(InterruptedException e){
        }
         balance = balance - money;
         System.out.println("withdraw 100 success");
         notify();     //使存款线程进入就绪状态
       }
   /*
   存款
   */
   public synchronized void deposite(double money){
     if (balance != 0)
       try {
         wait();      //使存款线程进入阻塞状态,并释放互斥对象的互斥锁    
       }
       catch (InterruptedException e) {
       }
     balance = balance + money;
     System.out.println("deposite 100 success");
     notify();       //使取款线程进入就绪状态
   }
}

//WithdrawThread.java
public class WithdrawThread extends Thread
{
    Account4 account;

    public WithdrawThread(Account4 acount)
    {
        this.account = acount;
    }

    public void run()
    {
        for (int i = 0; i < 5; i++)
            account.withdraw(100);
    }
}

//DepositeThread.java
class DepositeThread extends Thread {
  Account4 acount;
  public DepositeThread(Account4 acount) {
  this.acount = acount;
  }
  public void run(){
    for(int i=0;i<5;i++)
      acount.deposite(100);
  }
}

//TestProCon.java
public class TestProCon
{
    public static void main(String[] args)
    {
        Account4 acount = new Account4();
        WithdrawThread withdraw = new WithdrawThread(acount);
        DepositeThread deposite = new DepositeThread(acount);
        withdraw.start();
        deposite.start();
    }
}

运行结果:

线程通信

线程通信是指线程之间相互传递信息。线程之间有好几种通信方式,如数据共享、管道等。这里,我们主要讲解线程间通过管道来进行通信的方式。管道通信具有如下特点:
(1)管道是单向的。如果需要建立双向通信,可以通过建立多个管道来解决。
(2)管道通信是面向连接的。发送线程建立管道的发送端,接收线程建立与发送管道的连接。
(3)管道中的信息是严格按照发送的顺序进行传送的。收到的数据和发送方在顺序上完全一致。
java语言管道看作是一种特殊的I/O流,并提供了两对相应的基本类来支持管道通信。这些类都位于java.io包中。一对是PipedOutStream和PipedInputStream,用于建立基于字节的通信;另一对是PipedWriter和PipedReader,用于建立基于字符的管道通信。
下面这个例子建立的就是字符管道。

//SenderThread.java
import java.io.*;
class SenderThread extends Thread{
  PipedWriter pipedWriter;
  public SenderThread( ){
  pipedWriter = new PipedWriter( );
  }
  public PipedWriter getPipedWriter( ){
    return pipedWriter;
  }
  public void run( ){
    for (int  i =0; i<5;i++){
     try{
      pipedWriter.write(i); 
     }catch(IOException e){   
     }
     System.out.println("Send: "+i);
    }
  }
}

//ReceiverThread.java
import java.io.*;
class ReceiverThread extends Thread{
  PipedReader pipedReader;

  public ReceiverThread( SenderThread senderThread) throws IOException{
  pipedReader = new PipedReader(senderThread.getPipedWriter( ));

  }

  public void run( ){
    int i=0;
    while(true){
      try{
      i = pipedReader.read();
      System.out.println("Received: "+i);  
    }catch(IOException e){        
      }
     if(i == 4) 
         break;     
    }
  }
}

//ThreadComm.java
import java.io.*;

public class ThreadComm
{
    public static void main(String[] args) throws Exception
    {
        SenderThread sender = new SenderThread();
        ReceiverThread receiver = new ReceiverThread(sender);
        sender.start();
        receiver.start();
    }
}

运行结果:

线程死锁(哲学家用餐问题)

线程死锁是并发程序设计中可能遇到的问题之一,它是指程序运行中,多个线程竞争共享资源时可能出现的一种系统状态。该问题可以形象地描述为哲学家用餐问题(此处对其进行了简化):5个哲学家围坐在一圆桌旁,每人的两边放着一筷子,共5支筷子。并规定如下条件:
(1)每个人只有拿起位于自己两边的筷子,合成一双才可以用餐。
(2)用餐后每人必须将两只筷子放回原处。
如果每个哲学家都彬彬有礼,轮流吃饭,则这种融洽的气氛可以长久地保持下去,但是如果每个人都拿起自己左手边的筷子,并想要去拿自己右手边的筷子(这支在另一个哲学家手中),这样就会处于僵持状态,这就是相当于线程死锁。
要注意的是,死锁不是一定会发生的,相反它出现的可能性很小,简单的测试往往无法发现,只有在程序设计中尽量避免这种情况的发生。
示例代码如下:

//ChopStick.java
public class ChopStick
{
    private String name;

    public ChopStick(String name)
    {
        this.name = name;
    }

    public String getNumber()
    {
        return name;
    }

}

//Philosopher.java
import java.util.*;

public class Philosopher extends Thread
{
    private ChopStick leftChopStick;

    private ChopStick rightChopStick;

    private String name;

    private static Random random = new Random();

    public Philosopher(String name, ChopStick leftChopStick,
            ChopStick rightChopStick)
    {
        this.name = name;
        this.leftChopStick = leftChopStick;
        this.rightChopStick = rightChopStick;
    }

    public String getNumber()
    {
        return name;
    }

    public void run()
    {
        try {
            sleep(random.nextInt(10));
        } catch (InterruptedException e) {

        }
        synchronized (leftChopStick) {
            System.out.println(this.getNumber() + " has "
                    + leftChopStick.getNumber() + " and wait for "
                    + rightChopStick.getNumber());
            synchronized (rightChopStick) {
                System.out.println(this.getNumber() + " eating");
            }
        }
    }

    public static void main(String args[])
    {
        // 建立三个筷子对象
        ChopStick chopStick1 = new ChopStick("ChopStick1");
        ChopStick chopStick2 = new ChopStick("ChopStick2");
        ChopStick chopStick3 = new ChopStick("ChopStick3");
        // 建立哲学家对象,并在其两边摆放筷子。
        Philosopher philosopher1 = new Philosopher("philosopher1", chopStick1,
                chopStick2);
        Philosopher philosopher2 = new Philosopher("philosopher2", chopStick2,
                chopStick3);
        Philosopher philosopher3 = new Philosopher("philosopher3", chopStick3,
                chopStick2);
        // 启动三个线程
        philosopher1.start();
        philosopher2.start();
        philosopher3.start();
    }
}

运行结果一:

运行结果二:

运行结果一发生了死锁,结果二没发生死锁。可见,线程死锁存在偶然性,不是一定会发生的,并且发生概率一般比较小,不过我们还是要尽可能地避免它,这样才算是优雅的代码。

线程池

创建和清除线程垃圾都会大量占用CPU等系统资源,所以java中用线程池来解决这一问题。基本思想是:在系统中开辟一块区域,用来存放一些待命的线程,这个区域就叫线程池,如果需要执行任务,则从线程池中取一个待命的线程来执行指定的任务,到任务结束再将其放回,这样可以避免重复创建线程。
常用的两种线程池为:
固定尺寸线程池,待命线程数量一定;
可变尺寸线程池,待命线程数量是根据任务负载的需要动态变化的。
之前在探索资料的时候,发现有一篇详细介绍线程池的博客,讲得挺好的,可以学习下:http://blog.csdn.net/hsuxu/article/details/8985931


打赏
最近浏览
TiAmoXiaoMin  LV1 2022年4月28日
Luck_ZDM  LV11 2022年3月4日
没猫病  LV11 2021年4月22日
Atomy5011 2021年3月29日
暂无贡献等级
yinxiaochen 2021年3月13日
暂无贡献等级
aodiaodi 2021年1月14日
暂无贡献等级
568885778  LV12 2020年11月13日
qimeila  LV5 2020年11月5日
wanglt 2020年10月19日
暂无贡献等级
lygyuyun  LV1 2020年6月15日
顶部 客服 微信二维码 底部
>扫描二维码关注最代码为好友扫描二维码关注最代码为好友