【JAVA进化论】LV5-1:进程是什么?线程又是什么?

一、线程的简单认识

还记得自己写的main方法吗?它在被运行的那一刻就等于你启动了一个java线程,只不过它结束的很快,因为我们的main方法里的逻辑执行完它就结束掉了,简单来说就是这个线程完成了它的使命,可以当场去世了。

再来发散下思维,还记得我们写过的xxx管理系统吗?为什么你可以一直输指令,它一直运行?我们看康康代码:

代码块1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) {
StudentSystemView view = new StudentSystemView();
while (true) { //这里是一个死循环!
int opNum = view.home();
if (opNum == 0) { //终止程序
break;
} else if (opNum == 1) {
//需要新增学生信息
view.addStudent();
} else if (opNum == 2) {
view.updateStudent();
} else if (opNum == 3) {
view.delStudent();
} else if (opNum == 4) {
view.getStudentById();
} else if (opNum == 5) {
view.getAllStudents();
} else {
System.out.println("请输入有效的指令!");
}
}
}

看第三行,什么是死循环呢?死循环就是指一个循环体本身并不会主动结束,比如例子里的while(true),我们前面讲过while的语法含义,当一个条件满足时,就继续循环,当这个条件恒等于true,那么意味着这个循环程序会一直执行下去,直到你输入了指令0,通过break关键词终止循环体才算真正离开了这个死循环。

为什么要说这个例子?因为它更能直观的表达“线程”这个东西,我们来解释一下线程:

线程就是运行代码逻辑的最基本单位,每一段代码逻辑,都需要线程推动它被运行。

当你运行main方法时,jvm就已经自动开启一个线程帮你运行你main方法里的代码了,因为while(true)不会轻易结束,所以这个线程不会轻易结束,如果你输入0,break意味着跳出循环体,那么代码逻辑也就运行完毕了,此时线程自动销毁。

你之前写过的任何代码都是,只是它们不像上面的死循环那样由于自身逻辑特点不会让这个线程立刻销毁,程序一旦运行完毕,线程也就随之销毁了。

二、多线程

一个main方法被运行后开启运行它的线程,我们管这个线程叫”主线程“,其实没必要分那么细,名称不重要,只需要记住,在任意线程里都可以产生子线程来帮助你做一些”异步化“的操作。

2.1:如何开启子线程?

在java里开启子线程的方法有很多,我们只列举现在最常用的:

代码块2
1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
Thread t1 = new Thread();
t1.start(); //毫无意义的一个线程被创建并触发了,这个线程不会执行任何逻辑

//通过下面的方式可以加上你的逻辑代码,让程序变得有意义
new Thread(() -> {
//这里加上你需要异步的逻辑块
}).start();
}

这样说太虚了,我们来举个例子:

代码块3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
int i = 1; //线程内计数器
while (true) {
try {
Thread.sleep(100L); //线程睡眠100ms
System.out.println("线程1第" + i + "次打印");
i++;
if (i > 10) {
System.out.println("线程1打印完毕~");
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});

t1.start(); //启动线程1

Thread t2 = new Thread(() -> {
int i = 1; //线程内计数器
while (true) {
try {
Thread.sleep(100L); //线程睡眠100ms
System.out.println("线程2第" + i + "次打印");
i++;
if (i > 15) {
System.out.println("线程2打印完毕~");
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t2.start();

int i = 1;
while (true) {
try {
Thread.sleep(100L); //线程睡眠100ms
System.out.println("main方法线程第" + i + "次打印");
i++;
if (i > 12) {
System.out.println("main方法线程打印完毕~");
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

让我们来看看打印:

代码块4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
线程1第1次打印
main方法线程第1次打印
线程2第1次打印
线程1第2次打印
线程2第2次打印
main方法线程第2次打印
线程2第3次打印
线程1第3次打印
main方法线程第3次打印
main方法线程第4次打印
线程2第4次打印
线程1第4次打印
main方法线程第5次打印
线程1第5次打印
线程2第5次打印
线程2第6次打印
线程1第6次打印
main方法线程第6次打印
线程2第7次打印
main方法线程第7次打印
线程1第7次打印
main方法线程第8次打印
线程2第8次打印
线程1第8次打印
线程2第9次打印
main方法线程第9次打印
线程1第9次打印
线程1第10次打印
线程1打印完毕~
main方法线程第10次打印
线程2第10次打印
线程2第11次打印
main方法线程第11次打印
线程2第12次打印
main方法线程第12次打印
main方法线程打印完毕~
线程2第13次打印
线程2第14次打印
线程2第15次打印
线程2打印完毕~

是不是觉得杂乱无章?没关系,我们来试图使用流程图来说明下它的运行过程:

我们这个程序里,一共包含三个线程:

  • 运行main方法的线程
  • 运行线程1逻辑块的线程
  • 运行线程2逻辑块的线程

它们一起运行,然后各自执行自己的逻辑:

图1

如图可知,线程是运行程序块的最基本单位,一旦线程启动后,它是跟其他线程一起运行的,这也是为什么你看到的处理结果那么杂乱无章的原因,但是你会发现,它就只是杂乱而已,仔细看每个线程的打印结果,它是没有问题的,多线程并发执行,仍然可以保证每个线程里的逻辑没有任何问题。

线程是如何看起来”同时“运行的呢?让我们来看张图:

图2

看起来在一起执行的原因,是因为我们运行程序的核心,也就是CPU,它在不断的切换线程上下文,让这三个线程”看起来在同时运行“,你只需要关注绿线部分即可,因为不管CPU如何切换自己到不同的线程运行程序,但实际人肉眼看到的就是,它们都在同时运行,但你要知道,这是CPU切换着线程的上下文来运行的,我们常说的CPU利用率不高的问题主要就是我们利用单线程或较少的线程完成庞大复杂的程序,没有充分利用多线程的优势去解决问题。

2.2:多线程与java web

我们刚刚讲了main方法,回忆一下我们前面说过的tomcat,我们的servlet程序是如何被触发运行的呢?我们再来贴一下之前servlet的代码,这次我们打印一下运行它的线程的名字:

代码块5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class StudentInfoController extends HttpServlet {

private StudentDao studentDao = new StudentDao();

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json");
try {
Student result = studentDao.getStudentInfo();
resp.getOutputStream().write(JSON.toJSONBytes(result));
resp.setStatus(200);
System.out.println(Thread.currentThread().getName()); //使用线程类的currentThread静态方法可以取到执行当前代码的线程对象
} catch (Exception e) {
e.printStackTrace();
resp.setStatus(500);
}
}
}

我们在接口发生调用时尝试打印一下执行该接口的线程的名字。

运行打印如下:

代码块6
1
http-nio-8081-exec-5

它的名字叫http-nio-xxxx,这是tomcat运行时,用来分配执行业务逻辑的线程,也就是说,我们写的业务代码被放进tomcat身体后,就全部由tomcat触发了,所以运行我们servlet程序的线程肯定都由tomcat分配。

这跟执行main方法还不一样,main方法是直接由jvm分配的,而tomcat主程序肯定也是jvm分配线程运行的(之前说过tomcat本身也是java写的),然后tomcat自己作为独立运行的程序,就像我们开始举的那个main方法的例子一样,也可以产生线程,用来运行我们的servlet。那么我们是不是也可以在我们的接口里开线程异步处理一些事情呢?比如我们现在接口里有一段代码,假设需要耗时200ms,但是我们这个接口的响应时间要求不超过100ms,怎么办呢?异步就是最好的办法:

代码块7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json");
try {
Student result = studentDao.getStudentInfo();

//下面这段代码耗时很久,一下子要200ms,但我们这个接口需要在100ms就返回,而且这段耗时较久的逻辑还不影响你的主业务流程交互
Thread.sleep(200); //这里用线程睡眠的方式模拟运行时间200ms的程序段

resp.getOutputStream().write(JSON.toJSONBytes(result));
resp.setStatus(200);
} catch (Exception e) {
e.printStackTrace();
resp.setStatus(500);
}
}

如上,利用sleep模拟了一段运行超慢的代码块,注意这里只是模拟(⚠️ 注:类似这种代码逻辑特别复杂的代码只可能出现在Service层,这里只是为了说明问题写在了网关也就是servlet层)。怎么优化呢?首先上面已经说了,这段代码是每次都会运行,但是它不影响student_info的输出,那么就很适合把它异步出去,这样改即可:

代码块8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json");
try {
Student result = studentDao.getStudentInfo();

//异步
new Thread(() -> {
try {
Thread.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
}).start();

resp.getOutputStream().write(JSON.toJSONBytes(result));
resp.setStatus(200);
} catch (Exception e) {
e.printStackTrace();
resp.setStatus(500);
}
}

这样,我们这个接口的那200ms的复杂逻辑就会在不影响我们主题逻辑的响应速度的情况下,通过别的线程去运行了~

三、进程

前面讲完了线程,你就了解了,它是运行程序时的最小单位,由CPU来负责触发执行,它是有状态的,它可以被start触发进入运行状态,也可以通过sleep来由运行状态变成睡眠状态。那么进程是什么呢?

简单来说,tomcat是一个进程,一个main方法在运行时是一个进程,你的QQ是一个进程,你的浏览器也是;进程就是我们所说的运行着的一个个程序,程序包含需要执行的指令码(我们写的java源代码被编译后也是一个个的指令,你可以理解它就是你编写的程序),包含多个线程(线程是运行这些指令码的最小单位,由CPU不断切换线程上下文来完成运行)。

你可以利用线程做很多神奇的事情,我们在后面的文章里将会介绍到。