多线程——java线程方法详细介绍

1、线程调度
1.1 调整线程优先级
Java线程有优先级,优先级高的线程会获得较多的运行机会。Java线程的优先级用整数表示,取值范围是

1~10,Thread类有以下三个静态常量:
static int MAX_PRIORITY:线程可以具有的最高优先级,取值为10static int MIN_PRIORITY:线程可以具有的最低优先级,取值为1static int NORM_PRIORITY:分配给线程的默认优先级,取值为5

Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。

setPriority(); //更改线程的优先级
MIN_PRIORITY = 1
NORM_PRIORITY = 5
MAX_PRIORITY = 10
//用法:
Thread4 t1 = new Thread4("t1");
Thread4 t2 = new Thread4("t2");
t1.setPriority(Thread.MAX_PRIORITY);//这里设置具体数值也是可以的
t2.setPriority(Thread.MIN_PRIORITY);

每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。

JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。

1.2 线程睡眠
Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,线程自动转为就绪(Runnable)状态。sleep()平台移植性好。

1.3 线程等待
Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的notify()方法或notifyAll()唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用wait(0)一样。

1.4 线程让步
Thread.yield()方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。

1.5 线程加入
join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。

1.6 线程唤醒
Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。

线程通过调用其中一个wait方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。

注意:Thread中suspend()和resume()两个方法在JDK1.5中已经废除,不再介绍。因为有死锁倾向。

2、线程常用方法说明
2.1 sleep和join
sleep(long millis): 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。

join():指等待t线程终止。

2.1.1 使用方式
join是Thread类的一个方法,启动线程后直接调用,即join()的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。比如:

Thread t = new AThread();
t.start();
t.join();

2.1.2 为什么要用join()方法
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。

2.1.3 下面看看一个程序不加join和加了join后的变化

1)不加join

class Thread5 extends Thread {

    private String name;

    public Thread5(String name) {
        super(name);
        this.name = name;
    }

    public void run() {
        System.out.println(Thread.currentThread().getName() + " 线程运行开始!");
        for (int i = 0; i < 2; i++) {
            System.out.println("子线程" + name + "运行 : " + i);
            try {
                sleep((int) Math.random() * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " 线程运行结束!");
    }

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + "主线程运行开始!");
        Thread5 mTh1 = new Thread5("A");
        Thread5 mTh2 = new Thread5("B");
        mTh1.start();
        mTh2.start();
        System.out.println(Thread.currentThread().getName() + "主线程运行结束!");
    }
}

结果:

main主线程运行开始!
main主线程运行结束!
A 线程运行开始!
子线程A运行 : 0
子线程A运行 : 1
B 线程运行开始!
子线程B运行 : 0
子线程B运行 : 1
A 线程运行结束!
B 线程运行结束!

从运行结果可以看出主线程比子线程早结束!

2)加join(只是修改了main方法里面的代码)

class Thread6 extends Thread {

    private String name;

    public Thread6(String name) {
        super(name);
        this.name = name;
    }

    public void run() {
        System.out.println(Thread.currentThread().getName() + " 线程运行开始!");
        for (int i = 0; i < 2; i++) {
            System.out.println("子线程" + name + "运行 : " + i);
            try {
                sleep((int) Math.random() * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " 线程运行结束!");
    }

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + "主线程运行开始!");
        Thread6 mTh1 = new Thread6("A");
        Thread6 mTh2 = new Thread6("B");
        mTh1.start();
        mTh2.start();
        try {
            mTh1.join();
            mTh2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "主线程运行结束!");
    }
}

结果:

main主线程运行开始!
A 线程运行开始!
子线程A运行 : 0
子线程A运行 : 1
A 线程运行结束!
B 线程运行开始!
子线程B运行 : 0
子线程B运行 : 1
B 线程运行结束!
main主线程运行结束!

从结果可以看到:主线程一定会等子线程都结束了才结束!

2.2 sleep()和yield()
2.2.1 yield()
yield()方法作用是暂停当前正在执行的线程对象,并执行其他线程。yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,线实际中无法保证yield()达到让步目的,因为让步的线程还有可能被程调度程序再次选中。结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

class Thread7 extends Thread {
    public Thread7(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println("" + this.getName() + "-----" + i); // 当i为30时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)
            if (i == 3) {
                this.yield();
            }
        }
    }

    public static void main(String[] args) {
        Thread7 yt1 = new Thread7("张三");
        Thread7 yt2 = new Thread7("李四");
        yt1.start();
        yt2.start();
    }
}

结果:

第一种情况:李四(线程)当执行到3时会CPU时间让掉,这时张三(线程)抢到CPU时间并执行。
第二种情况:李四(线程)当执行到3时会CPU时间让掉,这时李四(线程)抢到CPU时间并执行。

2.2.2 sleep()和yield()的区别

sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;
yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。

sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的;yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程。

另外,sleep 方法允许较低优先级的线程获得运行机会,但yield()方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得CPU占有权。在一个运行系统中,如果较高优先级的线程没有调用sleep方法,又没有受到I\O阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。

2.3 interrupt()

interrupt()是中断某个线程,这种结束方式比较粗暴,如果t线程打开了某个资源还没来得及关闭也就是run方法还没有执行完就强制结束线程,会导致资源无法关闭。

要想结束进程最好的办法就是在线程类里面用一个boolean型变量来控制run()方法什么时候结束,run()方法一结束,该线程也就结束了。

2.4 wait()

Obj.wait(),与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait与notify是针对已经获取了Obj锁进行操作,从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){…}语句块内。

从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。相应的notify()就是对对象锁的唤醒操作。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。

Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。

单单在概念上理解清楚了还不够,需要在实际的例子中进行测试才能更好的理解。对Object.wait(),Object.notify()的应用最经典的例子,应该是三线程打印ABC的问题了吧,这是一道比较经典的面试题。在我以前的博文中已经就此问题做了详细介绍,请看:Java线程锁(二)——循环打印ABC

2.5 wait和sleep区别
2.5.1 共同点

  • 他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。
  • wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException。如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep/join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。
  • 需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException 。

2.5.2 不同点

  • Thread类的方法:sleep(),yield()等
    Object的方法:wait(),notify(),notifyAll()等
  • 每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。 sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
  • wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用 。
  • sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常,所以sleep()和wait()方法的最大区别是:sleep()睡眠时,保持对象锁,仍然占有该锁;而wait()睡眠时,释放对象锁。
  • wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。

2.5.3 总结
(1)sleep()

  • sleep()使当前线程进入停滞状态(阻塞当前线程),让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会。
  • sleep()是Thread类的Static(静态)的方法;因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象的机锁并木有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。
  • 在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。

(2)wait()

  • wait()方法是Object类里的方法;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁);
  • 其他线程可以访问;wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常。

3、常见线程名词解释

主线程:JVM调用程序main()所产生的线程。

当前线程:这个是容易混淆的概念。一般指通过Thread.currentThread()来获取的进程。

后台线程:指为其他线程提供服务的线程,也称为守护线程。如JVM的垃圾回收线程就是一个后台线程。

前台线程:是指接受后台线程服务的线程,其实前台后台线程是联系在一起,就像傀儡和幕后操纵者一样的关系。傀儡是前台线程、幕后操纵者是后台线程。由前台线程创建的线程默认也是前台线程。可以通过isDaemon()和setDaemon()方法来判断和设置一个线程是否为后台线程。

用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束。

4、线程类的一些常用方法

sleep(): 强迫一个线程睡眠N毫秒。 
isAlive(): 判断一个线程是否存活。 
join(): 等待线程终止。 
activeCount(): 程序中活跃的线程数。 
enumerate(): 枚举程序中的线程。 
currentThread(): 得到当前线程。 
isDaemon(): 一个线程是否为守护线程。 
setDaemon(T/F): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束) 
setName(): 为线程设置一个名称。 
wait(): 强迫一个线程等待。 
notify(): 通知一个线程继续运行。 
setPriority(): 设置一个线程的优先级。

5、线程同步synchronized

5.1 synchronized关键字的作用域

(1)是某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法。

(2)是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。

(3)除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/区块/},它的作用域是当前对象。

(4)synchronized关键字是不能继承的。也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法。

5.2 总结
总的说来,synchronized关键字可以作为方法的修饰符,也可作为方法内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static方法和class literals(类名称字面常量)身上。

5.3 synchronized用法
5.3.1 在进一步阐述之前,我们需要明确几点:

  • 无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或方法当作锁,而且同步方法很可能还会被其他线程的对象访问。
  • 每个对象只有一个锁(lock)与之相关联。
  • 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

5.3.2 synchronized用到不同地方对代码产生的影响
(1)把synchronized当作方法修饰符时,示例代码如下:

Public synchronized void methodAAA(){
//….
}

这也就是同步方法,那这时synchronized锁定的是哪个对象呢?它锁定的是调用这个同步方法对象。也就是说,当一个对象P1在不同的线程中执行这个同步方法时,它们之间会形成互斥,达到同步的效果。但是这个对象所属的Class所产生的另一对象P2却可以任意调用这个被加了synchronized关键字的方法。

上边的示例代码等同于如下代码:

public void methodAAA(){
    synchronized (this){      //  (1)
           //…..
    }
}

(1)处的this指的是什么呢?它指的就是调用这个方法的对象,如P1。可见同步方法实质是将synchronized作用于object reference。――那个拿到了P1对象锁的线程,才可以调用P1的同步方法,而对P2而言,P1这个锁与它毫不相干,程序也可能在这种情形下摆脱同步机制的控制,造成数据混乱:

(2)同步块,示例代码如下:

public void method3(SomeObject so)     {
    synchronized(so){
        //…..
    }
}

这时,锁就是so这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁:

class Foo implements Runnable{
       private byte[] lock = new byte[0];  // 特殊的instance变量
       Public void methodA(){
           synchronized(lock) { //… }
      }
     //…..
}

注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。

(3)将synchronized作用于static 方法,示例代码如下:

Class Foo{
    public synchronized static void methodAAA() {  // 同步的static 方法
        //….
    }
    public void methodBBB(){
        synchronized(Foo.class)   //  class literal(类名称字面常量)
    }
}

代码中的methodBBB()方法是把class literal作为锁的情况,它和同步的static方法产生的效果是一样的,取得的锁很特别,是当前调用这个方法的对象所属的类(Class,而不再是由这个Class产生的某个具体对象了)。将 Foo.class和 P1.getClass()用于作同步锁还不一样,不能用P1.getClass()来达到锁这个Class的目的。P1指的是由Foo类产生的对象。

可以推断:如果一个类中定义了一个synchronized的static方法A,也定义了一个synchronized 的instance方法B,那么这个类的同一对象Obj在多线程中分别访问A和B两个方法时,不会构成同步,因为它们的锁都不一样。A方法的锁是Obj这个对象,而B的锁是Obj所属的那个Class。

6、线程数据传递方式
在传统的同步开发模式下,当我们调用一个方法时,通过这个方法的参数将数据传入,并通过这个方法的返回值来返回最终的计算结果。但在多线程的异步开发模式下,数据的传递和返回和同步开发模式有很大的区别。由于线程的运行和结束是不可预料的,因此,在传递和返回数据时就无法象方法一样通过方法参数和return语句来返回数据。

6.1 通过构造方法传递数据

在创建线程时,必须要建立一个Thread类的或其子类的实例。因此,我们不难想到在调用start方法之前通过线程类的构造方法将数据传入线程。并将传入的数据使用类变量保存起来,以便线程使用(其实就是在run方法中使用)。

public class Thread9 extends Thread {

    private String name;

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

    public void run() {
        System.out.println("hello " + name);
    }

    public static void main(String[] args) {
        Thread thread = new Thread9("world");
        thread.start();
    }
}

由于这种方法是在创建线程对象的同时传递数据的,因此,在线程运行之前这些数据就就已经到位了,这样就不会造成数据在线程运行后才传入的现象。如果要传递更复杂的数据,可以使用集合、类等数据结构。使用构造方法来传递数据虽然比较安全,但如果要传递的数据比较多时,就会造成很多不便。由于Java没有默认参数,要想实现类似默认参数的效果,就得使用重载,这样不但使构造方法本身过于复杂,又会使构造方法在数量上大增。因此,要想避免这种情况,就得通过类方法或类变量来传递数据。

6.2 通过变量和方法传递数据
向对象中传入数据一般有两次机会,第一次机会是在建立对象时通过构造方法将数据传入,另外一次机会就是在类中定义一系列的public的方法或变量(也可称之为字段)。然后在建立完对象后,通过对象实例逐个赋值。下面的代码是对MyThread1类的改版,使用了一个setName方法来设置 name变量:

public class Thread10 implements Runnable {

    private String name;

    public void setName(String name) {
        this.name = name;
    }

    public void run() {
        System.out.println("hello " + name);
    }

    public static void main(String[] args) {
        Thread10 myThread = new Thread10();
        myThread.setName("world");
        Thread thread = new Thread(myThread);
        thread.start();
    }
}

6.3 通过回调方法传递数据

上面讨论的两种向线程中传递数据的方法是最常用的。但这两种方法都是main方法中主动将数据传入线程类的。这对于线程来说,是被动接收这些数据的。然而,在有些应用中需要在线程运行的过程中动态地获取数据,如在下面代码的run方法中产生了3个随机数,然后通过Work类的process方法求这三个随机数的和,并通过Data类的value将结果返回。从这个例子可以看出,在返回value之前,必须要得到三个随机数。
也就是说,这个value是无法事先就传入线程类的。具体代码如下:

public class ForData extends Thread {

    public static void main(String[] args) {
        Thread thread = new ForData(new Work());
        thread.start();

    }

    @Override
    public void run() {
        java.util.Random random = new java.util.Random();
        Data data = new Data();
        int n1 = random.nextInt(1000);// 定义一个0到1000的随机整数
        int n2 = random.nextInt(2000);// 定义一个0到2000的随机整数
        int n3 = random.nextInt(3000);// 定义一个0到3000的随机整数
        int[] n = { n1, n2, n3 };
        work.process(data, n); // 使用回调方法
        System.out.println(String.valueOf(n1) + "+" + String.valueOf(n2) + "+"
                + String.valueOf(n3) + "=" + data.value);
    }

    class Data {
        public int value = 0;
    }

    static class Work {
        public void process(Data data, int[] numbers) {
            for (int n : numbers) {
                data.value += n;
            }
        }
    }

    private Work work;

    public ForData(Work work) {
        this.work = work;
    }
}
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回答: 你提供的引用内容中包含了一个Java程序的代码片段,该代码片段展示了如何使用多线程进行文件下载。这段代码中,首先创建了一个`MultiThreadDownload`对象,传入了下载路径、保存路径和线程数。然后调用`download()`方法开始下载文件。\[1\] 在代码的第二个引用中,根据文件的长度计算了每个线程下载的起始位置和结束位置。通过循环遍历线程数,计算每个线程的起始位置和结束位置,并进行相应的下载操作。\[2\] 在代码的第三个引用中,首先根据访问的URL路径创建了一个`HttpURLConnection`对象,然后调用`getContentLengthLong()`方法获取文件的字节大小。接着使用`RandomAccessFile`对象调用`setLength()`方法设置本地文件的长度,这个文件是一个空数据文件,通过多线程进行对`RandomAccessFile`对象的本地文件随机位置写入数据。最后关闭文件和断开连接。\[3\] 综上所述,这段代码展示了如何使用多线程进行Java文件的下载操作,并且通过设置文件的长度和使用`RandomAccessFile`对象实现了多线程写入数据的功能。 #### 引用[.reference_title] - *1* *2* *3* [【Java】网络编程——多线程下载文件](https://blog.csdn.net/qq_42470947/article/details/105889839)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值