文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Java多线程小记,你学会了吗?

2024-12-13 15:29

关注

代码按照调用顺序依次往下执行不会出现代码交替运行的就叫做单线程程序,实现代码交替运行效果的叫做多线程程序。多线程程序运行时每个线程之间都是独立的,可以并发执行。虽然称可以并发执行但是实际上线程和进程一样都是由CPU控制轮流执行的,只是CPU的速度快让人感觉是同时执行的感觉。所以说多线程交替执行代码。

二、创建线程

java对多线程的支持主要有三种方法:

  1. 继承Thread类,重写run方法。(看起来和python的重写Threading挺像)
  2. 实现Runnable接口,重写run方法。
  3. 实现Callable接口,重写call方法并使用Future可以获取call方法的返回值。

1、Thread类

Thread位于 java.lang包。

优势:代码简单

缺陷:一个类只能继承一个父类,不利于代码拓展,不能获取线程的返回值。

主要步骤:

  1. 创建类并继承Thread,同时重写run方法。
  2. 创建子类的对象,调用start方法启动线程

代码示例:


publicclassMyThreadextendsThread{ //继承Thread
Stringname="";
publicMyThread(Stringname){
this.name=name;
}
publicvoidrun(){ //重写run方法,run方法的代码就是每个线程要执行的内容。
for(inti=1;i<=5;i++){
System.out.println("[+]> "+this.name+" <[+]");
}
}
}


publicclassTestThread{
publicstaticvoidmain(String[] args) {
MyThreadt1=newMyThread("t1");
t1.start();
MyThreadT2=newMyThread("T2");
T2.start();

}
}


//输出的顺序并没有按照我们写代码的顺序。

从输出内容看有两个线程在交互运行,但实际上运行这段代码之后会产生一个!进程 !,一个java的进程,这个java进程里面包含三个线程,其中两个就是我们定义的t1、T2,还有一个由main方法开启的主线程,只不过启动了两个线程实例时候没有做其他动作。

2、Runnable接口

优势:代码简单,不用继承。

缺陷:不能获取线程的返回值。

主要步骤:

  1. 创建Runnable接口的实现类并重写run方法
  2. 创建Runnable接口实现类的对象
  3. 使用Thread类创建线程实例,并传入Runnable接口实现类的对象
  4. 调用Thread实例的run方法

代码示例:


publicclassMyThreadimplementsRunnable{
Stringname="";
publicMyThread(Stringname){
this.name=name;
}
publicvoidrun(){
for(inti=1;i<=5;i++){
System.out.println("[+]> "+this.name+" <[+]");
}
}
}

publicclassTestThread{
publicstaticvoidmain(String[] args) {
MyThreadr1=newMyThread("t1");
MyThreadr2=newMyThread("t2");
Threadt1=newThread(r1);
Threadt2=newThread(r2);
t1.start();
t2.start();
}
}

因为run方法是Runnable接口唯一的抽象方法,Runnable就属于函数式接口,可以使用Lambda表达式来实现Runnable的线程实例。


publicclassTestThread{
publicstaticvoidmain(String[] args) {
Runnabler3=()->{
for(inti=1;i<=5;i++){
System.out.println("[+]> t3 <[+]");
}
};
Threadt3=newThread(r3);
t3.start();
}
}

3、Callable接口

优势:代码简单,不用继承,有返回值

主要步骤:

  1. 创建Callable接口实现类,并重写Callable接口的call方法
  2. 创建Callable接口实现类的对象
  3. 使用FutureTask类的有参构造方法封装Callable接口实现类对象
    FutureTask类:
    Callable接口实现多线程时靠FutureTask类来封装和管理返回值。FutureTask的父接口是RunnableFuture,是Runnable和Future的结合。FutureTask实现RunnableFuture接口,RunnableFuture接口又继承Runnable接口和Future接口。所以FutureTask本质是Runnable接口和Future接口的实现类。
    获取返回值:
Vget() //等待计算完成,然后检索其结果,这个会方法会发生阻塞,直到任务执行完毕才会返回。
Vget(longtimeout, TimeUnitunit) //在指定时间内获取执行结果。 指定时间内未取到结果就返回null
  1. 传入FutureTask对象创建Thread线程实例
  2. 调用Thread线程实例start方法

代码示例:


importjava.util.concurrent.*;
// 1-创建Callable实现类并重写call方法
publicclassMyThreadimplementsCallable<Object>{
Stringname="";
publicMyThread(Stringname){
this.name=name;
}
publicObjectcall() throwsException{
intj=0;
for(inti=1;i<=5;i++){
j=j+i;
System.out.println("[+]> "+this.name+" <[+]");
}
returnj;
}
}


importjava.util.concurrent.*;
publicclassTestThread{
publicstaticvoidmain(String[] args) throwsException{
MyThreadm1=newMyThread("m1");
MyThreadm2=newMyThread("m2");
FutureTask<Object>ft1=newFutureTask<>(m1);
FutureTask<Object>ft2=newFutureTask<>(m2);
Threadt1=newThread(ft1);
Threadt2=newThread(ft2);
t1.start();
System.out.println("t1返回: "+ft1.get());
t2.start();
System.out.println("t2返回: "+ft2.get());
}
}

Callable接口实现类和Runnable接口一样都要使用Thread类来实现多线程,不同的是,Callable传入的是!Runnable的子类 !FutureTask的实例对象,而我们在FutureTaskft1 = new FutureTask<>(m1);这一步时把Callable接口实现类给封装进来,这样Callable接口实现类就可以实现返回值了。

4、总结

尽量采用Runnable或者Callable来实现多线程操作。因为相对于Thread,Runnable和Callable有以下几点优势:

  1. 避免java单继承机制的局限性。
  2. Runnable和Callable更合适处理一个共享资源的情况,把线程和程序的代码、数据有效分离。
    举例说明:
    有5张票在售类比一个被多线程共享的资源,一共有两个售票窗口类比多线程,使用Thread会出现以下情况:

publicclassMyThreadextendsThread{
privateintts=5;
Stringname="";
publicMyThread(Stringname) {
this.name=name;
}
@Override
publicvoidrun() {
while(this.ts>0){
System.out.println(this.name+" ===> 在卖第 "+this.ts+" 张票");
this.ts--;
}
}
}

publicclassTestThread{
publicstaticvoidmain(String[] args) {
Threadt1=newMyThread("窗口AAAAA");
Threadt2=newMyThread("窗口VVVVV");
t1.start();
t2.start();
}
}

每张票都被卖了两次,这显然不合理。如果使用Runnable或者Callable,就可以使用同一个实现类创建两个Thread线程实例,二者访问的都是同一个资源就不会出现Thread的情况。

publicclassMyThreadimplementsRunnable{
privateintts=5;
@Override
publicvoidrun() {
while(this.ts>0){
System.out.println(Thread.currentThread().getName()+" ===> 在卖第 "+this.ts+" 张票");
this.ts--;
}
}
}
publicclassTestThread{
publicstaticvoidmain(String[] args) {
MyThreadm1=newMyThread();
// Thread(Runnable target, String name) 分配一个新的Thread对象。name是自定义的线程名。
Threadt1=newThread(m1,"窗口AAAAA");
Threadt2=newThread(m1,"窗口VVVVV");
t1.start();
t2.start();
}
}

三、后台线程

对于java程序而言,只要有一个前台线程在运行那么这个进程就不会结束,相反的如果一个进程只有后台线程运行,那么这个进程就会结束。新创建的线程默认都是前台线程,如果在某个线程启动(调用start方法)之前调用setDaemon(true)语句,就可以把这个线程设置为后台线程。

代码示例:

publicclassMyThreadextendsThread{
@Override
publicvoidrun() {
while(true){
System.out.println("这里是thread线程");
}
}
}

publicclassTestThread{
publicstaticvoidmain(String[] args) {
System.out.println("main方法的主线程是否为后台线程 : "+Thread.currentThread().isDaemon());
Threadt1=newMyThread();
System.out.println("子线程他t1是否为后台线程 : "+t1.isDaemon());
t1.setDaemon(true);
System.out.println("子线程他t1是否为后台线程 : "+t1.isDaemon());
t1.start();
}
}

四、线程的生命周期

1、NEW 新建状态

和其他java对象一样,由jvm分配了内存,还是不能运行,没有表现出任何线程的动态特征。

2、RUNNABLE 可运行

新建状态下的线程对象调用start方法。内部细分为两种,线程可以在二者之间相互转换。

READY 就绪:线程对象调用start方法之后等待JVM调度,并未运行。

RUNNING 运行:获得JVM调度,如果由多个CPU就允许多个线程并行运行。

3、BLOCKED 阻塞

处于运行状态的线程失去CPU执行权从而暂停运行进入阻塞状态,此时JVM不会给它分配CPU,直到进入就绪状态。

线程一般在以下情况会阻塞:

  • 线程A运行中试图获取同步锁时,却被线程B获取,此时JVM把A存到对象的锁池,A进入阻塞。
  • 线程运行过程中发出I/O请求时

4、WATING 等待

处于运行的线程调用了无时间参数限制的方法(wait、join...)就进入等待状态。处于等待的线程不能争夺CPU使用权,必须等待其他线程执行特定操作之后才可以继续争夺cpu使用。

5、TIMED_WAITING 定时等待

运行线程调用了有时间参数限制的方法(sleep...),处于定时的等待的线程也不能立即争夺CPU使用权。

6、TERMINATED 终止

线程的run、call方法正常执行完毕或者线程抛出一个未捕获的异常、错误,都会导致线程进入终止。进入终止之后就没有运行资格,不能转换到其他状态,声明周期结束。

五、线程调度

定义:程序中的多线程时并发执行的,却不是在统一时间执行的。若想被执行就需要获得CPU使用权。JVM会按照特定的机制为程序中的每个线程分配CPU使用权,这种机制叫线程的调度。

分时调度模型:让所有线程轮流获得cpu使用权,平均分配每个线程占用cpu的时间篇。

抢占式调度模型:让可运行池中所有就绪的线程争夺cpu使用权。优先级高的线程获取cpu使用权的概率大于优先级低的线程。

JVM默认采用抢占式调度模型。

5.1、线程的优先级

对线程进行调度最简单的方式就是设置线程的优先级。线程优先级使用1~10之间的整数表示,数字越大优先级越高。还可以使用Thread类中的三个静态常量表示:

static int MAX_PRIORITY   //最高级,=10
static int MIN_PRIORITY //最低级,=1
static int NORM_PRIORITY //普通级,=5,main方法就是普通优先级。

修改线程的优先级

setPriority(int newPriority)

代码示例:

public class TestThread {
public static void main(String[] args) {
Thread t1 =new Thread(()->{
for (int i=0;i<=5;i++){
System.out.println("高级输出: "+i);
}
});

Thread t2 = new Thread(()->{
for (int i=0;i<=5;i++){
System.out.println("低级输出: "+i);
}
});

t1.setPriority(10);
t2.setPriority(5);
t2.start();
t1.start();
}
}

5.2、sleep 线程休眠

Thread类提供了一个静态方法sleep,可以让线程进入定时等待。sleep方法会抛出InterruptedException。

代码示例:

public class TestThread {
public static void main(String[] args) throws Exception{
Thread t1 =new Thread(()->{
for (int i=0;i<=5;i++){
//使用异常处理调用sleep并捕获异常
if (i==2){
try{
Thread.sleep(500);
}catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.println("高级输出: "+i);
}
});

Thread t2 = new Thread(()->{
for (int i=0;i<=5;i++){
System.out.println("低级输出: "+i);
}
});

t1.setPriority(10);
t2.setPriority(2);
t2.start();
t1.start();
}
}

5.3、yield 线程让步

yield方法与sleep不同,yield不会阻塞进程,它只是将运行状态的线程转换为就绪状态使其被重新调度一次。java采用的抢占式调度,不能保证让步后立即就执行其他线程。

static native void yield()

代码示例:

public class TestThread {
public static void main(String[] args) throws Exception{
Thread t1 =new Thread(()->{
for (int i=0;i<=5;i++){
if (i==2){
Thread.yield(); //调用yield方法
}
System.out.println("高级输出: "+i);
}
});

Thread t2 = new Thread(()->{
for (int i=0;i<=5;i++){
System.out.println("低级输出: "+i);
}
});

t1.setPriority(10);
t2.setPriority(2);
t2.start();
t1.start();
}
}

5.4、join 线程插队

在线程中调用其他线程的join方法时,此线程会被阻塞知道join的线程被执行完成。

final void join() 
final void join(long millis)

代码示例:

public class TestThread {
public static void main(String[] args) throws Exception{
Thread t1 =new Thread(()->{
for (int i=0;i<=5;i++){
if (i==2){
Thread.yield();
}
System.out.println("t1: "+i);
}
});
t1.setPriority(2);
t1.start();
for (int i=0;i<=5;i++){
if (i==2){
t1.join();
}
System.out.println("主线程输出 : "+i);
}
}
}

六、多线程同步

线程安全问题:当多个线程同时去访问同一个资源时会导致一些安全问题,比如线程A访问资源c时,资源c的值为1,之后线程A休眠了500毫秒,在此休眠期间线程B去访问了资源c,导致资源c的值变成0,这是休眠结束的线程A再去执行时资源c的值已经变化。为了解决这样的问题就出现了多线程同步。

多线程同步:限制某个资源在同一时刻只能被一个线程访问。

6.1、synchronized 同步代码块

同步代码块是多线程同步的手段之一,当多个线程使用同一个资源时,将处理资源的代码放置在一个用synchronized关键字修饰的代码块中,这段代码块叫做同步代码块。

synchronized(lock){  //lock : 是一个锁对象,默认为1
//操作资源的代码
}

原理:同步代码块的关键在于lock,lock是一个锁对象,只有lock的标志位为1时线程才能执行同步代码块。当线程执行到同步代码块时先检查lock标志位,默认为1,线程会执行同步代码块同时lock的值置为0,这时其他线程就发生阻塞不能执行同步代码块中的代码。等线程执行完同步代码块之后lock又被置为1,以此循环往复。

重点:锁对象的类型可以时任意类型,但是各个线程使用的锁对象必须是同一个。也就是创建锁对象的代码不可以写在run方法,否则每个线程运行都会创建一个自己的锁对象,形同虚设。

代码示例:


class MyThread implements Runnable{
private int i=5;
Object lock = new Object();
@Override
public void run() {
while (this.i>0){
synchronized (lock){
if (this.i>0){
try{
Thread.sleep(500);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"在卖第"+this.i+"张票");
}
this.i--;
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread myt = new MyThread();
Thread t1 = new Thread(myt,"窗口1");
Thread t2 = new Thread(myt,"窗口2");
t1.start();
t2.start();
}
}

class MyThread2 implements Runnable{
private int i=5;
Object lock = new Object();
@Override
public void run() {
while (this.i>0){
if (this.i>0){
try{
Thread.sleep(500);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"在卖第"+this.i--+"张票");
}
}
}
}

public class ThreadTest2 {
public static void main(String[] args) {
MyThread2 myt2 = new MyThread2();
Thread t1 = new Thread(myt2,"窗口1");
Thread t2 = new Thread(myt2,"窗口2");
t1.start();
t2.start();
}
}

小结:

对比上面两段代码的输出结果,在使用synchronized 时,多运行几次就可以看到输出结果可能会有0,或者同一张票在两个窗口都被卖了一次。相较于使用synchronized 时,可以发现输出都是同一个窗口在卖,因为在一个线程进入同步代码块之后另一个线程就阻塞了,即使当前线程使用sleep休眠但是lock标志位任然还在0所以另一个线程无法进入执行。

6.2、synchronized 同步方法

synchronized 不仅可以修饰代码块,也可以修饰方法。

使用:定义同步方法,然后再创建线程对象的run方法中直接调用同步方法就行。

[修饰符] synchronized [返回值类型] 方法名(){

}

原理:再使用同步代码块时需要定义锁对象,而使用同步方法就没有这样的问题。同步方法和同步代码块的原理一样,只不过同步方法的锁对象就是调用该方法的对象,就是this指向的对象。同步方法被所有线程共享,同步方法所在的对象相对于所有线程而言是唯一的,当一个线程进入同步方法时其他线程也不能进入同步方法执行了。

synchronized 总结:

synchronized 使用一种封闭式锁机制,优点是使用起来非常简单,同时也有一些缺点,例如无法中断正在等候获得锁的线程,无法通过轮询获得锁等等。

6.3、Lock 同步锁

JDK5开始增加了Lock 锁,Lock 锁功能与synchronized 基本相同,但是Lock锁可以让线程再持续获得锁失败之后不再继续等待,使用上也比synchronized 更灵活。

使用:

  1. 导入 import java.util.concurrent.locks.*;
  2. 使用Lock的实现类ReentrantLock创建一个锁对象
  3. 使用lock方法和unlock方法进行上锁和解锁

代码示例:

import java.util.concurrent.locks.*;

class MyThread implements Runnable{
private int i=5;
private final Lock lk = new ReentrantLock(); //使用Lock的实现类ReentrantLock创建一个锁对象
@Override
public void run() {
while (this.i>0){
lk.lock(); //上锁:此时只有当前线程可以使用了
if (this.i>0){
try{
Thread.sleep(500);
lk.unlock(); //解锁:之后其他线程也可以访问
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"在卖第"+this.i--+"张票");
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread myt = new MyThread();
Thread t1 = new Thread(myt,"窗口1");
Thread t2 = new Thread(myt,"窗口2");
t1.start();
t2.start();

}
}

关于死锁问题:

两个正在运行的线程都在等待对方的锁,从而造成程序的停滞现象称为死锁。两个线程都需要对方占用的锁,但是二者又无法释放自己拥有的锁,于是双方都处于挂起状态。

七、多线程通信

为了控制多个线程按照一定的顺序轮流执行,就需要让线程之间进行通信保证线程任务协调进行。

线程通信常用方法:

这些方法位于Object类中可以直接使用。

final void wait() // 使当前线程放弃同步锁进入等待,直到其他线程进入此同步锁并且调用notify、notifyAll唤醒为止
final void notify() // 唤醒此同步锁上等待的第一个调用wait方法的线程
final void notifyAll() // 唤醒此同步锁上等待的所有调用wait方法的线程

代码示例:

// 厂家线程生产商品commodity数组加一个元素,商家线程卖出商品commodity删除一个元素。
import java.util.ArrayList;
import java.util.List;

class ThreadTest{
public static void main(String[] args) {
List<Object> commodity = new ArrayList<>(); // 定义数组存放商品,也作为锁对象
Long startTime = System.currentTimeMillis(); // 开始运行的时间
Thread t1 = new Thread(()->{
int num = 0; // 用于商品编号,无甚意义,下同。
while (System.currentTimeMillis()-startTime<=100){ // 运行100毫秒,下同。
synchronized (commodity){
if (commodity.size()>0){ // 意味着已经有商品存在
try{
commodity.wait(); // 已经生产了一些商品,厂家线程进入等待直到商家把这个商品卖出
}catch (InterruptedException e){
e.printStackTrace();
}
}else{
num++;
commodity.add("商品"+num);
System.out.println("生产商品"+num);
}
}
}
},"厂家");

Thread t2 = new Thread(()->{
int num = 0;
while (System.currentTimeMillis()-startTime<=100){
synchronized (commodity){
if (commodity.size()<=0){ // 意味着没有商品了
commodity.notify(); // 唤醒厂家继续生产
}else {
num++;
commodity.remove("商品"+(num));
System.out.println("商家卖出商品"+num);
}
}
}
},"商家");

t1.start();
t2.start();
}
}

八、线程池

线程对象的使用需要消耗大量的内存,使用线程池来创建多线程可以进一步优化线程管理。java主要提供了一个接口和一个类来实现线程池管理。

1、Executor 接口实现线程池管理

JDK5开始在 java.util.concurrent 包下增加了Executor 接口及其子类。

使用:

  1. 创建一个Runnable接口或者Callable接口的实现类,同时重写run或者call方法。
  2. 创建实现类的对象
  3. 创建线程池
// 创建一个线程池,根据需要创建新线程,但在可用时将重用以前构造的线程。
static ExecutorService newCachedThreadPool()
// 创建一个固定线程数量的线程池。
static ExecutorService newFixedThreadPool(int nThreads)
// 创建一个只执行一个任务的单线程的线程池
static ExecutorService newSingleThreadExecutor()
// 创建一个线程池,可以安排命令在给定延迟后运行,或定期执行。
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)
  1. 使用Executor 的子接口ExecutorService下的submit方法将实现类对象提交到线程池。
  2. 使用shutdown方法关闭线程池

代码示例:

import java.util.concurrent.*;
class Mythread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
public class TheadTest {
public static void main(String[] args) {
Mythread mt = new Mythread();
// 创建线程池 thpool
ExecutorService thpool = Executors.newCachedThreadPool();
// 提交10个线程任务到线程池
for (int i=0;i<10;i++){
thpool.submit(mt);
}
// 关闭线程池
thpool.shutdown();
}
}

2、CompletableFuture 类实现线程管理

使用Callable接口实现多线程时会用到FutureTask类对线程的返回值进行管理,由于FutureTask在获取返回结果时是通过阻塞或者轮询的方式进行耗费太多的资源。JDK8中对FutureTask增加了一个 !函数式异步编程辅助类CompletableFuture !该类同时实现Future接口和CompletionStage接口(JAVA8新增的线程任务完成结果接口)。

获取CompletableFuture 对象:

// 返回一个新的 CompletableFuture,它在运行给定操作后由ForkJoinPool.commonPool()中运行的任务异步完成。 
static CompletableFuture<Void> runAsync(Runnable runnable)
// 返回一个新的 CompletableFuture,它在运行给定操作后由给定执行程序executor中运行的任务异步完成。
static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
// 返回一个新的 CompletableFuture,它由在ForkJoinPool.commonPool()中运行的任务异步完成,其值通过调用给定的供应商获得。
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
// 返回一个新的 CompletableFuture,它由在给定执行程序中运行的任务异步完成,其值通过调用给定供应商获得。
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

runAsync 和 supplyAsync 的本质区别在于获取的 CompletableFuture 对象是否带有计算结果(类似Runnable和Callable的区别)。带有 Executor 参数的方法指定传入的线程池执行器来执行多线程,没有的默认使用ForkJoinPool.commonPool() 进行线程池的多线程管理。

代码示例:

import java.util.concurrent.*;
class ThreadTest{
public static void main(String[] args) throws Exception{
CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(()->{
int sum=0;
while (sum<5){
sum++;
System.out.println("这是cf1,sum="+sum);
}
return sum;
});

CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(()->{
String str="";
for (int i=6;i<=10;i++){
str=str+i;
System.out.println("这是cf2,str="+str);
}
return str;
});

System.out.println("cf1 返回 : "+cf1.get());
System.out.println("cf2 返回 : "+cf2.get());
}
}


免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 资料下载
  • 历年真题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容
咦!没有更多了?去看看其它编程学习网 内容吧