JUC并发编程(详细,稳妥)

一、相关包:

java.util.concurrent
java.util.concurrent.atomic
java.util.concurrent.locks
java.util.function

二、创建一个线程的方式:

Thread、Runable、Callable、ThreadPoolExecutor

详情资料:传送门.

三、进程与线程:

1、进程是一段正在执行的程序,是资源分配的基本单元,而线程是CPU调度的基本单元。
2、进程间相互独立进程,进程之间不能共享资源,一个进程至少有一个线程,同一进程的各线程共享整个进程的资源(寄存器、堆栈、上下文)。
3、线程的创建和切换开销比进程小

进程:进程是一段正在执行的程序,是资源分配的基本单元,可包含多个线程。进程间相互独立,进程之间不能共享资源,同一进程的各线程共享整个进程的资源(寄存器、堆栈、上下文)。如:QQ.exe
线程:线程是CPU调度的基本单元。
java默认有两个线程,main、GC

四、线程的并行、并发

并发编程的目的:充分利用CPU资源。
并行:CPU多核,多个线程同时执行;线程池
并发:多个线程操作同一个资源,cpu一核时可模拟出多线程,快速交替。

五、线程的状态

public enum State {
        //新建
        NEW,
        
        //运行
        RUNNABLE,

        //阻塞
        BLOCKED,

        //等待,一直等,等到天荒地老
        WAITING,

        //超时等待,过期不候
        TIMED_WAITING,

        //结束
        TERMINATED;
    }

六、wait\sleep:

1.来源不同
wait:Object类
sleep:Thread类
2.释不释放锁不同
wait 释放锁
sleep 睡着了 ,就不会释放锁
3.使用位置不同
sleep 可以在任何地方睡觉
wait 必须在同步代码块中
4.是否需要捕获异常
都需要抛异常
在这里插入图片描述

七、synchrionized锁:

使用位置:三处——>实例方法、静态方法、同步代码块

public class Demo001 {
    public static void main(String[] args) throws InterruptedException {
        Ticket aa =  new Ticket();
        new Thread(()-> {for(int a =0;a<20;a--){aa.sell();}},"A").start();
        new Thread(()-> {for(int a =0;a<20;a--){aa.sell();}},"B").start();
        new Thread(()-> {for(int a =0;a<20;a--){aa.sell();}},"C").start();
    }
}

class  Ticket {
    private int num =50;
    public synchronized void sell() {
        if(num>0){
        num--;
        System.out.println(num);
    }}
}

八、LOCK:

public class Demo001 {
    public static void main(String[] args) throws InterruptedException {
        Ticket aa =  new Ticket();

        new Thread(()-> {for(int a =0;a<20;a--)aa.sell();},"A").start();
        new Thread(()-> {for(int a =0;a<20;a--)aa.sell();},"B").start();
        new Thread(()-> {for(int a =0;a<20;a--)aa.sell();},"C").start();

    }
}

class   Ticket {
    private int num =50;
    Lock lock = new ReentrantLock(false);
    public  void sell() {
        lock.lock();
        try {
            if(num>0){
            num--;
            System.out.println(num);
        }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

九、synchronized与LOCK区别:

总述:lock比synchronized更灵活,功能更丰富

  • synchronized是关键字,lock是Lock接口,常见实现类ReentrantLock等
  • synchronized自动释放锁,lock需要手动释放锁 (不释放死锁)
  • synchronized如果线程1阻塞,线程2会一直等待,lock可以不必一直等待 (tryLock())
  • synchronized是可重入锁,不可中断,非公平锁;lock是可重入锁,可中断(lockInterruptibly),默认为非公平锁,但可以改为公平锁 (初始化是加参数fair)
  • synchronized不可判断锁的状态,lock可以判断是否获得了锁

在这里插入图片描述

十、synchronized的生产者与消费者的实现虚假唤醒问题:

if只能两个线程,两个以上用while;if(){等待、业务、通知}与while(){等待、业务、通知}

实现:wait(),notifyAll() 等待、业务、通知 为什么说是synchromized的消费者问题,是因为wait\notify必须用在synchronized代码块内
问题:传统的生产者和消费者模式,wait(),notifyAll()的方式实现,但只适用与两个线程,如果存在两个以上的线程,会出现虚假唤醒的问题。
原因:notifyAll()会唤醒所有的线程,即使if()条件不满足也不在等待。(都会尝试去获取锁并运行,即使if()中的条件不再成立,也不会再次处于等待状态)
解决方案:将if换为while,即使线程被虚假唤醒,也会再次判断条件是否成立,不成立则再次进入等待状态。

在这里插入图片描述

十一、Lock版的生产与消费者问题:

Lock:取代synchronized
Condition:取代监听,通过awaitsignal取代synchrionized的wait与notifyAll,并且精准唤醒
ReadWriteLock
在这里插入图片描述

class   Ticket {
    private int num =50;
    Lock lock = new ReentrantLock(false);
    Condition condition = lock.newCondition();
    public  void sell() throws InterruptedException {
        condition.await();
        condition.signalAll();

十二、Condition的精准唤醒通知

    Lock lock = new ReentrantLock(false);
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    public  void sell() throws InterruptedException {
        while(num!=1){
            condition1.await();
        }
        num=2;
        condition2.signal();

十三、八锁现象:

锁是什么?锁的是什么东西?

8锁:就是关于锁的8个问题;
1.两个同步方法,一个对象,谁先?执行synchrionized加方法上,锁的是方法的调用者,两个方法用同一个锁,谁先拿到锁,谁先执行
2.两个同步方法,一个对象,在第二个方法内sleep5秒,谁先?执行synchrionized加方法上,锁的是方法的调用者,两个方法用同一个锁,谁先拿到锁,谁先执行
3.增加一个非同步普通方法,谁先执行?普通方法,不加锁,不受锁的影响
4.两个对象,锁的对象不同,相互不影响
5.两个静态方法,只有一个对象,锁类,谁先拿到谁先执行
6.两个静态方法,两个对象,锁类,谁先拿到谁先执行
7.一个静态方法,一个同步方法,一个对象,锁的对象不同,互不影响
8.一个静态方法,一个同步方法,两个对象,锁的对象不同,互不影响

总结:new 锁的是this 对象 , static 锁的是Class模板

十四、集合不安全——List\ArrayList

ArrayList多线程下不安全,会出现java.util.ConcurrentModificationException
解决方案:

  • List list = new Vector<>(); //Vector加了Synchrionized
  • List list = Collections.synchronizedList(new ArrayList<>()); //使用Collentions工具类
  • List list = new CopyOnWriteArrayList(); //底层使用Arrays.copyOf+Lock锁实现

十五、集合不安全——set\HashSet

HashSet多线程下也不安全,会出现java.util.ConcurrentModificationException
解决方案:

  • Set set = Collections.synchronizedSet(new HashSet<>()); //
  • Set set2 = new CopyOnWriteArraySet();

题外话:HashSet是什么?其实就是一个Map的key,无法重复,无序

十六、集合不安全——map/HashMap

什么初始化容量、加载因子?
加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。如果当哈希表中的条目数超
出了加载因子与当前容量的乘积时,则要对该哈希表进行rehash()操作,从而哈希表将具有大约
两倍的数

HashMap默认初始化容量为16,加载因子为0.75,此时最大容量12,大于12就会两倍扩容。
Map map = new HashMap();等同于
Map map = new HashMap(16, (float) 0.75);

HashMap多线程下也不安全,会出现java.util.ConcurrentModificationException
解决方案:

  • Map map2 = Collections.synchronizedMap(new HashMap<>());
  • Map map3 = new ConcurrentHashMap();

ConcurrentHashMap原理:分段锁 :传送门//todo.

十六+、Callable接口(很简单):

与Runnable相比
1.Callable可以有返回值,
2.可以抛出异常,
3.方法是call()方法。

在这里插入图片描述Callable使用:
FutureTask 实现了 RunnableFuture ,RunnableFuture 继承 Runnable;
FutureTask 里面可以传Callable
在这里插入图片描述Callable返回值:

public class CallableTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        AAA aa = new AAA();
        FutureTask<String> stringFutureTask = new FutureTask<>(aa);
        new Thread(stringFutureTask).start();
        String s = stringFutureTask.get();
        System.out.println(s);
    }
}

class AAA implements Callable<String>{
    @Override
    public String call(){
        return "必将 再次 伟大 !";
    }
}

Callable缓存:

1.Callable结果会有缓存
2.返回值可能会阻塞,可添加异步来解决阻塞问题。

十七、线程常用的三个辅助类:

CountDownLatch类:出完关门
一个减法计数器,通过线程调用countDown(); await()进行等待,归零时继续往后走。

        CountDownLatch  ss = new CountDownLatch(6);
        for (int i=0;i<6;i++){
            int finalI = i;
            new Thread(()->{
                System.out.println(finalI);
                ss.countDown();
            }).start();
        }
        ss.await();
        System.out.println("OVER");

CyclicBarrier类 集齐:
在这里插入图片描述
Semaphore类:

semaphore.acquire();
semaphore.release();
在这里插入图片描述在这里插入图片描述

十八、ReadWriteLock:

加上写锁时,多线程进不可并发,读写也不可
读读:可共存(共享锁,读锁,读锁是为锁住写锁)
读写:不可共存
写写:不可共存(独占锁,写锁)

在这里插入图片描述

十九、阻塞队列BlockingQueue:

FIFO:
阻塞发生情况:

  • 队列满了,存操作阻塞
  • 队列空了,取操作阻塞,等待生产

在这里插入图片描述

二十、BlockingQueue的四组API:

实现类:ArrayBlockingQueue类
在这里插入图片描述

  • 阻塞——抛出异常: //add满后再add会报错,remove空后再remove会报错
    在这里插入图片描述
  • 阻塞——返回值: //offer,offer满后再offer会返回false;poll完后在poll会返回null

在这里插入图片描述- 阻塞——一直等待: //put,put满后再put会一直等待;take完后在take会一直等待
在这里插入图片描述

  • 阻塞——超时等待: //会等一段时间,超时了会返回结果

在这里插入图片描述

二十一、SynchronousQueue同步队列:

只能存一个值
在这里插入图片描述

二十一+、线程池(重点):

池化技术:

线程池使用阻塞队列实现:

优化资源的使用(事先准备好一些资源,用的时候直接使用,不用现创建,销毁,创建与销毁很浪费资源)。线程池、数据库连接池、内存池、对象池
线程池的好处:

  • 核心线程使用完毕不会被销毁,可复用,降低资源消耗。
  • 事先创建好线程用的时候直接用,提高响应速度。
  • 管理线程,如控制核心线程数、最大线程数、等待时间单位及大小、阻塞队列大小,决绝策略等,方便管理。

线程池有三大方法、七大参数、四种拒绝方式
Executors工具类、ThreadPoolExecutor:

在这里插入图片描述

三大方法:

    public static void main(String[] args) {
           ExecutorService executorService1 = Executors.newSingleThreadExecutor(); //单一线程
           ExecutorService executorService2 = Executors.newFixedThreadPool(5); //固定线程
           ExecutorService executorService3 = Executors.newCachedThreadPool(); //火力全开
           //new Thread(()->{}).start();
        try {
            for(int i=0;i<100;i++){
            //使用exexute调用线程执行
                executorService3.execute(()->{
                    System.out.println(Thread.currentThread().getName());
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
        //自己创建的线程池用完需要关闭线程池,但是公用的线程池不应关闭(一般来说应该有一个ioc应该维护一个公用的线程池)
            executorService3.shutdown();
        }
    }

七大参数:

阻塞队列数+核心线程数 不够时 启动备用线程
在这里插入图片描述在这里插入图片描述

四种拒绝策略:

在这里插入图片描述
请求线程数>核心线程数——>放入阻塞队列
请求线程数>核心线程数+阻塞队列大小——>启用备用线程
请求线程数>最大线程数+阻塞队列大小——>触发拒绝策略

  1. AbortPolicy会抛出异常
  2. CallerRunsPolicy哪里来的回哪里,让原来的线程处理
  3. DiscardPolicy丢弃线程,默默舍弃
  4. DiscardOldestPolicy尝试与最早的请求竞争,不一定会成功

二十二、CPU密集型与IO密集型(线程池调优):

线程可以有N个,但是并行的线程最大为cpu数
CPU密集型:即最大线程数==cpu数量

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor
                (       2,
                        Runtime.getRuntime().availableProcessors(),
                        1,
                        TimeUnit.MINUTES,
                        new LinkedBlockingQueue<Runnable>(3),
                        Executors.defaultThreadFactory(),
                        new ThreadPoolExecutor.DiscardPolicy());

IO密集型:大于程序中很消耗IO的线程数,一般是2倍。

主要使用场景:
多进程适合在CPU 密集型操作(cpu 操作指令比较多,如位数多的浮点运算)。
多线程适合在IO 密集型操作(读写数据操作较多的,比如爬虫等脚本语言)。

二十三、四大函数式接口

jdk1.5传统程序员:泛型、枚举、单例、反射
jdk1.8新时代程序员:函数式接口、响应式(链式)编程、lambda表达式、Stream流式计算

函数式接口:只有一个方法的接口,上面有注解@FunctionalInterface标注。如:Runnable (JUF:优势:简化编程模型,在新型的架构中大量使用)

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

1.函数型接口Function:

函数型接口,有一个输入参数,一个输出参数,只要是函数式接口均可以用lambda表达式简化;

    public static void main(String[] args) {

/*        Function function = new Function<String,String>(){
            @Override
            public String apply(String s) {
                return "abc";
            }
        };*/
        Function function = (a)->{return "abc";};
        System.out.println(function.apply("333"));
    }

2.断言型接口Predicate:

断言型接口,输入参数,输出boolean;

public static void main(String[] args) {

/*    Predicate predicate = new Predicate<String>() {
        @Override
        public boolean test(String o) {
            return false;
        }
    };*/

    Predicate predicate = (o)->{return false;};
    System.out.println(predicate.test("666"));
}

3.消费型接口Consumer:

消费型接口,只传入参数,不返回值;

    public static void main(String[] args) {
/*        Consumer consumer = new Consumer<String>() {
            @Override
            public void accept(String o) {
                System.out.println(o);
            }
        };*/
        Consumer consumer = (o)->{System.out.println(o);};
        consumer.accept("qwe");
    }

4.供给型接口Supplier:

供给型接口,不传参数,只返回值;

    public static void main(String[] args) {
/*        Supplier<Object> objectSupplier = new Supplier(){
            @Override
            public String get() {
                return "7890";
            }
        };*/
        Supplier<Object> objectSupplier = ()->{return "666";};
        System.out.println(objectSupplier.get());
    }

二十四、Stream流式计算

注==:lambda表达式如果只有一行,{}可以省略,此时如果{}省略,return与;也必须省略。
在这里插入图片描述

二十五、ForkJoin

JUC下:ForkJoin java1.7,并行执行任务!提高效率,大数据。
Map Reduce (把大任务拆分成小任务)
如何使用:
1.先写一个计算类,计算类需要继承ForkJoinPool类(子类RecursiveTask)
2.通过forkjoinpool.execute(ForkJoinTask task)执行
在这里插入图片描述
在这里插入图片描述
执行:
在这里插入图片描述
stream并行流计算:
在这里插入图片描述

二十六、异步回调

类似springBoot的@Async注解==
无返回值:
在这里插入图片描述

有返回值:

在这里插入图片描述在这里插入图片描述

    @GetMapping("/selectTemplateDetailList999")
    public R selectTemplateDetailList999() throws ExecutionException, InterruptedException {

        Date date0 = new Date();
        long time = date0.getTime();
        CompletableFuture<Integer> integerCompletableFuture001 = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("001跑了1秒!");
            return 1;
        }).exceptionally(e->{return 0;});


        CompletableFuture<Integer> integerCompletableFuture002 = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("002跑了5秒!");
            return 2;
        }).exceptionally(e->{return 0;});



        CompletableFuture<Integer> integerCompletableFuture003 = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("003跑了10秒!");
            return 3;
        }).exceptionally(e->{return 0;});


        CompletableFuture<Void> voidCompletableFuture = CompletableFuture.allOf(integerCompletableFuture001, integerCompletableFuture002, integerCompletableFuture003);
        voidCompletableFuture.join();
        //integerCompletableFuture.
        Integer integer1 = integerCompletableFuture001.get();
        Integer integer2 = integerCompletableFuture002.get();
        Integer integer3 = integerCompletableFuture003.get();
        System.out.println(integer1);
        System.out.println(integer2);
        System.out.println(integer3);

        Date date1 = new Date();
        System.out.println("跑了10秒hou1 !"+(date1.getTime()-date0.getTime()));

        return R.success("seccess");
    }

二十七、理解JMM

	**Java内存模型**(Java Memory Model,JMM),用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果,JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的`共享变量`的值,以及在必须时如何同步的访问共享变量。

在这里插入图片描述

一、jvm主内存与工作内存

    首先,JVM将内存组织为主内存和工作内存两个部分。

    主内存主要包括本地方法区和堆。每个线程都有一个工作内存,工作内存中主要包括两个部分,一个是属于该线程私有的栈和对主存部分变量拷贝的寄存器(包括程序计数器PC和cup工作的高速缓存区)1.所有的变量都存储在主内存中(虚拟机内存的一部分),对于所有线程都是共享的。

2.每条线程都有自己的工作内存,工作内存中保存的是主存中某些变量的拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
3.线程之间无法直接访问对方的工作内存中的变量,线程间变量的传递均需要通过主内存来完成。

执行static i++的流程:
JMM: 线程A如不加锁,先从主内存中将共享变量i拿到工作内存中,拿到以后,如果此时i被其他线程改动,线程A无法感知,(volatile)在i前面加volatile 可以在i被其他线程改动的时候做到可见性, i++不是原子操作【通过线程加锁(如:synchrioned)可以解决问题】,线程不安全,如不加锁,通过AtomicInteger可解决问题,底层使用cas解决,i.getAndIncrement()代替i++;

在这里插入图片描述
4组操作
在这里插入图片描述
线程A对主内存中变量的变化无感知
在这里插入图片描述

二十八、volatile

volatile特性:

  • 保证可见性 (JMM)

  • 不保证原子性 (不可分割,atomic可保证)

  • 避免指令重排 (通过内存屏障,在单例模式中使用的最多,解决new的非原子性,【1分配内存2执行构造函数,初始化对象3将此对象指向此内存空间`】,先3指向)

  • 1.加上volatile后,程序停止,说明,volatile能保证可见性

在这里插入图片描述反编译发现:javap -c Hello.class mun++不是原子操作:
在这里插入图片描述

  • 2.但是发现volatile无法保证原子性,atomic可以解决原子性(cas)
    因此静态的数字++需要atomic
    在这里插入图片描述

二十九、指令重排

  • 3.加volatile能避免指令重排(单例模式中使用的多内存屏障
    在这里插入图片描述

三十、彻底玩转单例模式

  • 单例:(通过构造函数私有化,外面不可以new,只能直接调用静态方法创建实例或用静态变量或内部类创建单例然后静态方法调用,确保单例)

  • 饿汉式:通过构造函数私有化,确保单例,很饿,就开始加载实例。

  • 懒汉式:

  • DCL懒汉式(Double Check Lock):通过构造函数私有化(确保单例,用的时候再加载实例)+加双锁(防并发)+volatile(防指令重排)+ 构造函数内防护(防反射) 。

  • 静态内部类实现:通过构造函数私有化,通过内部类创建对象。

  • 枚举enum

枚举可以防止单例模式被破坏?
饿汉式
饿汉更急,通过构造函数私有化,只能本类才能构造,确保单例; 饿汉式很饿,用静态变量创建单例,静态变量上添加volatile关键字可以避免指令重排;
但是因为他是一开始就进行实例化,可能会存在浪费空间的问题,因此出现了懒汉式单例模式,用的时候再加载实例。

public class Hungry {
    private Hungry(){
    }
    private final static  Hungry hungry = new Hungry();
    public static Hungry getInstance(){
        return hungry;
    }
}

在这里插入图片描述DCL懒汉式

  • 单线程下ok

  • 但是多线程下有问题,所以要做两次check并加锁,即,double check Lock;

  • 但是即使这样由于 new LazyMan()不是原子性操作,实际是三步操作:1分配内存2执行构造函数,初始化对象3将此对象指向此内存空间;由于可能发生指令重排,可能
    先指向此内存空间,在初始化对象,此时如果B线程过来判断发现已经指向此内存空间,则直接返回实例,就会有问题,因此要避免指令重排,volatile可解决问题。

  • 都不安全,反射都可以破解他们:不调用获取实例的方法,直接通过反射获取实例,可通过在构造函数中check避免反射破解。

单线程下ok:

//懒汉式单例:
public class LazyMan {
    private LazyMan(){
    }
    
    private static LazyMan lazyMan;
    public static LazyMan getInstance(){
        if(lazyMan==null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }
}

DCL:
外面一层判断是为了提高性能,加锁是为了线程同步
在这里插入图片描述
加volatile,避免指令重排:
在这里插入图片描述内部类
在这里插入图片描述
反射破解:
想了解反射相关知识的:传送门

  • 不在通过类的方法获取实例而是通过反射直接调用构造方法获取实例;
  • 策略:通过在构造方法中check防御,

在这里插入图片描述
在构造方法中check防御:
1、通過判实例为null,但如果实例都是通过反射创建的,会有问题。

在这里插入图片描述
2.通过标志位:但是如果标志位被破解,也不行
在这里插入图片描述
3.通过枚举来防止反射破坏单例:
enum本身也是类,枚举里面的类默认就是单例的
enum详情:枚举传送门

在这里插入图片描述

反序列化破坏单例:
反序列时候,会先去类中查找是否有readResolve()方法,有了就调用,没有的话会通过反射调用类的无参构造器,破坏单例,所以为了防止单例被破坏,可以在单例类中添加readResove()方法返回应有的单例。== 但是枚举类型的单例不会被序列化破坏。==
在这里插入图片描述

三十一、CAS

java层面的cas:是 原子类.CompareAndSet,原子类
底层的cas: 是CompareAndSwap
在这里插入图片描述
num++: java无法操作内存、java可以调用c++来操作内存,nusafe类是java操作内存的后门,java可通过这个类来操作内存。
自旋锁: do{} while();

三十二、ABA问题:

在这里插入图片描述

在这里插入图片描述

三十三、可重入锁:

1.公平锁\非公平锁

公平锁:先到先得
synchrionzied、lock默认都是非公平锁;lock可以改

Lock lock = new ReentrantLock(true);

2.可重入锁(递归锁)

拿到外面的锁,自动获取内部的锁,绑定包含关系;锁必须配对,加几把锁,就必须开几次锁。
synchronized、lock一样

3.自旋锁

在这里插入图片描述

4、死锁:

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
jps -l : 获取所有进程

jstack + 进程号,:找到死锁问题

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值