一、用户线程与内核线程
-
用户线程(ULT):指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统的核心,应用进程利用线程库创建、同步、调度和管理线程函数来控制用户线程。另外,因为用户线程是由应用进程利用线程库创建和管理,不依赖于操作系统核心,不需要进行用户态与核心态的切换,速度快,操作系统的内核不知道多线程的存在,因此一个线程阻塞将使得整个进程包括其所有线程阻塞
-
内核线程(KLT):线程的所有管理操作都是由操作系统内核完成的。内核保存线程的状态和上下文信息,当一个线程执行了引起阻塞的系统调用时,内核可以调度进程的其他线程执行,在多处理器系统上,内核可以分派属于同一进程的多个线程在多个处理器上运行,提高进程执行的并行度。由于需要内核完成线程的创建、调度和管理,所以和用户级线程相比这些操作要慢得多,但仍比进程创建和管理操作快的多
即内核线程的每个线程都会映射到操作系统的内核,由内核创建一个线程栈(线程表),之后才能有机会调度CPU。而用户线程的线程栈不会维护在内核中,内核并不知道这个进程中创建了哪些线程,内核只会维护一个进程表,分配CPU去执行进程
JVM使用的是内核级线程
我们可以试验一下,打开Windows资源管理器如下,可以看到此时约有2500个线程在运行
我们运行如下代码,创建300个线程,看运行后的资源管理器效果
public class ThreadTest {
public static void main(String[] args) {
for (int i = 0; i < 300; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
执行以上代码,可以看出瞬间就增加到了2800多个线程在运行
由此可以看出JVM确实是使用内核级线程,并且内核线程与Java-Thread是1:1的映射关系
线程的运行状态
二、线程池及其工作原理
线程是一种稀缺资源,它的创建与销毁是一个相对偏重且耗资源的操作,而Java线程依赖于内核线程,其线程的创建需要进行操作系统状态切换,为避免子安过低消耗需要设法重用线程执行多个任务,线程池就是一个线程缓存,负责对线程进行统一分配、调优与监控。
什么时候使用线程池?
- 单个任务处理时间比较短
- 需要处理的任务量很大
线程池优势
- 重用存在的线程,减少线程创建、消亡的开销,提高性能
- 提高响应速度,当任务到达时,任务不需要等线程创建就立即执行
- 提高线程的可管理性,如果无限制的创建,不仅会消耗资源,还会降低系统的稳定性,使用线程池可以统一的分配,调优和监控
线程池的种类很多,但常用的是通过newFixedThreadPool
创建固定数量的线程池,例如以下代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(5);
for (int i = 0; i < 3; i++) {
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("running...");
}
});
}
Thread.sleep(5000);
service.shutdownNow(); //终结线程
}
}
看一下其源码
其中有5个参数,第一个是核心线程数,第二个是最大线程数,第三个是最大空闲时间,这里为0则表示不会因为空闲等待而被回收掉,第四个是最大空闲时间的时间单位,第五个是当线程池满时再新入线程的处理方式,这里用的是一个阻塞队列来保存线程。
注意通过不同方式创建的线程池的一个重要区别在于当线程池已经无线程可用时,新入线程的处理方式,newFixedThreadPool
的处理方式为LinkedBlockingQueue
是一个有界队列,但理论上趋近于无界(最大元素存储量大概为100亿)
但是如果是为有界队列,当核心线程池已满,队列已满,非核心线程池(即定义的最大线程数)也满了的时候,线程就会执行拒绝操作,默认情况下是直接抛出异常终止程序运行
当一个线程执行完毕,核心线程和非核心线程都是从队列中取出任务执执行,而不是去外部接收新的任务执行,外部新的任务会再次填充到队列的空位中等待执行
线程池的整体执行步骤为:
- 任务先进入核心线程池执行
- 若核心线程池已满,则任务进入队列
- 若队列已满,则启动非核心线程执行任务,新任务填充到队列
- 若非核心线程也满了(队列也填充满),此时再进入任务则执行拒绝操作