一、摘要
在很多场景下,我们经常听到采用多线程编程,能显著的提升程序的执行效率。例如执行大批量数据的插入操作,采用单线程编程进行插入可能需要 30 分钟,采用多线程编程进行插入可能只需要 5 分钟就够了。
既然多线程编程技术如此厉害,那什么是多线程呢?
在介绍多线程之前,我们还得先讲讲进程和线程的概念。
从计算机角度来讲,进程是操作系统中的基本执行单元,也是操作系统进行资源分配和调度的基本单位,并且进程之间相互独立,互不干扰。
例如,我们windows
电脑中的 Chrome 浏览器是一个进程、WeChat 也是一个进程,正在操作系统中运行的.exe
都可以理解为一个进程。
关于线程,比较官方的定义是,线程是进程中的⼀个执⾏单元,也是操作系统能够进行运算调度的最小单位,负责当前进程中程序的执⾏。同时⼀个进程中⾄少有⼀个线程,⼀个进程中也可以有多个线程,它们共享这个进程的资源,拥有多个线程的程序,我们也称为多线程编程。
举个例子,Chrome 浏览器和 WeChat 是两个进程,Chrome 浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。
关于进程和线程,可能上面的解释过于抽象,还是很难理解,下面是一段出自阮一峰老师博客文章的介绍,可能描述不是非常严谨,但是足够形象,有助于我们对它们关系的理解。
不难看出,互斥锁 Mutex 是信号量 semaphore 的一种特殊情况(n = 1时)。也就是说,完全可以用后者替代前者。但是,因为 Mutex 较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种方式。
早期的操作系统都是以进程作为独立运行的基本单位的,直到后期计算机科学家们又提出了更小的能独立运行的基本单位,也就是线程。
那为什么要引入线程呢?我们只需要记住这句话:线程又称为迷你进程,但是它比进程更容易创建,也更容易撤销。
引入线程之后,可以将复杂的操作进一步分解,让程序的执行效率进一步提升。
举个例子,进程就如同一个随时背着粮草和机枪的士兵,这样肯定会造成士兵的执行战斗的速度。因此,一个简单想法就是:分配两个人来执行,一个士兵负责随时背着粮草,另一个士兵负责抗机枪战斗,这样执行战斗的速度会大幅提升。这些轻装上阵的士兵,可以理解为我们上文提到的线程!
从计算机角度来说,由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,需要较大的时间和空间开销。
为了减少进程切换的开销,把进程作为资源分配单位和调度单位这两个属性分开处理,即进程还是作为资源分配的基本单位,但是把调度执行与切换的责任交给线程,即线程成为独立调度的基本单位,它比进程更容易(更快)创建,也更容易撤销。
一句话总结就是:引入线程前,进程是资源分配和独立调度的基本单位。引入线程后,进程是资源分配的基本单位,线程是独立调度的基本单位,线程也是进程中的⼀个执⾏单元。
在 Java 里面,创建线程有以下两种方式:
java.lang.Thread
类,重写run()
方法java.lang.Runnable
接口,然后通过一个java.lang.Thread
类来启动不管是哪种方式,所有的线程对象都必须是Thread
类或其⼦类的实例,每个线程的作⽤是完成⼀定的任务,实际上就是执⾏⼀段程序流,即⼀段顺序执⾏的代码,任务执行完毕之后就结束了。
在 Java 中,通过Thread
类来创建并启动线程的步骤如下:
Thread
类的⼦类,并重写该类的run()
方法Thread
子类,初始化线程对象start()
方法启动线程下面我们具体来看看创建线程的代码实践。
/**
* 创建一个 Thread 子类
*/
public class Thread0 extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
System.out.println(time + " 当前线程:" + Thread.currentThread().getName() + ",正在运行");
}
}
}
/**
* 创建一个测试类
*/
public class ThreadTest0 {
public static void main(String[] args) {
// 初始化一个线程对象,然后启动线程
Thread0 thread0 = new Thread0();
thread0.start();
for (int i = 0; i < 5; i++) {
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
System.out.println(time + " 当前线程:" + Thread.currentThread().getName() + ",正在运行");
}
}
}
输出结果:
2023-08-23 17:58:03:726 当前线程:Thread-0,正在运行
2023-08-23 17:58:03:727 当前线程:Thread-0,正在运行
2023-08-23 17:58:03:726 当前线程:main,正在运行
2023-08-23 17:58:03:727 当前线程:Thread-0,正在运行
2023-08-23 17:58:03:727 当前线程:main,正在运行
2023-08-23 17:58:03:728 当前线程:Thread-0,正在运行
2023-08-23 17:58:03:728 当前线程:main,正在运行
2023-08-23 17:58:03:728 当前线程:Thread-0,正在运行
2023-08-23 17:58:03:728 当前线程:main,正在运行
2023-08-23 17:58:03:728 当前线程:main,正在运行
从执行时间上可以看到,main
线程和Thread-0
线程交替运行,效果十分明显!
所谓的多线程,其实就是两个及以上线程的代码可以同时运行,而不必一个线程需要等待另一个线程内的代码执行完才可以运行。
对于单核 CPU 来说,是无法做到真正的多线程的;但是对于多核 CPU 来说,在一段时间内,可以执行多个任务的,由于 CPU 执行代码时间很快,所以两个线程的代码交替执行看起来像是同时执行的一样,具体执行某段代码多少时间,就和分时机制系统有关了。
分时机制系统,简单的说,就是将 CPU 时间划分为多个时间片,操作系统以时间片为单位来执行各个线程的代码,越好的 CPU 分出的时间片越小。
例如某个时段, CPU 将 1 秒划分成 50 个时间片,1 个时间片耗时 20 ms,每个时间片均进行线程切换,也就是说 1 秒可以执行 50 个任务,给人的感觉好像计算机能同时处理多件事情,其实是 CPU 执行任务速度太快给人产生的错觉感。
/**
* 实现 Runnable 接口
*/
public class Thread2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
System.out.println(time + " 当前线程:" + Thread.currentThread().getName() + ",正在运行");
}
}
}
/**
* 创建一个测试类
*/
public class ThreadTest2 {
public static void main(String[] args) {
// 通过一个Thread来启动线程
Thread thread2 = new Thread(new Thread2());
thread2.start();
for (int i = 0; i < 5; i++) {
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
System.out.println(time + " 当前线程:" + Thread.currentThread().getName() + ",正在运行");
}
}
}
输出结果:
2023-08-23 18:30:28:664 当前线程:Thread-0,正在运行
2023-08-23 18:30:28:666 当前线程:Thread-0,正在运行
2023-08-23 18:30:28:666 当前线程:Thread-0,正在运行
2023-08-23 18:30:28:664 当前线程:main,正在运行
2023-08-23 18:30:28:666 当前线程:Thread-0,正在运行
2023-08-23 18:30:28:667 当前线程:Thread-0,正在运行
2023-08-23 18:30:28:668 当前线程:main,正在运行
2023-08-23 18:30:28:668 当前线程:main,正在运行
2023-08-23 18:30:28:668 当前线程:main,正在运行
2023-08-23 18:30:28:668 当前线程:main,正在运行
效果跟上面介绍的一样,如果循环的打印次数越多,效果越明显!
下图是一张从操作系统角度划分的线程模型状态!
线程被分为五种状态,各个状态说明如下:
Thread thread = new Thread()
start()
方法,就会处于就绪状态,也被称为可执行状态,随时可能被 CPU 调度执行针对操作系统的线程模型,Java 进行部分封装和扩充,JVM 中的线程状态总共有六种,它们之间的关系,可以用如下图来表示:
各个状态说明如下:
start()
方法,该线程处于就绪状态,获得 CPU 时间片后变为运行中状态synchronized
同步锁失败后,会把线程放入锁池中,线程进入同步阻塞状态。wait
方法,会把线程放在等待队列中,直到被唤醒或者因异常自动退出Thread.sleep(1000)
方法,当到达目标时间后,会自动唤醒或者因异常自动退出本文主要围绕进程和线程的一些基础知识,进行简单的入门知识总结。
线程的特征和进程差不多,进程有的它基本都有。
相对于进程而言,线程更加的轻量化,主要承担任务的执行工作,优点如下:
不过线程也有缺点:
总的来说,进程和线程各有各优势,站在操作系统的设计角度而言,可以归结为以下几点:
整篇内容难免有描述不对的地方,欢迎网友留言指出!
1、飞天小牛肉 - 五分钟扫盲:进程与线程基础必知
2、潘建南 - Java线程的6种状态及切换