一、线程与进程的区别
在上两篇博文中,我们主要讲述了什么是进程,什么是线程,总结一下进程和线程的区别:
- 进程包含线程,在一个进程中,包含至少一个线程。
- 进程是系统分配资源的基本单位,线程是系统进行调度的基本单位。创建进程的时候已经分配好资源了,后续创建线程的时候直接使用公共资源即可。
- 进程是独立执行的,它具有独立的地址空间,如果一个进程挂掉了不会影响另外一个进程。进程具有独立性,导致系统十分的稳定。但是线程不一样,多个线程共用一份地址空间,一旦某个线程出 bug,很可能会导致整个进程受到牵连,异常结束,所以多个线程之间容易相互影响。
以上就是线程和进程最主要的区别,只能说各有各的好处,根据实际的业务需求来考虑使用哪种方式。
二、创建线程的代码实现
2.1、继承 Thread
类
说了这么多,那作为一名 Java 程序猿的我们又该如何来创建一个线程呢?欸,这就不得不说我们的 API 了,那什么是 API 呢?大家可以把他理解成一个接口,一个操控操作系统的接口,其中有个 Thread
类,这个类中有个 run
方法的,重写这个方法之后,再调用 stert
方法,就是创建出一个新的线程了,具体代码如下:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello Thread");
}
}
public class Demo1 {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
如下是运行结果:
那有的人就会说了,如果我直接运行 MyThread
方法里面的 run
方法,不是也能得出相同的结果吗?大家可以试试,结果确实是一样的,但是和调用 run
方法有着本质的区别,那就是调用 run
只是调用了这个方法,并没有创造出新的线程,而 start
方法就不一样了,这个是让系统帮你创造了一个新的线程出来,我们更改一下代码更方便观察,如下:
class MyThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println("Hello Runnable");
}
}
}
public class Demo1 {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
while (true) {
System.out.println("Hello Main");
}
}
}
可以先来推测一下,因为我们 cpu 是并发执行线程的,那我们所执行的结果中,肯定是两种交叉打印,但是并不是有规律的交叉,这个对于系统来说,可以看成是 "随机" 的,执行结果如下:
因为我们的 cpu 是并发执行线程的所以并不会再某一个循环卡死。
2.2、实现 Runnable
接口
Runnable
接口也和上面 Thread
类的写法差不多,只不过意思不一样,大家看代码就知道了,如下:
class MyRunnable implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("Hello Runnable");
}
}
}
public class Demo2 {
public static void main(String[] args) {
MyRunnable r = new MyRunnable();
Thread t = new Thread(r);
t.start();
while (true) {
System.out.println("Hello Main");
}
}
}
这个代码相信大家都看明白了把?相当于就是把另外一个线程要执行的步骤和执行这一步给拆开了,降低了代码的耦合性,运行结果如下:
2.3、使用匿名内部类
Thread
匿名内部类:
代码实现如下:
public class Demo3 {
public static void main(String[] args) {
Thread t = new Thread() {
@Override
public void run() {
while (true) {
System.out.println("Hello Thread");
}
}
};
t.start();
while (true) {
System.out.println("Hello Main");
}
}
}
Runnable
匿名内部类:
代码如下:
public class Demo4 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("Hello Runnable");
}
}
});
t.start();
while (true) {
System.out.println("Hello Main");
}
}
}
2.4、使用lambda表达式
lambda表达式也是之后经常用到的方法,代码如下:
public class Demo5 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (true) {
System.out.println("Hello Thread");
}
});
t.start();
while (true) {
System.out.println("Hello Main");
}
}
}
三、线程的常见方法
3.1、手动结束线程
- 手动创建标志位
public class Demo6 {
public static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!flag) {
System.out.println("Hello Thread");
}
System.out.println("t 线程结束");
});
t.start();
Thread.sleep(3000);
flag = true;
}
}
这里不可以吧 flag
设置成局部变量,因为变量捕捉的缘故,设置成局部变量的话,如果说 flag
之后要改变就捕捉不到,除非把 flag
设置成 final
修饰的常量。
- 使用库标志位
public class Demo7 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("Hello Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
});
t.start();
Thread.sleep(3000);
// interrupt 方法会将上一个标志位变成 true,并且强制唤醒正在 sleep 的线程
// 这时候想要退出 t 线程,只需要在 catch 的地方加入 break 就好了,
// 还可以不立即退出,做一些其他的工作再退出也行,可供程序猿选择
t.interrupt();
System.out.println("线程结束!!!");
}
}
3.2、join()
方法
这个方法是在另外一个线程中使用,执行到 join
等 join
的线程结束,然后继续执行该线程,代码如下:
public class Demo8 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
int i = 0;
while (i++ < 5) {
System.out.println("Hello t1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1 结束了!!!");
});
Thread t2 = new Thread(() -> {
int i = 0;
while (i++ < 3) {
System.out.println("Hello t2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2 结束了!!!");
});
t1.start();
t2.start();
}
}
运行结果如下:
3.3、观察线程状态
观察线程状态的方法是 getState
方法,直接线程对象名.getState();
就行,一共有种状态,如下:
//NEW: 安排了工作, 还未开始行动
//RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
//BLOCKED: 这几个都表示排队等着其他事情
//WAITING: 这几个都表示排队等着其他事情
//TIMED_WAITING: 这几个都表示排队等着其他事情
//TERMINATED: 工作完成了.
四、总结
线程是提高应用程序并发性和响应性的关键工具,但它们也带来了复杂的设计和实现挑战。通过深入理解线程与进程的区别以及线程的生命周期、同步机制和错误处理,Java EE开发者可以更有效地利用线程来构建高效、可伸缩和健壮的应用程序。
通过这篇博客,我们希望能够帮助你在Java EE初阶阶段建立起对线程和进程的坚实理解,并为你在并发编程领域的进一步学习和实践打下良好的基础。
五、结语
总结来说,线程是轻量级的执行单元,它们共享进程的资源,这使得线程之间的上下文切换比进程之间的上下文切换要快得多。然而,线程的共享特性也带来了同步和数据共享的复杂性。在Java中,我们可以通过继承Thread
类或实现Runnable
接口来创建线程,并通过join
等方法来更高效地管理、控制线程。
学习线程和进程的区别以及线程的实现是一个持续的过程,需要不断实践和探索。在这个过程中,我们可能会遇到挑战,但这些挑战也是成长的机会。通过解决实际问题,我们能够更深入地理解并发编程的复杂性,并提高我们的设计和开发技能。
作为一名Java EE开发者,掌握线程和进程的概念将帮助你构建更高效、更健壮的应用程序。在这个快速发展的技术领域中,持续学习和实践是至关重要的。记住,每一个挑战都是成长的机会,每一次成功都是对你努力的肯定。
最后,愿你在并发编程的旅程中不断前进,不断探索,不断学习。愿你的代码像你的意志一样强大,愿你的应用程序像你的梦想一样高效。编码愉快,未来可期!