多线程核心


在这里插入图片描述

Thread类的常用方法:

currentThread(), isAlive()判断当前线程是否存活。 sleep(long millis)方法
StackTraceElement[] getStackTrace() 返回一个表示该线程堆栈跟踪元素数组。
Static void dumpStack() 是将当前线程的堆栈跟踪信息输出至标准错误流;
Static Map<Thread,StackTraceElement[]> getAllStackTrace()
getId() 用于获取线程的唯一标识。

使正在运行的一个线程终止运行:

1.使用退出标志,结合return()方法使用;
2.使用stop()方法强行退出,不建议使用;
3.使用interrupt()方法中断线程,interrupt()方法不会终止一个正在运行的线程,还需要加入一个判断才可以完成线程的停止。
判断线程是否停止:interrupted()判断当前线程是否已经是中断状态
isInterrupted()判断线程Thread对象是否是中断状态;
先判断线程对象是否是中断状态,然后可以通过throw一个新的异常来是线程停止运行;
示例代码:

public class Interrupt extends Thread{
    @Override
    public void run() {
        super.run();
        try {
            for (int i = 0; i < 500000; i++) {
                //判断是否处于中断状态
                if (this.isInterrupted()){
                    System.out.println("发现中断标志,我要退出了");
                    //通过抛出一个异常结束当前线程
                    throw new Exception("不好意思,我真的退出啦!");
                }
                System.out.println("i=" + (i +1));
            }
            System.out.println("for后边的代码继续执行");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args){
        try {
            Interrupt thread = new Interrupt();
            thread.start();
            Thread.sleep(200);
            thread.interrupt();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

暂停线程:

使用suspend()方法暂停线程,使用resume()方法恢复线程的执行。

  • suspend()方法用于使线程不在执行任务,线程对象并不销毁;
  • 极易造成公共同步对象被独占
    wait(), notify(), notifyAll()
    yield()的作用是 放弃当前CPU资源,让其他任务占用CPU执行时间,放弃的时间不确定,有可能刚刚放弃,马上又获得CPU时间片。

线程优先级

setPriority(),设置线程的优先级;java中线程的优先级分为1~10级,如果优先级小于1或者大于10就throw new IllegalArgumentException()
线程的优先级具有继承性,A线程启动B线程,则B线程的优先级与A线程是一样的。
线程的优先级与代码执行顺序无关;
获取当前线程的优先级:Thread.currentThread().getPriority();
守护线程:随着主线程的销毁而自动销毁,通过setDaemon(true)的方法来设置,必须在start()方法之前执行,否则会出现异常;

线程安全

1、方法中的变量具有私有特性,不存在非线程安全问题

public class MainThread {
    public static void main(String[] args) {
        MyService myService = new MyService();
        new ThreadA(myService).start();
        new ThreadB(myService).start();
    }
}
class MyService{
    public void add(String userName){
        try {
            int num = 0;
            if (userName.equals("a")){
                num = 100;
                System.out.println("a set over");
                Thread.sleep(2000);
            }else {
                num = 200;
                System.out.println("b set over");
            }
            System.out.println(userName + " num " + num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ThreadA extends Thread{
    private MyService myService;
    public ThreadA(MyService myService){
        this.myService = myService;
    }

    @Override
    public void run() {
        super.run();
        myService.add("a");
    }
}
class ThreadB extends Thread{
    private MyService myService;
    public ThreadB(MyService myService){
        this.myService = myService;
    }

    @Override
    public void run() {
        super.run();
        myService.add("b");
    }
}

在这里插入图片描述
2、如果多个变量共同访问同一个对象中的实例变量,则有可能出现非线程安全问题。
线程安全问题:指对同一资源或者同一数据进行并发操作时。
将上边Myservice的代码修改为:

class MyService{
    private int num  = 0;
    public void add(String userName){
        try {
            if (userName.equals("a")){
                num = 100;
                System.out.println("a set over");
                Thread.sleep(2000);
            }else {
                num = 200;
                System.out.println("b set over");
            }
            System.out.println(userName + " num " + num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

Synchronized锁

3、同步方法
将上边的Myservice修改为:

class MyService{
    private int num  = 0;
    public synchronized void add(String userName){
        try {
            if (userName.equals("a")){
                num = 100;
                System.out.println("a set over");
                Thread.sleep(2000);
            }else {
                num = 200;
                System.out.println("b set over");
            }
            System.out.println(userName + " num " + num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述
synchronized在字节码指令中的原理,同步方法是使用了flag标记ACC_SYNCHRONIZED,当调用方法时,指令会检查方法的ACC_SYNCHRONIZED访问标志是否设置,如果设置了,执行线程先持有同步锁然后执行方法,最后完成任务后释放锁。
使用synchronized同步代码块:monitorenter和monitorexit指令进行不同处理。

4、多个线程对不同业务实例对象进行访问的时候不存在线程安全问题,因为持的锁不是同一把锁。
将上边MainThread修改如下:

public class MainThread {
    public static void main(String[] args) {
        MyService myService1 = new MyService();
        MyService myService2 = new MyService();
        new ThreadA(myService1).start();
        new ThreadB(myService2).start();
    }
}

在这里插入图片描述
创建了两个实例对象,相当于创建了两把不同的锁,每个线程执行自己业务对象中的同步方法,不存在线程安全问题。

5、多个线程执行同一个业务对象中的不同同步方法时,是按顺序同步的方式调用的。(因为锁对象是一样的)

public class MainThread {
    public static void main(String[] args) {
        MyService myService = new MyService();
        new ThreadA(myService).start();
        new ThreadB(myService).start();
    }
}
class MyService{
    private int num  = 0;
    public synchronized void add(String userName){
        try {
            if (userName.equals("a")){
                num = 100;
                System.out.println("a set over");
                Thread.sleep(2000);
            }else {
                num = 200;
                System.out.println("b set over");
            }
            System.out.println(userName + " num " + num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public synchronized void methodB(){
        System.out.println("b method开始执行了");
    }
}

class ThreadA extends Thread{
    private MyService myService;
    public ThreadA(MyService myService){
        this.myService = myService;
    }

    @Override
    public void run() {
        super.run();
        myService.add("a");
    }
}
class ThreadB extends Thread{
    private MyService myService;
    public ThreadB(MyService myService){
        this.myService = myService;
    }

    @Override
    public void run() {
        super.run();
        myService.methodB();
    }
}

多个线程调用同一个对象中的不同名称的synchronized同步方法或synchronized(this)同步代码块时,调用的效果是按顺序执行,即同步的。

6、synchronized可重入锁:一个线程得到一个对象锁后,再次请求此对象锁时还是可以得到该对象锁的;也就是synchronized方法和代码块的内部调用本类的其他synchronized方法和代码块时,是可以获得到锁的。前提是锁的对象要相同。

public class MainThread {
    public static void main(String[] args) {
        MyService myService = new MyService();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                myService.methodA();
            }
        };
        new Thread(runnable).start();
    }
}
class MyService{
   public synchronized void methodA(){
       System.out.println("methodA");
       methodB();
   }
    public synchronized void methodB(){
        System.out.println("methodB");
        methodC();
    }
    public synchronized void methodC(){
        System.out.println("methodC");
    }
}

重入锁支持继承的环境:比如子类继承父类,子类的一个同步方法为A,在A方法里边调用父类的同步方法B是没有问题的;

public class MainThread {
    public static void main(String[] args) {
        MyService myService = new MyService();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                myService.methodA();
            }
        };
        new Thread(runnable).start();
    }
}
class MyService extends ParentService{
   public synchronized void methodA(){
       System.out.println("methodA");
       super.methodA();
   }
}
class ParentService{
    public synchronized void methodA(){
        System.out.println("这是父类的一个方法");
    }
}

在这里插入图片描述

当出现异常时,锁自动释放,但是线程调用了suspend()和sleep()方法后,锁不会被释放;

子类继承父类中的同步方法,如果子类不使用synchronized关键字就是非同步方法,使用后就变成了同步方法。

synchronized同步代码块

synchronized同步方法是将当前对象作为锁,而同步代码块是将任意对象作为锁。
使用同步代码块锁非this对象,则synchronized(非this)代码块中的程序与同步方法是异步的,因为有两把不同的锁。
同步方法放在非同步方法中进行声明,并不能保证调用方法的线程执行同步。
synchronized关键字加到static静态方法上的方式是将Class类对象作为锁,而加到非静态方法上是将当前所在类的对象作为锁。而Class类是单例的。这就造成Class锁可以对所有的对象实例起作用了。
一般情况下是不会将String作为锁对象的,因为String有常量缓存池这种东西,避免两个线程拿到的锁是同一把锁。
死锁:不同的线程都在等待根本不可能被释放的锁,互相等待对方释放锁,就有可能出现死锁的情况。
Volatile关键字:
可见性:一个线程能看到另一个线程被修改的值。
原子性:volatile i++操作时是非原子的
禁止指令重排序

线程间的通信

wait()、notify()机制

wait()方法是Object类的方法,使当前线程暂停执行,并释放锁,进入等待状态,直到接到通知或中断为止。
notify()方法唤醒调用了wait()方法的线程,使其进入就绪状态,并且尝试重新获取锁。notify()方法执行后并不会立即释放锁,要等待当前线程的任务执行完,才会释放。
wait(),notify()方法只能在同步方法或同步代码块中调用,且必须拥有相同的锁,也就是它们的锁对象必须是相同的。如果不是在同步方法或同步代码块中调用wait(),notify()方法,则会抛出java.lang.IllegalMonitorStateException异常。

public class MainThread {
    public static void main(String[] args) {
        try {
            //线程1和线程2持有相同的锁对象
            Object lock = new Object();
            Thread1 thread1 = new Thread1(lock);
            thread1.start();
            Thread.sleep(3000);
            Thread2 thread2 = new Thread2(lock);
            thread2.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
 class Thread1 extends Thread{
    private Object lock;
    public Thread1(Object lock){
        super();
        this.lock = lock;
    }

     @Override
     public void run() {
        synchronized (lock){
            try {
                System.out.println("开始等待时间" + System.currentTimeMillis());
                lock.wait();
                System.out.println("结束等待时间" + System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
     }
 }
class Thread2 extends Thread{
    private Object lock;
    public Thread2(Object lock){
        super();
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            System.out.println("开始唤醒时间" + System.currentTimeMillis());
            lock.notify();
            System.out.println("结束唤醒时间" + System.currentTimeMillis());
        }
    }
}

在这里插入图片描述
把业务代码封装进业务层

public class MainThread {
    public static void main(String[] args) {
        try {
            //线程1和线程2持有相同的锁对象
            MyService myService = new MyService();
            Thread1 thread1 = new Thread1(myService);
            thread1.start();
            Thread.sleep(3000);
            Thread2 thread2 = new Thread2(myService);
            thread2.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 class Thread1 extends Thread{
    private MyService myService;
     public Thread1(MyService myService){
        super();
        this.myService = myService;

    }
     @Override
     public void run() {
         myService.waitMethod();
     }
 }
class Thread2 extends Thread{
    private MyService myService;
    public Thread2(MyService myService){
        super();
        this.myService = myService;
    }
    @Override
    public void run() {
        myService.notifyMethod();
    }
}

class MyService{
    private Object lock = new Object();
    //等待方法
    public void waitMethod(){
        synchronized (lock){
            try {
                System.out.println("开始等待时间" + System.currentTimeMillis());
                lock.wait();
                System.out.println("结束等待时间" + System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    //唤醒方法
    public void notifyMethod(){
        synchronized (lock){
            System.out.println("开始唤醒时间" + System.currentTimeMillis());
            lock.notify();
            System.out.println("结束唤醒时间" + System.currentTimeMillis());
        }
    }
}
  • 当线程调用了wait()方法后,在对该线程对象执行interrupt()方法就会出现Interrupted-Exception异常。
  • 每调用一次notify()方法,只能唤醒一个线程,唤醒的顺序与执行wait()方法的顺序一致。
  • notifyAll()方法唤醒全部的线程,按照执行wait()方法的倒序依次对其他线程进行唤醒。

join()方法的使用

应用场景:主线程创建并启动子线程,如果子线程要进行大量的耗时运算,主线程往往将早于子线程结束,我们预期的是想主线程等待子线程执行完毕之后再结束。

public class MainThread {
    public static void main(String[] args) {
        JoinThread joinThread = new JoinThread();
        joinThread.start();
        System.out.println("主线程:我想等待joinThread线程执行完在执行");
    }
}
class JoinThread extends Thread{
    @Override
    public void run() {
        try {
            int secondValue = (int) (Math.random() * 10000);
            System.out.println("子线程:我执行需要耗时" + secondValue);
            Thread.sleep(secondValue);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

执行结果:
在这里插入图片描述
join()方法的作用就是使所属线程对象a正常执行完任务,而使当前线程b无限期阻塞,等待线程a销毁后再继续执行线程b的任务。可以理解为,有两个a,b线程,在b线程里边创建并启动a线程,那么b线程可以理解成一个主线程,a线程则为子线程,如果a线程的执行耗时比b线程要多,那么往往会b先执行完在执行a线程,加上a.join()方法后,则会使a线程先执行完,在执行b线程。

public class MainThread {
    public static void main(String[] args) {
         try {
            JoinThread joinThread = new JoinThread();
            joinThread.start();
            joinThread.join();
            System.out.println("我想等待joinThread线程执行完在执行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
class JoinThread extends Thread{
    @Override
    public void run() {
        try {
            int secondValue = (int) (Math.random() * 10000);
            System.out.println("子线程:我执行需要耗时" + secondValue);
            Thread.sleep(secondValue);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述
join()方法和sychronized的区别是,join()方法内部使用wait()方法进行等待,而sychronized关键字使用锁作为同步。
源码:

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }
  • join()方法使用过程中,当前线程不能被中断,不能和interrupt()方法一起使用,否则会抛java.lang.InterruptedException异常。
  • join(long)方法,等待一定时间,不管子线程是否执行完毕,时间到了当前线程将重新获取锁,继续往后执行。

类ThreadLocal的使用

类ThreadLocal主要将数据存入当前线程对象中的Map中,也就是ThreadLocalMap对象,Map中的key存储的是当前线程的对象也就是ThreadLocal对象,value存储的值,只对当前线程可见,其他线程不可以访问当前线程对象中map的值。随线程销毁,map随之销毁,map中的数据如果没有被引用则随时GC回收。
在这里插入图片描述

public class MainThread {
    public static void main(String[] args) {
        ThreadLocal local = new ThreadLocal();
        local.set("2324");
        System.out.println(local.get());
    }
}

源码解析:
ThreadLocal的set方法

ThreadLocal.ThreadLocalMap threadLocals = null;
 public void set(T value) {
        //当前线程对象
        Thread t = Thread.currentThread();
        //从当前线程中获取当前线程的Map对象
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else //第一次调用set方法时,执行createMap
            createMap(t, value);
    }

   ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
  void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

验证ThreadLocal的隔离性,以及ThreadLocal不能实现值继承:

public class MainThread {
    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        Thread2 thread2 = new Thread2();
        thread1.start();
        thread2.start();
        System.out.println("这是main线程的变量:" + ThreadLocalTest.local.get());
    }
}

class ThreadLocalTest{
    public static ThreadLocal local = new ThreadLocal();
}
class Thread1 extends Thread{
    @Override
    public void run() {
        ThreadLocalTest.local.set("这是thread1设置的变量");
        System.out.println(ThreadLocalTest.local.get());
    }
}
class Thread2 extends Thread{
    @Override
    public void run() {
        ThreadLocalTest.local.set("这是thread2设置的变量");
        System.out.println(ThreadLocalTest.local.get());
    }
}

在这里插入图片描述
在第一次调用ThreadLocal类的get()方法时,返回值是null,如果想解决第一次调用不反悔null,可以通过继承ThreadLocal类重写initialValue()方法来实现。

public class MainThread {
    public static ThreadLocalTest threadLocalTest = new ThreadLocalTest();
    public static void main(String[] args) {
        System.out.println(threadLocalTest.get());
    }
}
class ThreadLocalTest extends ThreadLocal{
    @Override
    protected Object initialValue() {
        return "返回默认值";
    }
}

Lock对象的使用

ReentrantLock锁

public class MainThread {
    public static void main(String[] args) {
        MyService myService = new MyService();
        //开启两个线程
        new Thread1(myService).start();
        new Thread1(myService).start();
    }
}
class MyService{
    private Lock lock = new ReentrantLock();
    public void testMethod(){
        //同步代码块,开启锁
        lock.lock();
        //执行业务代码
        for (int i = 0; i < 5; i++) {
            System.out.println("ThreadName" + Thread.currentThread().getName() +"  " + (i + 1));
        }
        //需要手动释放锁
        lock.unlock();
    }
}

class Thread1 extends Thread{
    private MyService myService;
    public Thread1(MyService myService){
        super();
        this.myService = myService;
    }

    @Override
    public void run() {
        myService.testMethod();
    }
}

没开锁之前:
在这里插入图片描述
开锁之后实现了同步:
在这里插入图片描述

公平锁与非公平锁

公平锁:
采用先到先得的策略,每次获取锁之前都会检查队列里边有没有排队等待的线程,有的话就将当前线程追加到队列里,没有才尝试获取锁。

Lock lock = new ReentrantLock(true);

非公平锁:
一个线程获取锁之前先去尝试获取锁,而不是在队列里等待,有可能后到的线程也可以获取到锁。

Lock lock = new ReentrantLock(false);

源码:

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

ReentrantLock锁的常用方法:
public int getHoldCount()方法:查询当前线程调用lock()方法的次数,调用lock()方法进行锁重入导致count计数加1,执行unlock()方法使count呈减1的效果。

public class MainThread {
    public static void main(String[] args) {
        MyService myService = new MyService();
        new Thread1(myService).start();
    }
}
class MyService{
    ReentrantLock lock = new ReentrantLock();
    public void testMethod(){
        System.out.println(lock.getHoldCount());
        //同步代码块,开启锁
        lock.lock();
        System.out.println(lock.getHoldCount());
        //需要手动释放锁
        lock.unlock();
        System.out.println(lock.getHoldCount());
    }
}

class Thread1 extends Thread{
    private MyService myService;
    public Thread1(MyService myService){
        super();
        this.myService = myService;
    }

    @Override
    public void run() {
        myService.testMethod();
    }
}

public final boolean isFair()
判断是不是公平锁。

ReentrantLock lock = new ReentrantLock(true)
System.out.println(lock.isFair()));  //true
ReentrantLock lock = new ReentrantLock(false)
System.out.println(lock.isFair()));  //false

public boolean isLocked()
查询此锁是否由任意线程保持,并没有释放。

public boolean tryLock()
嗅探拿锁,如果当前线程发现锁被其他线程持有,则返回false,程序继续执行后边的代码,而不是呈阻塞等待锁的状态。

public class MainThread {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                new MyService().testMethod();
            }
        };
        Thread threadA = new Thread(runnable);
        threadA.setName("A线程");
        threadA.start();
        Thread threadB = new Thread(runnable);
        threadB.setName("B线程");
        threadB.start();
    }
}
class MyService{
    ReentrantLock lock = new ReentrantLock();
    public void testMethod(){
        if (lock.tryLock()){
            System.out.println(Thread.currentThread().getName() + "获取到了锁");
        }else {
            System.out.println(Thread.currentThread().getName() + "没有获取到锁");
        }
    }
}

ReentrantReadWriteLock锁

ReentrantLock锁主要缺点就是所有操作都是同步的,哪怕只是对实例变量进行读操作,会降低运行效率。
读写锁有两个锁,一个是读操作相关的锁,也称共享锁,readLock;另一个是写操作相关的锁,也称排他锁,writeLock;
读锁不互斥,读锁和写锁互斥,写锁和写锁互斥,也就是只要出现写锁,就会出现互斥同步的效果。

读读共享

public class MainThread {
    public static void main(String[] args) {
       MyService myService = new MyService();
       Runnable runnable =  new Runnable() {
            @Override
            public void run() {
                myService.testMethod();
            }
        };
        //这样开启的锁不是同一把锁。
        //Runnable runnable =  new Runnable() {
//            @Override
//            public void run() {
//                MyService myService = new MyService();
//                myService.testMethod();
//            }
//        };
       //开启两个线程
        Thread threadA = new Thread(runnable);
        threadA.setName("A线程");
        threadA.start();
        Thread threadB = new Thread(runnable);
        threadB.setName("B线程");
        threadB.start();
    }
}
class MyService{
    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    public void testMethod(){
        try {
            //开启读锁
            readWriteLock.readLock().lock();
            System.out.println("begin" + Thread.currentThread().getName() + System.currentTimeMillis());
            Thread.sleep(3000);
            System.out.println("end" + Thread.currentThread().getName() + System.currentTimeMillis());
            readWriteLock.readLock().unlock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果:
beginA线程1623898098906
beginB线程1623898098906
endB线程1623898101914
endA线程1623898101914

写写互斥:

public class MainThread {
    public static void main(String[] args) {
        MyService myService = new MyService();
       Runnable runnable =  new Runnable() {
            @Override
            public void run() {
                myService.testMethod();
            }
        };
       //开启两个线程
        Thread threadA = new Thread(runnable);
        threadA.setName("A线程");
        threadA.start();
        Thread threadB = new Thread(runnable);
        threadB.setName("B线程");
        threadB.start();
    }
}
class MyService{
    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    public void testMethod(){
        try {
            //开启读锁
            readWriteLock.writeLock().lock();
            System.out.println("begin" + Thread.currentThread().getName() + System.currentTimeMillis());
            Thread.sleep(3000);
            System.out.println("end" + Thread.currentThread().getName() + System.currentTimeMillis());
            readWriteLock.writeLock().unlock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果:
beginB线程1623898802458
endB线程1623898805467
beginA线程1623898805467
endA线程1623898808476

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值