点击上方“方志朋”,选择“设为星标”

回复”666“获取新整理的面试资料

作者:Oo鲁毅oO

juejin.im/post/5e1b1fcce51d454d3046a3de

1.为什么需要线程池

在当今计算机的CPU计算速度非常快的情况下,为了能够充分利用CPU性能提高程序运行效率我们在程序中使用了线程。但是在高并发情况下会频繁的创建和销毁线程,这样就变相的阻碍了程序的执行速度,所以为了管理线程资源和减少线程创建以及销毁的性能消耗就引入了线程池。

2.什么场景下适合使用线程池

当服务器接收到大量任务时,如果使用线程池可以大量减少线程的创建与销毁次数,从而提升程序执行效率

在实际开发中,如果需要创建5个以上的线程,那么就可以使用线程池来管理

3.线程池参数介绍以及特点

3.1 corePoolSize和maxPoolSize

corePoolSize:线程池在创建完时,里面并没有线程,只有当任务到来时再去创建线程。

maxPoolSize:线程池可能会在核心线程数的基础上额外增加一些线程,但是线程数量的上限是maxPoolSize。比如第一天执行的任务非常多,第二天执行的任务非常少,但是有了maxPoolSize参数,就可以增强任务处理的灵活性。

3.2 添加线程的规则

  • 当线程数量小于corePoolSize即使线程没有在执行任务,也会创建新的线程。

  • 如果线程数量等于(或大于)corePoolSize,但小于maxPoolSize则将任务放入队列。

  • 如果队列已满,并且线程数小于maxPoolSize,则创建新的线程运行任务。

  • 如果队列已满,并且线程数大于或等于maxPoolSize,则拒绝该任务。

执行流程:

3.3 增减线程的特点

  • 将corePoolSize和maxPoolSize设置为相同的值,那么就会创建固定大小的线程池。

  • 线程池希望保持更少的线程数,并且只有在负载变得很大时才会增加它。

  • 如果将线程池的maxPoolSize参数设置为很大的值,例如Integer.MAX_VALUE,可以允许线程池容纳任意数量的并发任务。

  • 只有在队列满了的时候才会去创建大于corePoolSize的线程,所以如果使用了无界队列(如:LinkedBlockingQueue)就不会创建到超过corePoolSize的线程数。

3.4 keepAliveTime

如果线程池当前的线程数大于corePoolSize,那么如果多余的线程的空闲时间大于keepAliveTime,它们就会被终止。

keepAliveTime参数的使用可以减少线程数过多冗余时的资源消耗。

3.5 threadFactory

新的线程由ThreadFactory创建,默认使用Executors.defaultThreadFactory(),创建出来的线程都在同一个线程组,拥有同样的NORM_PRIORITY优先级并且都不是守护线程。如果自己指定ThreadFactory,那么就可以改变线程名、线程组、优先级、是否是守护线程等。通常情况下直接使用defaultThreadFactory就行。

3.6 workQueue

  • 直接交接(SynchronousQueue):任务不多时,只需要用队列进行简单的任务中转,这种队列无法存储任务,在使用这种队列时,需要将maxPoolSize设置的大一点。

  • 无界队列(LinkedBlockingQueue):如果使用无界队列当作workQueue,将maxQueue设置的多大都没有用,使用无界队列的优点是可以防止流量突增,缺点是如果处理任务的速度跟不上提交任务的速度,这样就会导致无界队列中的任务越来越多,从而导致OOM异常。

  • 有界队列(ArrayBlockingQueue):使用有界队列可以设置队列大小,让线程池的maxPoolSize有意义。

4.线程池应该手动创建还是自动创建

手动创建更好,因为这样可以让我们更加了解线程池的运行规则,避免资源耗尽的风险。

4.1 直接调用JDK封装好的线程池会带来的问题

newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());}

newFixedThreadPool线程池通过传入相同的corePoolSize和maxPoolSize可以保证线程数量固定,0L的keepAliveTime表示时刻被销毁,workQueue使用的是无界队列。

这样潜在的问题就是当处理任务的速度赶不上任务提交的速度的时候,就可能会让大量任务堆积在workQueue中,从而引发OOM异常。

4.2 演示newFixedThreadPool内存溢出问题

/*** 演示newFixedThreadPool线程池OOM问题*/
public class FixedThreadPoolOOM {private static ExecutorService executorService = Executors.newFixedThreadPool(1);public static void main(String[] args) {for (int i = 0; i < Integer.MAX_VALUE; i++) {executorService.execute(new SubThread());}}
}class SubThread implements Runnable {@Overridepublic void run() {try {//延长任务时间Thread.sleep(1000000000);} catch (InterruptedException e) {e.printStackTrace();}}
}

更改JVM参数

运行结果

4.3 newSingleThreadExecutor

使用线程池打印线程名

public class SingleThreadExecutor {public static void main(String[] args) {ExecutorService executorService = Executors.newSingleThreadExecutor();for (int i = 0; i < 1000; i++) {executorService.execute(new Task());}}
}

查看newSingleThreadExecutor源码

public static ExecutorService newSingleThreadExecutor() {return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}

从源码可以看出newSingleThreadExecutor和newFixedThreadPool基本类似,不同的只是corePoolSize和maxPoolSize的值,所以newSingleThreadExecutor也存在内存溢出问题。

4.4 newCachedThreadPool

newCachedThreadPool也被称为可缓存线程池,它是一个无界线程池,具有自动回收多余线程的功能。

public class CachedThreadPool {public static void main(String[] args) {ExecutorService executorService = Executors.newCachedThreadPool();for (int i = 0; i < 1000; i++) {executorService.execute(new Task());}}
}

public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}

newCachedThreadPool的maxPoolSize设置的值为Integer.MAX_VALUE,所以可能会导致线程被无限创建,最终导致OOM异常。

4.5 newScheduledThreadPool

该线程池支持周期性任务的执行

public class ScheduledThreadPoolTest {public static void main(String[] args) {ScheduledExecutorService scheduledExecutorService =Executors.newScheduledThreadPool(10);
//        scheduledExecutorService.schedule(new Task(), 5, TimeUnit.SECONDS);scheduledExecutorService.scheduleAtFixedRate(new Task(), 1, 3, TimeUnit.SECONDS);}
}

public ScheduledThreadPoolExecutor(int corePoolSize) {super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue());
}

4.6 正确的创建线程池的方法

根据业务场景不同,自己设置线程池参数,例如内存有多大,自己取线程名字等。

4.7 线程池里的线程数量设置多少比较合适?

  • CPU密集型(加密、计算hash等):最佳线程数设置为CPU核心数的1——2倍。

  • 耗时I/O型(读写数据库、文件、网络读写等):最佳线程数一般会大于CPU核心数很多倍,以JVM监控显示繁忙情况为依据,保证线程空闲可以衔接上。参考Brain Goezt推荐的计算方法:线程数=CPU核心数 × (1+平均等待时间/平均工作时间)

5.对比线程池的特点

  • FixedThreadPool:通过手动传入corePoolSize和maxPoolSize,以固定的线程数来执行任务

  • SingleThreadExecutor:corePoolSize和maxPoolSize默认都是1,全程只以1条线程执行任务

  • CachedThreadPool:它没有需要维护的核心线程数,每当需要线程的时候就进行创建,因为它的线程存活时间是60秒,所以它也凭借着这个参数实现了自动回收的功能。

  • ScheduledThreadPool:这个线程池可以执行定时任务,corePoolSize是通过手动传入的,它的maxPoolSize为Integer.MAX_VALUE,并且具有自动回收线程的功能。

5.1 为什么FixedThreadPool和SingleThreadExecutor的Queue是LinkedBlockingQueue?

因为这两个线程池的核心线程数和最大线程数都是相同的,也就无法预估任务量,所以需要在自身进行改进,就使用了无界队列。

5.2 为什么CachedThreadPool使用的Queue是SynchronousQueue?

因为缓存线程池的最大线程数是“无上限”的,每当任务来的时候直接创建线程进行执行就好了,所以不需要使用队列来存储任务。这样避免了使用队列进行任务的中转,提高了执行效率。

5.3 为什么ScheduledThreadPool使用延迟队列DelayedWorkQueue?

因为ScheduledThreadPool是延迟任务线程池,所以使用延迟队列有利于对执行任务的时间做延迟。

5.4 JDK1.8中加入的workStealingPool

  • workStealingPool适用于执行产生子任务的环境,例如进行二叉树的遍历。

  • workStealingPool具有窃取能力。

  • 使用时最好不要加锁,而且不保证执行顺序。

6.停止线程池的正确方法

shutdown:调用了shutdown()方法不一定会立即停止,这个方法仅仅是初始整个关闭过程。因为线程池中的线程有可能正在运行,并且队列中也有待处理的任务,不可能说停就停。所以每当调用该方法时,线程池会把正在执行的任务和队列中等待的任务都执行完毕再关闭,并且在此期间如果接收到新的任务会被拒绝。

/*** 演示关闭线程池*/
public class ShutDown {public static void main(String[] args) throws InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(10);for (int i = 0; i < 1000; i++) {executorService.execute(new ShutDownTask());}Thread.sleep(1500);executorService.shutdown();//再次提交任务executorService.execute(new ShutDownTask());}
}class ShutDownTask implements Runnable {@Overridepublic void run() {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName());}
}

isShutdown:可以用于判断线程池是否被shutdown了

/*** 演示关闭线程池*/
public class ShutDown {public static void main(String[] args) throws InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(10);for (int i = 0; i < 1000; i++) {executorService.execute(new ShutDownTask());}Thread.sleep(1500);System.out.println(executorService.isShutdown());executorService.shutdown();System.out.println(executorService.isShutdown());//再次提交任务
//        executorService.execute(new ShutDownTask());}
}class ShutDownTask implements Runnable {@Overridepublic void run() {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName());}
}

isTerminated:可以判断线程是否被完全终止了

/*** 演示关闭线程池*/
public class ShutDown {public static void main(String[] args) throws InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(10);for (int i = 0; i < 1000; i++) {executorService.execute(new ShutDownTask());}Thread.sleep(1500);System.out.println(executorService.isShutdown());executorService.shutdown();System.out.println(executorService.isShutdown());System.out.println(executorService.isTerminated());//再次提交任务
//        executorService.execute(new ShutDownTask());}
}

将循环的次数改为100次,并且在第一次调用isTerminated方法的地方休眠10s

awaitTermination:传入等待时间,等待时间达到时判断是否停止了,主要用于检测。

//在3s后判断线程池是否被终止,返回boolean值
System.out.println(executorService.awaitTermination(3L, TimeUnit.SECONDS));

shutdownNow:调用了这个方法时,线程池会立即终止,并返回没有被处理完的任务。如果需要继续执行这里的任务可以再次让线程池执行这些返回的任务。

7.任务太多,怎么拒绝?

7.1 拒绝的时机

  • 当Executor关闭时,新提交的任务会被拒绝。

  • 以及Executor对最大线程数和工作队列容量使用有限边界并且已经饱和时。

7.2 拒绝策略

  • AbortPolicy(中断策略):直接抛出异常进行拒绝

  • DiscardPolicy(丢弃策略):不会得到通知,默默的抛弃掉任务

  • DiscardOldestPolicy(丢弃最老的):由于队列中存储了很多任务,这个策略会丢弃在队列中存在时间最久的任务。

  • CallerRunsPolicy:比如主线程给线程池提交任务,但是线程池已经满了,在这种策略下会让提交任务的线程去执行。

总结:第四种拒绝策略相对于前三种更加“机智”一些,可以避免前面三种策略产生的损失。在第四种策略下可以降低提交的速度,达到负反馈的效果。

8.使用钩子为线程池加点料(可用于日志记录)

/*** 演示每个任务执行的前后放钩子函数*/
public class PauseableThreadPool extends ThreadPoolExecutor {private boolean isPaused;private final ReentrantLock lock = new ReentrantLock();private Condition unPaused = lock.newCondition();public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue) {super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);}public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);}public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);}public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory, RejectedExecutionHandler handler) {super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);}@Overrideprotected void beforeExecute(Thread t, Runnable r) {super.beforeExecute(t, r);lock.lock();try {while (isPaused) {unPaused.await();}} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}private void pause() {lock.lock();try {isPaused = true;} finally {lock.unlock();}}public void resume() {lock.lock();try {isPaused = false;//唤醒全部unPaused.signalAll();} finally {lock.unlock();}}public static void main(String[] args) throws InterruptedException {PauseableThreadPool pauseableThreadPool = new PauseableThreadPool(10, 20, 10L,TimeUnit.SECONDS, new LinkedBlockingQueue<>());Runnable runnable = new Runnable() {@Overridepublic void run() {System.out.println("我被执行");try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}};for (int i = 0; i < 10000; i++) {pauseableThreadPool.execute(runnable);}Thread.sleep(1500);pauseableThreadPool.pause();System.out.println("线程池被暂停了");Thread.sleep(1500);pauseableThreadPool.resume();System.out.println("线程池被恢复了");}
}

9.线程池实现原理

9.1 线程池组成部分

  • 线程池管理器

  • 工作线程

  • 任务队列

  • 任务

9.2 Executor家族

Executor:它是一个顶层接口,其他接口以及类都i继承或实现于它,包含以下方法:

  • void execute(Runnable command);

ExecutorService:它继承于Executor,是Executor的子接口,在接口内部增加了一些新的方法,例如第6小节讲到的几个方法

Executors:这个类是一个工具类,里面包含一些创建线程池的方法

9.3 线程池实现任务复用的原理

利用相同线程执行不同任务

源码分析

public void execute(Runnable command) {// 判断任务是否为空,为空就抛出异常if (command == null)throw new NullPointerException();int c = ctl.get();// 如果当前线程数小于核心线程数,就增加Workerif (workerCountOf(c) < corePoolSize) {// command就是任务,点击addWorker方法// 第二个参数用于判断当前线程数是否小于核心线程数if (addWorker(command, true))return;c = ctl.get();}// 此时线程数大于等于核心线程数// 判断线程池是不是正在运行并将任务放到工作队列中if (isRunning(c) && workQueue.offer(command)) {// 再次检查线程状态int recheck = ctl.get();// 如果线程不是正在运行的,就删除掉任务并且拒绝if (! isRunning(recheck) && remove(command))reject(command);else if (workerCountOf(recheck) == 0)   //这里用于避免已经提交的任务没有线程进行执行addWorker(null, false);}// 如果任务无法添加或者大于最大线程数就拒绝任务else if (!addWorker(command, false))reject(command);
}

因为要查看的是Worker所以进入到addWorker()方法后点击Worker类查看runWorker()方法

w = new Worker(firstTask);
private final class Workerextends AbstractQueuedSynchronizerimplements Runnable
final void runWorker(Worker w) {Thread wt = Thread.currentThread();// 获取到任务Runnable task = w.firstTask;w.firstTask = null;w.unlock(); // allow interruptsboolean completedAbruptly = true;try {// 只要任务不为空或者能够获取到任务就执行下面的方法while (task != null || (task = getTask()) != null) {w.lock();if ((runStateAtLeast(ctl.get(), STOP) ||(Thread.interrupted() &&runStateAtLeast(ctl.get(), STOP))) &&!wt.isInterrupted())wt.interrupt();try {beforeExecute(wt, task);Throwable thrown = null;try {// task是一个Runnable类型,调用run()方法就是运行线程task.run();} catch (RuntimeException x) {thrown = x; throw x;} catch (Error x) {thrown = x; throw x;} catch (Throwable x) {thrown = x; throw new Error(x);} finally {afterExecute(task, thrown);}} finally {task = null;w.completedTasks++;w.unlock();}}completedAbruptly = false;} finally {processWorkerExit(w, completedAbruptly);}
}

总结:核心原理就是获取到task,如果task不为空就调用run()方法,这样就实现了线程的复用,达到让相同的线程执行不同任务的目的。

10.线程池状态

  • RUNNING:接受新任务并处理排队任务

  • SHUTDOWN:不接受新的任务但是处理排队任务

  • STOP:不接受新的任务,也不处理排队的任务,并中断正在执行的任务,就是调用shutdownNow()带来的效果

  • TIDYING:中文意思是整洁,意思就是说任务都已经终止,workerCount为零时,线程会转换到TIDYING状态,并将运行terminate()钩子方法

  • TERMINATED:terminate()运行完成

11.使用线程池的注意点

  • 避免任务的堆积(堆积容易产生内存溢出)

  • 避免线程数过多增加(缓存线程池会导致线程数过度增加)

  • 排查线程泄漏(线程已经执行完毕却无法被回收)

热门内容:

  • 互联网公司的中年人都去哪了?

  • Github 标星 11.5K!这可能是最好的 Java 博客系统

  • 大批 IDEA 激活码到期之后的乱象...

  • 全面了解 Nginx 主要应用场景

  • 为什么微服务一定要有网关?

  • 那些在一个公司死磕了5-10年的人,最后都怎么样了?

最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

明天见(。・ω・。)ノ♡

线程池:治理线程的法宝相关推荐

  1. Python 多线程总结(2)— 线程锁、线程池、线程数量、互斥锁、死锁、线程同步

    主要介绍使用 threading 模块创建线程的 3 种方式,分别为: 创建 Thread 实例函数 创建 Thread 实例可调用的类对象 使用 Thread 派生子类的方式 多线程是提高效率的一种 ...

  2. ReentrantLock+线程池+同步+线程锁

    1.并发编程三要素? 1)原子性 原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行. 2)可见性 可见性指多个线程操作一个共享变量时,其中一个线程对变量 ...

  3. eclipse mysql 线程池_JAVA5线程池使用

    线程池是Java5提供的一个新技术,方便我们快速简洁的定义线程池.包括如下: 诸如 Web 服务器.数据库服务器.文件服务器或邮件服务器之类的许多服务器应用程序都面向处理来自某些远程来源的大量短小的任 ...

  4. 【Java 并发编程】线程池机制 ( 线程池状态分析 | 线程池状态转换 | RUNNING | SHUTDOWN | STOP | TIDYING | TERMINATED )

    文章目录 一.线程池状态分析 一.线程池状态分析 线程池的状态在 ThreadPoolExecutor 源码中定义 : private final AtomicInteger ctl = new At ...

  5. 【Java 并发编程】线程池机制 ( 线程池执行任务细节分析 | 线程池执行 execute 源码分析 | 先创建核心线程 | 再放入阻塞队列 | 最后创建非核心线程 )

    文章目录 一.线程池执行任务细节分析 二.线程池执行 execute 源码分析 一.线程池执行任务细节分析 线程池执行细节分析 : 核心线程数 101010 , 最大小成熟 202020 , 非核心线 ...

  6. 【Java 并发编程】线程池机制 ( 线程池阻塞队列 | 线程池拒绝策略 | 使用 ThreadPoolExecutor 自定义线程池参数 )

    文章目录 一.线程池阻塞队列 二.拒绝策略 三.使用 ThreadPoolExecutor 自定义线程池参数 一.线程池阻塞队列 线程池阻塞队列是线程池创建的第 555 个参数 : BlockingQ ...

  7. 【Android 异步操作】线程池 ( 线程池简介 | 线程池初始化方法 | 线程池种类 | AsyncTask 使用线程池示例 )

    文章目录 一.线程池简介 二.线程池初始化方法简介 三.线程池使用示例 一.线程池简介 线程池一般是实现了 ExecutorService 接口的类 , 一般使用 ThreadPoolExecutor ...

  8. 【Android 异步操作】线程池 ( 线程池作用 | 线程池种类 | 线程池工作机制 | 线程池任务调度源码解析 )

    文章目录 一.线程池作用 二.线程池种类 三.线程池工作机制 四.线程池任务调度源码解析 一.线程池作用 线程池作用 : ① 避免创建线程 : 避免每次使用线程时 , 都需要 创建线程对象 ; ② 统 ...

  9. 线程池中阻塞队列的作用?为什么是先添加列队而不是先创建最大线程?线程池中线程复用原理

    1.一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务.阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使 ...

  10. 由浅入深理解Java线程池及线程池的如何使用

    前言 多线程的异步执行方式,虽然能够最大限度发挥多核计算机的计算能力,但是如果不加控制,反而会对系统造成负担.线程本身也要占用内存空间,大量的线程会占用内存资源并且可能会导致Out of Memory ...

最新文章

  1. 从奥运订票系统说起——谈FastCGI 与IT 架构
  2. 7-4 螺旋方阵 (20 分)
  3. 最近关于linux的一些小问题。
  4. 第一章 ASP.NET MVC简介(1.1)
  5. iphone4s上市时间_iPhone 4S才是真正意义上的王者, 网友: 现在依旧能打
  6. 华为交换机VTY用户界面属性配置教程
  7. matlab插值法计算根号,怎么用matlab利用拉格朗日插值计算法的原理编写并计算函数所在节点的近似值....
  8. vs2017调用目标发生了异常
  9. 3、git 暂存区撤销与删除
  10. 风青杨:马云为何被浙商“炮…
  11. 我学历低,学软件测试能找到工作吗?
  12. 关于NC6.X企业报表取不了数的问题及其解决方法。
  13. java主机哪儿好_java虚拟主机哪个好,香港java虚拟主机哪里有!
  14. shell命令:ls命令
  15. java 组合实现汽车类 问题描述:一辆Car有(has)四个轮子(Wheels)和一个发动机(Engine)。现在要求用组合方法设计类Car、类Wheel和类Engine。
  16. 西部学刊杂志西部学刊杂志社西部学刊编辑部2022年第22期目录
  17. VBA - 粘贴为数值型
  18. 微信php功能整合,微信公众平台开发功能整合
  19. kartoSLAM报错 transform_tolerance修改解决
  20. 【滤波器学习笔记】一阶RC低通滤波

热门文章

  1. 关于 MongoDB 与 SQL Server 通过本身自带工具实现数据快速迁移 及 注意事项 的探究...
  2. 前端 ----jQuery的动画效果
  3. tensorflow基于csv数据集实现多元线性回归并预测
  4. 2017年50道Java线程面试题
  5. jquery总结和注意事项
  6. 如何用Asp判断你的网站的虚拟物理路径
  7. Matlab与数据结构 -- 搜索向量或矩阵中非零元素的位置
  8. c语言将字母与数字分开存放,2017年计算机二级《C语言》考前提分试题及答案9...
  9. 编程能力差,学不好Python、AI、Java等技术,90%是输在了这点上!
  10. 谁说AI无用?疫情下,AI已经代替人类做了很多...