1、进程
在解释“线程”之前,首先要搞清楚“进程”的这一概念。
进程(Process)是操作系统中的重要概念。
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。(来自“百度百科-进程”)
对于计算机而言,操作系统其主要的功能之一是为了更好的管理各种资源,因为对任何计算机而言,资源是有限的(例如:内存等)。而使用、消耗计算机中的各种资源的,毫无疑问是计算机中的程序,特别是运行当中的程序,如果没有相应的资源,程序无法正常执行。
进程,从某种意义而言是操作系统与计算机程序之间的“桥梁”,程序运行通过某种过程,这个过程可以理解为是一种“映射”,“映射”成为进程,操作系统能够识别并有效管理进程,为进程分配所需要的资源,从而实现程序的成功运行。所以会有“进程是程序的实体”这一说法。对于不同的操作系统而言,对于“进程”相关细节方面可能有所不同,但总体的理念、思路等方面是基本一致的。
目前主流的操作系统都支持多进程,为了让操作系统同时可以执行多个任务,但进程是重量级的,计算机的资源有限,新建一个进程会消耗 CPU 和内存空间等系统资源,因此进程的数量比较局限(进程数量会有上限)。
Windows 系统可以通过“任务管理器”查看进程。
2、线程
线程(Thread)同样也是操作系统中的重要概念。
线程,是轻量级的进程,是程序执行的最小单位。是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。(来自“百度百科-线程”)
一个进程下,可能会有多个线程。进程是线程的容器,操作系统为进程分配资源,其中的线程可以共享这部分资源,即同一进程下的线程都可以使用进程分配到的资源。
使用多线程而不是用多进程进行并发程序的设计,是因为线程之间的切换和调度的成本远远小于进程。
线程就是进程内部的程序流,也就是说操作系统内部支持多进程的,而每个进程的内部又是支持多线程的,线程是轻量的,新建线程会共享所在进程的系统资源,因此目前主流的开发都是采用多线程。
多线程是采用时间片轮转来保证多个线程的并发执行(见下图),所谓并发就是指“宏观并行,微观串行”的机制。
“并行”与“并发”:“并行”的意义是说“多个任务同时执行”,特别强调“同时”;而“并发”实际上是“多个任务交替执行”,如果这个过程中,任务交替十分迅速,给人的“并行”的感受,也就是“宏观并行,微观串行”。实现真正的“并行”,成本是很高,比如说现代计算机会基于“多核的 CPU”来真正实现“并行”。所以,对于程序设计而言,更多的角度是“并发”,因为容易实现且成本低。
2.1、线程的生命周期
- 对于不同的操作系统而言,在线程的生命周期上可能会有小细节上的不同,总体上是一致的。
- 注意:当线程仅是由于“时间片轮转”的原因而暂停运行,线程的状态转变为“就绪态”而不是“阻塞态”。
3、Java 中的线程-Thread 类
3.1、概述
Java 提供
java.lang.Thread
类来实现有关线程的操作。在 Java 开发中,
java.lang.Thread
类是专门用来描述线程的一个类,任何线程(对象)都是java.lang.Thread
类(或其子类)的实例。java.lang.Thread
类是线程的模板,封装了复杂的线程开启等操作,同时也封装了不同操作系统的差异性。结合
java.lang.Thread
类,再来看线程的生命周期:
- 一些基础的方法:
3.2、创建线程
实现“多线程”,首先要能够创建线程。
创建线程方法一:自定义类继承
java.lang.Thread
类并重写run
方法。然后创建该类的对象调用start
方法。(启动线程的时候,需要我们调用的不是run
方法,而是start
方法!)创建线程方法二:自定义类实现
java.lang.Runnable
接口并重写run
方法,将该类的对象作为实参来构造java.lang.Thread
类型的对象(直接使用匿名内部类也可以)。然后使用java.lang.Thread
类型的对象调用start
方法。(启动线程的时候,需要我们调用的不是run
方法,而是start
方法!)
/* 自定义类继承 java.lang.Thread 类并重写 run 方法 */
public class SubThread extends Thread {
@Override
public void run() {
for (int i = 1; i < 10; i++) {
System.out.println("Sub Thread: " + i);
}
}
}
public class SubThreadTest {
public static void main(String[] args) {
Thread subThread = new SubThread();
subThread.start();
for (int i = 1; i < 10; i++) {
System.out.println("Main Thread: " + i);
}
}
}
输出:
Main Thread: 1
Sub Thread: 1
Main Thread: 2
Main Thread: 3
Sub Thread: 2
······
可以看到,输出的结果与之前有所不同,这个输出并不是按照代码的先后顺序的输出,而是“启动了所创建的线程”(调用start
方法)后,形成了一种类似于“交替执行”的情况,并且,每一次运行得到的输出是不一样的,但是会是“交替执行”的情况。这个就是“多线程”。
main
方法的执行,也是一个线程,这个线程一般称为“主线程”。
上述代码是主线程与子线程两个线程并发执行。
/* 自定义类实现 java.lang.Runnable 接口并重写 run 方法,
将该类的对象作为实参来构造 java.lang.Thread 类型的对象 */
public class SubThreadRunnable implements Runnable {
@Override
public void run() {
for (int i = 1; i < 10; i++) {
System.out.println("Sub Thread: " + i);
}
}
}
public class SubThreadRunnableTest {
public static void main(String[] args) {
Thread subThread = new Thread(new SubThreadRunnable());
subThread.start();
for (int i = 1; i < 10; i++) {
System.out.println("Main Thread: " + i);
}
}
}
/* 匿名内部类 */
public class ThreadTest {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i < 10; i++) {
System.out.println("Sub Thread: " + i);
}
}
}).start();
for (int i = 1; i < 10; i++) {
System.out.println("Main Thread: " + i);
}
}
}
/* 匿名内部类,lambda 表达式 */
public class ThreadTest {
public static void main(String[] args) {
new Thread(() -> {
for (int i = 1; i < 10; i++) {
System.out.println("Sub Thread: " + i);
}
}).start();
for (int i = 1; i < 10; i++) {
System.out.println("Main Thread: " + i);
}
}
}
- 并不是说在多线程中就不会按照代码顺序执行,代码的执行还是会按照代码编写的顺序的,只不过是启动多个线程后,会存在线程调度的问题也就是谁先被调度运行,会有一种看似比较“混乱”的执行结果,但是代码按顺序执行与多线程之间并不矛盾。
public class ThreadTest {
// 按照下面的写法是一定不会出现那种“交替执行”的结果
// 因为代码是按顺序执行的,在新的线程 start 之前,需要将下面的 for 循环先执行完
public static void main(String[] args) {
for (int i = 1; i < 10; i++) {
System.out.println("Main Thread: " + i);
}
new Thread(() -> {
for (int i = 1; i < 10; i++) {
System.out.println("Sub Thread: " + i);
}
}).start();
}
}
对于上述两种创建线程并启动的方案中,要说明的是,启动线程调用的是
start
方法而不是run
方法,run
方法是在start
方法被调用后由 JVM 自动去调用的(当然run
方法执行结束后,一般情况下线程也就自然结束了)。如果选择采用“实现
java.lang.Runnable
接口的对象作为java.lang.Thread
构造方法的参数”,从源码可以得到,JVM 所调用的run
方法是实现java.lang.Runnable
接口时重写的run
方法。
/* 源码 */
public class Thread implements Runnable {
······
public Thread(Runnable target) {
this(null, target, "Thread-" + nextThreadNum(), 0);
}
private Thread(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
······
this.target = target;
······
}
@Override
public void run() {
if (target != null) {
target.run(); // this.target.run();
}
}
······
}
- 一般情况下,采用实现
java.lang.Runnable
接口的情况比继承java.lang.Thread
情况要多,因为继承是“单继承”,如果决定采用继承java.lang.Thread
,便无法继承其他的类,变得不灵活。
3.3、线程的名称、编号
-
java.lang.Thread
类提供了一些关于线程名称、编号的方法。
public class SubThread extends Thread {
public SubThread() {
}
public SubThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println("子线程的编号:" + getId() + ", 子线程的名称:" + getName());
}
}
public class SubThreadTest {
public static void main(String[] args) {
// 获取当前正在执行的线程的引用,因为是在 main 方法中调用,所以获得的是主线程的引用
// 因为代码按顺序执行,那么先输出的一定是主线程的信息
// 如果前两行代码与后两行代码交换位置,先输出什么哪个线程的信息就不确定了,因为启动所创建出的子线程后,就看哪个线程会被调度运行
Thread mainThread = Thread.currentThread();
System.out.println("主线程的编号:" + mainThread.getId() + ", 主线程的名称:" + mainThread.getName());
Thread subThread = new SubThread("sub-thread");
subThread.start(); // 启动子线程
}
}
public class SubThreadRunnable implements Runnable {
@Override
public void run() {
Thread currentThread = Thread.currentThread(); // 对于通过实现 Runnable 接口的,如果进行获取线程名称、线程编号等操作,需要通过 currentThread 方法获取当前正在执行线程的引用
System.out.println(currentThread.getId());
System.out.println(currentThread.getName());
}
}
3.4、有关线程生命周期的方法
- Java 中
java.lang.Thread
类实际上已经帮助我们屏蔽了很多细节问题,让实现“多线程”更为方便,特别要说的就是其针对线程生命周期操作,对外提供了一些方法。(结合上面的图“码出高效-线程生命周期1、2”会更好理解下面的内容)
3.4.1、线程睡眠
-
sleep
方法:线程睡眠,可以将线程由 RUNNING 状态转变为 BLOCKED 状态,正常情况待“睡眠”时间结束后,线程会转为 RUNNABLE 状态。
/* 利用创建的线程,每隔1s输出数字 */
public class ThreadSleep implements Runnable {
@Override
public void run() {
int i = 0;
while (true) {
System.out.println(i++);
try {
Thread.sleep(1000); // 此处处理异常不能选择“抛出”,因为重写的方法不能抛出更大的异常 或者 原来没有抛出异常然而重写时候出现“抛出”
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadSleepTest {
public static void main(String[] args) {
Thread subThread = new Thread(new ThreadSleep());
subThread.start();
}
}
3.4.2、线程优先级
- 线程的优先级,在
java.lang.Thread
类中,定义了一个priority
的私有成员变量来表示的线程的优先级。同时,在java.lang.Thread
类中还定义了几个线程优先级常量:
/**
* The minimum priority that a thread can have.
*/
public static final int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public static final int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public static final int MAX_PRIORITY = 10;
可以看出,Java 中线程的优先级的范围在1~10,数字越大代表优先级越高。
public class ThreadPriority implements Runnable {
@Override
public void run() {
Thread currentThread = Thread.currentThread();
currentThread.setPriority(Thread.MIN_PRIORITY); // 子线程的优先级设置为最低
int i = 1;
while (true) {
System.out.println("Sub Thread: " + i);
i++;
}
}
}
public class ThreadPriorityTest {
public static void main(String[] args) {
Thread currentThread = Thread.currentThread();
currentThread.setPriority(Thread.MAX_PRIORITY); // 主线程的优先级设置为最高
Thread subThread = new Thread(new ThreadPriority());
subThread.start();
int i = 1;
while (true) {
System.out.println("Main Thread: " + i);
i++;
}
}
}
因为上面的两个线程都编写了“死循环”的代码,所以通过上面的代码运行的结果可以看出优先级的作用,当程序手动停止的时候,最后是优先级高的输出的i
值越大。
再次说明一下,优先级高并不代表着可以优先执行,也不代表着优先级高的线程可以一直会占用着计算机的资源(CPU 等)“不放手”。只是说,在线程调度的时候,优先级高的线程获得运行的机会即获得的时间片要比优先级低线程的要多一些。
3.4.3、线程等待
java.lang.Thread
类中,提供join
方法来实现线程等待。join
方法不是静态方法,需要线程类引用调用。一旦调用,意味着“方法调用位置”所在的线程要“等待”引用所代表的线程终止才能进行。join
的中文含义有“加入”的意思,形象地说就是“你在那等下我,我处理完后,你再继续”。
/* 模拟“倒计时10s”的一个线程 */
public class ThreadJoin implements Runnable {
@Override
public void run() {
for (int i = 10; i >= 1; i--) {
System.out.println("倒计时 " + i + " 秒");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadJoinTest {
public static void main(String[] args) {
Thread subThread = new Thread(new ThreadJoin());
subThread.start();
try {
subThread.join(); // 此处调用,主线程将会等待“倒计时10s”的线程结束(终止)后再进行
System.out.println("新年快乐!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
-
join
方法编写的位置一般在start
方法之后。
3.4.4、守护线程
Java 中的线程,可以分为两类,一类是普通线程(也有称“用户线程”,因为这些线程的创建是因为用户使用计算机上的资源);另一类是守护线程(Daemon Thread),也称“服务线程”。
操作系统中并没有“守护线程”这一概念,Java 语言机制是构建在 JVM 的基础之上的,这一机制意味着 Java 平台是把操作系统的底层给屏蔽了起来,所以它可以在它自己的虚拟的平台里面构造出对自己有利的机制,“守护线程”就是一个例子。
守护线程的作用,为的是“守护”其他线程、“服务”其他线程。
守护线程所“守护”、“服务”的线程,一旦终止了,守护线程随之也会终止。基于目前个人所理解的总结,Java 中的守护线程能够“守护”当前所编写的程序中的所有用户线程。
Java 中通过使用
java.lang.Thread
类创建的线程,默认情况下都是普通线程。java.lang.Thread
类中,提供了isDaemon
方法来判断线程是否是守护线程;使用setDaemon
方法可以将普通线程设置成守护线程,但是要求“调用setDaemon
方法将某一线程设置为守护线程必须在该线程启动即调用start
之前”,否则“会发生异常”。
/* 线程1:模拟“倒计时3s” */
public class SubThreadOne implements Runnable {
@Override
public void run() {
for (int i = 3; i >= 1; i--) {
System.out.println("sub thread 1: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/* 线程2:将设置为守护线程,0一直累加并输出,便于观察守护线程的终止 */
public class SubThreadTwo implements Runnable {
@Override
public void run() {
int i = 0;
while (true) {
System.out.println("sub thread 2: " + i);
i++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/* main 方法,另外在主线程中有个 for 循环 */
public class Test {
public static void main(String[] args) {
Thread subThreadTwo = new Thread(new SubThreadTwo());
Thread subThreadOne = new Thread(new SubThreadOne());
subThreadTwo.setDaemon(true); // 线程2设置为守护线程
subThreadTwo.start(); // 启动线程2即启动守护线程
subThreadOne.start(); // 启动线程1
// 主线程
for (int i = 1; i <= 10; i++) {
System.out.println("main thread: " + i);
}
}
}
上述代码,最后的输出结果可以看到,当线程1与主线程全部执行完后,守护线程即线程2才终止。
3.5、案例
编程创建两个线程,线程一负责打印1~100之间的所有奇数,其中线程二负责打印1~100之间的所有偶数。在
main
方法启动上述两个线程同时执行,主线程等待两个线程终止。
public class SubThreadOne implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 100; i += 2) {
System.out.println("Sub Thread 1: " + i);
}
}
}
public class SubThreadTwo implements Runnable {
@Override
public void run() {
for (int i = 2; i <= 100; i += 2) {
System.out.println("Sub Thread 2: " + i);
}
}
}
public class Test {
public static void main(String[] args) {
Thread subThreadOne = new Thread(new SubThreadOne());
Thread subThreadTwo = new Thread(new SubThreadTwo());
subThreadOne.start();
subThreadTwo.start();
System.out.println("Main Thread is waiting!");
try {
subThreadOne.join();
subThreadTwo.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main Thread Finish!");
}
}