java线程详解

线程与进程的区别

(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

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值