黑马程序员_Java基础_线程池

------- android培训java培训、期待与您交流! ----------

首先,我们先了解一下什么是线程池?又什么时候使用到线程池技术?


 我们生活中很多事情的处理和线程池有很大的关系,比如:火车售票,银行系统等
这些行业内部的高性能服务就是使用到了线程池技术,并且得到安全和高效的保障!

 线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。
 
那什么时候使用到线程池技术呢? 
假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。  
如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。

 
一个线程池包括以下四个基本组成部分:
 
1、线程池管理器(ThreadPool):用于创建、销毁并管理线程池,将工作线程放入线程池中。
 
2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务
 
3、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。
 
4、 任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,
它主要规定了任务的入口,任务执行完后的收尾工作,
任务的执行状态等;

线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。
它把
T1T3分别安排在服务器程序的启动和结束的时间段或者一
些空闲的时间段,
这样在服务器程序处理客户请求时,不会有T1T3的开销了。


下面我们就逐一去了解有关java线程池使用到的类,及一些相关应用 
 

 例子1:
我们先了解一下线程中的定时器:
这种可以使用定时查收邮件的,或者发送邮件(实用):  quartz(开源项目)
还有游戏制作的时候可以使用到定时技术。

 下面使用线程的定时技术制作一个定时炸弹:代码如下

public class TraditionalTimer {
    static int count = 0;
    public static void main(String[] args) {
        // method1();
        // method2();
//      method3();
        method4();
    }
    public static void method4() {
        Timer t = new Timer();
        t.schedule(new MyTimerTask2(), 2000);
        timecount();
    }
    /**
     * 方法功能:每隔两秒就爆炸一次
     */
    public static void method3() {
        Timer t = new Timer();
        t.schedule(new MyTimerTask(true), 3000);
        timecount();
    }
    /**
     * 方法功能:每隔两秒就爆炸一次
     */
    public static void method2() {
        Timer t = new Timer();
        t.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("爆炸");
            }
        }, 2000, 2000);
        timecount();
    }
    /**
     * 方法功能:定时炸弹2秒钟之后爆炸
     */
    public static void method1() {
        Timer t = new Timer();
        t.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("爆炸");
            }
        }, 2000);
        timecount();
    }
    /**
     * 方法功能:输出时间计时
     */
    public static void timecount() {
        new Thread() {
            public void run() {
                while (true) {
                    try {
                        System.out.println(new Date().getSeconds());
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
        }.start();
    }
}
// 第一种实现方式:自定义一个定时的任务,间接性的经过2秒钟之后爆炸一次,有经过四秒钟之后爆炸一次,,一次循环
class MyTimerTask extends TimerTask {
    private boolean flag = true;
    MyTimerTask(boolean flag) {
        this.flag = flag;
    }
    @Override
    public void run() {
        System.out.println("爆炸");
        new Timer().schedule(new MyTimerTask(flag ? false : true), flag ? 2000
                : 4000);
    }
}
// 第二种实现方式:
class MyTimerTask2 extends TimerTask {
    private  static int count = 0; 
    @Override
    public void run() {
        this.count = (this.count + 1) % 2;
        System.out.println("爆炸");
        new Timer().schedule(new MyTimerTask2(), 2000 + 2000 * this.count);
    }
}

需求:用面试宝典中的子线程循环10次和主线程循环5次,两者交替运行50的例子
 
  为了观察方便,我们可以将运行的结果通过重定向到一个文本文件中,在 run configuration中配置
例子2:


public class TraditionalThreadTest {
    public static void main(String[] args) {
        final MyThread t = new MyThread();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int j = 1; j <= 50; j++) {
                    t.sub(j);
                }
            }
        }).start();
        for (int j = 1; j <= 50; j++) {
            t.main(j);
        }
    }
}
class MyThread {
    private boolean flag = true;
    public synchronized void sub(int j) {
        while (flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        for (int i = 1; i <= 10; i++) {
            System.out.println("sub-------------->" + i + " : " + j);
        }
        this.flag = true;
        this.notify();
    }
    public synchronized void main(int j) {
        while (!flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        for (int i = 1; i <= 100; i++) {
            System.out.println("main--->" + i + " : " + j);
        }
        this.notify();
        flag = false;
    }
}

需求:同一线程必须操作同一对象的元素,不然会出现安全问题
一个线程使用一个独立的对象数据,不能使用其他线程对象的数据 
ThreadLocal该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联我们通过例子来测试:
例子3: 
 使用两个线程,并将两个数据分别放入到ThreadLocal类中,

这个类能保证线程之间的数据不会干扰 

public class ThreadScopeTest {
//    private static Map<Thread, Integer> map = new HashMap<Thread, Integer>();
    private static ThreadLocal<Integer> x = new ThreadLocal<Integer>();
    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Random r = new Random();
                    int data = r.nextInt();
                     System.out.println(Thread.currentThread().getName() + " creat: "
                     + data);
//                  map.put(Thread.currentThread(), data);
                    x.set(data);
                    new A().print();
                    new B().print();
                }
            }).start();
        }
    }
    static class A {
        public void print() {
//          int data = map.get(Thread.currentThread());
            int data = x.get();
            System.out.println("A:" + Thread.currentThread().getName() + ":"
                    + data);
        }
    }
    static class B {
        public void print() {
//          int data = map.get(Thread.currentThread());
            int data = x.get();
            System.out.println("B:" + Thread.currentThread().getName() + ":"
                    + data);
        }
    }
}

 输出结果:A:Thread-0:-704470354 A:Thread-1:-795936612 B:Thread-1:-795936612
 B:Thread-0:-704470354
 
 结论:同一线程使用的是自己的数据,不能使用其他线程的数据

需求:测试同一线程在执行过程中,自己使用的对象不会被其他线程使用,他们之间是互斥的
 
public class ThreadLocalTest {
    private static ThreadLocal<MyTreadScopeData> map = new ThreadLocal<MyTreadScopeData>();
    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Random r = new Random();
                    int data = r.nextInt();
                    MyTreadScopeData my = MyTreadScopeData.getInstance();
                    my.setAge(data);
                    my.setName("name" + data);
                    new A().print();
                    new B().print();
                }
            }).start();
        }
    }
    static class A {
        public void print() {
            MyTreadScopeData data = MyTreadScopeData.getInstance();
            System.out.println("A:" + Thread.currentThread().getName() + ":"
                    + data);
        }
    }
    static class B {
        public void print() {
            MyTreadScopeData data = MyTreadScopeData.getInstance();
            System.out.println("B:" + Thread.currentThread().getName() + ":"
                    + data);
        }
    }
}
class MyTreadScopeData {
    private static ThreadLocal<MyTreadScopeData> map = new ThreadLocal<MyTreadScopeData>();
    private MyTreadScopeData() {
    }
    public static MyTreadScopeData getInstance() {
        MyTreadScopeData instance = map.get();
        if (instance == null) {
            instance = new MyTreadScopeData();
            map.set(instance);
        }
        return instance;
    }
    private String name;
    private int age;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return " [name=" + name + ", age=" + age + "]";
    }
}
 Executor类的应用:(用于创建线程池)

 (在线程执行完任务之后,线程是不会消亡的)可以使用ExecutorService.shutdown()将线程池中的线程关闭
 
 使用线程池技术,是为了减少线程在创建和销毁的过程中所浪费的资源和时间
 
线程池中的线程没有接收到任务的时候,线程将出入wait状态,当有任务进入时,将唤醒notify线程
 
  tomcat服务器就是使用线程池的原理
我们可以使用这个方法将线程池中的线程进行消亡(默认是不会关闭线程的)
 ThreadPool.shutdown();


 使用Executor的静态 newFixedThreadPool()方法创建线程池,并 指定线程数量
  使用ExecutorService类接受线程池
   ExecutorService是固定的线程池
   ExecutorService ThreadPool = Executors.newFixedThreadPool(3);

 使用线程池的缓冲技术,线程的数量会顺着任务的多少而改变,
   当任务多的时候.可以动态的创建线程的数量来完成任务,
 ExecutorService ThreadPool = Executors.newCachedThreadPool();

创建一个单一线程,当线程池中的单一线程死亡之后,会自动创建一个新的线程保证线程池中有唯一一个线程
ExecutorService ThreadPool = Executors.newSingleThreadExecutor();

 例子:创建缓冲线程池,会动态创建线程,一般线程数量和任务数量相等

public class ThreadPoolTest {
    public static void main(String[] args) {
        ExecutorService ThreadPool = Executors.newCachedThreadPool();
        // 往线程池中添加10个任务
        for (int i = 0; i <= 10; i++) {
            final int task = i;
            ThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j <= 10; j++) {
                        try {
                            Thread.sleep(20); // 让线程休眠一会
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()
                                + " loop of " + j + "for task " + task);
                    }
                }
                private void extracted() throws InterruptedException {
                    Thread.sleep(20);
                }
            });
        }
        System.out.println("添加完成十个任务");
        ThreadPool.shutdownNow();
    }
}

  Lock类比传统线程模型中的sychronized方式更加面向对象,
 实现允许更灵活的结构,可以具有差别很大的属性
Lock 实现提供了比使用  synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的  Condition 对象。  
  
  与生活中的锁类似,锁本身也应该是一个对象,2个线程执行的代码片段要实现同步互斥的效果,
  他们必须用同一个Lock对象,
  
 读写锁:分为读锁和写锁,多个读硕不互斥,读锁与写锁互斥,写锁和写锁互斥,
 这是有JVM自己控制的,你只要上好相应的锁即可,
 如果你代码只读数据,可以很多人同时读,但是不能同时写,那就使用读锁;
 如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁
 总之,读的时候上读硕,写的时候上写锁 
例子:
public class LockTest {
    public static void main(String[] args) {
        final Add add1 = new Add();
        // 创建2个线程进行打印名字
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    add1.add("---------");
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    add1.add("++++++++++");
                }
            }
        }).start();
    }
}
class Add {
    Lock lock = new ReentrantLock();
    public void add(String name) {
        // 枷锁
        lock.lock();
        try {
            for (int i = 0; i < name.length(); i++) {
                System.out.print(name.charAt(i));
            }
            System.out.println();
        } finally {
            lock.unlock(); // 使用finally捕捉,使得代码更加安全
        }
    }
}

ReadWriteLock   维护了一对相关的 ,一个用于只读操作,另一个用于写入操作。


只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。
锁是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问。

一次只能有一个线程获得锁,对共享资源的所有访问都需要首先获得锁。

不过,某些锁可能允许对共享资源并发访问,如 ReadWriteLock 的读取锁。
 
例子: 需求:模拟Hibernate获取数据库信息

public class CacheTest {
    private Map<String, Object> cache = new HashMap<String, Object>();
    public static void main(String[] args) {
    }
    private ReadWriteLock rwl = new ReentrantReadWriteLock();
    public Object getData(String key) {
        // 上读锁
        rwl.readLock().lock();
        Object value = null;
        try {
            value = cache.get(key);
            if (value == null) {
                rwl.readLock().unlock();
                rwl.writeLock().lock();
                try {
                    if (value == null) {
                        value = "aaaa"; // 模拟数据库获取数据,当内存中没有资源时,到数据库中获取
                    }
                } finally {
                    rwl.writeLock().unlock();
                }
                rwl.readLock().lock();
            }
        } finally {
            rwl.readLock().unlock(); // 解锁
        }
        return value;
    }
}

例子:需求:制作一个定时器,程序运行6秒后,爆炸一次,以后每隔2秒爆炸一次


public class ExecutorTest {
    public static void main(String[] args) {
        Executors.newScheduledThreadPool(3).scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println("爆炸");
            }
        }, 6, 2, TimeUnit.SECONDS);
    }
}

Condition 类
 
将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,
 以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。
 其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用

 Condition 实例实质上被绑定到一个锁上。
 要为特定 Lock 实例获得 Condition 实例,请使用其 newCondition() 方法  

注意, Condition  实例只是一些普通的对象,它们自身可以用作  synchronized  语句中的目标,
并且可以调用自己的  wait  和  notification  监视器方法。获取  Condition  实例的监视器锁或者使用其监视器方法,
与获取和该  Condition  相关的  Lock  或使用其  waiting  和  signalling  方法没有什么特定的关系。
为了避免混淆,建议除了在其自身的实现中之外,切勿以这种方式使用  Condition  实例。   
 
例子:
 
需求:有三个线程,主线程,二线程,三线程,它们依次排序执行循环十次(主线程----> 二 线程---->三线程)  
public class ThreadConditionCommunication {
    public static void main(String[] args) {
        final ThreadQueue t = new ThreadQueue();
        // 创建线程2
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50; i++) {
                    t.subThread2(i);
                }
            }
        }).start();
        // 创建线程3
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50; i++) {
                    t.subThread3(i);
                }
            }
        }).start();
        //主线程执行的部分
        for (int i = 0; i < 50; i++) {
            t.main(i);
        }
    }
}
class ThreadQueue {
    Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();
    private int queue = 1;
    public void subThread2(int j) {
        lock.lock();
        try {
            while (queue != 2)
                condition2.await();
            for (int i = 0; i < 10; i++) {
                System.out.println("sub2 ---------->" + i + ":" + j);
            }
            queue = 3;
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void subThread3(int j) {
        lock.lock();
        try {
            while (queue != 3)
                condition3.await();
            for (int i = 0; i < 10; i++) {
                System.out.println("sub3---------->" + i + ":" + j);
            }
            queue = 1;
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void main(int j) {
        lock.lock();
        try {
            while (queue != 1)
                condition1.await();
            for (int i = 0; i < 10; i++) {
                System.out.println("main----------->" + i + ":" + j);
            }
            queue = 2;
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

例子:
需求:定义一个工厂 ,工厂容量为100; 
 使用三个线程生产 ,三个线程消费
 
 没有产品的使用不能消费  


public class InOutFactory {
    public static void main(String[] args) throws Exception {
        final Factory factory = new Factory();
        for (int i = 0; i < 3; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        Random r = new Random();
                        factory.put(r.nextInt());
                    }
                }
            }).start();
        }
        for (int i = 0; i < 3; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        try {
                            System.out.println(Thread.currentThread().getName()
                                    + "消费:" + factory.take());
                        } catch (InterruptedException e) {
                            System.out.println("消费失败");
                        }
                    }
                }
            }
            ).start();
        }
    }
}
class Factory {
    Lock lock = new ReentrantLock();
    final Condition noFull = lock.newCondition();
    final Condition noEmpty = lock.newCondition();
    private Object[] items = new Object[100];
    int putptr, takeptr, count;
    /**
     * 生产方法
     */
    public void put(Object x) {
        lock.lock();
        try {
            while (count == items.length)
                // 当生产的数量大于一百的时候停止生产
                noFull.await();
            items[putptr] = x;
            if (++putptr == items.length)
                putptr = 0;
            count++;
            System.out.println(Thread.currentThread().getName() + "生产:" + x);
            Thread.sleep(1000);
            noEmpty.signal();
        } catch (Exception e) {
            System.out.println("生产出现异常");
        } finally {
            lock.unlock();
        }
    }
    /**
     * 消费的方法
     * 
     */
    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                noEmpty.await();
            }
            Object x = items[takeptr];
            if (++takeptr == items.length)
                takeptr = 0;
            --count;
            noFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}

Semaphore类实现信号灯 

Semaphore可以维护当前访问自身的线程个数,并提供同步线程机制。 
  
  例如:实现一个文件允许的并发访问数
  
  Semaphore实现的功能类似厕所有5个坑,假如有10个人上厕所,那么同时只能有5个人能够占用,
  当5个人中的任何一个人让开后,其中在等待的另外5个人中有一个可以占用了
 
 单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁” 再由另一个线程放“锁”,
 这可应用于死锁恢复的一些场合  

一个计数信号量。从概念上讲,信号量维护了一个许可集。

如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。

每个release() 添加一个许可,从而可能释放一个正在阻塞的获取者。

但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。

Semaphore 通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。  

例子: 
需求:有资源数目为3,多个线程排队使用资源,只能有三个线程同时使用资源


public class SemaphoreTest {
    public static void main(String[] args) {
        
        //创建线程缓冲池,有多少任务将创建对应数量的线程个数
        ExecutorService ThreadPool = Executors.newCachedThreadPool();
        
        //定义资源的数量为3个
        final Semaphore sp = new Semaphore(3);
        
        for (int i = 0; i < 10; i++) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try {
                        //从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断
                        sp.acquire();   
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    
                    //availablePermits()返回此信号量中当前可用的许可数
                    System.out.println("线程" + Thread.currentThread().getName()
                            + "进入,当前已有" + (3 - sp.availablePermits()));
                    try {
                        Thread.sleep((long) (Math.random() * 10000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程" + Thread.currentThread().getName()
                            + "即将离开");
                    //  释放一个许可,将其返回给信号量。
                    sp.release();
                    System.out.println("线程" + Thread.currentThread().getName()
                            + "已离开,当前已有" + (3 - sp.availablePermits()));
                }
            };
            ThreadPool.execute(runnable);
        }
    }
}

Future类用于接受线程返回结果; 

Future  表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。
计算完成后只能使用 
get  方法来获取结果,如有必要,计算完成前可以阻塞此方法。
取消则由 
cancel  方法来执行。
还提供了其他方法,以确定任务是正常完成还是被取消了。
一旦计算完成,就不能再取消计算。
如果为了可取消性而使用 
Future  但又不提供可用的结果,
则可以声明 
Future<?>  形式类型、并返回  null  作为底层任务的结果。  


 它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。
 计算完成后只能使用 get 方法来获取结果,如有必要,计算完成前可以阻塞此方法。

public class CallableAndFuture {
    public static void main(String[] args) {
        ExecutorService  threadPool = Executors.newSingleThreadExecutor();
        Future<String> futuer = threadPool.submit(new Callable<String>(){
            @Override
            public String call() throws Exception {
                Thread.sleep(2000);
                return "hello";
            }
            
        });
        System.out.println("等待线程返回的结果:");
        try {
            //等待线程返回的结果
//          System.out.println("线程返回的结果:" + futuer.get());
            //Futuer.get(1,TimeUnit.SECONDS)指定1秒内接收不到数据,就报出异常
            System.out.println("线程返回的结果:" + futuer.get(2,TimeUnit.SECONDS));
        } catch (Exception e) {
            e.printStackTrace();
        } 
    }
}

  CompletionService类
   将生产新的异步任务与使用已完成任务的结果分离开来的服务。 
  生产者 submit 执行的任务。 使用者 take
  已完成的任务,并按照完成这些任务的顺序处理它们的结果。

例如,CompletionService 可以用来管理异步 IO ,执行读操作的任务作为程序或系统的一部分提交,然后,当完成读操作时,会在程序的不同部分执行其他操作,执行操作的顺序可能与所请求的顺序不同。

通常, CompletionService  依赖于一个单独的  Executor  来实际执行任务,在这种情况下, CompletionService  只管理一个内部完成队列。 ExecutorCompletionService  类提供了此方法的一个实现。
例子: 
 
public class CompletionServiceTest {
    public static void main(String[] args) {
        //创建线程池,并往线程池中添加十个线程
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        CompletionService<Integer> completionService = new ExecutorCompletionService<Integer>(
                threadPool);
        //向线程中添加十个任务
        for (int i = 0; i < 10; i++) {
            final int temp = i;
            completionService.submit(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    //随机睡眠时间
                    Thread.sleep(new Random().nextInt(5000));
                    return temp;
                }
            });
        }
        for (int i = 0; i < 10; i++) {
            try {
                System.out.println(completionService.take().get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值