进程概述:
在这之前,有必要了解一下什么是进程?
在一个操作系统中,每个独立的执行的程序都可称为一个进程,也就是“正在运行的程序”。如图所示:
线程概述:
如上所述,每个运行的程序都是一个进程,在一个进程中还可以有多个执行单元同时运行,这些执行单元可以看做程序的执行的一条条线索,被称为线程。操作系统中的每一个进程都至少存在一个线程。
多线程的概念:
多线程是指一个应用程序中有许多条并发执行的线索,每条线索都被称作一个线程,他们会交替执行,彼此间进行通信。
多线程是实现并发机制的一种有效手段。进程和线程一样,都是实现并发的一个基本单位。线程是比进程更小的执行单位,线程是进程的基础之上进行进一步的划分。所谓多线程是指一个进程在执行过程中可以产生多个更小的程序单元,这些更小的单元称为线程,这些线程可以同时存在,同时运行,一个进程可能包含多个同时执行的线程。
Java中线程实现的方式
在 Java 中实现多线程有两种手段,一种是继承 Thread 类,另一种就是实现 Runnable 接口。
继承Thread类创建多线程
1 @SpringBootTest
2 public class Example {
3 public static void main(String[] args) {
4 // 实例化对象
5 MyThread myThread = new MyThread();
6 // 调用线程主体
7 new Thread(myThread,"线程A").start();
8 new Thread(myThread,"线程B").start();
9 }
10 }
11 // 继承Thread类
12 class MyThread extends Thread{
13 //覆写run()方法,作为线程的操作主体
14 public void run(){
15 for (int i = 0; i <5 ; i++) {
16 Thread th = Thread.currentThread();//获取当前线程
17 String name = th.getName();//获取当前线程的名字
18 System.out.println(name +"运行:i = "+i);
19 }
20 }
21 }
程序运行结果:
实现Runnable接口创建多线程
1 @SpringBootTest
2 public class Example {
3 public static void main(String[] args) {
4 // 实例化对象
5 MyThread myThread = new MyThread();
6 // 调用线程主体
7 new Thread(myThread,"线程A").start();
8 new Thread(myThread,"线程B").start();
9 }
10 }
11 // 实现Runnable接口,作为线程的实现类
12 class MyThread implements Runnable{
13 //重写run()方法,作为线程的操作主体
14 public void run(){
15 for (int i = 0; i <5 ; i++) {
16 Thread th = Thread.currentThread();//获取当前线程
17 String name = th.getName();//获取当前线程的名字
18 System.out.println(name +"运行:i = "+i);
19 }
20 }
21 }
程序运行结果:
从程序可以看出,现在的两个线程对象是交错运行的,哪个线程对象抢到了 CPU 资源,哪个线程就可以运行,所以程序每次的运行结果肯定是不一样的,在线程启动虽然调用的是 start() 方法,但实际上调用的却是 run() 方法定义的主体。
两种实现多线程方式的对比
虽说百度答案都千篇一律,但没有比自己动手验证更令人印象深刻,毕竟:实践是检验真理的唯一标准!
假设售票厅有四个窗口可发售某日某次列车的100张车票,这时,100张车票可以看做共享资源,四个售票窗口要创建四个线程。
继承Thread类
1 @SpringBootTest
2 public class Example {
3 public static void main(String[] args) {
4 // 实例化对象
5 MyThread myThread1 = new MyThread();
6 MyThread myThread2 = new MyThread();
7 MyThread myThread3 = new MyThread();
8 MyThread myThread4 = new MyThread();
9 // 调用线程主体
10 myThread1.setName("窗口一");
11 myThread2.setName("窗口二");
12 myThread3.setName("窗口三");
13 myThread4.setName("窗口四");
14 myThread1.start();
15 myThread2.start();
16 myThread3.start();
17 myThread4.start();
18
19 }
20 }
21
22 // 继承Thread类
23 class MyThread extends Thread{
24 private int tickets = 100;
25 //覆写run()方法,作为线程的操作主体
26 public void run(){
27 while (true){ //通过死循环打印语句
28 if (tickets > 0) {
29 Thread th = Thread.currentThread();//获取当前线程
30 String name = th.getName();//获取当前线程的名字
31 System.out.println(name + ":正在发售第" + tickets-- + "张票!");
32 }
33 }
34 }
35 }
程序运行结果:
从运行结果可以看出,每张票都被打印了四次,四个线程没有共享100张票,而是各自售出了100张。
现实中铁路系统中的票资源是共享的,因此上面的运行结果显然不合理。为了保证资源共享,在程序中只能创建一个售票对象,然后开启多个线程去运行同一个售票对象的售票方法,简单来说就是四个线程运行同一个售票程序,这时候就需要通过Runnable接口来实现。
实现Runnable接口
1 @SpringBootTest
2 public class Example {
3 public static void main(String[] args) {
4 // 实例化对象
5 MyThread myThread = new MyThread();
6 // 调用线程主体
7 new Thread(myThread,"窗口一").start();
8 new Thread(myThread,"窗口二").start();
9 new Thread(myThread,"窗口三").start();
10 new Thread(myThread,"窗口四").start();
11 }
12 }
13 // 实现Runnable接口,作为线程的实现类
14 class MyThread implements Runnable{
15 private int tickets = 100;
16 //重写run()方法,作为线程的操作主体
17 public void run(){
18 while (true){ //通过死循环打印语句
19 if (tickets > 0) {
20 Thread th = Thread.currentThread();//获取当前线程
21 String name = th.getName();//获取当前线程的名字
22 System.out.println(name + ":正在发售第" + tickets-- + "张票!");
23 }
24 }
25 }
26 }
只创建了一个MyThread 对象,然后创建了四个线程,在每个线程上面都去调用MyThread 对象中的run()方法,这样就可以确保四个线程访问的是同一个tickets变量,共享100张车票。
程序运行结果:
通过对比可以看出,实现Runnable接口相对于继承Thread类来说,有这如下显著的好处:
- 适合多个相同程序代码的线程去处理同一个资源的情况,把线程同程序代码,数据有效的分离,很好的体现了面向对象的设计思想。
- 可以避免由于java单继承带来的局限性。由于一个类不能同时有两个父类,使用一个已经继承了某一个类的子类创建线程,所以不能用继承Thread类的方式,那么就只能采用实现Runnable接口的方式。
后台线程
在上面的案例中,当main()方法创建并启动四个新的线程后,main()方法中的代码执行完毕,这时方法结束,main线程也就随之结束了。实际上,虽然main线程结束了,但整个java程序却没有随之结束,仍会执行售票的代码。对于java程序来说,只要还有一个前台线程在运行,这个进程就不会结束,如果一个进程中只有后台线程运行,这个进程就会结束,前台线程和后台线程是一种相对的概念,新创建的线程默认都是前台线程,如果某个线程对象在启动之前调用了setDaemon语句,这个线程就变成了一个后台线程。
1 @SpringBootTest
2 //创建DamonThread类,实现Runnable接口
3 class DamonThread implements Runnable{
4
5 //实现接口中的run()方法
6 @Override
7 public void run() {
8 while (true){
9 System.out.println(Thread.currentThread().getName() + "正在运行!");
10 }
11 }
12 }
13 public class Example {
14 public static void main(String[] args) {
15 System.out.println("main线程是后台线程吗?" +Thread.currentThread().isDaemon());
16 //创建一个DamonThread对象dt
17 DamonThread dt = new DamonThread();
18 //创建线程thread共享dt资源
19 Thread thread = new Thread(dt,"后台线程");
20 //判断是否为后台线程
21 System.out.println("thread默认是后台线程吗?" + thread.isDaemon());
22 //将线程thread设置为后台线程
23 thread.setDaemon(true);
24 //开启线程
25 thread.start();
26 for (int i = 0; i < 5; i++) {
27 System.out.println(i);
28 }
29 }
30 }
程序运行结果:
当开启线程thead后,会执行死循环中的打印语句,将线程thread设置为后台线程后,前台线程就会死亡,JVM会通知后台线程。后台线程从接收指令到做出响应,需要一定的时间,因此,打印了几次“后台线程正在运行!”语句后,后台线程也结束了,由此说明进程中只有后台线程时,进程就会结束。
注意:要将某个线程设置为后台线程,必须在该线程启动之前,也就是说setDaemon()方法必须在start()方法调用之前,否则会引发异常。