首页 > 编程 > Java > 正文

Java多线程ThreadAPI详细介绍

2019-11-26 08:41:02
字体:
来源:转载
供稿:网友

1.Thread的构造方法

package threadAPI; public class CreateThread {  public static void main(String[] args) {    Thread t1 = new Thread();    Thread t2 = new Thread();     t1.start();    t2.start();     System.out.println(t1.getName());    System.out.println(t2.getName());   }}

总结1:

创建线程对象Thread,默认有一个线程名,以Thread-开头,从0开始计数

Thread-0

Thread-1

Thread-2

可以看到Thread()中默认传入的第二个参数,即Runnable接口为null

在init方法中,会将我们传入的target给Thread的成员变量

然后在调用run方法的时候,会做如下判断

所以当target为null的时候,默认的run方法中什么也不做

总结2:

如果在构造Thread的时候,没有传递Runnable接口或者没有复写Thread的run方法,该Thread将不会调用任何东西

如果传递了Runnable接口的实例,则会执行该方法的逻辑代码

如果复写了Thread的run方法,则会执行复写的逻辑代码

为线程传递一个线程名

这时我们传入的参数名,会传递给线程对象的成员变量name

为线程传递线程名的同时,传递Runnbale接口的实现类对象,调用原理同上

我们还可以在为线程传入线程组

其实在上述的方法中没有传入线程组的情况下,init方法的ThreadGroup默认被传入null

parent即调用Thread对象的start方法的线程

package threadAPI; public class CreateThread {  public static void main(String[] args) {    Thread t = new Thread();    t.start();     System.out.println(t.getThreadGroup());    System.out.println(Thread.currentThread().getName());    System.out.println(Thread.currentThread().getThreadGroup());   }}

总结:

如果构造线程对象时未传入ThreadGroup,Thread默认会获取父线程的ThreadGroup作为该线程的ThreadGroup,此时子线程和父线程在同一个ThreadGroup中

我们可以查看当前ThreadGroup中有多少个线程在运行

package threadAPI; public class CreateThread {  public static void main(String[] args) {    Thread t = new Thread();    t.start();     ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();    System.out.println(threadGroup.activeCount());        //创建一个Thread数组    Thread[] threads = new Thread[threadGroup.activeCount()];    //将threadGroup中的数组枚举到threads数组中    threadGroup.enumerate(threads);     //打印threads接收到的线程    for (Thread thread : threads) {      System.out.println(thread);    }  }}

线程组的详细介绍见后续博文:

stackSize:

演示代码:

public class CreateThreadDemo {   private static int counter;   public static void main(String[] args) {    Thread t1 = new Thread(null, new Runnable() {      @Override      public void run() {        try {          add(1);        }catch (Error e){          System.out.println(counter);          e.printStackTrace();        }      }       private void add(int i){        counter++;        add(i+1);      }     },"",1<<24);     t1.start();  }}

运行结果:

将stackSize修改:

修改为

运行结果:

总结:

构造Thread的时候传入stacksize代表着该线程占用虚拟机栈的大小(虚拟机栈本身的大小在程序运行时就已经确定),如果没有指定stacksize的大小,默认是0,0代表着会忽略该参数,该参数会被JNI函数去使用

需要注意的是:该参数有一些平台有效,在有些平台则无效2.start方法

2、调用start

方法,会执行run方法

但不能调用start方法两次,会抛出异常

也可以直接调用Thread的run方法,但不会启动另一个线程

当你第一次调用线程的start方法时候,会返回两个线程,一个是调用线程,一个是新创建的执行run方法的线程

start方法的源码实现使用了模板方法,以下是模拟它的实现技巧:

public class TemplateMethod {  //此处final,是因为start方法的逻辑是固定的,不允许子类重写此方法  public final void start(String message)  {    System.out.println("################");    run(message);    System.out.println("################");  }   protected void run(String message)  {   }   public static void main(String[] args) {    TemplateMethod t1 = new TemplateMethod() {      @Override      protected void run(String message) {        System.out.println("*"+message+"*");      }    };     t1.start("hello,world");     TemplateMethod t2 = new TemplateMethod() {      @Override      protected void run(String message) {        System.out.println("+"+message+"+");      }    };     t2.start("hello,world");  }}

上述代码使用模板方法的大致思想是定义一个模板方法,它其中一些代码已经实现,而另一些需要交给用户去上实现,该方法确定结构,对外提供统一的调用接口start,定义另一个方法,提供给用户继承,用户可以重写,也可以不重写run方法,即不变的部分用模板实现,变化的部分提取出来,交给用户继承重写

3.setDaemon

Java中的线程分为两种:

  • 用户线程(setDaemo(false))
  • 守护线程(setDaemo(true))

什么是守护线程?

专门用于服务其他的线程,如果其他的线程(即用户线程(包括main线程))都执行完毕,JVM中只剩下守护线程时,此时JVM不管守护线程是否执行完毕,都会结束执行

示例代码1:

public class DaemonDemo {  public static void main(String[] args) throws InterruptedException {    Thread t = new Thread(){      @Override      public void run() {        try {          System.out.println(Thread.currentThread().getName()+"/trun");          Thread.sleep(10*1000);          System.out.println(Thread.currentThread().getName()+"/tout");        }catch (Exception e){          e.printStackTrace();        }      }    };     t.setDaemon(true);    t.start();     Thread.sleep(5*1000);     System.out.println(Thread.currentThread().getName());  }}

当main线程执行结束时,运行的唯一线程是守护线程,JVM退出,并未执行此句

疑问:在我们创建的t线程中创建一个守护线程t1,当t线程结束,主线程未结束的时候,守护线程是否结束呢?

示例代码2:

/** * t1、t3、main是用户线程 * t2、t4是守护线程 * * t1执行1秒结束 * main执行5秒结束 * t3执行8秒结束 * * t2、t4正常执行完需要10秒 */public class DaemonDemo {  public static void main(String[] args) throws InterruptedException {     //t1线程睡眠1秒就执行结束    Thread t1 = new Thread(){      @Override      public void run() {         //t2线程是守护线程,如果像用户线程一样,必须执行10秒才执行结束        Thread t2 = new Thread(()-> {          try {            for (int i = 1; i <= 10; i++) {              Thread.sleep(1*1000);              System.out.println("t2守护线程执行:"+i);            }              System.out.println("t2守护线程结束");          } catch (InterruptedException e) {            e.printStackTrace();          }        });         t2.setDaemon(true);        t2.start();          try {          Thread.sleep(1*1000);          System.out.println("t1线程结束");        }catch (Exception e){          e.printStackTrace();        }      }    };     t1.start();      //t3线程执行8秒执行结束    Thread t3 = new Thread(()-> {      try {        for (int i = 1; i <= 8; i++) {          Thread.sleep(1*1000);          System.out.println("t3守护线程执行:"+i);        }          System.out.println("t3守护线程结束");      } catch (InterruptedException e) {        e.printStackTrace();      }    });     t3.start();      //t4线程是守护线程,如果像用户线程一样,必须执行10秒才执行结束    Thread t4 = new Thread(()-> {      try {        for (int i = 1; i <= 10; i++) {          Thread.sleep(1*1000);          System.out.println("t4守护线程执行:"+i);        }          System.out.println("t4守护线程结束");      } catch (InterruptedException e) {        e.printStackTrace();      }    });     t4.setDaemon(true);    t4.start();     //main线程执行5秒就执行结束    Thread.sleep(5*1000);     System.out.println("main线程结束");  }}

总结:只有当JVM中的用户线程(包括main线程)执行完的时候,未执行完的守护线程会随着JVM退出而被强制退出,不会再执行后续的代码

示例代码3:

public class DaemonDemo {  public static void main(String[] args) throws InterruptedException {     Thread t1 = new Thread(){      @Override      public void run() {         try {          Thread.sleep(1*1000);          System.out.println("t1线程结束");        }catch (Exception e){          e.printStackTrace();        }      }    };     t1.start();    t1.setDaemon(true);  }}

总结:

setDaemon(true)方法必须在调用start方法之前调用,

否则会抛出java.langIllegalThreadStateException异常

4.获取线程的名称、id、优先级

(1)获取线程的名称

1.使用Thread类中的方法getName    

2.可以获得当前正在执行的线程,使用线程中的方法getName()获取线程的名称    

示例代码:

MyThread.java

package Demo01; //1.创建一个Thread类的子类public class MyThread extends Thread{    //2.在Thread类的子类中重写Thread类的run方法,设置线程任务,即线程要干什么  @Override  public void run() {    //第一种方式:直接调用getName获取线程名称    System.out.println(this.getName());     //第二种方式:获得当前正在执行的线程,使用线程中的方法getName()获取线程的名称    //System.out.println(Thread.currentThread().getName());  }}

ThreadDemo01.java

package Demo01; public class ThreadDemo01 {  public static void main(String[] args) {     //3.创建Thread类的子类对象    MyThread mt=new MyThread();    //4.调用Thread类的start方法,开启线程,执行run方法    mt.start();     new MyThread().start();    new MyThread().start();    //获得当前正在执行的线程,使用线程中的方法getName()获取线程的名称    System.out.println(Thread.currentThread().getName());  }}

(2)获取id

public class IdDemo {  public static void main(String[] args) throws InterruptedException {     Thread t1 = new Thread(()->{       try {        Thread.sleep(100*1000);      }catch (Exception e){        e.printStackTrace();      }      },"t1");     System.out.println(t1.getName()+"线程的线程id为:"+t1.getId());    System.out.println("main线程的id为:"+Thread.currentThread().getId());  }}

在JVM启动时,除过main线程,JVM还会启动的9个后台线程,我们的线程id从11开始递增

(3)获取线程优先级

说明:高优先级的线程要抢占低优先级线程CPU的执行权,但是只是从概率上讲,高优先级的线程高概率的情况下被执行,并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行

通过优先级可以企图改变线程执行的优先顺序,但是不一定会按照我们定义的顺序去执行,所以不要通过线程优先级去控制先去执行哪个线程再去执行哪个线程

示例代码:

public class PriorityDemo {  public static void main(String[] args) throws InterruptedException {     Thread t1 = new Thread(()->{      for (int i = 0; i < 1000; i++) {        System.out.println(Thread.currentThread().getName()+"-Index"+i);      }    },"t1");     t1.setPriority(Thread.MAX_PRIORITY);     Thread t2 = new Thread(()->{      for (int i = 0; i < 1000; i++) {        System.out.println(Thread.currentThread().getName()+"-Index"+i);      }    },"t2");    t2.setPriority(Thread.NORM_PRIORITY);     Thread t3 = new Thread(()->{      for (int i = 0; i < 1000; i++) {        System.out.println(Thread.currentThread().getName()+"-Index"+i);      }    },"t3");    t3.setPriority(Thread.MIN_PRIORITY);     t1.start();    t2.start();    t3.start();   }}

可以看到还是可以看到它们的交替执行,并非一定按照我们赋予的优先级顺序来执行

5.join

含义:调用join方法的线程等待执行join方法的线程执行结束(下面的方法是执行join方法的线程固定时间,然后再继续与存活的其他线程一起交替执行下面的代码)

示例代码1:

当没有join之前

import java.util.stream.IntStream; public class ThreadJoin {  public static void main(String[] args) throws InterruptedException {    Thread t1 = new Thread(()->{      IntStream.range(1,10)          .forEach(i-> System.out.println(Thread.currentThread().getName()+"->"+i));    },"t1");     t1.start();     IntStream.range(1,10)        .forEach(i-> System.out.println(Thread.currentThread().getName()+"->"+i));    }}

main线程和t1线程交替执行

当在main线程中调用t1线程的join方法时

import java.util.stream.IntStream; public class ThreadJoin {  public static void main(String[] args) throws InterruptedException {    Thread t1 = new Thread(()->{      IntStream.range(1,10)          .forEach(i-> System.out.println(Thread.currentThread().getName()+"->"+i));    },"t1");     t1.start();    t1.join();     IntStream.range(1,10)        .forEach(i-> System.out.println(Thread.currentThread().getName()+"->"+i));    }}

main线程要等到t1线程执行完,再执行它自己的代码

示例代码2:

import java.util.stream.IntStream; public class ThreadJoin {  public static void main(String[] args) throws InterruptedException {    Thread t1 = new Thread(()->{      IntStream.range(1,10)          .forEach(i-> System.out.println(Thread.currentThread().getName()+"->"+i));    },"t1");      Thread t2 = new Thread(()->{      IntStream.range(1,10)          .forEach(i-> System.out.println(Thread.currentThread().getName()+"->"+i));    },"t2");     t1.start();    t2.start();    t1.join();    t2.join();     IntStream.range(1,10)        .forEach(i-> System.out.println(Thread.currentThread().getName()+"->"+i));   }}

在A线程中调用B线程的join方法,则A等待B线程,与其他线程无关

上述代码中,在main线程中调用了t1和t2的join方法,所以是main线程等t1和t2执行完了,才继续执行下面的代码,但是t1和t2线程之间没有仍然交替执行

示例代码3:

import java.util.stream.IntStream; public class ThreadJoin {  public static void main(String[] args) throws InterruptedException {    Thread t1 = new Thread(()->{      try {        Thread.sleep(1000);        IntStream.range(1,10)            .forEach(i-> System.out.println(Thread.currentThread().getName()+"->"+i));      } catch (InterruptedException e) {        e.printStackTrace();      }      },"t1");        t1.start();    t1.join(1000);      IntStream.range(1,10)        .forEach(i-> System.out.println(Thread.currentThread().getName()+"->"+i));      }}

上述代码main线程等待了t1线程执行了1秒之后,又继续与t1线程交替执行即并发执行

6.Interrupt

6.1 interrupt()和isInterrupted

其作用是中断此线程(此线程不一定是当前线程,而是指调用该方法的Thread实例所代表的线程),但实际上只是给线程设置一个中断标志,线程仍会继续运行。

而当调用wait、sleep、join方法时,就会清除该中断标志,并且抛出InterruptedException异常,我们可以捕获该异常,然后做一些事情,比如,break跳出循环继续向下执行结束程序

只有一个作用:判断此线程(此线程不一定是当前线程,而是指调用该方法的Thread实例所代表的线程)的线程状态(即中断标志是否被设置)

不会清除中断标志

示例代码1:

public class InterruptDemo {  public static void main(String[] args) throws InterruptedException {    Thread t = new Thread(){      @Override      public void run() {        while(true){          System.out.println("t线程正在执行");          System.out.println(">>"+this.isInterrupted());          if(this.isInterrupted()){            break;          }        }         System.out.println("只是设置一个中断标志");        System.out.println("中断标志还在否?"+this.isInterrupted());      }    };     t.start();    //简单进行休眠,保证线程t进入运行状态    Thread.sleep(100);    System.out.println("main/t"+t.isInterrupted());    t.interrupt();    System.out.println("main/t"+t.isInterrupted());  }}

结论:

  • 即使我们调用了interrupt,它并不会结束程序,而是设置一个中断标志
  • isInterrupted方法不会清除中断标志

示例代码2:

public class InterruptDemo {  public static void main(String[] args) throws InterruptedException {    Thread t = new Thread(){      @Override      public void run() {        while(true){          try {            Thread.sleep(10);          }catch (InterruptedException e){            System.out.println("收到中断信号");            e.printStackTrace();          }        }      }    };     t.start();    //简单进行休眠,保证线程t进入运行状态    Thread.sleep(100);    System.out.println("main/t"+t.isInterrupted());    t.interrupt();    System.out.println("main/t"+t.isInterrupted());  }}

此种结果下程序是按照如下顺序被调度的:

此种结果下程序是按照如下顺序被调度的:

结论:

sleep方法会清除中断标志并抛出InterruptedException异常

示例代码3:

public class InterruptDemo {   private static final Object MONITTOR = new Object();   public static void main(String[] args) throws InterruptedException {    Thread t = new Thread(){      @Override      public void run() {        while(true){           synchronized (MONITTOR){            try {              MONITTOR.wait(10);            }catch (InterruptedException e){              System.out.println("收到中断信号");              e.printStackTrace();            }          }         }      }    };     t.start();    //简单进行休眠,保证线程t进入运行状态    Thread.sleep(100);    System.out.println("main/t"+t.isInterrupted());    t.interrupt();    System.out.println("main/t"+t.isInterrupted());  }}

结论:

wait方法会清除中断标志并抛出InterruptedException异常

示例代码4:

public class InterruptDemo {   public static void main(String[] args) {    Thread t = new Thread(){      @Override      public void run() {        while(true){         }      }    };     t.start();      //使用t2线程去执行interrupt()线程t    Thread t2 = new Thread(){      @Override      public void run() {        try {          Thread.sleep(100);        } catch (InterruptedException e) {          e.printStackTrace();        }         t.interrupt();        System.out.println("interrupt");      }    };     t2.start();     try {      t.join();    } catch (InterruptedException e) {      e.printStackTrace();    }  }}

当我们调用了join方法时,使用interrupt方法设置线程标志,我们发现程序并没有被打断,抛出异常,与API描述的不符?

我们反思一下,sleep,wait都是当前线程sleep,wait,而这里调用join的是main线程,即main线程join,而我们中断的是t线程,所以没有抛出异常,代码修改如下:

public class InterruptDemo {   public static void main(String[] args) {    Thread t = new Thread(){      @Override      public void run() {        while(true){         }      }    };     t.start();     Thread main = Thread.currentThread();    //使用t2线程去执行interrupt()线程t    Thread t2 = new Thread(){      @Override      public void run() {        try {          Thread.sleep(100);        } catch (InterruptedException e) {          e.printStackTrace();        }         main.interrupt();        System.out.println("interrupt");      }    };     t2.start();     try {      t.join();    } catch (InterruptedException e) {      e.printStackTrace();    }  }}

可以看到join的底层还是在调用wait方法

6.2 interrupted()

两个作用:

判断当前线程状态(即中断标志是否被设置)

清除中断标志(如果已经被设置)

  • 如果线程被interrupt()方法设置过中断标志,当执行interrupted()方法就会清除该中断标志并返回true,就相当于我们接收到了interrupt()方法传来的“中断信号”,我们可以通过true或false这样的判断,来做一些事情
  • 如果线程中的中断标志没有被设置,它就会返回false

有了isInterrupted()方法,为什么还要interrupted()方法呢?

因为如果我们是传给Thread一个Runnabe接口,重写其中的run方法,我们又想调用判断是否有中断标志,我们又无法调用isInterrupted()方法,所以Thread提供静态方法interrupted()供我们使用

并且isInterrupted和interrupted一个很大区别就是:interrupted会清除中断标志,而isInterrupted不会

源码如下:

供我们调用的isInterrupted:

本地方法isInterrupted:

供我们调用的静态interrupted:

public class InterruptDemo {   public static void main(String[] args) throws InterruptedException {    Thread t = new Thread(new Runnable() {      @Override      public void run() {        {          while(true){             //此处在Runnable接口中重写run方法,只能调用静态方法interrupted判断是否有中断标志            if(Thread.interrupted())            {              System.out.println("收到中断信号");              System.out.println("中断标志还在否?"+Thread.interrupted());              break;            }          }        }      }    });     t.start();    //简单进行休眠,保证线程t进入运行状态    Thread.sleep(100);    System.out.println("main/t"+t.isInterrupted());    t.interrupt();    System.out.println("main/t"+t.isInterrupted());  }}

6.3 如何采用优雅的方式结束线程?

(1)方式1:通过开关的方式即一个flag的方式去结束一个线程

/** * 通过开关的方式即一个flag的方式去结束一个线程 */public class ThreadCloseGraceful {   private static class Worker extends Thread{    private volatile boolean on = true;     @Override    public void run() {      while(on){       }    }      public void shutdown(){      on = false;    }  }   public static void main(String[] args) {    Worker worker = new Worker();    worker.start();     try {      Thread.sleep(10000);    }catch (InterruptedException e){      e.printStackTrace();    }     worker.shutdown();  }  }

(2)方式2:通过中断结束一个线程

/** * 通过捕获中断异常来结束程序 */public class ThreadCloseGraceful {   private static class Worker extends Thread{      @Override    public void run() {      while(true){        try {          Thread.sleep(1000); //遇到sleep清除中断状态,并捕获抛出的中断异常        }catch (InterruptedException e){          System.out.println("Worker线程捕获到了中断异常");          break;        }      }      //这里还可以实现一些逻辑的代码       System.out.println("Worker线程被中断结束");    }   }   public static void main(String[] args) {    Worker worker = new Worker();    worker.start();     try {      Thread.sleep(5000);    }catch (InterruptedException e){      e.printStackTrace();    }     worker.interrupt(); //设置worker线程的中断状态    System.out.println("主线程结束");  } }

/** * 通过线程调用静态方法interrupted来结束程序 */public class ThreadCloseGraceful {   private static class Worker extends Thread{      @Override    public void run() {      while(true){        if(Thread.interrupted()) //通过调用此方法,清除中断状态,清除后返回true          break;              }      //这里还可以实现一些逻辑的代码       System.out.println("Worker线程被中断结束");    }   }   public static void main(String[] args) {    Worker worker = new Worker();    worker.start();     try {      Thread.sleep(5000);    }catch (InterruptedException e){      e.printStackTrace();    }     worker.interrupt(); //设置中断状态    System.out.println("主线程结束");  } }

(3)方式3:封装一个类按照时间强制结束一个线程

分析上述两种方式不能解决的问题:

当我有一个很耗时的任务,本来预期半个小时结束,结果它运行了两个小时,我想结束它,也就是它在一次循环中一直执行或阻塞,它没有机会去判断开关状态或者中断标志状态

方式3的代码:

public class ThreadService {   //定义一个执行线程用于控制守护线程执行我们的业务  private Thread excuteThread;  //用于判断线程是否执行完,执行完的话,关闭方法就不用再去调用中断方法  private boolean finished = false;   public void excute(Runnable task){       excuteThread = new Thread(){      @Override      public void run() {        //创建一个守护线程        Thread runner = new Thread(task);        runner.setDaemon(true);        runner.start();        try {          runner.join();          finished = true;        } catch (InterruptedException e) {         }       }    };     excuteThread.start();  }   public void shutdown(long mills){    long currentTime = System.currentTimeMillis();    while (!finished){      if(System.currentTimeMillis() - currentTime > mills){        System.out.println("任务超时,需要结束它");        //我们执行任务的线程由excuteThread线程去join        // 那么我们设置中断标志,异常会被捕获,然后不执行任何代码,直接结束执行线程        excuteThread.interrupt();        break;      }       //业务既没有完成,也没有到我们设定的关闭时间,短暂的进行休眠      try {        excuteThread.sleep(1);      } catch (InterruptedException e) {        System.out.println("执行线程被打断");        break;      }    }     finished = false;  }}
/** * 通过守护线程执行业务,通过一个用户线程控制守护线程 * * 注意:这种方法的前提是JVM中只有用户线程, *    即当我们调用shutdown以后不能再有其他用户线程还在执行,有的话,守护线程不会被结束 */public class ThreadCloseGraceful {    public static void main(String[] args) {     ThreadService service = new ThreadService();    long start = System.currentTimeMillis();    service.excute(()->{       //假设在执行一个很耗时的任务      while(true){         System.out.println("守护线程执行的时间:"+(System.currentTimeMillis()-start)+"ms");      }    });     //等待5000ms结束该线程    service.shutdown(5000);     long end = System.currentTimeMillis();     System.out.println(end - start);     /**     * 此处如果不注释,则为结果2     *     * 因为中断结束了执行线程,但是main作为用户线程并没有结束,所以守护线程并没有结束     */    /*    try {      Thread.sleep(10*1000);    } catch (InterruptedException e) {      e.printStackTrace();    }    */  } }

运行结果1:守护线程随着唯一的excuteThread被结束而结束

运行结果2:由于main线程在excuteThread结束后并未执行完,守护线程未结束

所以注意:

这种方法的前提是JVM中只有一个用户线程,即当我们调用shutdown以后不能再有其他用户线程还在执行,有的话,守护线程不会被结束

7.yield

当调用Thread.yield函数时,会给线程调度器一个当前线程愿意让出CPU使用的暗示,只是大概率下会把CPU的执行权交给其他线程,但是不是绝对的,线程调度器可能会忽略这个暗示,还可能再次把执行权分配给当前线程

yield也不会对锁的行为有影响

public class YieldDemo {  public static void main(String[] args) {    Runnable yieldTask = new Runnable() {      @Override      public void run() {        for (int i = 0; i < 10; i++) {          System.out.println(Thread.currentThread().getName()+i);          if(i == 5){            Thread.yield();          }        }      }    };     Thread t1 = new Thread(yieldTask,"A");    Thread t2 = new Thread(yieldTask,"B");    t1.start();    t2.start();  }}

             

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持武林网。

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表