Java中程序的执行单元_【java学习】进程、线程、程序

1,进程

1)定义

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

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

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

2)特征

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

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

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

4)进程状态

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

2,线程

1)概念

CPU调度的最小单元,是一种有限的系统资源。 Android主线程为UI线程。 如果主线程执行了很多耗时操作,会造成ANR(应用无响应,Application Not Responding)。

2)特征

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

3)Android中线程

i>主线程(UI线程) ii>子线程(工作线程) 处理耗时操作,如网络请求、IO操作等。 从Android3.0开始,网络访问必须在子线程中进行,否则抛出NetworkOnMainThreadException异常。

注意:不可在子线程更新UI。

对于负责检查线程的:ViewRootImpl,在onCreate时还没有创建,此时子线程中是可以操作UI的;当onStart方法执行后,ViewRootImpl创建,子线程不能更新UI。

4)分类

①守护线程(Daemon Thread)

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

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

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

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

②用户线程(User Thread)

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

5)JVM创建线程

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

6)生命周期

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

①新建(New)

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

MyThread mythread =new MyThread();

②就绪(Runnable)

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

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

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

③运行(Running)

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

④死亡(Dead)

当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。

自然终止:正常运行run()方法后终止 异常终止:调用stop()方法让一个线程终止运行

只有停止所有用户线程,JVM才能正常停止。

thread.stop()(已弃用)

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

run.stop()(已弃用)

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

退出标志(推荐)

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

ThreadDemo.java

package Demo;

import Thread.MyThread;

import Thread.Sync;

public class ThreadDemo {

public static void main(String[] args) {

Thread t = new     MyThread();

t.start();

Sync.set(true);

}

}

MyThread.java

package Thread;

public class MyThread extends Thread{

@Override

public void run() {

while(true){

System.out.println("this is MyThread run()!");

if(Sync.get()){

break;

}

}

}

}

Sync.java

package Thread;

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;

}

}

其中,关于volatile以及synchronized,在多线程并发编程中,synchronized和volatile都是很重要。 详细查看:synchronized和volatile详解

thread.interrupt()(不安全的)

使用interrupt方法来中断线程可分为两种情况

i>线程处于阻塞状态,如使用了sleep方法。 ii>使用while(isInterrupted()){……}来判断线程是否被中断。 在第一种情况下使用interrupt方法,sleep方法将抛出一个InterruptedException例外,而在第二种情况下线程将直接退出。

interrupted() 和isInterrupted()方法的区别

二个方法都是判断线程是否停止的方法。 i>前者是静态方法,后者是非静态方法。 interrupted 是作用于当前正在运行的线程,isInterrupted 是作用于调用该方法的线程对象所对应的线程。(线程对象对应的线程不一定是当前运行的线程。例如我们可以在A 线程中去调用B 线程对象的isInterrupted 方法,此时,当前正在运行的线程就是A 线程。) ii>前者会将中断状态清除而后者不会。

⑤阻塞(blocked)

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

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

7)方法

①sleep()

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

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

Thread.sleep(10000);//占着CPU不工作

②wait()

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

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

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

sleep() 与 wait()的区别

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

③suspend()(已过时)

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

④resume() (已过时)

⑤yield()

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

⑥join方法

join()

join(long millis)

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

⑦getState() 5.0

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

8)线程属性

①线程优先级

一个线程继承父线程的优先级,可以用setPriority(int newPriority)方法设置线程优先级。newPriority范围为:Thread.MIN_PRIORITY(最小优先级,值为1)与Thread.MAX_PRIORITY(最大优先级,值为10)之间。一般使用Thread.NORM_PRIORITY(默认优先级,值为5)优先级。

②守护线程

t.setDaemon(true);

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

9)java中,多线程的实现

有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()方法

i>步骤

(1)创建Callable 接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。 (2)创建Callable 实现类的实例,使用FutureTask 类来包装Callable对象,该FutureTask 对象封装了该Callable 对象的call()方法的返回值。 (3)使用FutureTask 对象作为Thread 对象的target 创建并启动新线程。 (4)调用FutureTask 对象的get()方法来获得子线程执行结束后的返回值。

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{

public String call() throws Exception{

return "Hello World";

}

}

public static void main(String[] args){

ExecutorService threadPool = Executors.newSingleThreadExecutor();

//启动线程

Future 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()方法并没有创建线程,跟普通方法调用一样,是顺序执行的

10)线程计数器

①CyclicBarrier

(见juc)

②CountDownLatch

(见juc)

11)线程的上下文切换

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

12)线程锁

①holdsLock(Object obj)方法

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

13)ThreadLocal类

①概念

无继承无实现,是Thread的局部变量。 该变量的类型是ThreadLocalMap,也就是一个Map,它的键是threadLocal,值是变量的副本。通过ThreadLocal的get()方法可以获取该线程变量的本地副本,在get方法之前要先set,否则就要重写initialValue()方法。

②功能

i>场景:多个线程希望使用相同的初始值,同时要求线程隔离。 ii>功能:保证数据的线程隔离。 ThreadLocal类为每一个线程都维护了自己独有的变量拷贝,重要作用并不在于多线程间的数据共享。 iii>采用哈希表的方式来为每个线程都提供一个变量的副本 利用它的set、get方法,采用哈希表的方式来为每个线程都提供一个变量的副本。

static class ThreadLocalMap {

static class Entry extends WeakReference {

/** 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;

}

③场景

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

14)线程资源

①线程ID

每个线程都有自己的线程ID,这个ID在本进程中是唯一的。进程用此来标识线程。

②寄存器组的值

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

③线程的堆栈

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

④错误返回码

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

⑤线程的信号屏蔽码

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

⑥线程的优先级

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

15)保证线程安全措施

①通过架构设计

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

②保证类无状态

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

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

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

④锁

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

3,程序

1)程序的并发执行特征

①间断性

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

②失去封闭性

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

③不可再现性

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

4,区别

1)进程和线程的区别

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

2)进程和程序区别

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

5,ANR(Application Not Respondin,应用程序无响应)

1)分类

KeyDispatchTimeout(5 seconds) --主要是类型按键或触摸事件在特定时间内无响应 BroadcastTimeout(10 seconds) --BroadcastReceiver在特定时间内无法处理完成 ServiceTimeout(20 secends) --小概率事件 Service在特定的时间内无法处理完成

2)导致ANR操作

在主线程执行以下操作:

高耗时的操作,如图像变换磁盘读写,数据库读写操作大量的创建新对象

3)避免

①UI线程尽量只做跟UI相关的工作

②耗时的操作放在单独的线程处理

比如数据库操作,I/O,连接网络或者别的有可能阻塞UI线程的操作。 使用AsyncTask,在doInBackground()方法中执行耗时操作,在onPostExecuted()更新UI。

③用Handler来处理UIThread和别的Thread之间的交互

使用Handler实现异步任务:

在子线程中处理耗时操作处理完成之后,通过handler.sendMessage()传递处理结果在handler的handleMessage()方法中更新UI 或者使用handler.post()方法将消息放到Looper中

4)监测ANR的Watchdog

LeakCanary

5)FC(Force Close)

①什么时候会出现

Error

OOM,内存溢出

StackOverFlowError

Runtime,比如说空指针异常

②解决的办法

注意内存的使用和管理

使用Thread.UncaughtExceptionHandler接口

6,操作系统

1)处理器调度

①批处理作业调度算法

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

②交互系统进程调度

时间片轮转调度算法

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

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

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

优先级调度算法

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

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

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

2)中断源

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

②自愿性中断

如:时间片到时。

7,其他

1)临界区

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值