- 一、进程与线程
- 二、线程模型
- 2.1、一对一模型
- 2.2、多对一模型
- 2.3、多对多模型
- 三、内核态与用户态
- 四、线程的创建函数
- 五、线程的状态
- 5.1、start() 与 run() 方法
- 5.2、sleep() 与 yield() 方法
- 5.3、join() 方法
- 5.4、setDaemon() 守护线程
- 六、线程的优先级
进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、 磁盘 IO 等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程之间是相互独立的。
进程是程序在计算机上的一次执行活动。当运行了一个程序,就启动了一个进程。进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身,用户进程就是所有由你启动的进程。
线程是CPU进行调度的最小单位,必须依赖于进程而存在,它是比进程更小的、 能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是可与同属一个进程的其他的线程共享进程所拥有的全部资源。
线程无处不在,任何一个程序都必须要创建线程,特别是Java不管任何程序都必须启动一个main函数的主线程,Java Web开发里面的定时任务、定时器、JSP 和 Servlet、异步消息处理机制,远程访问接口RMI等,任何一个监听事件,onclick的触发事件等都离不开线程和并发的知识。
二、线程模型
在Java中经常说的线程(Thread)实际上应该叫作“用户线程”,而对应到操作系统,还有另外一种线程叫作“内核线程”。 用户线程和内核线程之间必然存在某种关系,多对一模型、一对一模型和多对多模型。
2.1、一对一模型即一个用户线程对应一个内核线程,内核负责每个线程的调度。
- 优点: 比如JVM几乎把所有对线程的操作都交给了内核,实现线程模型的容器(JVM)简单,所以我们经常听到在Java中使用线程一定要慎重就是这个原因。
- 缺点: 对用户线程的大部分操作都会映射到内核线程上,引起用户态和内核态的频繁切换;内核为每个线程都映射调度实体,如果系统出现大量线程,会对系统性能有影响。
多个用户线程对应到同一个内核线程上,线程的创建、调度、同步的所有细节全部由进程的用户空间线程库来处理。
- 优点:
用户线程的很多操作对内核来说都是透明的,不需要用户态和内核态的频繁切换,使线程的创建、调度、同步等非常快。 - 缺点:
由于多个用户线程对应到同一个内核线程,如果其中一个用户线程阻塞,那么该其他用户线程也无法执行;内核并不知道用户态有哪些线程,无法像内核线程一样实现较完整的调度、优先级等。
又叫作两级线程模型,它充分吸收前面两种线程模型的优点且尽量规避它们的缺点。在此模型下用户线程与内核线程是多对多(M : N,通常M >= N)的映射模型。
- 优点:
兼具多对一模型的轻量,并且由于对应了多个内核线程,则一个用户线程阻塞时,其他用户线程仍然可以执行,可以实现较完整的调度、优先级等。 - 缺点:
实现比较复杂。
三、内核态与用户态
Intel X86 中将 CPU 指令权限划分为了 4 个执行级别 Ring0-Ring3,Linux只使用了其中的 Ring0级 和 Ring3 级分别来表示内核态和用户态,所谓的内核态和用户态其实仅仅是CPU的一个权限而已。
例如在内存分配中,有一部分内存是仅为内核态使用的,用户态是不允许访问那些内存地址,每个进程只允许访问自己申请到的内存,且不允许访问外围设备。大多数时间各类程序都是执行在用户态下。
当用户态的程序需要向操作系统申请更高权限的服务时,就通过系统调用向内核发起请求。内核自然也会提供很多的接口来供调用,应用程序可以通过库函数来调用内核的提供的接口,而库函数就会发起系统调用,发起了系统调用之后,用户态就会切换成内核态去执行对应的内核方法。
用户态切换到内核态的3种方式:
- 系统调用
- 异常(这个异常不是Java当中的异常)
- 外设中断
用户态相较于内核态有较低的执行权限,很多操作是不被操作系统允许的,原因简单来说就是用户态出现问题(程序中的bug),也不能影响操作系统的运行。
其实站在Java程序员的角度只需要关注系统调用,因为系统调用可以认为是用户进程主动发起的,比如调用线程的 park() 方法会对应到一个OS的一个函数,从而使当前线程进入了内核态;再比如遇到 synchronized 关键字,如果是重量锁则会调用 pthread_mutex_lock 这样我们的线程也会切换到内核态;当执行完系统调用切换到用户态。
四、线程的创建函数
通过上述线程模型的介绍,知道Java是采用的一对一模型,所以Java代码中通过 Thread.start() 方法启动一个线程(让一个线程进入就绪状态等待CPU调度),其底层也是通过调用 JNI 去实现的。
可以看到这个方法最核心的就是调用了一个 start0() 方法,而该方法又是一个 native 方法,其实在Hotspot源码中是调用了 pthread_create 函数来启动线程的,所以说Java级别的线程其实就是操作系统级别的线程。
这里我们可以执行 man pthread_create 来查看下该命令的作用,如下:
根据man配置的信息可以得 pthread_create 会创建一个线程,这个函数是linux系统的函数,可以用C或者C++直接调用,上面信息也告诉我们该函数在
| 参数名称 | 参数定义 | 参数解释 |
|---|---|---|
| pthread_t *thread | 传出参数,调用之后会传出被创建线程的ID | 定义 pthread_t pid; 继而取地址 &pid |
| const pthread_attr_t *attr | 线程属性,关于线程属性是linux的知识 | 一般传null,保持默认属性 |
| void (start_routine) (void *) | 线程的启动后的主体函数 | 需要你定义一个函数,然后传函数名即可 |
| void *arg | 主体函数的参数 | 没有可以传null |
五、线程的状态
线程是有多种状态,其关系主要如下图所示:
这里先简单介绍如下几个方法,其余方法后续会进行详细介绍。
5.1、start() 与 run() 方法-
start() 方法让一个线程进入就绪队列等待CPU调用,分配到CPU后才调用实现的 run() 方法,start() 方法不能重复调用,如果重复调用会抛出异常(会校验 threadStatus 的值)。
-
run() 方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,也可以被单独调用,同步执行。
-
sleep() 方法会使线程进入阻塞状态,等待休眠时间达到,再进入就绪状态等待CPU调用,需要注意的是 sleep() 是不会释放锁资源的。
-
yield() 方法会立即进入就绪状态,有可能在进入到就绪状态后会被操作系统再次选中马上又被运行,同样 yield() 也不会释放锁资源。
join() 方法的作用,是等待这个线程结束。
多个线程之间是没有先后顺序的,但是如果我们希望线程A执行结束之后,我们线程B再执行,这时就可以用 join() 方法来解决。比如说可以在 main() 主线程内,等待其它线程完成再结束 main() 主线程。
5.4、setDaemon() 守护线程
守护线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。可以通过调用 thread.setDaemon(true) 将线程设置为守护线程。
当我们启动一个main()方法时,就会创建很多的守护线程,可以通过 ThreadMXBean 接口打印出来,其中除main线程外都是守护线程(包括GC线程),都会随我们的main线程的终止而终止。
public class App {
public static void main(String[] args) {
// ThreadMXBean虚拟机线程管理的接口
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
for (ThreadInfo threadInfo : threadInfos) {
System.out.println(threadInfo.getThreadId() + "..." + threadInfo.getThreadName());
}
}
}
在我们的使用守护线程时,也是一个比较重要的点,需要注意是,finally() 方法不一定会被执行,如下:
六、线程的优先级
在Java线程中,通过一个整型成员变量 priority 来控制优先级,优先级的范 围从 1~10,在线程构建的时候可以通过 setPriority(int) 方法来修改优先级,默认优先级是 5,优先级高的线程分配时间片的数量要多于优先级低的线程。
但是我们非常要注意的问题时,我们在设置了线程的优先级时,是不可靠的,比如我们把一个线程的优先级设置最高时,它不一定每次都是优先执行的,它只是优先执行的概率比较高,并不一定的,所以不能依赖线程的优先级来进行设置。



