Thread详解

本文详细介绍了线程的概念、线程安全性和线程的六种状态,并深入解析了Java中Thread类的各种属性和方法,包括start()、run()、setPriority()、sleep()、yield()、isAlive()、join()、interrupt()和setDaemon()等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.1什么是线程? 

线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。

  线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。

 1.2 什么是线程安全性?

如果一个类可以安全地被多个线程使用,它就是线程安全的。你无法对此论述提出任何争议,但也无法从中得到更多有意义的帮助。那么我们如何辨别线程安全与非线程安全的类?我们甚至又该如何理解“安全”呢?任何一个合理的“线程安全性”定义,其关键在于“正确性”的概念。在<<JAVA并发编程实践>>书中作者是这样定义的:一个类是是线程安全的,是指在被多个线程访问时,类可以持续进行正确的行为。或当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的。

 1.3 线程的六种状态

线程从创建到销毁期间有六种状态:

  • New: 至今尚未启动的线程的状态。 
  • Runnable :可运行线程的线程状态。
  • Blocked :受阻塞并且正在等待监视器锁的某一线程的线程状态。
  • Waiting :某一等待线程的线程状态。
  • Timed_waiting:具有指定等待时间的某一等待线程的线程状态。
  • Terminated:已终止线程的线程状态。线程已经结束执行。
  • 如下图所示:
  •  

 

 

 

二、Thread类

   2.1 属性

[java] view plain copy

  1. //线程名字,通过构造参数来指定  
  2. private char        name[];  
  3. //表示线程的优先级,优先级越高,越优先被执行(最大值为10,最小值为1,默认值为5)  
  4. private int         priority;  
  5.   
  6. private Thread      threadQ;  
  7. private long        eetop;  
  8.   
  9. /* Whether or not to single_step this thread. */  
  10. private boolean     single_step;  
  11.   
  12. //线程是否是守护线程:当所有非守护进程结束或死亡后,程序将停止   
  13. private boolean     daemon = false;  
  14.   
  15. /* JVM state */  
  16. private boolean     stillborn = false;  
  17.   
  18. //将要执行的任务  
  19. private Runnable target;  
  20.   
  21. /* 线程组表示一个线程的集合。此外,线程组也可以包含其他线程组。线程组构成一棵树,在树中,除了初始线程组外,每个线程组都有一个父线程组。  */  
  22. private ThreadGroup group;  
  23.   
  24. /* The context ClassLoader for this thread */  
  25. private ClassLoader contextClassLoader;  
  26.   
  27. /* The inherited AccessControlContext of this thread */  
  28. private AccessControlContext inheritedAccessControlContext;  
  29.   
  30. /*第几个线程,在init初始化线程的时候用来赋给thread.name */    
  31. private static int threadInitNumber;  
  32. private static synchronized int nextThreadNum() {  
  33.     return threadInitNumber++;  
  34. }  
  35.   
  36. /* ThreadLocal values pertaining to this thread. This map is maintained 
  37.  * by the ThreadLocal class. */  
  38. ThreadLocal.ThreadLocalMap threadLocals = null;  
  39.   
  40. /* 
  41.  * InheritableThreadLocal values pertaining to this thread. This map is 
  42.  * maintained by the InheritableThreadLocal class. 
  43.  */  
  44. ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;  
  45.   
  46. /* 
  47.  * The requested stack size for this thread, or 0 if the creator did 
  48.  * not specify a stack size.  It is up to the VM to do whatever it 
  49.  * likes with this number; some VMs will ignore it. 
  50.  */  
  51. private long stackSize;  
  52.   
  53. /* 
  54.  * JVM-private state that persists after native thread termination. 
  55.  */  
  56. private long nativeParkEventPointer;  
  57.   
  58. /* 
  59.  * Thread ID 
  60.  */  
  61. private long tid;  
  62.   
  63. /* For generating thread ID */  
  64. private static long threadSeqNumber;  
  65.   
  66. /* 
  67.  *线程从创建到最终的消亡,要经历若干个状态。 
  68.  *一般来说,线程包括以下这几个状态:创建(new)、就绪(runnable)、运行(running)、阻塞(blocked)、time waiting、waiting、消亡(dead)。 
  69.  *  
  70.  */  
  71. private volatile int threadStatus = 0;  

   2.3 start()操作

[java] view plain copy

  1. //启动新创建的线程  
  2. public synchronized void start() {  
  3.     /** 
  4.      * This method is not invoked for the main method thread or "system" 
  5.      * group threads created/set up by the VM. Any new functionality added 
  6.      * to this method in the future may have to also be added to the VM. 
  7.      * 
  8.      * A zero status value corresponds to state "NEW". 
  9.      */  
  10.     /*这个方法不会被主线程调用或通过虚拟机系统线程组创建起来。未来任何添加到该方法里的新功能可能需要加入到虚拟机中 
  11.      *  
  12.      * 状态new的值是0. 
  13.      * */  
  14.     if (threadStatus != 0)  
  15.         throw new IllegalThreadStateException();  
  16.   
  17.     /* Notify the group that this thread is about to be started 
  18.      * so that it can be added to the group's list of threads 
  19.      * and the group's unstarted count can be decremented. */  
  20.     /* 通知线程组新线程将要启动,以便它可以添加到线程组列表并且线程组没有开始计数*/  
  21.     group.add(this);  
  22.   
  23.     boolean started = false;  
  24.     try {  
  25.         start0();  
  26.         started = true;  
  27.     } finally {  
  28.         try {  
  29.             if (!started) {  
  30.                 group.threadStartFailed(this);  
  31.             }  
  32.         } catch (Throwable ignore) {  
  33.             /* do nothing. If start0 threw a Throwable then 
  34.               it will be passed up the call stack */  
  35.         }  
  36.     }  
  37. }  

   2.3 run()操作

[java] view plain copy

  1. public void run() {  
  2.        if (target != null) {  
  3.             target.run();  
  4.        }  
  5.  }  

   2.4 start()和run()之间有什么区别?

   2.4.1 代码示例:run()方法使用

 

[java] view plain copy

  1. package com.game.thread;  
  2.   
  3. /** 
  4.  *  
  5.  * @author liulongling 
  6.  * 
  7.  */  
  8. public class ThreadTest extends Thread{  
  9.   
  10.     public ThreadTest(String name) {  
  11.         super.setName(name);  
  12.     }  
  13.   
  14.     @Override  
  15.     public void run() {  
  16.         for(int i = 0; i < 5;i++)  
  17.         {  
  18.             System.out.println(super.getName()+":"+i);  
  19.         }  
  20.     }  
  21.   
  22.     public static void main(String[] args) {  
  23.         ThreadTest test = new ThreadTest("A");  
  24.         ThreadTest test1 = new ThreadTest("B");  
  25.           
  26.         test.run();  
  27.         test1.run();  
  28.           
  29.         if(Thread.activeCount()>=1)  
  30.         {  
  31.             Thread.yield();  
  32.         }  
  33.     }  
  34. }  
控制台:
+------------------------------------------------------------------+

A:0 A:1 A:2 A:3 A:4 B:0 B:1 B:2 B:3 B:4

+------------------------------------------------------------------+

   2.4.2 代码示例:start()方法使用

[java] view plain copy

  1. package com.game.thread;  
  2.   
  3. /** 
  4.  *  
  5.  * @author liulongling 
  6.  * 
  7.  */  
  8. public class ThreadTest extends Thread{  
  9.   
  10.     public ThreadTest(String name) {  
  11.         super.setName(name);  
  12.     }  
  13.   
  14.     @Override  
  15.     public void run() {  
  16.         for(int i = 0; i < 5;i++)  
  17.         {  
  18.             System.out.println(super.getName()+":"+i);  
  19.         }  
  20.     }  
  21.   
  22.     public static void main(String[] args) {  
  23.         ThreadTest test = new ThreadTest("A");  
  24.         ThreadTest test1 = new ThreadTest("B");  
  25.           
  26.         test.start();  
  27.         test1.start();  
  28.           
  29.         if(Thread.activeCount()>=1)  
  30.         {  
  31.             Thread.yield();  
  32.         }  
  33.     }  
  34. }  
控制台:
+------------------------------------------------------------------+

B:0 A:0 B:1 A:1 B:2 A:2 B:3 A:3 B:4 A:4

+------------------------------------------------------------------+

   2.4.3 结果分析

   以上结果可以发现run()和单线程一样每次只能执行一个线程,而start()不一样有多个线程交叉执行着。我们知道start()方法被用来启动新创建的线程,而且start()内部通过本地系统调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法会启动新的线程,其中多个线程在CPU中是支持并发执行的。那么有没有什么方法可以让A线程优先执行呢?

 

   2.5 setPriority()操作

 

 

[java] view plain copy

  1. public final void setPriority(int newPriority) {  
  2.        ThreadGroup g;  
  3.        checkAccess();  
  4.        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {  
  5.            throw new IllegalArgumentException();  
  6.        }  
  7.        if((g = getThreadGroup()) != null) {  
  8.            if (newPriority > g.getMaxPriority()) {  
  9.                newPriority = g.getMaxPriority();  
  10.            }  
  11.            setPriority0(priority = newPriority);  
  12.        }  
  13.    }  

[java] view plain copy

  1. private native void setPriority0(int newPriority);  

 

   setPriority使用了native关键字,在Java API中,一个native方法意味着这个方法没有使用Java语言实现,只能通过代码示例去分析它的原理。在代码中将线程A的执行优先级设置为最高,同时线程B的优先级设置为最低,那么预期的结果应该是线程A先执行完后再执行线程B,代码如下:

 

[java] view plain copy

  1. package com.game.thread;  
  2.   
  3. /** 
  4.  *  
  5.  * @author liulongling 
  6.  * 
  7.  */  
  8. public class ThreadTest extends Thread{  
  9.   
  10.     public ThreadTest(String name) {  
  11.         super.setName(name);  
  12.     }  
  13.   
  14.     @Override  
  15.     public void run() {  
  16.         for(int i = 0; i < 5;i++)  
  17.         {  
  18.             System.out.println(super.getName()+":"+i);  
  19.         }  
  20.     }  
  21.   
  22.     public static void main(String[] args) {  
  23.         ThreadTest test = new ThreadTest("A");  
  24.         ThreadTest test1 = new ThreadTest("B");  
  25.         //MAX_PRIORITY是最高优先级  
  26.         test.setPriority(MAX_PRIORITY);  
  27.           
  28.         test.setPriority(MIN_PRIORITY);  
  29.         test.start();  
  30.         test1.start();  
  31.           
  32.         if(Thread.activeCount()>=1)  
  33.         {  
  34.             Thread.yield();  
  35.         }  
  36.     }  
  37. }  
控制台:
+------------------------------------------------------------------+
A:0
A:1
A:2
A:3
A:4
B:0
B:1
B:2
B:3
B:4
+------------------------------------------------------------------+

   2.5 sleep(long millis)操作

 

[java] view plain copy

  1. public static native void sleep(long millis) throws InterruptedException;  

 

  sleep也是使用了native关键字,调用了底层方法。sleep是指线程被调用时,占着CPU不工作,形象地说明为“占着CPU睡觉”,此时,系统的CPU部分资源被占用,其他线程无法进入。多线程下使用时需要注意的是sleep方法不会释放锁。比如:线程A和线程B执行一段加锁代码,线程A先进去,线程B在外面等待,其中代码程序有sleep方法让线程休眠,休眠后锁并不会被释放,线程B也只能继续在外面等待直到休眠时间结束。代码如下:

[java] view plain copy

  1. package com.game.thread;  
  2.   
  3. import java.io.IOException;  
  4.   
  5. /** 
  6.  *  
  7.  * @author liulongling 
  8.  * 
  9.  */  
  10. public class ThreadTest{  
  11.   
  12.     private int i = 10;  
  13.     private Object object = new Object();  
  14.       
  15.     MyThread thread1 = new MyThread("A");  
  16.     MyThread thread2 = new MyThread("B");  
  17.        
  18.     public static void main(String[] args) throws IOException  {  
  19.         ThreadTest test = new ThreadTest();  
  20.         test.thread1.start();  
  21.         test.thread2.start();  
  22.     }   
  23.        
  24.        
  25.     class MyThread extends Thread{  
  26.         public MyThread(String name) {  
  27.             super.setName(name);  
  28.         }  
  29.         @Override  
  30.         public void run() {  
  31.             synchronized (object) {  
  32.                 System.out.println(Thread.currentThread().getName()+":"+i++);  
  33.                 try {  
  34.                     System.out.println("线程"+Thread.currentThread().getName()+"进入睡眠状态");  
  35.                     Thread.currentThread().sleep(1000);  
  36.                 } catch (InterruptedException e) {  
  37.                     // TODO: handle exception  
  38.                 }  
  39.                 System.out.println("线程"+Thread.currentThread().getName()+"被唤醒");  
  40.                 System.out.println(Thread.currentThread().getName()+":"+i);  
  41.             }  
  42.         }  
  43.     }  
  44. }  

 

控制台:
+------------------------------------------------------------------+

 

A:10

线程A进入睡眠状态

线程A被唤醒

A:11

B:11

线程B进入睡眠状态

线程B被唤醒

B:12

+------------------------------------------------------------------+

   2.6 yield()操作

 

   调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。

 

 注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。

   2.7 isAlive()   

 

[java] view plain copy

  1. /** 
  2.  * Tests if this thread is alive. A thread is alive if it has 
  3.  * been started and has not yet died. 
  4.  * 
  5.  * @return  <code>true</code> if this thread is alive; 
  6.  *          <code>false</code> otherwise. 
  7.  */  
  8. public final native boolean isAlive();  

  表示线程当前是否为可用状态,如果线程已经启动,并且当前没有任何异常的话,则返回true,否则为false

 

   2.8 join()操作

  join有3个重载方法:

 

   2.8.1 join() 

 

[java] view plain copy

  1. //立即阻塞调用线程,直到该线程执行结束  
  2. public final synchronized void join(long millis)  
  3.         throws InterruptedException {  
  4.     long base = System.currentTimeMillis();  
  5.     long now = 0;  
  6.   
  7.     if (millis < 0) {  
  8.         throw new IllegalArgumentException("timeout value is negative");  
  9.     }  
  10.   
  11.     if (millis == 0) {  
  12.         //线程状态正常  
  13.         while (isAlive()) {  
  14.             wait(0);  
  15.         }  
  16.     } else {  
  17.         while (isAlive()) {  
  18.             long delay = millis - now;  
  19.             if (delay <= 0) {  
  20.                 break;  
  21.             }  
  22.             wait(delay);  
  23.             now = System.currentTimeMillis() - base;  
  24.         }  
  25.     }  
  26. }  

 

[java] view plain copy

  1. package com.game.thread;  
  2.   
  3. import java.io.IOException;  
  4.   
  5. /** 
  6.  *  
  7.  * @author liulongling 
  8.  * 
  9.  */  
  10. public class ThreadTest{  
  11.   
  12.     private int i = 10;  
  13.     private Object object = new Object();  
  14.   
  15.     MyThread thread1 = new MyThread("A");  
  16.     MyThread thread2 = new MyThread("B");  
  17.   
  18.     public static void main(String[] args) throws IOException  {  
  19.         ThreadTest test = new ThreadTest();  
  20.         test.thread1.start();  
  21.         System.out.println("线程"+Thread.currentThread().getName()+"等待");  
  22.         try {  
  23.             test.thread1.join();  
  24.         } catch (InterruptedException e) {  
  25.             // TODO Auto-generated catch block  
  26.             e.printStackTrace();  
  27.         }  
  28.         System.out.println("线程"+Thread.currentThread().getName()+"执行");  
  29.         for (int j = 0; j < 5; j++)  
  30.         {  
  31.             System.out.println(Thread.currentThread().getName() + ":" + j);  
  32.         }  
  33.     }   
  34.   
  35.   
  36.     class MyThread extends Thread{  
  37.         public MyThread(String name) {  
  38.             super.setName(name);  
  39.         }  
  40.         @Override  
  41.         public void run() {  
  42.             synchronized (object) {  
  43.                 System.out.println(Thread.currentThread().getName()+":"+i++);  
  44.                 try {  
  45.                     System.out.println("线程"+Thread.currentThread().getName()+"进入睡眠状态");  
  46.                     Thread.currentThread().sleep(1000);  
  47.                 } catch (InterruptedException e) {  
  48.                     // TODO: handle exception  
  49.                 }  
  50.                 System.out.println("线程"+Thread.currentThread().getName()+"被唤醒");  
  51.                 System.out.println(Thread.currentThread().getName()+":"+i);  
  52.             }  
  53.         }  
  54.     }  
  55. }  
控制台:
+------------------------------------------------------------------+

 

线程main等待

A:10

线程A进入睡眠状态

线程A被唤醒

A:11

线程main执行

main:0

main:1

main:2

main:3

main:4

 

  MSDN上解释join无参方法其作用为:阻塞 “调用线程” 直到某个线程结束。从上面结果可以分析出,A线程其实在main线程上运行,我们可以说main线程调用了A线程或称main线程为“调用线程”,A线程调用join()方法后将调用线程阻塞直到A线程结束后控制台才输出来main:0...。我们去掉join方法看下控制台输出结果。

 

控制台:
+------------------------------------------------------------------+

 

 

main:0

main:1

A:10

main:2

线程A进入睡眠状态

main:3

main:4

线程A被唤醒

A:11

+------------------------------------------------------------------+

   2.8.2 join(long millis)

  join(long millis)的参数作用是指定“调用线程”的最大阻塞时间。代码如下:

 

[java] view plain copy

  1. package com.game.thread;  
  2.   
  3. import java.io.IOException;  
  4.   
  5. /** 
  6.  *  
  7.  * @author liulongling 
  8.  * 
  9.  */  
  10. public class ThreadTest{  
  11.   
  12.     private int i = 10;  
  13.     private Object object = new Object();  
  14.   
  15.     MyThread thread1 = new MyThread("A");  
  16.     MyThread thread2 = new MyThread("B");  
  17.   
  18.     public static void main(String[] args) throws IOException  {  
  19.         ThreadTest test = new ThreadTest();  
  20.         test.thread1.start();  
  21.         System.out.println("线程"+Thread.currentThread().getName()+"等待");  
  22.         try {  
  23.             test.thread1.join(500);  
  24.         } catch (InterruptedException e) {  
  25.             // TODO Auto-generated catch block  
  26.             e.printStackTrace();  
  27.         }  
  28.         System.out.println("线程"+Thread.currentThread().getName()+"执行");  
  29.         for (int j = 0; j < 5; j++)  
  30.         {  
  31.             System.out.println(Thread.currentThread().getName() + ":" + j);  
  32.         }  
  33.     }   
  34.   
  35.   
  36.     class MyThread extends Thread{  
  37.         public MyThread(String name) {  
  38.             super.setName(name);  
  39.         }  
  40.         @Override  
  41.         public void run() {  
  42.             synchronized (object) {  
  43.                 System.out.println(Thread.currentThread().getName()+":"+i++);  
  44.                 try {  
  45.                     System.out.println("线程"+Thread.currentThread().getName()+"进入睡眠状态");  
  46.                     Thread.currentThread().sleep(1000);  
  47.                 } catch (InterruptedException e) {  
  48.                     // TODO: handle exception  
  49.                 }  
  50.                 System.out.println("线程"+Thread.currentThread().getName()+"被唤醒");  
  51.                 System.out.println(Thread.currentThread().getName()+":"+i);  
  52.             }  
  53.         }  
  54.     }  
  55. }  

 

 

 

线程main等待

A:10

线程A进入睡眠状态

线程main执行

main:0

main:1

main:2

main:3

main:4

线程A被唤醒

A:11

 

+------------------------------------------------------------------+   从上面结果可以分析出,在A线程还未结束,主线程已经开始执行。原因是我们给主线程设置的阻塞时间是500ms,小于A线程run()方法里的1000ms休眠时间。

   2.8.3 join(long millis, int nanos)

  阻塞调用线程的时间最长为 millis 毫秒 + nanos 纳秒...这里就不举例说明了,原理和上一个方法一样。

 

 

 

   2.9 interrupt()操作

 

  interrupt的作用是中断正被阻塞的线程,比如我给某一线程休眠了10s时间,如果我在这个线程上调用了interrupt方法,看看会有什么效果。代码如下:

 

[java] view plain copy

  1. package com.game.thread;  
  2.   
  3. import java.io.IOException;  
  4.   
  5. /** 
  6.  *  
  7.  * @author liulongling 
  8.  * 
  9.  */  
  10. public class ThreadTest{  
  11.   
  12.     private int i = 10;  
  13.     private Object object = new Object();  
  14.   
  15.     MyThread thread1 = new MyThread("A");  
  16.     MyThread thread2 = new MyThread("B");  
  17.   
  18.     public static void main(String[] args) throws IOException  {  
  19.         ThreadTest test = new ThreadTest();  
  20.         test.thread1.start();  
  21.         test.thread1.interrupt();  
  22.     }   
  23.   
  24.   
  25.     class MyThread extends Thread{  
  26.         public MyThread(String name) {  
  27.             super.setName(name);  
  28.         }  
  29.         @Override  
  30.         public void run() {  
  31.             synchronized (object) {  
  32.                 System.out.println(Thread.currentThread().getName()+":"+i++);  
  33.                 try {  
  34.                     System.out.println("线程"+Thread.currentThread().getName()+"进入睡眠状态");  
  35.                     Thread.currentThread().sleep(10000);  
  36.                 } catch (InterruptedException e) {  
  37.                     System.out.println("你被中断了");  
  38.                 }  
  39.                 System.out.println("线程"+Thread.currentThread().getName()+"被唤醒");  
  40.                 System.out.println(Thread.currentThread().getName()+":"+i);  
  41.             }  
  42.         }  
  43.     }  
  44. }  
控制台:
+------------------------------------------------------------------+

 

A:10

线程A进入睡眠状态

你被中断了

线程A被唤醒

A:11 +------------------------------------------------------------------+

 使用了 interrupt的结果是在休眠代码处抛出一个异常,并且阻塞马上停止了。

 

   3.0 setDaemon()操作

  用来设置线程是否成为守护线程和判断线程是否是守护线程。
  守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。Thread类有一个比较常用的静态方法currentThread()用来获取当前线程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值