关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回新闻公共列表

云南大王-这不就是多线程ThreadPoolExecutor和阻塞队列吗

发布时间:2020-04-16 00:00:00
无处不在的线程,多线程,阻塞队列,并发 编程世界无新鲜事,看你翻墙翻得厉不厉害 场景:现在的软件开发迭代速度(一周一更新,甚至一天一发布)真是太快了,今天进行软件更新的时候,看到了有趣的现象,这不就是线程池,ThreadPoolExecutor,阻塞队列,任务(下载和安装)最好的案例嘛!经常看到很多博文在写多线程,并发,队列,却举不出现实生活的场景例子,都在背书吗(天下文章一大抄,看你会抄不会抄)。 现象图示: ​ ​ 我开启了全部更新38个要更新的app,可最多时看到了3个在同时下载,剩下的下载任务在排队(队列),安装过程中,明明已经下载了多个app,可同一时刻只有一个在安装,其他下载好的app也在排队   0.  线程thread     说起线程,不得不提起进程,      线程,还真不好下定义,你要问十个人就会有十种答案,我就当线程就是一个可以执行的任务程序(比如上面图片里的下载和安装)。java里线程主要通过继承java.lang.Thread类或实现java.lang.Runnable接口,其实Thread也是实现了Runnable接口的类,所有,线程还是围绕着java.lang.Thread类扩展包装,比如下面要要说的线程池。核心类如下 1 package java.lang; 2 3 public class Thread implements Runnable { 4 5 .... 6 //线程的优先级(类似于现实中的会员级别) 7 /** 8 * The minimum priority that a thread can have. 9 */ 10 public final static int MIN_PRIORITY = 1; 11 12 /** 13 * The default priority that is assigned to a thread. 14 */ 15 public final static int NORM_PRIORITY = 5; 16 17 /** 18 * The maximum priority that a thread can have. 19 */ 20 public final static int MAX_PRIORITY = 10; 21 22 //构造线程的几种方式 23 public Thread( ); 24 25 public Thread(Runnable target); 26 27 public Thread(ThreadGroup group, Runnable target); 28 29 public Thread(String name); 30 31 public Thread(ThreadGroup group, String name); 32 33 public Thread(Runnable target, String name); 34 35 public Thread(ThreadGroup group, Runnable target, String name); 36 37 public Thread(ThreadGroup group, Runnable target, String name, 38 39 long stackSize); 40 41 //下面的代码和生命周期有关 42 public void start( ); 43 44 public void run( ); 45 46 public void stop( ); // Deprecated, do not use 47 48 public void resume( ); // Deprecated, do not use 49 50 public void suspend( ); // Deprecated, do not use 51 52 public static void sleep(long millis); 53 54 public static void sleep(long millis, int nanos); 55 56 public boolean isAlive( ); 57 58 public void interrupt( ); 59 60 public boolean isInterrupted( ); 61 62 public static boolean interrupted( ); 63 64 public void join( ) throws InterruptedException; 65 66 ......... 67 68 } View Code   一个线程创建之后,总是处于其生命周期的4个状态之一中。线程的状态表明此线 程当前正在进行的活动,而线程的状态是可以通过程序来进行控制的,就是说,可以对线程进行操作来改变状态。这些操作包括启动(start)、终止(stop)、睡眠(sleep)、挂起 (suspend)、恢复(resume)、等待(wait)和通知(notify)。每一个操作都对应了一个方法,这些方法是由java.lang提供的。 线程状态在Java中是通过一个Thread的内部枚举State标识的。 public enum State { /** * Thread state for a thread which has not yet started. */ NEW, /** * Thread state for a runnable thread. A thread in the runnable * state is executing in the Java virtual machine but it may * be waiting for other resources from the operating system * such as processor. */ RUNNABLE, /** * Thread state for a thread blocked waiting for a monitor lock. * A thread in the blocked state is waiting for a monitor lock * to enter a synchronized block/method or * reenter a synchronized block/method after calling * {@link Object#wait() Object.wait}. */ BLOCKED, /** * Thread state for a waiting thread. * A thread is in the waiting state due to calling one of the * following methods: *
    *
  • {@link Object#wait() Object.wait} with no timeout
  • *
  • {@link #join() Thread.join} with no timeout
  • *
  • {@link LockSupport#park() LockSupport.park}
  • *
* *

A thread in the waiting state is waiting for another thread to * perform a particular action. * * For example, a thread that has called Object.wait() * on an object is waiting for another thread to call * Object.notify() or Object.notifyAll() on * that object. A thread that has called Thread.join() * is waiting for a specified thread to terminate. */ WAITING, /** * Thread state for a waiting thread with a specified waiting time. * A thread is in the timed waiting state due to calling one of * the following methods with a specified positive waiting time: *

    *
  • {@link #sleep Thread.sleep}
  • *
  • {@link Object#wait(long) Object.wait} with timeout
  • *
  • {@link #join(long) Thread.join} with timeout
  • *
  • {@link LockSupport#parkNanos LockSupport.parkNanos}
  • *
  • {@link LockSupport#parkUntil LockSupport.parkUntil}
  • *
*/ TIMED_WAITING, /** * Thread state for a terminated thread. * The thread has completed execution. */ TERMINATED; } View Code   创建状态(Thread.State.NEW)      如果创建了一个线程而没有启动它,那么,此线程就处于创建状态。比如,下述语句执行 以后,使系统有了一个处于创建状态的线程myThread: Thread t= new ThreadClass(); 其中,ThreadClass()是Thread的子类,而Thread是由java.lang提供的。      处于创建状态的线程还没有获得应有的资源,所以,这是一个空的线程。线程只有通过启动后,系统才会为它分配资源。对处于创建状态的线程可以进行两种操作:一是启动 (start)操作,使其进入可运行状态,二是终止(stop)操作,使其进入消亡状态。如果进入到消 亡状态,那么,此后这个线程就不能进入其他状态,也就是说,它不再存在了。      start方法是对应启动操作的方法,其具体功能是为线程分配必要的系统资源;将线程设置为可运行状态,从而可以使系统调度这个线程。      通过调用t.start()启动一个线程,使该线程进入可运行(Thread.State.RUNNABLE)的状态。     由JVM的决定去调度(Scheduler) 在可运行状态(Runnable)下的线程,使该线程处于运行 (Running) 状态,由于JVM的调度会出现不可控性,即不是优先级高的先被调用,可能先调用,也可能后调用的的情况。运行状态(Running)下,调用礼让yield()方法,可以使线程回到可运行状态(Runnable)下,再次JVM的调度(并不依赖优先级)。     线程执行完毕或异常退出会进入终止状态(Thread.State.TERMINATED)。 其余的还有几个状态: Thread.State.BLOCKED 受阻塞并且正在等待监视器锁的某一线程的线程状态。处于受阻塞状态的某一线程正在等待监视器锁,以便进入一个同步的块/方法,或者在调用 Object.wait 之后再次进入同步的块/方法。  Thread.State.WAITING 某一等待线程的线程状态。某一线程因为调用下列方法之一而处于等待状态: 不带超时值的 Object.wait 不带超时值的 Thread.join LockSupport.park 处于等待状态的线程正等待另一个线程,以执行特定操作。 例如,已经在某一对象上调用了 Object.wait() 的线程正等待另一个线程,以便在该对象上调用 Object.notify() 或 Object.notifyAll()。已经调用了 Thread.join() 的线程正在等待指定线程终止。 TIMED_WAITING具有指定等待时间的某一等待线程的线程状态。某一线程因为调用以下带有指定正等待时间的方法之一而处于定时等待状态: Thread.sleep 带有超时值的 Object.wait 带有超时值的 Thread.join LockSupport.parkNanos LockSupport.parkUntil 谨记: 在给定时间点上,一个线程只会处于一种状态,状态转换图 ​ 线程优先级 java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量: static int MAX_PRIORITY           线程可以具有的最高优先级,取值为10。 static int MIN_PRIORITY           线程可以具有的最低优先级,取值为1。 static int NORM_PRIORITY           分配给线程的默认优先级,取值为5。 Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。 每个线程都有默认的优先级,主线程的默认优先级为Thread.NORM_PRIORITY。 线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。 JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。 1.  阻塞队列 BlockingQueue队列是一种数据结构,它有两个基本操作:在队列尾部加人一个元素,和从队列头部移除一个元素就是说,队列以一种先进先出的方式管理数据,如果你试图向一个已经满了的阻塞队列中添加一个元素或者是从一个空的阻塞队列中移除一个元素,将导致线程阻塞。 在多线程进行合作时,阻塞队列是很有用的工具。工作者线程可以定期地把中间结果存到阻塞队列中而其他工作者线程把中间结果取出并在将来修改它们。队列会自动平衡负载。如果第一个线程集运行得比第二个慢,则第二个线程集在等待结果时就会阻塞。如果第一个线程集运行得快,那么它将等待第二个线程集赶上来。 而BlockingQueue队列也是一组数据集合,它继承了Queue接口,而Queue接口继承了Collection接口。 阻塞队列提供的相关操作和特点 ​ 在java包"java.util.concurrent"中提供了若干种队列,大神给你写好了 ArrayBlockingQueue 一个由数组结构组成的有界阻塞队列 LinkedBlockingQueue 一个由链表结构组成的有界阻塞队列 PriorityBlockingQueue 一个支持优先级排序的无界阻塞队列 DelayQueue 一个使用优先级队列实现的无界阻塞队列 SynchronousQueue 一个不存储元素的阻塞队列 LinkedTransferQueue 一个由链表结构组成的无界阻塞队列 LinkedBlockingDeque 一个由链表结构组成的双向阻塞队列 ArrayBlockingQueue 底层用数组实现的有界阻塞队列,默认情况下不保证线程公平的访问队列(按照阻塞的先后顺序访问队列),队列可用的时候,阻塞的线程都可以争夺队列的访问资格,当然也可以使用以下的构造方法创建一个公平的阻塞队列。ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(10, true)。 伪代码: public ArrayBlockingQueue(int capacity, boolean fair) { if (capacity <= 0) throw new IllegalArgumentException(); this.items = new Object[capacity]; lock = new ReentrantLock(fair); notEmpty = lock.newCondition(); notFull = lock.newCondition(); } (其实就是通过将ReentrantLock设置为true来 达到这种公平性的:即等待时间最长的线程会先操作)。用ReentrantLock condition 实现阻塞。 有界就是队列的长度有限制,例如数组队列,在构建的时候就指定了长度。无界就是可以无限地添加。 LinkedBlockingQueue 底层基于链表实现的有界阻塞队列。此队列的默认和最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。这个队列的实现原理和ArrayBlockingQueue实现基本相同。也是采用ReentrantLock 控制并发,不同的是它使用两个独占锁来控制消费和生产。即用takeLock和putlock,这样的好处是消费者和生产者可以并发执行,对吞吐量有提升。 PriorityBlockingQueue PriorityBlockingQueue是一个带优先级的队列,而不是先进先出队列。元素按优先级顺序被移除,该队列也没有上限(PriorityBlockingQueue是对 PriorityQueue的再次包装,是基于堆数据结构的,而PriorityQueue是没有容量限制的,与ArrayList一样,所以在优先阻塞 队列上put时是不会受阻的。虽然此队列逻辑上是无界的,但是由于资源被耗尽,所以试图执行添加操作可能会导致 OutOfMemoryError),但是如果队列为空,那么取元素的操作take就会阻塞,所以它的检索操作take是受阻的。也是用ReentrantLock控制并发。 DelayQueue DelayQueue是在PriorityQueue基础上实现的,底层也是数组构造方法,是一个存放Delayed 元素的无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的 Delayed 元素。如果延迟都还没有期满,则队列没有头部,并且poll将返回null。当一个元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于或等于零的值时,则出现期满,poll就移除这个元素了。此队列不允许使用 null 元素。 SynchronousQueue 一个没有容量的队列 ,不会存储数据,每执行一次put就要执行一次take,否则就会阻塞。未使用锁。通过cas实现,吞吐量异常高。内部采用的就是ArrayBlockingQueue的阻塞队列,所以在功能上完全可以用ArrayBlockingQueue替换,但是SynchronousQueue是轻量级的,SynchronousQueue不具有任何内部容量,我们可以用来在线程间安全的交换单一元素。所以功能比较单一,优势就在于轻量。 LinkedBlockingDeque LinkedBlockingDeque是双向链表实现的双向并发阻塞队列。该阻塞队列同时支持FIFO和FILO两种操作方式,即可以从队列的头和尾同时操作(插入/删除);并且,该阻塞队列是支持线程安全,当多线程竞争同一个资源时,某线程获取到该资源之后,其它线程需要阻塞等待。此外,LinkedBlockingDeque还是可选容量的(防止过度膨胀),即可以指定队列的容量。如果不指定,默认容量大小等于Integer.MAX_VALUE。 LinkedTransferQueue jdk7才提供这个类,这个类实现了TransferQueue接口,也是基于链表的,对于所有给定的生产者都是先入先出的。与其他阻塞队列的区别是:其他阻塞队列,生产者生产数据,如果队列没有满,放下数据就走,消费者获取数据,看到有数据获取数据就走。而LinkedTransferQueue生产者放数据的时候,如果此时消费者没有获取,则需阻塞等待直到有消费者过来获取数据。有点类似SynchronousQueue,但是LinkedTransferQueue是被设计有容量的。LinkedTransferQueue 通过使用CAS来实现并发控制,是一个无界的安全队列。其长度可以无限延伸,当然带来的问题也是显而易见的。   2.  线程池ThreadPool 线程池,可以理解为存放线程的容器。 既然可以通过new出线程,那为什么要线程池呢,因为有以下优点: (1)重用存在的线程,减少对象创建、消亡的开销,性能佳。 (2)可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。 (3)提供定时执行、定期执行、单线程、并发数控制等功能。 而单独建立线程(特别是项目组开发人员多的时候,各创建各自的线程),却有以下缺点: (1) 每次new Thread新建对象性能差。因为每次都会创建一个对象。这是既耗时又消耗资源的。 (2) 线程缺乏统一管理,可能会造成自锁,或者是内存溢出。 (3)缺乏更多功能,如定时执行、定期执行、线程中断。   Java通过Executors提供四种线程池,分别为:newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 不过这几种创建线程池方便,可隐藏了细节也不好,既然四种创建线程池最后是通过java.util.concurrent.ThreadPoolExecutor类构造,不如直接考察它,代码如下: public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; } 请务必搞懂方方法里的参数: (1)int corePoolSize(核心线程数): 线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,核心线程默认情况下会一直存活在线程池中;如果设置了 allowCoreThreadTimeOut 为 true,那么核心线程如果不干活的话,超过一定时间,就会被销毁掉。 (2)int maximumPoolSize(线程池能容纳的最大线程数量): 线程总数 = 核心线程数 + 非核心线程数。 (3)long keepAliveTime(非核心线程空闲存活时长): 非核心线程空闲时长超过该时长将会被回收 (4)TimeUnit unit 空闲线程的存活时间 在这里表示的是时间的单位,比如说秒。 TimeUnit.DAYS; //天 TimeUnit.HOURS; //小时 TimeUnit.MINUTES; //分钟 TimeUnit.SECONDS; //秒 TimeUnit.MILLISECONDS; //毫秒 TimeUnit.MICROSECONDS; //微妙 TimeUnit.NANOSECONDS; //纳秒 (5)BlockingQueue workQueue(任务队列),就上面说的几种队列: 当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务。常用的workQueue类型: ArrayBlockingQueue:这里表示接到新任务,如果没有达到核心线程数,则新建核心线程执行任务,如果达到了,则入队等候,如果队列已满,则新建非核心线程执行任务,又如果总线程数到了 maximumPoolSize,并且队列也满了,则发生错误。 LinkedBlockingQueue:这里表示接到新任务,如果当前线程数小于核心线程数,则新建核心线程处理任务;如果当前线程数等于核心线程数,则进入队列等待。 DelayQueue:这里表示接到新任务,先入队,达到了指定的延时时间,才执行任务。 SynchornousQueue:这里表示接到新任务,直接交给线程处理,如果其他的线程都在工作,那就创建一个新的线程来处理这个任务。 (6).ThreadFactory threadFactory(线程工厂): 用来创建线程池中的线程。 (7).RejectedExecutionHandler handler(拒绝策略): 指的之超过了maximumPoolSize,无法再处理新的任务,就会直接拒绝,提供了以下 4 种策略: AbortPolicy:默认策略,在拒绝任务时,会抛出RejectedExecutionException。 CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前的被丢弃的任务。 DiscardOldestPolicy:该策略将丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。 DiscardPolicy:该策略默默的丢弃无法处理的任务,不予任何处理  附图两张: 找到ThreadPoolExecutor和BlockingQueue了吗 3.  进入正题:下载和安装 普及了0,1,2后,开始说正事,怕直接说多线程并发,队列,任务,不好接受。 最后代码如下: package code.vivo; import java.io.BufferedInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import java.util.concurrent.ExecutorService; /** * * @author dongguangming * @describe "要更新的app url地址" * @date 2020年4月15日 */ public class DownloadAppThread extends Thread { /** * app url */ private String appUrl; /** * 下载存放的路径 */ private String filePath; /** * 安装软件包的池 */ private ExecutorService installExecutorService; public DownloadAppThread(String appUrl, String filePath,ExecutorService executorService) { super(); this.appUrl = appUrl; this.filePath = filePath; this.installExecutorService = executorService; } @Override public void run() { BufferedInputStream bis = null; FileOutputStream fos = null; HttpURLConnection conn = null; try { conn = getHttp(); conn.connect(); // bis = new BufferedInputStream(conn.getInputStream()); fos = new FileOutputStream(filePath); int len = 0; byte[] buf = new byte[1024*8]; while ((len = bis.read(buf)) != -1) { fos.write(buf, 0, len); } System.out.println(appUrl+"下载完毕,要进入安装状态"); //给安装池发消息发一个消息 //Message message = new Message(appUrl); //handler.sendMessage(message); this.installExecutorService.execute(new InstallAppThread(this.appUrl)); } catch (Exception e) { e.getMessage(); } finally { //关闭各种资源 try { fos.close(); bis.close(); conn.disconnect(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } /** * 连接app下载地址 * @return * @throws IOException */ public HttpURLConnection getHttp() throws IOException { URL url = null; if (appUrl != null) { url = new URL(appUrl); } HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setReadTimeout(5000); conn.setRequestMethod("GET"); return conn; } } package code.vivo; /** * * @author dongguangming * @describe "把下载好的app安装" * @date 2020年4月15日 */ public class InstallAppThread extends Thread { private String appName; public InstallAppThread(String appName) { super(); this.appName = appName; } public void run() { System.out.println("安装app:"+appName); } } 测试下载安装(可以自定义线程池执行器,自定义工厂,自定义策略) package code.vivo; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * 测试下载 * @author dongguangming * @describe "" * @date 2020年4月15日 */ public class DownLoadPool { // 要更新的app数量,我当时要升级更新的app数据 private static final int APP_NUM = 38; public static void main(String[] args) throws InterruptedException { // 下载软件包的池 ExecutorService downloadPool = new ThreadPoolExecutor(3, 3, 10, TimeUnit.SECONDS, new ArrayBlockingQueue(APP_NUM), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); // 安装软件包的池(网速太快,虽然下载软件apk很快,但安装很缓慢,导致安装包堆积) ExecutorService installPool = new ThreadPoolExecutor(1, 1, 10, TimeUnit.SECONDS, new ArrayBlockingQueue(APP_NUM), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); // 以下载图片为例,生产上下载就是apk System.out.println("开始下载apk程序表完毕*******"); for (int i = 1; i <= APP_NUM; i++) { downloadPool .execute(new DownloadAppThread( "http://static.jszg.edu.cn/public/184/Ig9l4KTB9m7xgMYnsdm5.jpg", "f:\\download\\" + i + ".jpg", installPool)); Thread.sleep(1000); } // pool.submit(runnable); // downloadPool.shutdown(); } } 执行效果图 ​ ​ 总结:好好学习,天天向上,写也很累(有时候构思一篇文章两三个小时很快就过去了,甚至一下午),也是思考的过程,看别人的文章像过天书一样和听别人讲座,容易忘记(可以问问你的朋友同学同事等),要自己汇总,记忆会更深刻。 有时间了接着拆分!!! 扩展阅读参考: 0.  https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html 1.  能有比官方更权威的吗 https://docs.oracle.com/javase/tutorial/essential/concurrency/runthread.html 2. Java Concurrency in Practice http://jcip.net/,  国内已有翻译版 3. http://gee.cs.oswego.edu/   你们用的并发包java.util.concurrent(简称JUC),便出于他和其他人之手,经常看到国内的码农在拼命研究并发源码(也有研究spring源码的),不知道到什么程度了,了解他这个人吗。。。担心就算看懂了代码(我想只要是个码农都能看得懂),也只是看懂了代码,了解他的思想吗,他当时是如何筹划构思出来的,别人的技术,能学通50%(毕竟人家花了二十年循序渐进才做出来的库和框架比如spring,http://gee.cs.oswego.edu/dl/classes/EDU/oswego/cs/dl/util/concurrent/intro.html,是经验和阅历的成果),能灵活运用就算成功 ****************************************************************************************************** 4.  Java阻塞队列实现原理分析  https://developer.51cto.com/art/201704/536821.htm 5.  聊聊并发(七)——Java 中的阻塞队列 https://www.infoq.cn/article/java-blocking-queue/ 6. Java线程池架构原理和源码解析(ThreadPoolExecutor)https://mp.weixin.qq.com/s?__biz=MjM5NTg2NTU0Ng==&mid=214688037&idx=6&sn=d1c989e7f539732cda5ceaa6cabd8b29 7. Java线程池使用说明 https://mp.weixin.qq.com/s?__biz=MzIyNjA1MjAyNg==&mid=212879024&idx=1&sn=a05bf0b28846850a0730e5844f95126d 8. Java线程池---Executor框架源码深度解析https://mp.weixin.qq.com/s?__biz=MzUyNDk2MTE1Mg==&mid=2247483663&idx=1&sn=cd57cf503c31eb6e4a423173520081f7 9. 深入源码分析Java线程池的实现原理https://mp.weixin.qq.com/s/-89-CcDnSLBYy3THmcLEdQ 10. 深度解读 Java 线程池设计思想及源码实现 https://mp.weixin.qq.com/s?__biz=MzUxNDA1NDI3OA==&mid=2247486041&idx=1&sn=2bc12fd0b57bedb84eb11aca8a574306

相关阅读

云计算的三个优点选取小程序服务商时的注意事项不同数据库的不同区别什么是VPS主机它的优势是什么?你知道5G的优点和缺点吗?云南三级分销商城开发的目的和对企业的价值云南网站优化的3个方面和网页的优化我们晋级拉!!!云南网站链接维护的具体方法云南网站应该如何做优化云南网站维护的主要内容云南云服务器和虚拟主机的操作区别云服务器部署和注意点云南服务网器托管应该选择怎样的机房云南虚拟建设网站主机的优点和缺点云服务器和物理服务器的区别云服务器有哪些优势C# List用法 List介绍C#和Java有什么不同PHP的优点和缺点智慧新餐饮和传统餐饮的区别云数据库对比传统数据库有哪些优点裸金属服务器是什么它的作用是什么白盒测试的特点js中添加scriptjs中[]、{}、()的用法和区别php 字符串的整型转换ipa如何安装到苹果手机邓白氏码是什么?iOS开发者账号到期续费教程在C#中有哪些引用类型和值类型小程序搭建时需要准备些什么云数据库对比传统数据库的优势什么样的企业适合SEMjs中的常见错误C#数据类型转换字符串与数值之间的转换C#的几种循环遍历方式物联网是什么它和互联网又有什么区别?云南网站建设时应该注意些什么云计算是什么?它能干些什么?展望未来5G会给我们的生活带来哪些影响C#常见的几种报错类别C#的学习流程有哪些dedecms 绑定二级域名的正确方法SEM是什么它和SEO之间是什么关系?云南新餐饮料模式是如何运行的?云南网站建设初期应该注意哪些问题云南网站建设中原创文章对网站起什么作用Javascript中如何中断forEach循环云南零售小程序前景怎么样?如何优化WEB应用数据库访问慢的问题?Javascript中用let和var声明变量的区别是什么redis 的主从复制和哨兵?递归和迭代有何区别?<input> 标签的 readonly 属性怎么用?云南网站建设中网页版商城如何保证网站安全云南企业官网如何推广云南企业网站建设为什么把文章类型的栏目排在前面云南餐饮小程序能带给餐饮业什么?云南网站建设需要注意什么地方云南云服务器配置如何选择合适的云南网站建设和网站设计有何区别?云南网站建设—企业官网的好处在哪?云南做小程序的优势在哪?为什么建议做小程序?网站为什么要配置ssl证书?网站为什么要做seo,做seo有什么好处?微信小程序_企业付款到零钱 API 开发如何利用缓存提高asp.net网站访问速度中小型企业如何选择服务器配置?如何优化中小型企业网站C#.net做网站后台 需要记录日志基于asp.net企业门户网站设计霓裳民族服装seo优化分析建议Javascript的函数封装认识及使用Ajax请求中data与后端的交互有哪几种方法JavaScript如何实现组合模式???SQL之子查询的基本用法有哪些?什么是静态测试、动态测试、黑盒测试、白盒测试、α测试 β测试?C# 引入委托的目的是什么c# 委托的本质是什么C# DataGridView添加新行的2个方法C#支付宝扫码支付代码完整版c# MVC 微信支付教程系列之扫码支付代码实例Redis基础通用操作指令有哪些?String、StringBuffer和StringBuilder的各自用法常见的一些Dos命令有哪些?如何在C#中复制一个Windows窗体类前端js中的运算符的种类,=、==与===的有何区别?网站前端怎么设置页面禁止转载?解决XSS脚本攻击恶意代码的方法你知道?javascript 之 apply()、call() 探索net core实现跨域ASP.NET Core程序部署到LinuxASP.NET Core使用AutoMapper.NET Core:处理全局异常.NET Core在ASP.NET Core WebApi中使用CookieASP.NET Core:ASP.NET Core中使用NLog记录日志如何重启iis服务器SQL Server代理服务无法启动怎么办js 判断手机版与PC版自适应跳转升级版 PC版手机版界面对应跳转
/template/Home/Zkeys/PC/Static