一、线程与进程
线程定义
进程中执行的一个代码段,来完成不同的任务
组成:线程ID,当前指令指针(PC),寄存器集合(存储一部分正在执行线程的处理器状态的值)和堆栈
进程定义
执行的一段程序,一旦程序被载入到内存中准备执行就是一个进程
组成:文本区域(存储处理器执行的代码)、数据区域(存储变量和进程执行期间的动态分配的内存)和堆栈(存储着活动过程调用的指令和本地变量)
例如Windows系统中运行的一个exe就是一个进程
进入任务管理器,可以查看系统运行的进程,以及每个进程中的线程数
线程与进程的关系
1. 进程是系统内存分配的最小单位,线程是系统调度的最小单位
进程拥有自己的内存空间,因为线程是属于进程的,多线程直接共享该进程中内存,提高了程序的运行效率
2. 一个程序至少有一个进程,一个进程中包括一条 or 多条线程,线程不能独立于进程
在Java中,每次程序运行至少启动2个线程:一个是main线程,一个是垃圾收集线程。因为每当使用 Java 命令执行一个类时,都会启动一个JVM,每一个 JVM 实际上就是在操作系统中启动了一个进程
3. 进程与线程都可以并发执行
问题:如何了解 “并发” 执行 ,它与 “并行”执行一样吗?
并行执行:从宏观和微观的角度,都是同时执行的
并发执行:从宏观角度,似乎是同时执行;但从微观角度,不是同时执行
操作系统采取时间片的机制,使多个进程(线程)快速切换执行,在宏观上就有并行执行的错觉
在单核情况下,不存在并行执行;但在多核情况下,进程(线程)分布在不同的CPU中,可以并行执行程序
二、线程的生命周期
线程是一个动态执行的过程
1. 新建状态 New
创建线程对象,进入新建状态,此时线程属于 not alive,直到执行 start()
创建线程: 使用 new 关键字和Thread 类或其子类, 例如:Thread t = new MyThread();
2. 就绪状态 Runnable
调用线程对象的 start() 方法,进入就绪状态,此时线程属于 alive ,但还未进入执行,只是做好了被 CPU 调度的准备
3. 运行状态 Running
当线程获取到CPU,进入运行状态,线程的 run() 方法才开始被执行,此时线程属于 alive
只有当线程处于就绪状态,才能被CPU调度,所以就绪状态是运行状态的唯一入口
4. 阻塞状态 Blocked
处于运行状态的线程,由于某种原因,放弃使用CPU,停止运行,进入阻塞状态,此时线程属于 alive
同步阻塞:
同步锁 synchronized,当某线程占有了该同步锁, 则其他线程就不能进入到同步锁中,则这些线程就会进入阻塞状态
当在阻塞队列的线程获取到同步锁时,才能进入到就绪状态等待被调度
等待阻塞:(理解得有点绕)
调用线程的 wait() ,线程进入等待状态,此时会释放占用的 CPU 资源和锁(wait()方法需要在锁中使用)
当被其他线程调用 notify() 唤醒之后,需要重新获取对象的锁,所以会先进入Blocked状态,才会进入就绪状态
其他阻塞:
调用线程的 sleep() 或 join() 或 发出了I/O请求,线程会进入到阻塞状态
当 sleep() 状态超时、join() 等待线程终止或者超时、或者 I/O 处理完毕时,线程重新进入就绪状态
5. 死亡状态 Dead
当一个线程的 run() 方法运行完毕 or 被中断 or 被异常退出,该线程进入死亡状态
三、线程的创建
- 实现 Runnable 接口,实例化 Thread 类(线程无返回值)
- 继承 Thread 类,重写 Thread 的 run() 方法(线程无返回值)
- 实现 Callable 接口,通过FutureTask 包装器创建线程(线程有返回值)
1. 实现 Runnable 接口,实例化 Thread 类(线程无返回值)
step1: 创建一个类,例如 RunnableThread,实现 Runnable 接口
step2: 实例化 RunnableThread 对象, 创建 Thread 对象,将 RunnableThread 作为参数传给 Thread 类的构造函数,然后通过 Thread.start() 方法启动线程
运行结果
问题: 为什么创建 RunnableThread 对象后,需要将它和 Thread 对象进行关联?
查看 Runnable 接口的源代码,可以看到 Runnable 接口只有一个 run() 方法,所以需要通过 Thread 类的 start() 方法来启动线程
启动线程后,Thread 类中的 run() 方法会先判断传入的 target Runnable 对象的 run() 方法是否为空,若不为空,则调用 target Runnable 对象的 run() 方法
而且,RunnableThread 类实现 Runnbale 接口中不能直接使用 Thread 类中的方法,需要先获取到Thread 对象后,才能调用 Thread 方法
2. 继承 Thread 类,重写 Thread 的 run() 方法(线程无返回值)
step1: 创建一个类,例如 MyThread,继承 Thread 类,重写 Thread 的 run() 方法
step2: 实例化 MyThread 对象,直接调用 start() 方法启动线程
运行结果
问题:实现 Runnable 接口 和 继承 Thread 类,运行结果不一样,这是为什么?
继承 Thread 类和实现 Runnable 接口实现多线程,会发现这是两个不同的实现多线程
继承 Thread 类是多个线程分别完成自己的任务
实现 Runnable 接口是多个线程共同完成一个任务,其实用继承Thread类也可以实现,只是比较麻烦
这样的话,实现 Runnable 接口比继承 Thread 类具有一定的优势
1)适合多个相同的程序代码的线程去处理同一个资源
2)可以避免 Java 中的单继承的限制
当一个类继承 Thread 类后,则不能在继承别的类,而接口比较灵活,可以实现多个接口,而且实现接口了还可继续继承一个类
3)增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
3. 实现 Callable 接口,通过 FutureTask 包装器创建线程(线程有返回值)
step1: 创建一个类,例如 CallableThread,实现 Callable 接口,重写 Callable 接口的 call() 方法
step2: 实例化 CallableThread 对象,使用 FutureTask 类来包装 CallableThread 对象
然后 FutureTask 对象作为参数传给 Thread 类的构造函数,通过 Thread.start() 方法启动线程
使用 FutureTask.get() 得到 Callable 接口的 call() 方法的返回值
返回结果
Callable 和 Runnable 相似,类实例都需要被 Thread 执行,但 Callable 接口能返回一个值或者抛出一个异常,Runnable 不能
实现 Callable 接口需要重写其唯一的 call() 方法
FutureTask 实现了 Runable 接口 和 Future 接口,所以如果想 Callable 实例作为 Thread 的执行体就必须通过 FutureTask 来作为桥梁