第一部分 Java基础
第二部分 Java进阶

Java多线程笔试题

说明:答案要变成自己的话去描述,不要死记硬背。

多线程专题

● 在实际开发中用过多线程吗,具体怎么用的,解决什么问题?

我们通常在后台执行定时任务的时候会用到多线程,例如我们在P2P项目中给用户回款的时候,数据量比较大,收益需要在指定的时间之前全部返还,不然客户这边投诉的电话就来了,当时我们使用了JUC包下的Excutor线程池,启动多线程跑数据。这样问题就解决了。

● 你对java的内存模型有了解吗?

面试官问这个问题的时候,同学们要注意了,面试官要听的不是“JVM的内存结构”,而是Java的内存模型,简称JMM(Java Memory Model),参见另一个文件:Java内存模型JMM.docx

● 线程之间怎么通信?

另请参见:Java线程间的通信.docx

● 什么是线程安全,怎么理解的?

如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。

导致线程不安全的原因是:多线程同时对某个共享的数据进行修改操作,此时就会存在数据不一致问题。在Java中线程安全也是有几个级别的:

不可变

像String,其中String字符串在JVM中有字符串常量池的存在,字符串对象一旦创建不可变,由于这些对象在多线程并发的情况下不会被修改,所以不存在线程安全问题。

绝对线程安全

不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet。

相对线程安全

相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是 fail-fast机制。

非线程安全

这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类。

● 线程之间如何共享数据?

(1)多个线程对共享数据的操作是相同的,那么创建一个Runnable的子类对象,将这个对象作为参数传递给Thread的构造方法,此时因为多个线程操作的是同一个Runnable的子类对象,所以他们操作的是同一个共享数据。比如:买票系统,所以的线程的操作都是对票数减一的操作。

(2)多个线程对共享数据的操作是不同的,将共享数据和操作共享数据的方法放在同一对象中,将这个对象作为参数传递给Runnable的子类,在子类中用该对象的方法对共享数据进行操作。比如实现生产者和消费者模型。

(3)多个线程对共享数据的操作是不同的, 用内部类的方式去实现,创建Runnable的子类作为内部类,将共享对象作为全局变量,在Runnable的子类中对共享数据进行操作。

● 线程的start()和run()方法的区别?

start()方法表示启动一个新的线程,在JVM内存中会开启一个新的栈空间。而run()方法只是普通方法调用,不会启动新的线程。

● 实现线程的方式分别是什么?

第一种:继承Thread

第二种:实现Runnable接口,这种方式使用较多,面向接口编程一直是被推崇的开发原则。

第三种:实现Callable接口用来实现返回结果的线程

● 怎么获取线程的返回值?

使用ExecutorService、Callable、Future实现有返回结果的线程。可返回值的任务必须实现Callable接口。执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了。get方法是阻塞的,即:线程无返回结果,get方法会一直等待。再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。

● volatile关键字的作用是什么?

参见《java并发编程之volatile关键字解析.docx》

● CyclicBarrier(篱栅)和CountDownLatch(倒计时锁存器)的区别是什么?

都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:

(1)CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;

CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行。

(2)CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务。

(3)CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了。

● volatile(不稳定的)和synchronized(同步的)对比?

读以下内容之前,参见:《java并发编程之volatile关键字解析.docx》
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

第一:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

第二:禁止进行指令重排序。

volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;

(1)synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

(2)volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。

(3)volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可见性和原子性。

(4)volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

(5)volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

● 怎么唤醒一个阻塞的线程?

如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。

● Java中如何获取到线程dump文件?

当程序遇到死循环、死锁、阻塞、页面打开慢等问题,查看dump信息是最好的解决问题的途径。线程dump也就是线程堆栈信息。
获取到线程堆栈dump文件内容分两步:

(1)第一步:获取到线程的pid,Linux环境下可以使用ps -ef | grep java
(2)第二步:打印线程堆栈,可以通过使用jstack pid命令
具体实现步骤,参照:https://blog.csdn.net/u010271462/article/details/70171553

● sleep方法和wait方法的相同点和不同点?

相同点

二者都可以让线程处于冻结状态。

不同点

(1)首先应该明确sleep方法是Thread类中定义的方法,而wait方法是Object类中定义的方法。

(2)sleep方法必须人为地为其指定时间。wait方法既可以指定时间,也可以不指定时间。

(3)sleep方法时间到,线程处于临时阻塞状态或者运行状态。wait方法如果没有被设置时间,就必须要通过notify或者notifyAll来唤醒。

(4)sleep方法不一定非要定义在同步中。wait方法必须定义在同步中。

(5)当二者都定义在同步中时,线程执行到sleep,不会释放锁。线程执行到wait,会释放锁。

● 生产者和消费者模型的作用是什么?

(1)通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用

(2)解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要受到相互的制约。

● ThreadLocal有什么作用?

ThreadLocal用来解决多线程程序的并发问题。将数据放到ThreadLocal当中可以将该数据和当前线程绑定在一起,这样可以保证一个线程对应一个对象,这样对象就是线程安全的。

● wait方法和notify/notifyAll方法在放弃对象监视器时有什么区别?

wait()方法立即释放对象监视器;

notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。

● Lock和synchronized对比?

(1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

(2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

(3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

(4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

(5)Lock可以提高多个线程进行读操作的效率。

(6)在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞式的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。相比之下使用Java提供的Lock对象,性能更高一些。但是,JDK1.6,发生了变化,对synchronize加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在JDK1.6上synchronize的性能并不比Lock差。因此。提倡优先考虑使用synchronized来进行同步。

● ConcurrentHashMap的并发度是什么?

ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势。

● ReadWriteLock是什么?

首先明确一下,不是说ReentrantLock不好,只是ReentrantLock某些时候有局限。如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离, 读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

● 不可变对象对多线程有什么帮助

不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。

● FutureTask是什么?

FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。

● Java中用到的线程调度算法是什么?

抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

● 怎么得到一个线程安全的单例模式?

饿汉本来就是线程安全的,就不再多说了。懒汉单例本身就是非线程安全的,可以使用双检锁的方式实现线程安全的单例模式。双检锁就是volatile+synchronized实现。

● 什么是乐观锁和悲观锁?

(1)乐观锁:乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

(2)悲观锁:对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

● Java编写一个会导致死锁的程序?

(1)两个线程里面分别持有两个Object对象:lock1和lock2。这两个lock作为同步代码块的锁;

(2)线程1的run()方法中同步代码块先获取lock1的对象锁,Thread.sleep(xxx),时间不需要太多,50毫秒差不多了,然后接着获取lock2的对象锁。这么做主要是为了防止线程1启动一下子就连续获得了lock1和lock2两个对象的对象锁;

(3)线程2的run)(方法中同步代码块先获取lock2的对象锁,接着获取lock1的对象锁,当然这时lock1的对象锁已经被线程1锁持有,线程2肯定是要等待线程1释放lock1的对象锁的。

这样,线程1”睡觉”睡完,线程2已经获取了lock2的对象锁了,线程1此时尝试获取lock2的对象锁,便被阻塞,此时一个死锁就形成了。

● 如何确保N个线程可以同时访问N个资源又不导致死锁?

使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。

● Thread.sleep(0)的作用是什么?

这个问题和上面那个问题是相关的,我就连在一起了。由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。

● 高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?

(1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换

(2)并发不高、任务执行时间长的业务要区分开看:
假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务
假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换

(3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。

● 同步方法和同步块,哪个是更好的选择,你怎么看?

同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越少越好。借着这一条,我额外提一点,虽说同步的范围越少越好,但是在Java虚拟机中还是存在着一种叫做 锁粗化 的优化方法,这种方法就是把同步范围变大。这是有用的,比方说StringBuffer,它是一个线程安全的类,自然最常用的append()方法是一个同步方法,我们写代码的时候会反复append字符串,这意味着要进行反复的加锁->解锁,这对性能不利,因为这意味着Java虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此Java虚拟机会将多次append方法调用的代码进行一个锁粗化的操作,将多次的append的操作扩展到append方法的头尾,变成一个大的同步块,这样就减少了加锁–>解锁的次数,有效地提升了代码执行的效率。

● 线程类的构造方法、静态块是被哪个线程调用的

这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。如果说上面的说法让你感到困惑,那么我举个例子, 假设Thread2中new了Thread1,main函数中new了Thread2,那么:

(1)Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的

(2)Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的

● Hashtable的size()方法中明明只有一条语句”return count”,为什么还要做同步?

这是我之前的一个困惑,不知道大家有没有想过这个问题。某个方法中如果有多条语句,并且都在操作同一个类变量,那么在多线程环境下不加锁,势必会引发线程安全问题,这很好理解,但是size()方法明明只有一条语句,为什么还要加锁?

关于这个问题,在慢慢地工作、学习中,有了理解,主要原因有两点:
(1) 同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程同时访问 。所以,这样就有问题了,可能线程A在执行Hashtable的put方法添加数据,线程B则可以正常调用size()方法读取Hashtable中当前元素的个数,那读取到的值可能不是最新的,可能线程A添加了完了数据,但是没有对size++,线程B就已经读取size了,那么对于线程B来说读取到的size一定是不准确的。而给size()方法加了同步之后,意味着线程B调用size()方法只有在线程A调用put方法完毕之后才可以调用,这样就保证了线程安全性

(2) CPU执行代码,执行的不是Java代码,这点很关键,一定得记住 。Java代码最终是被翻译成汇编代码执行的,汇编代码才是真正可以和硬件电路交互的代码。 即使你看到Java代码只有一行,甚至你看到Java代码编译之后生成的字节码也只有一行,也不意味着对于底层来说这句语句的操作只有一个 。一句”return count”假设被翻译成了三句汇编语句执行,完全可能执行完第一句,线程就切换了。

● 多线程有什么用?

(1)发挥多核CPU的优势

随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4核、8核甚至16核的也都不少见,如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费了75%。 单核CPU上所谓的”多线程”那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程”同时”运行罢了 。多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的。

(2)防止阻塞

从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。

● Semaphore有什么作用,信号量是什么?

Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。

● 一个线程如果出现了运行时异常会怎么样?

如果这个异常没有被捕获的话,这个线程就停止执行了。另外重要的一点是: 如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放。

● 为什么使用线程池?

避免频繁地创建和销毁线程,达到线程对象的重用。另外,使用线程池还可以根据项目灵活地控制并发的数目。

● synchronized和ReentrantLock的区别

synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:

(1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁

(2)ReentrantLock可以获取各种锁的信息

(3)ReentrantLock可以灵活地实现多路通知

另外,二者的锁机制其实也是不一样的。ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的是对象头中mark word。

● 什么是多线程的上下文切换

多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。

● 如果你提交任务时,线程池队列已满,这时会发生什么

如果你使用的LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务;如果你使用的是有界队列比方说ArrayBlockingQueue的话,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy。

● 什么是CAS?

CAS,全称为Compare and Set,即比较-设置。假设有三个操作数: 内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false 。当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。

● 什么是AQS?

简单说一下AQS,AQS全称为AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。

如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,ReentrantLock、CountDownLatch、Semaphore等等都用到了它。AQS实际上以双向队列的形式连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行。

AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能。

● notify和notifyAll的区别?

notifyAll使所有原来在该对象上等待被唤醒的线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。notify是通知其中一个线程,不会通知所有的线程。

● 你是如何合理配置线程池大小的?

首先,需要考虑到线程池所进行的工作的性质:IO密集型?CPU密集型?
简单的分析来看,如果是CPU密集型的任务,我们应该设置数目较小的线程数,比如CPU数目加1。如果是IO密集型的任务,则应该设置可能多的线程数,由于IO操作不占用CPU,所以,不能让CPU闲下来。当然,如果线程数目太多,那么线程切换所带来的开销又会对系统的响应时间带来影响。

在《linux多线程服务器端编程》中有一个思路,CPU计算和IO的阻抗匹配原则。如果线程池中的线程在执行任务时,密集计算所占的时间比重为P(0 下面验证一下边界条件的正确性:

假设C = 8,P = 1.0,线程池的任务完全是密集计算,那么T = 8。只要8个活动线程就能让8个CPU饱和,再多也没用了,因为CPU资源已经耗光了。

假设C = 8,P = 0.5,线程池的任务有一半是计算,有一半在等IO上,那么T = 16.考虑操作系统能灵活,合理调度sleeping/writing/running线程,那么大概16个“50%繁忙的线程”能让8个CPU忙个不停。启动更多的线程并不能提高吞吐量,反而因为增加上下文切换的开销而降低性能。

如果P < 0.2,这个公式就不适用了,T可以取一个固定值,比如 5*C。另外公式里的C不一定是CPU总数,可以是“分配给这项任务的CPU数目”,比如在8核机器上分出4个核来做一项任务,那么C=4
文章如何合理设置线程池大小里面提到了一个公式:

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:

最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目
可以得出一个结论:

线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。

以上公式与之前的CPU和IO密集型任务设置线程数基本吻合。

● 线程池具体配置参数?

corePoolSize:核心线程数

queueCapacity:任务队列容量(阻塞队列)

maxPoolSize:最大线程数

keepAliveTime:线程空闲时间

allowCoreThreadTimeout:允许核心线程超时

rejectedExecutionHandler:任务拒绝处理器

● 线程池按以下行为执行任务:

(1)当线程数小于核心线程数时,创建线程。

(2)当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。

(3)当线程数大于等于核心线程数,且任务队列已满

若线程数小于最大线程数,创建线程

若线程数等于最大线程数,抛出异常,拒绝任务

全部教程