多线程基础

18 篇文章 0 订阅
7 篇文章 0 订阅

格式混乱。。。。可至我的有道云查看:http://note.youdao.com/noteshare?id=277d8d512f6f80f111bc7a058aa3cb27&sub=743EACECE382418AAEEE56CA086E5CD1

同步(Synchronous)和异步(Asynchronous)

同步和异步通常用来形容一次方法调用。同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。
而异步方法通常会在另外一个线程中“真实”地执行。整个过程,不会阻碍调用者的工作。

并发(Concurrency)和并行(Parallelism)
并发偏重于多个任务 交替 执行,而多个任务之间有可能还是串行的。
并行是真正意义上的“同时执行”。

临界区
临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。

阻塞(Blocking )和非阻塞(Non-Blocking )
比如一个线程占用了临界区资源,那么其他所有需要这个资源的线程就必须在这个临界区中进行等待。等待会导致线程挂起,这种情况就是阻塞。
此时,如果占用资源的线程一直不愿意释放资源,那么其他所有阻塞在这个临界区上的线程都不能工作。
非阻塞的意思与之相反,它强调没有一个线程可以妨碍其他线程执行。所有的线程都会尝试不断前向执行。

死锁(Deadlock )、饥饿(Starvation )和活锁(Livelock )
死锁(Deadlock ):两个或多个线程相互占用了其他线程的锁,并都未释放。、
饥饿(Starvation ):是指某一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行。
比如它的线程优先级可能太低,而高优先级的线程不断抢占它需要的资源,导致低优先级线程无法工作。
活锁(Livelock): 两个线程都主动的将资源释放给他人使用,导致资源在两个线程中跳动,而没有一个线程可以同时拿到所有资源而正常执行。

并发级别
阻塞、无饥饿、无障碍、无锁、无等待。
阻塞(Blocking ):一个线程是阻塞的,那么在其他线程释放资源之前,当前线程无法继续执行。当我们使用synchronized 关键字,或者重入锁时,我们得到的就是阻塞的线程。
无饥饿(Starvation-Free ):如果线程之间是有优先级的,那么线程调度的时候总是会倾向于满足高优先级的线程。也就是说,对于同一资源的分配是不公平的。对于非公平锁,系统允许高优先
级的线程插队,这样有可能导致低优先级线程产生饥饿。但如果锁是公平的,就是无饥饿。
无障碍(Obstruction-Free ):无障碍是一种最弱的非阻塞调度。两个线程如果是无障碍的执行,那么他们不会因为临界区的问题导致一方被挂起。
无锁(Lock-Free ):锁的并行都是无障碍的。在无锁的情况下,所有的线程都能尝试对临界区进行访问,但不同的是,无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区。
无等待(Wait-Free ):无锁只要求有一个线程可以在有限步内完成操作,而无等待则在无锁的基础上更进一步进行扩展。它要求所有的线程都必须在有限步内完成,这样就不会引起饥饿问题。如
果限制这个步骤上限,还可以进一步分解为有界无等待和线程数无关的无等待几种,它们之间的区别只是对循环次数的限制不同。

多线程的原子性、可见性和有序性
原子性(Atomicity):原子性是指一个操作是不可中断的。即使是多个线程一起执行的时候,一个操作一旦开始就不会不给其他线程干扰。
可见性(Visibility ):可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。
有序性(Ordering ):程序在执行时,可能会进行指令重排,重排后的指令与原指令的顺序未必一致。(指令重排不会使串行的语义逻辑发生问题。但无法保证多线程间的语义也一致。)

线程状态(Thread在State中定义的):
NEW 状态表示刚刚创建的线程,这种线程还没开始执行。等到线程的 start() 方法调用时,才表示线程开始执行。
当线程执行时,处于 RUNNABLE 状态,表示线程所需的一切资源都已经准备好了。如果线程在执行过程中遇到了
synchronized 同步块,就会进入 BLOCKED 阻塞状态,这时线程就会暂停执行,直到获得请求的锁。 WAITING 和 TIMED_WAITING 都表示等待状态,它们的区别是 WAITING 会进入一个无时间
限制的等待, TIMED_WAITING 会进行一个有时限的等待。那等待的线程究竟在等什么呢?一般来说, WAITING 的线程正是在等待一些特殊的事件。比如,通过 wait() 方法等待的线程在等待
notify() 方法,而通过 join() 方法等待的线程则会等待目标线程的终止。一旦等到了期望的事件,线程就会再次执行,进入 RUNNABLE状态。
当线程执行完毕后,则进入 TERMINATED 状态,表示结束。

注意: 从 NEW 状态出发后,线程不能再回到 NEW 状态,同理,处于 TERMINATED的线程也不能再回到 RUNNABLE 状态。

线程的基本操作
新建线程
使用 new 关键字创建一个线程对象,并且将它 start() 起来即可。
Thread t1=new Thread(){
@Override
public void run(){
System.out.println(“Hello, I am t1”);
}
};
t1.start();

需要执行的任务必须重载run()方法

上述代码使用匿名内部类,重载了 run() 方法,还可以集成Thread类重载run()方法和实现Runnable接口。

常用操作:
public class CreateThread3 implements Runnable {
public static void main(String[] args) {
Thread t1=new Thread( new CreateThread3() );
t1.start();
}
@Override
public void run() {
System.out.println(“Oh, I am Runnable”);
}
}

终止线程
一般来说,线程执行完毕就会结束,无须手动关闭。
关闭手段:
Thread.stop(); //已废弃(强行终止线程)会直接终止线程,并且会立即释放这个线程所持有的锁。易导致数据错误。

可以定义标记变量,指示线程是否需要退出。

线程中断
线程中断并不会使线程立即退出,而是给线程发送一个通知,告知目标线程,有人希望你退出啦!至于目标线程接到通知后如何处理,则完全由目标线程自行决定。这点很重要,如果中断后,
线程立即无条件退出,我们就又会遇到 stop()方法的老问题。

线程中断有如下三个方法:

public void Thread.interrupt() // 中断线程(通知目标线程中断,也就是设置中断标志位,中断标志位表示当前线程已经被中断了。)
public boolean Thread.isInterrupted() // 判断是否被中断(通过检查中断标志位)
public static boolean Thread.interrupted() // 判断是否被中断,并清除当前中断状态

@Test
/**
 *虽然对 t1 进行了中断,但是在 t1 中并没有中断处理的逻辑,因此,即使 t1 线程被置上了中断状态,但是这个中断不会发生任何作用。
 */
public void test1() throws Exception {
	Thread t1 = new Thread() {
		@Override
		public void run() {
			while (true) {
				System.out.println("Interruted!");
				Thread.yield();
			}
		}
	};
	t1.start();
	Thread.sleep(2000);
	t1.interrupt();
}


@Test
 /**
 *t1在中断后处理一下,才会增加相应的中断处理代码
 */
public void test2() throws Exception {

	Thread t1 = new Thread() {
		@Override
		public void run() {
			while (true) {
				if (Thread.currentThread().isInterrupted()) {
					System.out.println("Interruted!");
					break;
				}
				Thread.yield();
			}
		}
	};

	t1.start();
	Thread.sleep(2000);
	t1.interrupt();
}



@Test
 /**
 *当线程在 sleep()休眠时,如果被中断,会产生InterruptedException异常。
 *Thread.sleep()方法由于中断而抛出异常,此时,它会清除中断标记,如果不加处理,那么在下一次循环开始时,就无法捕获这个中断,故在异常处理中,再次设置中断标记位。
 * 输出:Interruted When Sleep
 *      Interruted!
 */
public void test3() throws Exception {
	Thread t1 = new Thread() {
		@Override
		public void run() {
			while (true) {
				if (Thread.currentThread().isInterrupted()) {
					System.out.println("Interruted!");
					break;
				}
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					System.out.println("Interruted When Sleep");
					// 设置中断状态
					Thread.currentThread().interrupt();
				}
				Thread.yield();
			}
		}
	};
	t1.start();
	Thread.sleep(2000);
	t1.interrupt();
}

等待(wait)和通知(notify)
当在一个对象实例上调用 wait() 方法后,当前线程就会在这个对象上等待。

Object.wait() 方法并不是可以随便调用的。它必须包含在对应的synchronzied 语句中,无论是 wait() 或者 notify() 都需要首先获得目标对象的一个监视器。

notify() 唤醒等待的线程,如下图:

wait() 和 notify() 的工作流程细节,如下图:

public class SimpleWN {

final static Object object = new Object();

public static class T1 extends Thread {
	public void run() {
		synchronized (object) {
			System.out.println(System.currentTimeMillis() + ":T1 start!");
			try {
				System.out.println(System.currentTimeMillis() + ":T1 wait for object ");
				object.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(System.currentTimeMillis() + ":T1 end!");
		}
	}
}

public static class T2 extends Thread {
	public void run() {
		synchronized (object) {
			System.out.println(System.currentTimeMillis() + ":T2 start!notify one thread");
			object.notify();
			System.out.println(System.currentTimeMillis() + ":T2 end!");
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
			}
		}
	}
}

/**
 *输出:
 *1544081985067:T1 start!
 *1544081985067:T1 wait for object 
 *1544081985067:T2 start!notify one thread
 *1544081985067:T2 end!
 *1544081987067:T1 end!
 */
public static void main(String[] args) {
	Thread t1 = new T1();
	Thread t2 = new T2();
	t1.start();
	t2.start();
}

}

在 T2 通知 T1 继续执行后, T1 并不能立即继续执行,而是要等待 T2 释放 object 的锁,并重新成功获得锁后,才能继续执行。

注意: Object.wait() 和 Thread.sleep()方法都可以让线程等待若干时间。除了 wait() 可以被唤醒外,另外一个主要区别就 是
wait() 方法会释放目标对 象的锁,而Thread.sleep()方法不会释 放任何资源。

挂起(suspend )和继续执行(resume )线程(已废弃)
suspend()在导致线程暂停的同时,并不会去释放任何锁资源。resume()操作, 被挂起的线程才能继续。

suspend()方法导致线程进入类似死锁的状态,如下图:

public class BadSuspend {

public static Object u = new Object();
static ChangeObjectThread t1 = new ChangeObjectThread("t1");
static ChangeObjectThread t2 = new ChangeObjectThread("t2");

public static class ChangeObjectThread extends Thread {
	public ChangeObjectThread(String name) {
		super.setName(name);
	}

	@Override
	public void run() {
		synchronized (u) {
			System.out.println("in " + getName());
			Thread.currentThread().suspend();
		}
	}
}
/** 
 *虽然主函数中已经调用了resume(),但是由于时间先后顺序的缘故,那个 resume 并没有生效!这就导致了线程 t2 被永远挂起,并且永远占用了对象 u 的锁。
 */
public static void main(String[] args) throws InterruptedException {
	t1.start();
	Thread.sleep(100);
	t2.start();
	t1.resume();
	t2.resume();
	t1.join();
	t2.join();
}

}

可以设置标记变量利用wait和notify 实现suspend和resume功能,如下例:
public class GoodBadSuspend {

public static Object u = new Object();

public static class ChangeObjectThread extends Thread {
	volatile boolean suspendme = false;

	public void suspendMe() {
		suspendme = true;
	}

	public void resumeMe() {
		suspendme = false;
		synchronized (this) {
			notify();
		}
	}
	
	public ChangeObjectThread(String name) {
		super.setName(name);
	}

	@Override
	public void run() {
		while (true) {
			
			synchronized (this) {
				while (suspendme)
					try {
						wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
			}

			synchronized (u) {
				System.out.println(super.getName() + "-----in ChangeObjectThread");
			}
			Thread.yield();
		}
	}
}

public static class ReadObjectThread extends Thread {
	
	public ReadObjectThread(String name) {
		super.setName(name);
	}
	@Override
	public void run() {
		while (true) {
			synchronized (u) {
				System.out.println(super.getName() + "-----in ReadObjectThread");
			}
			Thread.yield();
		}
	}
}

public static void main(String[] args) throws InterruptedException {
	ChangeObjectThread t1 = new ChangeObjectThread("t1");
	ReadObjectThread t2 = new ReadObjectThread("t2");
	t1.start();
	t2.start();
	Thread.sleep(1000);
	t1.suspendMe();
	System.out.println("suspend t1 2 sec");
	Thread.sleep(2000);
	System.out.println("resume t1");
	t1.resumeMe();
}

}

等待线程结束(join )和谦让(yield)

//表示无限等待,它会一直阻塞当前线程,直到目标线程执行完毕。
public final void join() throws InterruptedException

//给出了一个最大等待时间,如果超过给定时间目标线程还在执行,当前线程也会因为“等不及了”,而继续往下执行。
public final synchronized void join(long millis) throws InterruptedException

如下例:
public class JoinMain {
public volatile static int i=0;
public static class AddThread extends Thread{
@Override
public void run() {
for(i=0;i<10000000;i++);
}
}
public static void main(String[] args) throws InterruptedException {
AddThread at=new AddThread();
at.start();
at.join();
System.out.println(i);//10000,若不适用join()可能输出0或者很小的数字,用join(),则当前线程需等待AddThread执行完毕
}
}

join() 的本质是让调用线程 wait() 在当前线程对象实例上。
源码:
public final void join() throws InterruptedException {
join(0);
}

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()方法其实是让调用线程在当前线程对象上进行等待。当线程执行完成后,被等待的线程会在退出前调用 notifyAll() 通知所有的等待线程继续执行。
因此, 值得注意的一点是:
不要在应用程序中, 在 Thread 对象实例上使用类似 wait() 或者 notify() 等方法, 因为这很有可能会影响系统 API 的工作,或者被系统 API 所影响。

Thread.yield():使当前线程让出CPU。(当前线程在让出 CPU 后,还会进行 CPU 资源的争夺)

volatile关键字
使用volatile声明的变量被修改后,应用程序范围内的所有线程都能够“看到”这个改动,此外, volatile 也能保证数据的可见性和有序性

public class VolatileTest {

static volatile int i = 0;

public static void main(String[] args) throws Exception {
	Thread[] threads = new Thread[10];
	for (int i = 0; i < 10; i++) {
		threads[i] = new Thread(new PlusTask());
		threads[i].start();
	}
	for (int i = 0; i < 10; i++) {
		threads[i].join();
	}
	System.out.println(i);
}
//输出跳动  结果不定, 但大概率小于100000, 因为 i++是线程不安全的操作。
public static class PlusTask implements Runnable {

	@Override
	public void run() {
		for (int k = 0; k < 10000; k++)
			i++;
	}

}

}

线程组

public class ThreadGroupName implements Runnable {

public static void main(String[] args) {
	ThreadGroup tg = new ThreadGroup("PrintGroup");//建立名为PrintGroup的线程组
        //将T1、T2加入到线程组并命名(线程命名最好有意义)
	Thread t1 = new Thread(tg, new ThreadGroupName(), "T1");
	Thread t2 = new Thread(tg, new ThreadGroupName(), "T2");
	t1.start();
	t2.start();
	System.out.println(tg.activeCount());//获得活动线程的总数
	tg.list();//打印线程组的中所有的线程信息
       Thread.sleep(5000);
       tg.stop();//停止线程组所有线程(有Thread.stop()一样的问题))
}

@Override
public void run() {
	String groupAndName = Thread.currentThread().getThreadGroup().getName() + "-"
			+ Thread.currentThread().getName();
	while (true) {
		System.out.println("I am " + groupAndName);
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

}

守护线程(Daemon)
守护线程是一种特殊的线程,它是系统的守护者,在后台默默地完成一些系统性的服务, 比如垃圾回收线程、 JIT 线程就可以理解为守护线程。
用户线程可以认为是系统的工作线程,如果用户线程全部结束,这也意味着这个程序实际上无事可做了。守护线程要守护的对象已经不存在了,那么整个应用程序就自然应该结束。
因此当一个 Java 应用内,只有守护线程时, Java虚拟机就会自然退出。

public class DaemonDemo {

public static class DaemonT extends Thread {
	public void run() {
		while (true) {
			System.out.println("I am alive");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

public static void main(String[] args) throws InterruptedException {
	Thread t = new DaemonT();
	t.setDaemon(true);//将线程t设置为守护线程(必须在线程t  start之前设置,否则主线程会报错,t则正常运行,但会被当做普通线程)
	t.start();

	Thread.sleep(2000);
}

}

线程优先级

在 Java 中,使用 1 到 10 表示线程优先级,数字越大则优先级越高, 但有效范围在 1 到 10 之间。一般可以使用内置的三个静态标量表示:
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

DEMO: 大部分情况下HightPriority 总是比 LowPriority 快(不是所有情况)
public class PriorityDemo {

public static void main(String[] args) throws InterruptedException {
	Thread high = new HightPriority();
	LowPriority low = new LowPriority();
	high.setPriority(Thread.MAX_PRIORITY);
	low.setPriority(Thread.MIN_PRIORITY);
	low.start();
	high.start();
}

public static class HightPriority extends Thread {
	static int count = 0;

	public void run() {
		while (true) {
			synchronized (PriorityDemo.class) {
				count++;
				if (count > 10000000) {
					System.out.println("HightPriority is complete");
					break;
				}
			}
		}
	}
}

public static class LowPriority extends Thread {
	static int count = 0;

	public void run() {
		while (true) {
			synchronized (PriorityDemo.class) {
				count++;
				if (count > 10000000) {
					System.out.println("LowPriority is complete");
					break;
				}
			}
		}
	}
}

}

线程安全与synchronize关键字

synchronized可以用于线程同步,保证线程安全,还可以保证线程间的可见性和有序性

如下DEMO,由于i++是线程不安全的操作,所以输出大概率小于20000000(volatile并不能保证线程安全):
public class AccountingVol implements Runnable {

static AccountingVol instance = new AccountingVol();
static volatile int i = 0;

public static void main(String[] args) throws InterruptedException {
	Thread t1 = new Thread(instance);
	Thread t2 = new Thread(instance);
	t1.start();
	t2.start();
	t1.join();
	t2.join();
	System.out.println(i);
}

public static void increase() {
	i++;
}

@Override
public void run() {
	for (int j = 0; j < 10000000; j++) {
		increase();
	}
}

}

只需如下操作, 即可获得期望值20000000:
for (int j = 0; j < 10000000; j++) {
synchronized (instance) {
increase();
}
}

synchronize关键字用法:
指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。

上述代码,将 synchronized 作用于一个给定对象 instance,因此,每次当线程进入被synchronized 包裹的代码段,就都会要求请求 instance 实例的锁。如果当前有其他线程正持有这把锁,那
么新到的线程就必须等待。这样,就保证了每次只能有一个线程执行 i++ 操作。

//也可以这样,和上面是等价的
public synchronized void increase() {
i++;
}

错误DEMO(t1和t2不是同一个对象,在进入increase()方法时,锁其实是当前线程new 的对象本身,两个线程使用的不是同一把锁,所以线程安全是无法保证的):
public class AccountingSyncBad implements Runnable {

static int i = 0;

public static void main(String[] args) throws InterruptedException {
	Thread t1 = new Thread(new AccountingSyncBad());
	Thread t2 = new Thread(new AccountingSyncBad());
	t1.start();
	t2.start();
	t1.join();
	t2.join();
	System.out.println(i);
}

public synchronized void increase() {
	i++;
}

@Override
public void run() {
	for (int j = 0; j < 10000000; j++) {
		increase();
	}
}

}

但是把increase()方法改为static就可以了, 因为如此的话,两个线程的锁其实就变成了AccountingSyncBad.class。:
public static synchronized void increase() {
i++;
}

并发下的集合DEMO

并发下ArrayList
错误DEMO(可能一:正常结束,输出2000000(并行程序有问题也不会每次都表现出来);可能二:抛出ArrayIndexOuOfBoundException),1000098(因为ArrayList在扩容过程中,内
部一致性被破坏,但是由于没有锁,访问到了不一致的内部状态,导致越界异常;可能三:输出1793758(于多线程访问冲突,使得保存容器大小的变量被多线程不正常的访问,同时两个
线程也同时对 ArrayList 中的同一个位置进行赋值导致的。)):
public class ArrayListMultiThread {

static ArrayList<Integer> al = new ArrayList<Integer>(10);

public static void main(String[] args) throws InterruptedException {
	Thread t1 = new Thread(new AddThread());
	Thread t2 = new Thread(new AddThread());
	t1.start();
	t2.start();
	t1.join();
	t2.join();
	System.out.println(al.size());
}

public static class AddThread implements Runnable {
	@Override
	public void run() {
		for (int i = 0; i < 1000000; i++) {
			al.add(i);
		}
	}
}

}

改进:
可以使用线程安全的集合代替;或者:
for (int i = 0; i < 1000000; i++) {
synchronized (AddThread.class) {
al.add(i);
}
}

并发下的HashMap

错误DEMO:期望输出100000,实际:可能一:100000(原因同ArrayList);可能二:94243(原因同ArrayList);可能三:ClassCastException(cannot be cast to
java.util.HashMap$TreeNode);可能四:无法结束()
public class HashMapMultiThread {
static Map<String, String> map = new HashMap<String, String>();

public static void main(String[] args) throws InterruptedException {
	Thread t1 = new Thread(new HashMapMultiThread.AddThread(0));
	Thread t2 = new Thread(new HashMapMultiThread.AddThread(1));
	t1.start();
	t2.start();
	t1.join();
	t2.join();
	System.out.println(map.size());
}

public static class AddThread implements Runnable {
	int start = 0;

	public AddThread(int start) {
		this.start = start;
	}

	@Override
	public void run() {
		for (int i = start; i < 100000; i += 2) {
			map.put(Integer.toString(i), Integer.toBinaryString(i));
		}
	}
}

}

错误加锁DEMO

public class BadLockOnInteger implements Runnable {

public static Integer i = 0;
static BadLockOnInteger instance = new BadLockOnInteger();

public static void main(String[] args) throws InterruptedException {
	Thread t1 = new Thread(instance);
	Thread t2 = new Thread(instance);
	t1.start();
	t2.start();
	t1.join();
	t2.join();
	System.out.println(i);
}

@Override
public void run() {
	for (int j = 0; j < 10000000; j++) {
		synchronized (i) {
			i++;
		}
	}
}

}

上述DEMO输出10500781远小于20000000,因为Integer在Java里属于不变对象。i++实际执行的是
i=Integer.valueOf(i.intValue()+1);

进一步看Integer.valueOf():
public static Integer valueOf(int i) {
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

所以 i++ 其实每次都是新创建了一个Integer对象,并把它的引用赋值给 i。

可以改为
synchronized (instance) {
i++;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值