【java学习】进程、线程、程序

1,概念

1)程序

程序的并发执行特征:

①间断性

并发执行中的程序因资源限制,状态为“执行-暂停-执行”。

②失去封闭性

由于资源共享及相同写作,打破了程序单道执行时所具有的封闭性。

③不可再现性

并发执行速度不确定,具有随机性,失去了可再现性。
可能发生与时间有关的错误。

2)进程和线程的区别

①划分尺度:线程更小,所以多线程程序并发性更高;
②资源分配&处理器调度:进程是资源分配的基本单位,线程是处理器调度的基本单位。  
③地址空间:进程拥有独立的地址空间;线程没有独立的地址空间,同一进程内多个线程共享其资源;
④ 执行:每个线程都有一个程序运行的入口,顺序执行序列和程序的出口,但线程不能单独执行,必须组成进程,一个进程至少有一个主线程。简而言之,一个程序至少有一个进程,一个进程至少有一个线程。

3)进程和程序区别

①进程是一个动态概念,程序是静态概念。
进程 存在于程序的执行过程中。
②进程具有并发特性,程序没有。
③进程间相互制约,而程序没有。
资源的共享和竞争造成进程相互制约。
④进程与程序之间不是一对一关系。
一个程序执行在不同的数据集上就成为不同的进程,可以用进程控制块来唯一地标识每个进程。一个进程肯定有一个与之对应的程序,而且只有一个。而一个程序有可能没有与之对应的进程**(因为它没有执行),也有可能有多个进程与之对应(运行在几个不同的数据集上)。

4)进程

进程是可并发执行的程序在某个数据集合上的一次计算活动,也是OS进行资源分配和运行调度的基本单位。

运行状态的程序以进程的形态存储在内存中。

进程指一个执行单元,在PC、mobile中指一个程序或者一个应用。一个进程可以包含多个线程。

1>特征

动态性
并发性
独立性(进程是系统中资源分配、保护和调度的基本单位)
异步性
结构性(进程有一定的结构,由程序、数据集合和进程控制块组成)

2>进程控制块(PCB, Process Control Block)

PCB随着进程的创建而建立,随着进程的撤销而取消,PCB是进程存在的唯一标志。PCB是OS用来记录和刻画进程状态及有关信息的数据结构。PCB常驻内存,其包括进程执行时的情况以及进程 让出CPU之后所处的状态、断点等信息。
PCB

3>进程状态

3种状态与转换
3种状态
5种状态与转换
5种状态
7种状态与转换
7种状态

5)线程

线程是 CPU调度的最小单元,是一种有限的系统资源。是程序执行的一种路径。
每个线程都有自己的局部变量表、程序计数器(指向正在执行的程序指令)、各自的生命周期。

1>特征

  1. 线程是进程中的一个相对可独立运行的单元
  2. 线程是操作系统中的基本调度单位,在线程中包含调度所需要的基本信息。
  3. 在具备线程机制的操作系统中,进程不再是调度单位,一个进程中至少包含一个线程,以线程作为调度单位。
  4. 线程自己并不拥有资源,它与同进程中的其他线程共享该进程所拥有的资源。由于线程之间涉及资源共享,所以需要同步机制来实现进程内多线程之间的通信。
  5. 与进程类似,线程还可以创建其他线程,线程也有声明周期,也有状态的变化。

2>分类

①守护线程(Daemon Thread)

用来服务用户线程的,比如说GC线程、内存管理等线程、数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。守护线程则不会影响JVM的正常停止,因此,守护线程通常用于执行一些重要性不是很高的任务,例如监视其他线程的运行情况。
用户线程可以通过System.exit(status)(status为0时表示正常退出,非0表示非正常退出)来退出JVM。

父线程是守护线程子线程默认为守护线程,父线程是用户线程子线程默认为用户线程。父线程在创建子线程后,启动子线程之前,可以调用Thread实例的setDaemon方法来修改线程属性。

当没有用户线程时,即使有守护线程(后台线程),JVM将exit。

通过.setDaemon(true)将线程设置为守护线程,处理一些不重要的事物!

②用户线程(User Thread)

包括常规的用户线程或诸如用于处理GUI事件的事件调度线程,Java虚拟机(JVM)在它所有非守护线程已经离开后自动离开。
具体内容查看:守护进程的实现及进程拉活详解

6)OS处理器调度

①批处理作业调度算法

先来先服务
短作业优先
高响应比调度算法(照顾了短作业,又考虑了作业到达的先后次序,不会使长作业长期得不到服务)

②交互系统进程调度

时间片轮转调度算法

分时系统调度算法,是一种抢占式调度算法。

每个进程只能依次循环轮流运行,如果时间片结束时进程还在运行,CPU将剥夺该进程的使用权转而将CPU分配给另一个进程。如果进程再时间片结束之前阻塞或结束,CPU当即进行切换。

缺点:系统耗费再进程/线程切换上的开销较大,而开销大小与时间片的长短又很大关系。但若时间片太长,每个进程可以再其时间片内完成,该算法退化为先来先服务算法。

优先级调度算法

分为非抢占式优先权调度和抢占式优先调度。

多级反馈队列调度算法(反馈循环队列)

采用动态分配优先数,调度策略是一种抢占式调度方法

7)OS中断源

①强迫性中断

由随机事件引起而非程序员事先安排。
如:
输入/输出中断(设备出差、执行print语句)、
硬件故障中断(断电)、时钟中断、控制台中断、程序性中断。

②自愿性中断

如:时间片到时。

7)临界区

指一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。

2,线程实战

1)JVM创建线程

①创建一个java.lang.Thread类的实例。
②JVM为该线程分配两个调用栈(Call Stack)所需的内存空间。
一个栈用于跟踪Java代码间的调用关系。
一个栈用于跟踪Java代码对本地代码(Native)的调用关系。

2)生命周期

线程生命周期有5种状态:新建(New)、就绪(Runnable)、运行(Running)、死亡(Dead)、阻塞(Blocked)。

1>新建(New)

此线程进入新建状态(未被启动)。

MyThread mythread =new MyThread();

2>就绪(Runnable,可执行状态)

线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中正在等待CPU调度,也就是说此时线程正在就绪队列中排队等候得到CPU资源。

mythread.start();//开启线程的方法

start()和run()的区别
start()方法来启动线程;run()方法并不能启动线程。
start()是启动该线程的方法; run()方法是线程执行的入口。
当调用start方法的时候,该线程就进入就绪状态。等待CPU进行调度执行,此时还没有真正执行线程;当调用run方法的时候,是已经被CPU进行调度,执行线程的主要任务。

3>运行(Running)

线程获得CPU资源正在执行任务(run()方法),此时除非此线程自动放弃CPU资源或者有优先级更高的线程进入,线程将一直运行到结束。

running状态的线程,可能进入如下状态:

  1. 直接Terminated:比如调用stop(已废弃)或者判断某个逻辑标志
  2. 进入Blocked状态:
    比如调用了sleep、或调用wait方法进入waitSet中。
    进行某个阻塞的IO操作;
    获取某个锁资源,从而加入到该锁的阻塞队列;
  3. 进入Runnable状态:
    由于CPU的调度器轮询放弃当前执行;
    线程主动调用yield方法,放弃cpu执行权。

4>阻塞(Blocked)

由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,即进入堵塞状态。直到线程进入就绪状态,才有机会转到运行状态。

阻塞的情况分三种:
  (一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
  (二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
  (三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

Blocked状态的线程,可能进入如下状态:

  1. 直接Terminated:比如调用stop(已废弃)或者意外死亡(JVM Crash)
  2. 进入Runnable状态:
    线程阻塞操作结束;
    线程完成了指定时间的sleep;
    wait中的线程被notify、notifyall唤醒;
    线程获取到某个锁资源;
    线程在阻塞过程中被打断,比如其它线程调用了interrupt方法。

5>死亡(Dead,Terminated)

当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。只有停止所有用户线程,JVM才能正常停止。

下面这些情况将导致线程进入TERMINATED状态:

  1. 线程正常结束
  2. 线程运行出错意外结束
  3. JVM Crash,导致所有线程都结束

如何结束一个线程:

thread.stop()(已弃用)

stop()方法不安全,可能发生不可预料的结果,已被弃用。

run.stop()(已弃用)

run方法执行完后,线程里没有东西就会退出。此方法过于粗暴,废弃。因为大多数情况下,run方法可能永远不能停止,比如用while(true){}。

退出标志(推荐)

run方法里的while(!exit){}, 其中exit为boolean类型,用来控制循环是否结束,此时run可结束,线程即可结束。
在开始定义public volatile boolean exit = false; 其中关键字volatile是为了保证使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值。
举例:
ThreadDemo

public class ThreadDemo {
	public static void main(String[] args) {
		Thread t = new 	MyThread();
		t.start();	
			Sync.set(true);		
	}
}

public class MyThread extends Thread{

	@Override
	public void run() {
		while(true){
			System.out.println("this is MyThread run()!");			
			if(Sync.get()){
				break;
			}		
		}
	}
	
}
public class Sync {
	
	//通过标志来退出
	private static volatile boolean isExit = false;
	//通过加锁,控制每次只有一个线程可以修改这个值
	public static synchronized boolean get() {
		return isExit;
	}
	public static synchronized void set(boolean shouldExit) {
		isExit = shouldExit;
	}
}
进程假死、僵死

进程依然存在,但是没有日志输出、也不作业。
一般出现原因是某个线程被阻塞,或者某个线程死锁。

3)方法

1>sleep()

线程休眠:一个睡眠着的线程在指定的时间过去可进入就绪状态。

sleep()是Thread类的方法,执行此方法会导致当前线程暂停执行,进入TIMED_WAITING状态,将执行机会给其他线程,但是监控状态依然保持。所以调用sleep()不会释放对象锁,但是会让出CPU。

Thread.sleep(10000);
//JDK1.5以后,通过TimeUnit可以优雅的使用sleep方法:同理有MINUTES SECOUNDS MICROSECONDS
TimeUnit.HOURS.sleep(3);

2>wait()

wait()方法是Object类方法,一个对象调用wait()会导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法或notifyAll后本线程才获得对象锁进入运行状态。
线程等待:

wait(10000);//线程进入等待状态,等待使用CPU,不占用任何资源。

wait()用notify唤醒回到就绪状态,或等待时间超时时回到就绪状态。如果有多个线程在此同步监视器上等待,则会唤醒其中的一个。
正在等待:调用wait()方法。(调用notify()方法)
wait() 和notify()必须在synchronized函数或synchronized block中进行调用。

sleep() 与 wait()的区别
  1. 这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object类;
  2. sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
    sleep不出让系统资源;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制, 因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。
  3. wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而
    sleep可以在任何地方使用
  4. Sleep需要捕获异常,而wait不需要

3>suspend()(已过时)

被另一个线程所阻塞:调用suspend()方法。(调用resume()方法恢复)

4>resume() (已过时)

5>yield()

public static native void yield()

使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
注意:
yield()只能使同优先级或更高优先级的线程有执行的机会。

6>join方法

public final void join();
public final synchronized void join(long millis);

等待该线程终止。等待调用join方法的线程结束,再继续执行。
如:t.join();主要用于等待t线程运行结束,若无此句,main则会执行完毕,导致结果不可预测。
有三个线程T1,T2,T3,用join方法可以确保它们按顺序执行。

7>getState() 5.0

得到线程状态。
即NEW、 RUNNABLE、 BLOCKED、 WAITING、 TIMED_WAITING、 TERMINATED之一。

8>currentThread()

获得当前线程

Thread.currentThread().interrupt();

9>interrupt()、interrupted()、isInterrupted()

public void interrupt()

其作用是中断此线程(此线程不一定是当前线程,而是指调用该方法的Thread实例所代表的线程),但实际上只是给线程设置一个中断标志,线程仍会继续运行。

public static boolean interrupted()

作用是测试当前线程是否被中断(检查中断标志),返回一个boolean并清除中断状态,第二次再调用时中断状态已经被清除,将返回一个false。

public boolean isInterrupted()

作用是只测试此线程是否被中断 ,不清除中断状态。

4)线程属性

①线程优先级

一个线程继承父线程的优先级,可以用setPriority(int newPriority)方法设置线程优先级;getPriority()方法获取优先级。
newPriority范围为:Thread.MIN_PRIORITY(最小优先级,值为1)Thread.MAX_PRIORITY(最大优先级,值为10)之间。一般使用Thread.NORM_PRIORITY(默认优先级,值为5)优先级。
注意:不要让业务依赖线程优先级来实现,一般线程使用过程中使用默认优先级。

②守护线程

t.setDaemon(true);

守护线程唯一的作用是给其他线程提供服务,如计时线程(发送计时信号给其他线程或清空过时的高速缓存项的线程)。
如果只剩下守护线程,JVM就退出了。
注意:守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候深圳在一个操作的中间发生中断。

5)线程的实现

有3种方式。

①实现Runnable接口,并实现接口的run()方法。

i>步骤

①定义类实现Runnable接口
②覆盖Runnable接口中的run方法
③通过Thread类建立线程对象
④将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数
⑤调用Thread类的start方法开启线程并调用Runnabe接口子类的run方法

class MyThread implements Runnable{
	public void run(){
	}
}
public class Test{
	public static void main(String[] args){
		MyThread thread = new MyThread();
		Thread t = new Thread(thread);
		t.start();
	}
}
ii>实现Runnable接口相比继承Thread类好处

①避免单继承的局限,一个类可以继承多个接口。
②适合于资源的共享。
关于资源的共享,实现Runnable,线程代码存在接口的子类的run方法中。而继承Thread,线程代码存放Thread子类run方法中。

②继承Thread类,重写run方法

class MyThread extentds Thread{
	public void run(){
	}
}

public class Test{
	public static void main(String[] args){
		MyThread thread = new MyThread();
		thread.start();
	}
}

③实现Callable接口,重写call()方法,通过FutureTask包装Callable对象

i>步骤
  1. 创建Callable 接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
  2. 创建Callable 实现类的实例,使用FutureTask 类来包装Callable对象,该FutureTask 对象封装了该Callable 对象的call()方法的返回值。
  3. 使用FutureTask 对象作为Thread 对象的target 创建并启动新线程。
  4. 调用FutureTask 对象的get()方法来获得子线程执行结束后的返回值
    FutureTask是Future接口的一个实现,它实现了一个可以提交给Executor执行的任务,并且可以用来检查任务的执行状态和执行结果。
    get方法会阻塞直到所有结果都好了,如果想取消调用cancel方法。
package CallableAndFuture;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CallableAndFuture{
	//线程类
	public static class CallableTest implements Callable<String>{
		public String call() throws Exception{
			return "Hello World";
		}
	}
	
	public static void main(String[] args){
		ExecutorService threadPool = Executors.newSingleThreadExecutor();
		//启动线程
		Future<String> future = threadPool.submit(new CallableTest());
		try{
			System.out.println("waiting thread to finish");
			System.out.println(future.get());//等待线程结束,并获取返回结果
		}catch(Exception e){
			
			System.out.println("Exception:" + e.getMessage());
		}
	}
}

ii>Callable与Runnable区别

Callable对象是属于Executor框架中的功能类,与Runnable接口类似,但功能更加强大:
①Callable可以在任务结束后提供一个返回值,Runnable不可以。
②Callable中的call()方法可以抛出异常(有返回值),而Runnable的run()方法不能抛出异常。
③运行Callable可以拿到一个Future对象,Future对象表示异步计算的结果。它提供了检查计算是否完成的方法。由于线程属于异步计算模型,所以无法从其他线程中得到方法的返回值,通过使用Future来监视目标线程调用call()方法的情况,当跳跃Futured的get()方法获取结果时,当前线程就会阻塞,直到call()方法结束返回结果。

④通过线程池创建线程

利用线程池不用new 就可以创建线程,线程可复用,利用Executors 创建线程池。

⑤Java 创建线程之后,直接调用start()方法和run()的区别

i>start()方法来启动线程
会在新线程中运行run()方法,真正实现了多线程运行。
这时无需等待run 方法体代码执行完毕,可以直接继续执行下面的代码;通过调用Thread 类的start()方法来启动一个线程,这时此线程是处于就绪状态,并没有运行,然后通过此Thread 类调用方法run()来完成其运行操作,这里方法run()称为线程体,它包含了要执行的这个线程的内容,run()方法运行结束,此线程终止。然后CPU 再调度其它线程。
ii>直接调用run()方法的话,会把run()方法当作普通方法来调用,会在当前线程中执行run()方法,而不会启动新线程来运行run()方法。程序还是要顺序执行,要等待run 方法体执行完毕后,才可继续执行下面的代码; 程序中只有主线程——这一个线程, 其程序执行路径还是只有一条, 这样就没有达到多线程的目的。
如下:假线程

public class Example extends Thread{
     @Override
     public void run(){
        try{
             Thread.sleep(1000);
             }catch (InterruptedException e){
             e.printStackTrace();
             }
             System.out.print("run");
     }
     public static void main(String[] args){
             Example example=new Example();
             example.run();
             System.out.print("main");
     }
}

这个类虽然继承了Thread类,但是并没有真正创建一个线程。
创建一个线程需要覆盖Thread类的run方法,然后调用Thread类的start()方法启动
这里直接调用run()方法并没有创建线程,跟普通方法调用一样,是顺序执行的

6)线程计数器

CyclicBarrier

(见juc)

CountDownLatch

(见juc)

7)线程的上下文切换

对于单核CPU,CPU 在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换(对于进程也是类似)。
线程上下文切换过程中会记录程序计数器、CPU 寄存器的状态等数据。虽然多线程可以使得任务执行的效率得到提升,但是由于在线程切换时同样会带来一定的开销代价,并且多个线程会导致系统资源占用的增加,所以在进行多线程编程时要注意这些因素。

8)线程锁

①holdsLock(Object obj)方法

java.lang.Thread 中的方法,返回true表示当前线程拥有某个具体对象的锁。

9)ThreadLocal类

java.lang.ThreadLocal

1>概念

  1. 线程内共享
  2. 线程间互斥

2>原理

Thread类中有一个局部变量用于保存数据,这个变量是ThreadLocalMap类型。
ThreadLocalMap类型是一个map,k-v结构。k是每个threadLocal,值是变量的副本,采用哈希表的方式来为每个线程都提供一个变量的副本。

3>常见方法

  1. initialValue
    返回当前线程局部变量的初始值;
  2. get
    获取当前线程局部变量的值;如果是第一次调用,创建并初始化此副本。
  3. set
    设定此线程的值;
  4. remove
    移除此线程局部变量的值;
    使用时注意不用变量时进行remove,以免造成内存泄漏。为了防止数据污染,对于ThreadLocal在线程池场景使用前或者使用后一定要先remove。
static class ThreadLocalMap {

        static class Entry extends WeakReference<ThreadLocal> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;
} 

4>场景

  1. 数据库连接:
    在多线程中,如果使用懒汉式的单例模式创建Connection对象,由于该对象是共享的,那么必须要使用同步方法保证线程安全,这样当一个线程在连接数据库时,那么另外一个线程只能等待。这样就造成性能降低。如果改为哪里要连接数据库就来进行连接,那么就会频繁的对数据库进行连接,性能还是不高。这时使用ThreadLocal就可以既可以保证线程安全又可以让性能不会太低。但是ThreadLocal的缺点时占用了较多的空间。
    如: 通过hibernate的例子,可以看出这个session在不同线程中是不同的,但是在同一个线程中是相同的。可以直接通过getSession获取实例,所以避免了参数传递,实现线程内共享。

10)线程资源

①线程ID

每个线程都有自己的线程ID,这个ID在本进程中是唯一的。getId()

②寄存器组的值

由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线程切换到另一个线程上时,必须将原有的线程的寄存器集合的状态保存,以便将来该线程在被重新切换到时能得以恢复。

③线程的堆栈

堆栈是保证线程独立运行所必须的。
线程函数可以调用函数,而被调用函数中又是可以层层嵌套的,所以线程必须拥有自己的函数堆栈,使得函数调用可以正常执行,不受其他线程的影响。

④错误返回码

由于同一个进程中有很多个线程在同时运行,可能某个线程进行系统调用后设置了errno值,而在该线程还没有处理这个错误,另外一个线程就在此时被调度器投入运行,这样错误值就有可能被修改。
所以,不同的线程应该拥有自己的错误返回码变量。

⑤线程的信号屏蔽码

由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自己管理。但所有的线程都共享同样的信号处理器。

⑥线程的优先级

由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参数,这个参数就是线程的优先级。

11)保证线程安全措施

①通过架构设计

通过上层的架构设计和业务分析来避免并发场景。
比如需要用多线程或分布式集群统计一堆用户的相关统计值,由于用户的统计值是共享数据,因此需要保证线程安全。从业务上分析出用户之间的数据并不共享,因此可以设计一个规则来保证一个用户的计算工作和数据访问只被一个线程或一台机器完成,这样从设计上避免了接下来可能的并发问题。

②保证类无状态

有状态会限制横向扩展能力,也可能产生并发问题。如果类是无状态的,那它永远是线程安全的。因此在设计阶段尽可能用无状态的类来满足业务需求。

③区别原子操作和复合操作

常见的复合操作包括check-then-act, i++等。
虽然check-then-act从表面上看很简单,但却普遍存在与我们日常的开发中,特别是在数据库存取这一块。比如我们需要在数据库里存一个客户的统计值,当统计值不存在时初始化,当存在时就去更新。如果不把这组逻辑设计为原子性的就很有可能产生出两条这个客户的统计值。
在单机环境下处理这个问题还算容易,通过锁或者同步来把这组复合操作变为原子操作,但在分布式环境下就不适用了。一般情况下是通过在数据库端做文章,比如通过唯一性索引或者悲观锁来保障其数据一致性。当然任何方案都是有代价的,这就需要具体情况下来权衡。

④锁

使用锁应注意:
每个共享变量必须由一个确定的锁保护。
使用锁会有性能损失
锁不能解决在分布式环境共享变量的并发问题。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值