1. 认识线程(Thread)
1.1 概念
1) 线程是什么
一个线程就是一个 "执行流". 每个线程之间都可以按照顺序执行自己的代码. 多个线程之间 "同时" 执行着多份代码.
举例:
还是回到我们之前的银⾏的例⼦中。之前我们主要描述的是个⼈业务,即⼀个⼈完全处理⾃⼰的业务。我们进⼀步设想如下场景:⼀家公司要去银⾏办理业务,既要进⾏财务转账,⼜要进⾏福利发放,还得进⾏缴社保。如果只有张三⼀个会计就会忙不过来,耗费的时间特别⻓。为了让业务更快的办理好,张三⼜找来两位同事李四、王五⼀起来帮助他,三个⼈分别负责⼀个事情,分别申请⼀个号码进⾏排队,⾃此就有了三个执⾏流共同完成任务,但本质上他们都是为了办理⼀家公司的业务。 此时,我们就把这种情况称为多线程,将⼀个⼤任务分解成不同⼩任务,交给不同执⾏流就分别排队执⾏。其中李四、王五都是张三叫来的,所以张三⼀般被称为主线程(Main Thread)。
2) 为何要有线程
首先,我们来说一下进程。在多任务操作系统中,希望系统能够同时运行多个程序,这就引入了进程。如果是单任务的操作系统,就完全不涉及进程,也不需要管理(进程),更不需要调度。本质上说,进程是解决”并发编程“问题的,事实上,进程也可以很好地解决并发编程这样的问题。
但是在一些特定的环境下,进程的表现不尽人意,比如,有些场景下,需要频繁的创建和销毁进程,举例,最早的web开发,是使用C语言来编写的服务器程序(基于一种CGI这样的技术,其基于多进程的编程模式),服务器同一时刻会收到很多请求,针对每个请求,都会创建出一个进程,给这个请求提供一定的服务,返回对应的响应;一旦这个请求处理完了,此时这个进程就要销毁了。如果请求很多,就意味着服务器要不停地创建进程、销毁进程,此时使用多进程编程,系统的开销就会很大(开销主要体现在资源的申请和释放上)。
⾸先, "并发编程" 成为 "刚需"
- 单核 CPU 的发展遇到了瓶颈. 要想提⾼算力, 就需要多核 CPU,而并发编程能更充分利⽤多核 CPU资源
- 有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做⼀些其他的⼯作, 也需要⽤到并发编程
- 创建线程⽐创建进程更快.
- 销毁线程⽐销毁进程更快.
- 调度线程⽐调度进程更快.
多线程并发编程,效率更高,尤其是对于java进程,是要启动java虚拟机的,启动java虚拟机开销是更大的,搞多个java进程,就要多个java虚拟机。所以,java中不太去鼓励多进程编程。
3) 进程和线程的区别
- 进程是包含线程的. 每个进程⾄少有⼀个线程存在,即主线程。
- 进程和进程之间不共享内存空间. 同⼀个进程的线程之间共享同⼀个内存空间.
⽐如之前的多进程例⼦中,每个客户来银⾏办理各⾃的业务,但他们之间的票据肯定是不想让别⼈知道的,否则钱不就被其他⼈取⾛了么。⽽上⾯我们的公司业务中,张三、李四、王五虽然是不同的执⾏流,但因为办理的都是⼀家公司的业务,所以票据是共享着的。这个就是多线程和多进程的最⼤区别。
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
- ⼀个进程挂了⼀般不会影响到其他进程. 但是⼀个线程挂了, 可能把同进程内的其他线程⼀起带⾛(整个进程崩溃).
另外注意:
- 同一个进程中的线程之间,可能会互相干扰,引起线程安全问题
- 线程也不是越多越好,要能够合适,如果线程太多了,调度开销可能会非常明显
4) Java 的线程 和 操作系统线程 的关系
操作系统内核,是操作系统最核心部分的功能模块(管理硬件、给软件提供稳定的运行环境)。操作系统 = 内核 + 配套的应用程序这里用银行为例来说明一下:当你到银行进行各种业务的办理的时候,都是需要在办事窗口前,给工作人员说清楚你的需求,由工作人员代办。我们知道银行中的办事窗口内部和银行大厅是分隔开的,你是进不去办事窗口内部的, 这里的办事窗口内部就相当于操作系统内核空间(内核态),你所在大厅则是用户空间(用户态)。
为什么划分出用户态、内核态:
最主要的目的,还是为了“稳定”。防止你的应用程序,把硬件设备或软件资源给搞坏了。系统封装了一些api,这些api都属于是一些“合法”的操作,应用程序只能调用这些api,这样就不至于对系统/硬件设备产生太大的危害。
假设让应用程序直接操作硬件,可能极端情况下,代码出现bug,就把硬件干烧了。
1.2 第⼀个多线程程序
- 每个线程都是⼀个独立的执行流
- 多个线程之间是 "并发" 执行的
class MyThread2 extends Thread {
//Thread类不用导包,属于特殊的包java.long,该包默认自动导入
@Override
public void run() {
//run方法就是该线程的入口方法
while (true) {
System.out.println("hello run");
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
//2、根据刚才的类,创建出具体的实例(线程实例,才是真正的线程)
Thread t = new MyThread2();
//3、调用Thread的start方法,才会真正调用系统api,在系统内核中创建出线程
t.start();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
对于上述代码,运行结果为两个循环不停地同时输出(验证了多个线程之间是 "并发" 执行的)。
我们知道,若对于普通程序来说,当遇到一个无限循环,会停留在这个循环,不停的打印输出,后续的代码是执行不到的。然而这个多线程程序,两个循环都执行到了,是因为每个线程都是⼀个独立的执行流 。代码中 t.start() ,即调用start()之后会创建一个新的线程,该线程进入到 run 方法,进行循环;而此时main线程,这个主线程会继续自己的执行,执行后续代码,也进行循环。
这里可以使用 jconsole 命令观察线程:
2. 创建线程的几种方法
-
方法1 继承 Thread 类
我们上面写的第一个多线程程序就是用的该方法。
class MyThread extends Thread {
@Override
public void run() {
System.out.println("这⾥是线程运⾏的代码");
}
}
MyThread t = new MyThread();
t.start(); //调用start才会真正地创建线程
-
方法2 实现 Runnable 接口
1、实现 Runnable 接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("这⾥是线程运⾏的代码");
}
}
2、创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.
Thread t = new Thread(new MyRunnable());
//或者另一种写法
Runnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
3、调用start方法
t.start(); // 线程开始运⾏
该方法完整代码示例:
class MyThread3 implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("hello runnable");
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
// Runnable runnable = new MyThread3();
// Thread t = new Thread(runnable);
Thread t = new Thread(new MyThread3());
t.start();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 继承 Thread 类, 直接使用 this 就表示当前线程对象的引用.
- 实现 Runnable 接口, this 表示的是 MyRunnable 的引用,需要使用Thread.currentThread()来表示当前线程对象
其他创建方法
-
匿名内部类创建 Thread 子类对象
// 使⽤匿名类创建 Thread ⼦类对象
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("使⽤匿名类创建 Thread ⼦类对象");
}
};
-
匿名内部类创建 Runnable 子类对象
// 使⽤匿名类创建 Runnable ⼦类对象
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使⽤匿名类创建 Runnable ⼦类对象");
}
});
-
lambda 表达式创建 Runnable 子类对象
// 使⽤ lambda 表达式创建 Runnable ⼦类对象
Thread t3 = new Thread(() -> System.out.println("使⽤匿名类创建 Thread ⼦类对象"));
Thread t4 = new Thread(() -> {
System.out.println("使⽤匿名类创建 Thread ⼦类对象");
});
3. 多线程的优势-增加运行速度
- 使用 System.nanoTime() 可以记录当前系统的 纳秒 级时间戳.
- serial 串行的完成⼀系列运算. concurrency 使用两个线程并行的完成同样的运算.
public class ThreadAdvantage {
// 多线程并不⼀定就能提⾼速度,可以观察,count 不同,实际的运⾏效果也是不同的
private static final long count = 10_0000_0000;
public static void main(String[] args) throws InterruptedException {
// 使⽤并发⽅式
concurrency();
// 使⽤串⾏⽅式
serial();
}
private static void concurrency() throws InterruptedException {
long begin = System.nanoTime();
// 利⽤⼀个线程计算 a 的值
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a--;
}
}
});
thread.start();
// 主线程内计算 b 的值
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
// 等待 thread 线程运⾏结束
thread.join();
// 统计耗时
long end = System.nanoTime();
double ms = (end - begin) * 1.0 / 1000 / 1000;
System.out.printf("并发: %f 毫秒%n", ms);
}
private static void serial() {
// 全部在主线程内计算 a、b 的值
long begin = System.nanoTime();
int a = 0;
for (long i = 0; i < count; i++) {
a--;
}
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
long end = System.nanoTime();
double ms = (end - begin) * 1.0 / 1000 / 1000;
System.out.printf("串⾏: %f 毫秒%n", ms);
}
}
该篇是对多线程的初步认识,接下来我会继续更新多线程的相关内容,请多多关注!