多线程学习笔记

1.什么是线程?

进程就是在运行过程中的程序,就好像手机运行中的微信,QQ,这些就叫做进程。

线程就是进程的执行单元,就好像一个音乐软件可以听音乐,下载音乐,这些任务都是由线程来完成的。

  • 一个进程可以拥有多个线程,一个线程必须要有一个父进程
  • 线程之间共享父进程的共享资源,相互之间协同完成进程所要完成的任务
  • 一个线程可以创建和撤销另一个线程,同一个进程的多个线程之间可以并发执行

2.中断线程

​ 早期的java版本支持直接调用线程类的stop方法来中断线程,但是现在已经被启用了,取而代之的是

interrupt()将可以用来请求终止线程,每调用一次interrupt() 线程的中断状态将被置位,这是每一个线程都有boolean标志

Thread.currentThread().isInterrupted()
    //通常要判断一个线程是否被中断可以调用.isInterrupted()这个方法  true表示已被中断
复制代码

但是也有列外 当调用interrupt()方法时,线程刚好被sleep()或wait()方法阻塞的时候,将会抛出interruptException(同时如果线程被阻塞也将无法检测线程的阻塞状态)

3.线程状态

3.1新创建线程

使用继承 Thread 类创建线程的步骤如下:

  1. 新建一个类继承 Thread 类,并重写 Thread 类的 run() 方法。
  2. 创建 Thread 子类的实例。
  3. 调用该子类实例的 start() 方法启动该线程。

代码举例如下:

public class ThreadDemo extends Thread {
	
	// 1. 新建一个类继承 Thread 类,并重写 Thread 类的 run() 方法。
	@Override
	public void run() {
		System.out.println("Hello Thread");
	}
	
	public static void main(String[] args) {
		
		// 2. 创建 Thread 子类的实例。
		ThreadDemo threadDemo = new ThreadDemo();
		// 3. 调用该子类实例的 start() 方法启动该线程。
		threadDemo.start();
		
	}

}
复制代码

使用实现 Runnable 接口创建线程步骤是:

  1. 创建一个类实现 Runnable 接口,并重写该接口的 run() 方法。
  2. 创建该实现类的实例。
  3. 将该实例传入 Thread(Runnable r) 构造方法中创建 Thread 实例。
  4. 调用该 Thread 线程对象的 start() 方法。

代码举例如下:

public class RunnableDemo implements Runnable {

	// 1. 创建一个类实现 Runnable 接口,并重写该接口的 run() 方法。
	@Override
	public void run() {
		System.out.println("Hello Runnable");
	}

	
	public static void main(String[] args) {
		
		// 2. 创建该实现类的实例。
		RunnableDemo runnableDemo = new RunnableDemo();
		
		// 3. 将该实例传入 Thread(Runnable r) 构造方法中创建 Thread 实例。
		Thread thread = new Thread(runnableDemo);
		
		// 4. 调用该 Thread 线程对象的 start() 方法。
		thread.start();
		
	}
	

}
复制代码

用Callable与Future创建线程:

​ Runnable封装一个异步运行的任务,可以把他想象为一个没有返回值和参数的异步方法,Callable和Runnale类似,但是有返回值,Callable接口是一个参数和的类型,只有一个call方法. 参数类型是返回值的类型

public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}
复制代码

Future保存异步计算的结果

package java.util.concurrent;

public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

 
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}
复制代码

3.2可运行线程

当线程调用.start(); 线程就处于runnable状态,一个可运行的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供的运行时间,事实上运行中的线程被中断是为了给其它线程运行机会.

线程调度的细节依赖于操作系统提供的服务,抢占式调度系统给每一个可运行的线程一个时间片来执行任务,当时间片用完了,操作系统剥夺该线程的运行全,并给另一个线程运行机会,当选择下一个线程时,操作系统考虑线程的优先级.

​ 通常所有的桌面及服务器操作系统都使用抢占式调度,像手机这样的小型设备可能使用协作式调度,一个线程只有在调用yield方法或者被阻塞或等待时线程才会失去控制权.

3.3被阻塞线程和等待线程

​ 当线程处于被阻塞或等待状态的时候,它暂时不活动,他不运行任何代码且消耗最小 的资源,直到线程调度器重新激活它.

​ 当一个线程的锁被其他线程所持有,该线程将会进入阻塞状态,只有请求到这个锁,并且线程调度器允许本线程持有它的时候,该线程才变为非阻塞状态.

​ 当线程等待另一个线程通知调度器一个条件时,他自己进入等待状态. (详情等第5节)

3.4被终止的线程

​ 1.因为run方法正常推出而自然死亡.

​ 2.因为一个没有捕获的异常终止了run方法而意外死亡.

4.线程属性

4.1线程优先级

​ 每个线程都有一个优先级,默认继承它的父类的线程的优先级,也可以通过调用setPriority(1~10)来设置线程的优先级

​ 通常线程调度器有机会选择新线程时,他会优先选择具有较高优先级的线程(线程的优先级时高度依赖于系统的) 例如windows有7个优先级别,一些java优先级映射到宿主机平台的优先级上,优先级个数也许更多也许更少.

4.2守护线程

​ t.setDaemon(true); 将线程转换为守护线程(daemon thread)

​ 守护线程的目的就是为了其他线程提供服务的. 计时线程就是一个列子,它定时的发送"计时器滴答"信号给其他线程,或清空过时的高速缓存的线程. 当只剩下守护线程 的时候,虚拟机就退出了.

4.3未捕获的异常处理器

​ 线程的run方法不能抛出任何被检测异常,但是不被检测异常将导致线程终止.

​ 不需要任何catch子句来处理可以被传播的异常,,早在死亡之前异常被传递到一个用于未捕获异常的处理器

        //为每个线程安装一个默认的处理器
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                
                System.out.println(t+"默认处理器");
            }
        });
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getId());
                //throw ....;
            }
        }).start();
复制代码

​ 线程的run方法不能抛出任何被检测的异常 ,通过测试只是抛出异常并不捕获,异常会被传递到,线程设置的未捕获异常处理器中的uncaughtException(Thread t,Throwable e)方法中.

​ 如果不为线程安装默认的处理器,那么默认的处理器将会是空的,如果不为独立的线程安装默认的处理器,那么此时的处理器就是线程的ThreadGroup对象

​ 线程组是一个可以统一管理的线程集合.默认情况下,创建的线程都属于同一个线程组,但也可能创建其他的组.现在引入了更好的特性用于线程集合的操作,所以最好不要在自己的程序中使用线程组

ThreadGroup类实现了Thread.UncaughtException接口,它的uncaughtException方法做如下操作:
复制代码

​ 1.如果该线程有父线程组,那么父线程组的uncaughtException方法将被调用

​ 2.否则,如果Thread.getDefaultExceptionHandler方法返回一个非空的处理器,则调用该处理器

​ 3.否则如果Throwable是ThreadDeath的一个实例,什么都不做

​ 4.否则线程的名字以及Throwable的栈踪迹被输出到System.err上.

5.同步

​ 在大多数实际应用情况下,两个或者以上的线程需要共享对同一数据的存取,如果两个线程存取相同的对象,并且每一个线程都修改了该对象状态的方法. 那么会导致很严重的后果,根据各线程访问数据的的次序,可能会产生讹误的对象,这种情况通常被称为竞争条件(race condition)

5.1竞争条件的一个列子

5.2竞争条件详解

5.3锁对象

​ 为了防止代码受并发的影响,java提供了synchroonized关键字,并在java SE5.0引入了ReentrantLock类

synchronized自动的提供了一个锁以及相关的条件.

​ 通常在手动加锁后,把解锁放在finally子句中是必须的,以及使用锁的时候,就不能使用带资源的try语句

    private double[] accounts;
    private Lock mlock;
    private Condition sufficientFunds;

    public Bank(int n, int initMenoy) {
        accounts = new double[n];
        for (int i = 0; i < n; i++) {
            accounts[i] = initMenoy;
        }
        mlock = new ReentrantLock();
        sufficientFunds = mlock.newCondition();
    }

	public void transfer(int from, int to, double amont) throws InterruptedException {
        mlock.lock();
        try {
            if (accounts[from] < amont) {
                sufficientFunds.await();
            }
            accounts[from] -= amont;
            accounts[to] += amont;
            sufficientFunds.signalAll();
        } finally {
            mlock.unlock();
            System.out.println("转账成功,转出金额:" + amont);
            getAccoutsSum();
        }

    }
复制代码

如果在一个对象的方法中使用了锁,当一个线程访问这个对象的方法,线程会试图获取这个锁,如果这个锁被另一个线程所持有,则这个线程就进入阻塞状态,直到另一个线程对锁对象执行unlock()方法,才可以对这个方法进行操作.

锁保持一个持有计数器用来跟踪对lock方法的嵌套调用

5.4条件对象

​ 通常一个线程调用某个方法,却发现当前的条件不足以执行下面的步骤,这时候线程就结束了,但是还有一种办法可以不用频繁的执行线程来判断是否满足当前的条件,只需要在条件判断语句中,调用Condition的await()方法.

.await()方法使该线程进入阻塞状态并且放弃了这个锁,并且将该线程加入到该条件的 等待集

当锁可用时,这个线程并不能解除阻塞状态,只有当其他的线程的条件对象调用 .signalAll()方法时他才可以解除阻塞状态,并且等待调度器重新激活这个线程.

    private double[] accounts;
    private Lock mlock;
    private Condition sufficientFunds;

    public Bank(int n, int initMenoy) {
        ...
    }

	public void transfer(int from, int to, double amont) throws InterruptedException {
        mlock.lock();
        try {
            if (accounts[from] < amont) {
                sufficientFunds.await();
            }
            ...
        } finally {
            mlock.unlock();
        }

    }
复制代码

.signal() :只是随机的从等待集合中挑选一个线程激活

.signalAll() :激活等待集合中的所有线程(不是立即激活,只是解除等待线程的阻塞,以便这些线程可以在当前线程退出同步方法以后,通过竞争实现对对象的访问.)

​ 重新激活因为这一条件而等待的所有线程,当这些线程从等待集合中移出的时候,这些线程将变为可运行的,调度器将再次激活他们,同时他们将试图进入对象的方法中,当锁变为可用的时候,它们中的某个将从await()调用返回,获得该锁并从被阻塞的地方继续执行.

​ 当没有线程来重新激活等待的线程的时候,这时候就导致了死锁现象

​ 没有任何线程可以解除其他的线程的阻塞那么这个程序就挂起了.

5.5synchronized关键字

​ Lock和Condition接口为程序设计人员提供了高度的锁定控制,大多数情况下不需要那样的控制.

可以使用synchronized关键字,java1.0开始,java中的每个对象都有一个内部锁,如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法,也就是说,要调用该方法,线程就必须获得内部的对象锁.

    public synchronized void getAccoutsSum() {
        double sum = 0;
        for (double b : accounts) {
            sum += b;
        }

//        mlock.unlock();
        System.out.println("当前的总金额:" + sum);
    }
复制代码

这个代码块和上面使用Lock的效果是一样的

.wait()方法将添加一个线程到等待集当中

.notifyAll()方法将解除等待集中的所有线程

.nofity()方法将解除等待集中的某个线程

5.6同步阻塞

synchronized(obj) {
	// 同步代码块
}
复制代码

    public  void getAccoutsSum() {  
        synchronized (this){  //当前对象的锁
            double sum = 0;
            for (double b : accounts) {
                sum += b;
            }
            System.out.println("当前的总金额:" + sum);
        }
    }
复制代码

5.7监视器概念

5.8Volatile域

5.9final变量

5.10原子性

原子性:在 Java 中就是指一些不可分割的操作。

假设对共享变量除了赋值之外并不完成其他操作,那么可以将这些共享变量声明为volatie

5.11死锁

​ 所有线程都被阻塞就被称为死锁

5.12线程局部变量

5.13锁测试与超时

    public void transfer(int from, int to, double amont) throws InterruptedException {
        if(mlock.tryLock()){
        mlock.lock();
        try {
            if (accounts[from] < amont) {
                sufficientFunds.await();
            }
            accounts[from] -= amont;
            accounts[to] += amont;
            sufficientFunds.signalAll();
        } finally {
            mlock.unlock();
            System.out.println("转账成功,转出金额:" + amont);
            getAccoutsSum();
        }}else {
            System.out.println("没申请到锁,先跳过");
        }

    }
复制代码

使用这种方法申请lock方法试图申请一个锁,在成功以后返回true否则返回false,这样线程可以立即去做其他的事情,就不会阻塞.

public void transfer(int from, int to, double amont) throws InterruptedException {
    if(mlock.tryLock(100,TimeUnit.MILLISECONDS)){
    mlock.lock();
    try {
        if (accounts[from] < amont) {
            sufficientFunds.await();
        }
        accounts[from] -= amont;
        accounts[to] += amont;
        sufficientFunds.signalAll();
    } finally {
        mlock.unlock();
        System.out.println("转账成功,转出金额:" + amont);
        getAccoutsSum();
    }}else {
        System.out.println("没申请到锁,先跳过");
    }

}
复制代码

5.14读/写锁

​ 如果多线程经常读取一个数据而很少修改其中的数据的话,使用下面的方法可以节省内存的开销.

read Lock();//得到一个可以被多个读操作共用的读锁,但会排斥所有写操作.

write Lock();//得到一个写锁,排斥所有其他的读操作和写操作

//构造一个对象
private ReentrantReadWriteLock rwl=new ReentrantReadWriteLock();
//抽取读锁和写锁
private Lock readLock=rwl.readLock();
private Lock writeLock=rwl.writeLock();
//对所有的获取方法加读锁
public double getBalance(){
    readLock.lock();
    try{...}finally{readLock.unlock();}
}
//对所有的修改方法加写锁
public voi setBalance(){
    writeLock.lock();
    try{...}
    finally{writeLock.unlock();}
}


复制代码

5.15为什么启用stop和supend方法

stop:其实stop方法天生就不安全,因为它在终止一个线程时会强制中断线程的执行,不管run方法是否执行完了,并且还会释放这个线程所持有的所有的锁对象。这一现象会被其它因为请求锁而阻塞的线程看到,使他们继续向下执行。这就会造成数据的不一致,我们还是拿银行转账作为例子,我们还是从A账户向B账户转账500元,我们之前讨论过,这一过程分为三步,第一步是从A账户中减去500元,假如到这时线程就被stop了,那么这个线程就会释放它所取得锁,然后其他的线程继续执行,这样A账户就莫名其妙的少了500元而B账户也没有收到钱。这就是stop方法的不安全性。

supend:suspend被弃用的原因是因为它会造成死锁。suspend方法和stop方法不一样,它不会破换对象和强制释放锁,相反它会一直保持对锁的占有,一直到其他的线程调用resume方法,它才能继续向下执行。

假如有A,B两个线程,A线程在获得某个锁之后被suspend阻塞,这时A不能继续执行,线程B在或者相同的锁之后才能调用resume方法将A唤醒,但是此时的锁被A占有,B不能继续执行,也就不能及时的唤醒A,此时A,B两个线程都不能继续向下执行而形成了死锁。这就是suspend被弃用的原因。

6.阻塞队列

什么是阻塞队列? 阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

阻塞队列提供了四种处理方法:

方法\处理方式抛出异常返回特殊值一直阻塞超时退出
插入方法add(e)offer(e)put(e)offer(e,time,unit)
移除方法remove()poll()take()poll(time,unit)
检查方法element()peek()不可用不可用
  • 抛出异常:是指当阻塞队列满时候,再往队列里插入元素,会抛出IllegalStateException("Queue full")异常。当队列为空时,从队列里获取元素时会抛出NoSuchElementException异常 。

  • 返回特殊值:插入方法会返回是否成功,成功则返回true。移除方法,则是从队列里拿出一个元素,如果没有则返回null

  • 一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到拿到数据,或者响应中断退出。当队列空时,消费者线程试图从队列里take元素,队列也会阻塞消费者线程,直到队列可用。

  • 超时退出:当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出。

    Java里的阻塞队列

    JDK7提供了7个阻塞队列。分别是

    • ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
    • LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
    • PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
    • DelayQueue:一个使用优先级队列实现的无界阻塞队列。
    • SynchronousQueue:一个不存储元素的阻塞队列。
    • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
    • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

代码实例

package thread;

import java.io.File;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.BlockingQueue;

public class BlockingQueneTest {
    public static void main(String[] args) {
        String directpry = "C:\\Program Files\\Java\\jdk-9.0.4\\bin";
        BlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(20);   //阻塞队列的长度
        int pro=5;  //生产者线程个数
        int con=3;	//消费者线程个数
        for (int i = 0; i < pro; i++) {
            new Thread(new Producer(blockingQueue)).start();
        }
        for (int i = 0; i < con; i++) {
            new Thread(new Consumer(blockingQueue)).start();
        }

    }
}

class Producer implements Runnable {
    private BlockingQueue<Object> m;

    public Producer(BlockingQueue<Object> m) {
        this.m = m;
    }

    @Override
    public void run() {
        try {
            while (true) {
                m.put(get());
                System.out.println("生产时-阻塞队列"+m.size());
            }
        } catch (InterruptedException e) {
            System.out.println("生产者中断");
        }
    }

    Object get() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println("生产时中断");
        }
        return new Object();
    }
}

class Consumer implements Runnable {
    private BlockingQueue<Object> m;

    public Consumer(BlockingQueue<Object> m) {
        this.m = m;
    }

    @Override
    public void run() {
        try {
            while (true) {

                Object o = m.take();
                System.out.println("取出时-阻塞队列"+m.size());
                take(o);
            }
        } catch (InterruptedException e) {
            System.out.println("消费者中断");
        }
    }

    void take(Object o) {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println("消费时中断");
        }
        System.out.println("消费者对象" + o);
    }

}

复制代码

7.线程安全的集合

7.1高效的映射表,集合和队列

java.util.concurrent 包提供了映射表,有序集和队列的高效实现,ConcurrentHashMap,ConcurrentSkipListMap,ConcurrentSkipListSet和ConcurrentLinkedQuene

这些集合使用复杂的算法,通过允许并发的访问数据结构的不同部分来使竞争最小化

7.2写数组的拷贝

​ CopyOnWriteArrayList和CopyOnWriteArraySet是线程安全的集合,其中的所有的修改线程对底层数组进行复制

9.执行器

​ 构建一个新的线程是有一档代价的,因为涉及与操作系统的交互,如果程序中创建了大量的生命期很短的线程,那么就应该使用线程池,一个线程池包含许多准备运行的空闲线程.将Runnable交给线程池,就会有一个线程调用run方法,当run方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务.

​ 执行器(Executor)类有许多静态工厂方法来构建线程池

方法描述
newCachedThreadPool必要时创建线程,空闲线程保留60秒
newFixedThreadPool该线程池包含固定数量的线程,不会
newSingleThreadExecutor只有一个线程的线程池,该线程顺序执行每一个提交的任务
newScheduledThreadPool用于预定执行而构建的单线程"池"
newSingleThreadScheduledExecutor用于预定执行而构建的固定线程池,替代java.util.Timer

9.1线程池

public class Pool_demo {

    public static void main(String[] args) {

        Runnable runnable1=new Runnable() {
            @Override
            public void run() {
                while (true){
                    try {
                        Thread.sleep(1000);
                        System.out.println("当前线程id"+Thread.currentThread().getId()+Thread.currentThread().toString());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
	ExecutorService executorService1=Executors.newCachedThreadPool();//创建新线程池,如果有空线程可用就立即让他执行任务,如果没有可用空闲线程,那么就创建一个新线程
        ExecutorService executorService=Executors.newFixedThreadPool(2);  //创建固定大小的线程池
        for (int i = 0; i < 3; i++) {
            executorService.submit(runnable1);//提交指定的任务给线程池,让他去执行.
        }
        executorService.shutdown();//关闭服务,会先完成已经提交的任务而不再接受新的任务
        executorService.shutdownNow();//关闭服务,不管当前的任务是否完成都直接关闭.并返回从未开始执行任务的列表.
    }

复制代码

9.2预定执行

ScheduledExecutorService接口具有预定执行或者重复执行任务而设计的方法,它是一种允许使用线程池机制的java.util.Timer的泛化

ScheduledExecutorService scheduledExecutorService=Executors.newScheduledThreadPool(5);
        Runnable runnable=new Runnable() {
            @Override
            public void run() {
                System.out.println("1");
            }
        };
        scheduledExecutorService.schedule(runnable,1000,TimeUnit.MILLISECONDS);//指定一定时间之后执行任务
        scheduledExecutorService.scheduleAtFixedRate(runnable,1000,1000,TimeUnit.MILLISECONDS);//预定在初始的延迟结束之后,周期性的运行给定的任务,周期长度是period
        scheduledExecutorService.scheduleWithFixedDelay(runnable,1000,1000,TimeUnit.MILLISECONDS);//预定在促使的延迟结束后周期性的运行任务,在一次调用完成和下一次调用开始之间有长度为delay的延迟
复制代码

9.3控制任务组

9.4Fork-Join框架

​ java se7 中引入了fork-join框架,专门用来支持一些应用(应用可能对每个处理器内核分别使用一个线程来完成计算密集型任务,如图像或者视频处理),假设有一个处理任务,它可以很自然的分解为子任务

10.同步器

Java.util.concurrent包包含了几个能帮助人们相互合作的线程集的类. 这些机制具有为线程之间的公用集结点模式(common rendezvous patterns) 提供的 "预置功能" 如果有一个相互合作的线程集满足这些行为模式之一,那么应该直接重用合适的库类而不要试图提供手工的锁与条件的集合.

同步器

它能做什么?何时使用
CyclicBarrier允许线程集等待直至其中预定数目的线程到达一个公共障栅,然后可以选择执行一个处理障栅的动作.当大量的线程需要在它们的结果可用之前完成时
CountDownLatch允许线程等待直到计算器减为0当一个或多个线程需要等到直到指定数目的事件发生
Exchanger允许两个线程在要交换的对象准备号时交换对象当两个线程工作在同一数据结构的两个实例上的时候,一个向实例添加数据而另一个从实例清除数据
Semaphore允许线程集等待直到被允许继续运行为止限制访问资源的线程总数,如果许可数时1,常常阻塞线程直到另一个线程给出许可为止
SynchronousQueue允许一个线程把对象交给另一个线程在没有显示同步的情况下,当两个线程准备好将一个对象从一个线程传递到另一个时

10.1信号量

10.2倒计时门栓

10.3障栅

10.4交换器

10.5同步队列

11.线程与Swing

11.1运行耗时的任务

11.2使用Swing工作线程

11.3单一线程规则

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值