一、线程简介
1、什么是线程
现代操作系统在运行一个程序时,会为其创建一个进程。例如启动一个java程序,就会创建一个java进程。线程是操作系统最小的调度单位,也叫做轻量级进程,是进程中实际的运作单位。在一个进程里可以创建多个线程,每个线程的实体都包含程序计数器、堆栈、保留局部变量、寄存器、少数状态参数等,这些线程都能访问共享的内存变量。
线程和进程的区别是:
进程是操作系统资源分配的基本单位,线程是任务调度和执行的基本单位,进程不是一个可执行实体,线程是能独立运行的基本单位。
进程间相互独立,进程有单独完整的虚拟地址空间,而线程间共享所属进程的资源对其他进程不可见。
进程间通信主要是管道、消息队列、信号、共享存储、套接字,而线程可以直接读写进程数据段(如全局变量)来通信。
线程的上下文切换比进程的上下文切换要快的多,因为线程共享所属进程的虚拟空间内存,所以线程的上下文切换不必切换虚拟空间内存,代价要小的多。
一个java程序从main()方法开始执行,java实际上天生就是多线程程序,执行main方法的是名叫main的线程,大量的其他子线程都是从它这里产生的,mian线程不是守护线程,通常它是最后一个执行完毕的线程。下面用JMX来查看一个普通的Java程序包含哪些线程。
《java并发编程的艺术》第4章第1节例子:
public static void main(String[] args) {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "]" + threadInfo.getThreadName());
}
}
书中给出的输出内容是:
[4] Signal Dispatcher //分发处理发送给JVM信号的线程
[3] Finalizer //调用对象 finalize 方法的线程
[2] Reference Handler //清除Refernce的线程
[1] main //main线程,用户程序入口
据考证,在linux系统环境下会输出书中一样的结果
在windows下 jdk11会输出:
[1]main
[2]Reference Handler
[3]Finalizer
[4]Signal Dispatcher
[5]Attach Listener // jvm接收外部命令的线程,如果JVM启动时每初始化,那第一次执行jvm命令时会启动。
[20]Common-Cleaner
在Mac系统下:
[9] Monitor Ctrl-Break
[4] Singal Dispatcher
[3] Finalizer
[2] Refernce Handler
[1] main
2、为什么要使用多线程
“执行一个简单的hello world程序,却启动了那么多无关线程,是不是把简单的问题复杂化?”,《java并发编程的艺术》书中回答说:当然不是,因为正确使用多线程,总是能够给开发人员带来显著的好处....
我认为如果只是执行helloworld这种程序,肯定不需要多线程环境执行,但JVM的存在的意义不是执行这么简单的程序,而是根据需求得出最佳的编程模型,以较高的效率执行代码。所以,使用多线程的原因主要有以下几点:
1、从执行效率上讲,在多核CPU系统上,将要执行的任务分割成多个任务并行执行,就可以显著提高执行效率。单核单线程处理器上,只能实现并发,也就是在一个时间区间内,任务根据系统分配的时间片上下文切换先后执行,因为时间片非常短,所以感觉是同时执行的,但实际上不考虑阻塞会比单线程更慢。而并行执行可以充分利用多核CPU的优势,多个任务同时跑在CPU上,无疑显著的减少程序处理时间,提高代码的执行效率。
2、从程序设计角度上讲,在一些适合的场合,用多线程可以避免阻塞。比如在网络编程上,IO操作会导致当前线程阻塞,但如果采用多线程,可以避免因IO操作而导致无法接收其他客户端的连接请求。在实际开发中,比如查询数据库的操作,会导致当前线程阻塞直到得到查询结果,这对服务器程序来讲无法接受,所以可以在主线程外另起一个线程来做查询操作,将一些后续的操作放在这个线程里处理,可以充分避免因查询阻塞而导致服务器卡顿的情况。
二、线程的使用
1、线程优先级
线程优先级在早期的java版本中可能很有用,但现在不推荐使用它。在java线程中,可以通过一个整型成员变量priority来控制优先级,范围从1到10,在线程构建时可通过setPriority来修改优先级,默认优先级是5。优先级高的获得时间片的几率会大于优先级低的。
线程优先级不能作为程序正确性的依赖,因为操作系统可能完全不理会java线程对优先级的设定。
2、线程的状态
java线程在运行的生命周期中可能处于6种不同的状态,在一个给定的时刻,线程只能处于其中的一个状态。
状态名称 说明
NEW 初始状态,线程被构建,但还没调用start方法
RUNNABLE 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中”
BLOCKED 阻塞状态,表示线程阻塞于锁
WAITING 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
TIME_WAITING 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的
TERMINATED 终止状态,表示当前线程已经执行完毕
3、Daemon守护线程
Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以调用Thread.setDaemon(true)将线程设置为Daemon线程,但要在线程执行start方法之前初始状态设置,不能在启动线程之后设置。
Daemon线程被用作完成支持性工作,但是在java虚拟机退出时Daemon线程中的finally块并不一定会执行,所以在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。
4、线程的启动和终止
4.1 构造线程:
在运行线程之前首先要构造一个线程对象,线程对象在构造的时候需要提供线程所需要的属性,如线程所属的线程组、线程优先级、是否是Daemon线程等信息。
在构建过程中,一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程继承了parent是否为Daemon、优先级和加载资源的contextClassLoader以及可继承的ThreadLocal,同时还会分配一个唯一ID来标识这个child线程。至此,一个能够运行得线程对象就初始化好了,在堆内存中等待着运行。
创建线程的四种方式:继承Thread类、实现runnable接口、实现callable接口、使用线程池创建
4.2 启动线程:
线程对象初始化完成后,调用start()方法就可以启动这个线程。线程start()方法的含义是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程,调用该线程的run()方法。
4.3 理解中断:
在程序中免不得要碰到需要暂停或停止当前线程的情况,比如负责下载的程序会占用大部分带宽,用户想玩网游就要暂停下载任务,这个时候就要停止当前的线程让出带宽给用户玩游戏,而这种情况就需要用到线程的中断机制。
中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程调用该线程的interrupt()方法对其进行中断操作。
线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。如果该线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返回false。
从Java的API中可以看到,许多声明抛出InterruptedException的方法(例如Thread.sleep(long millis)方法)这些方法在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException,此时调用isInterrupted()方法将会返回false。
可以看一下以下例子观察两个线程的中断标识位。
输出的结果:
从结果可以看出,抛出InterruptedException的线程SleepThread,其中断标识位被清除了,而一直忙碌运作的线程BusyThread,中断标识位没有被清除。
4.4 过期的suspend()、resume()和stop()
大家对于CD机肯定不会陌生,如果把它播放音乐比作一个线程的运作,那么对音乐播放做出的暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。
在以下代码中,创建了一个线程PrintThread,它以1秒的频率进行打印,而主线程对其进行暂停、恢复和停止操作。
输出如下:(输出内容中的时间与示例执行的具体时间相关)
在执行过程中,PrintThread运行了3秒,随后被暂停,3秒后恢复,最后经过3秒被终止。通过示例的输出可以看到,suspend()、resume()和stop()方法完成了线程的暂停、恢复和终止,而且非常“人性化”。但是这些API是过期的,不建议使用。
不建议使用的主要原因有:suspend()方法在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。
暂停和恢复操作可以用等待/通知机制替代。
4.5 安全地终止线程
可以采用对线程设置中断标识后安全的终止线程。使用interrupt()方法设置线程的中断标识位,由该线程检测进行中断。另外还可以在该线程中自主增加一个boolean类型用于判断是否终止该线程。
输出结果:(输出内容可能不同)
三、常见面试题问题
1、线程和进程的区别是什么
2、线程的状态有哪几种
3、Java中创建线程有几种方式,你喜欢哪一种,为什么
4、如何安全的终止线程
5、为什么不推荐使用suspend()、resume()和stop()
摘抄出处:《java并发编程的艺术》