大家好,又见面了,我是你们的朋友全栈君。
大家好,从今天开始,我和大家一起来探讨 java 中线程的使用。线程是 java 知识体系中非常重要的一部分,我将写一系列的文章来详细的介绍 java 线程中需要掌握的知识。如果你是 java 线程的初学者,本系列文章你一定不要错过哦。
本篇文章是 java 线程系列文章的第一篇文章,主要介绍进程与线程的概念和 java 中如何使用线程。
首先我们先来介绍一下什么是进程。
进程可以理解为一个个正在执行的应用程序,比如我们使用网易云音乐软件播放音乐,同时我们在使用 WPS 编辑我们的文档,并且还打开了 Chrome 浏览器查询资料等等。类似于下图:
这三个正在执行的应用程序就是我们的进程。这些进程之间是互不干扰的,即播放音乐的网易云音乐不会干扰到我们正在编辑文档的 WPS。
进程的定义:进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。简言之,就是一些处于运行状态的程序。
首先还是先来体验一下什么是线程,比如我们在使用网易云播放音乐的时候,在听歌曲的同时我们会去浏览该歌曲相关的评论或者是自己发表一个评论等。 一般情况下,这时就有两个线程:一个是播放音乐的线程,另一个是我们正在输入评论信息。这两个线程是互不干扰的,我们在发表评论时,音乐照样可以播放,并且这两个线程都是属于网易云音乐这个进程的。
线程的定义:是操作系统能够进行运算调度的最小单位。它被包含在进程之中
,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,多条线程并行执行不同的任务
。
进程与线程的区别:进程只是一个动态
的概念,代表的是程序代码在一个数据集上的一次运行过程。进程的主要作用就是获取到操作系统给它分配的硬件资源:包括用于存放程序正文、数据的磁盘和内存地址空间,以及在运行时所需要的I/O设备,已打开的文件,信号量等。具体完成任务的是进程管理的线程。
一个进程至少包含一个线程。
类似下图:
在了解了线程和进程的基本概念后,我们来学习一下 java 中的线程如何使用。
我们只需要关注如何创建和启动一个线程,关于进程的创建等是不需要我们考虑的,当我们启动一个 java 程序时,操作系统会创建进程。比如我们执行 main() 方法时,会启动 jvm 虚拟机,然后在虚拟机中执行我们的 main() 方法,虚拟机会分配一个线程来执行我们的 main() 方法,这个线程一般被称作主线程。
在 java 中有两种方式创建一个线程,我们下面分别介绍。
定义一个线程类,该线程的任务是每隔半秒输出一个数字。
PrintNumberThread.java:
public class PrintNumberThread extends Thread {
private int number;
//线程要执行的任务写在 run() 方法中
@Override
public void run(){
while(number<100){
System.out.println(Thread.currentThread().getName()+":输出数字 "+number++);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
ThreadDemo.java:
public class ThreadDemo {
//创建一个线程实例
PrintNumberThread printNumberThread = new PrintNumberThread();
// 给线程设置名字
printNumberThread.setName("printNumberThread");
//启动线程
printNumberThread.start();
}
在该类中创建一个 PrintNumberThread 类的实例,然后调用 start() 方法启动该线程实例,如果我们不调用 printNumberThread 对象的 start() 方法,该线程是不会执行的,它就和我们的普通的 java 对象一样。
运行结果:
控制台会一直打印,直到 number 加到 100 才停止。
此段程序运行时至少启动了两个线程,一个是主线程,一个是我们自定义的 PrintNumberThread 线程。
由于 java 中是单继承,通过继承 Thread 类的方式有时候就不难么方便呢,于是 java 还为我们提供了另外一种方式:实现 Runnable 接口。
PrintNumberRunnable.java:
public class PrintNumberRunnable implements Runnable {
private int number;
public void run() {
while(number<100){
System.out.println(Thread.currentThread().getName()+":输出数字 "+number++);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Runnable 接口只有一个 run() 方法需要我们实现,与 Thread 类的 run() 方法一样,我们只需要将想要完成的任务写在 run() 方法中即可。
ThreadDemo.java:测试类
public class ThreadDemo {
public static void main(String[] args) {
//创建一个线程实例
PrintNumberRunnable printNumberRunnable = new PrintNumberRunnable();
Thread thread = new Thread(printNumberRunnable);
thread.setName("printNumberRunnable");
thread.start();
}
}
在创建线程实例时与前面不一样,需要先创建一个 PrintNumberRunnable 的实例 printNumberRunnable,再创建一个 Thread 类的实例 thread,并需要 PrintNumberRunnable 实例作为构造函数的参数,然后再调用 thread 的 start() 方法启动线程。
运行结果与前面的一样。
经过前面的例子,大家可能没有看出使用线程和不使用线程的差别,下面我们再举一个例子来体验一下。
编写两个线程,一个线程每隔 1 秒输出一个数字,一个线程每隔 0.5 秒输出一个字母。
我们的目的是要感受到多个任务是可以同时执行的。比如我们用网易云听音乐时,播放音乐的同时,我们还可以写评论。
LetterThread.java: 每隔一秒输出一个字母
public class LetterThread extends Thread {
private char[] letters = {
'A','B','C','D','E'};
//每隔 1 秒输出一个字母
@Override
public void run() {
String name = Thread.currentThread().getName();
Long start = System.currentTimeMillis();
System.out.println(name + " 输出字母开始时间:"+start);
for (int i = 0;i<5;i++){
System.out.println(name+" 输出字母:"+letters[i]);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Long end = System.currentTimeMillis();
System.out.println(name + " 输出字母结束时间:"+end);
}
}
NumberThread.java: 每隔半秒输出一个数字
public class NumberThread extends Thread {
//每隔 0.5 秒输出一个数字
@Override
public void run() {
String name = Thread.currentThread().getName();
Long start = System.currentTimeMillis();
System.out.println(name + " 输出数字开始时间:"+start);
for (int i = 0;i<10;i++){
System.out.println(Thread.currentThread().getName()+" 输出数字:"+i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Long end = System.currentTimeMillis();
System.out.println(name + " 输出数字结束时间:"+end);
}
}
Test.java: 测试类
public class Test {
public static void main(String[] args) {
NumberThread numberThread = new NumberThread();
numberThread.setName("numberThread");
//启动打印数字的线程
numberThread.start();
//启动打印字母的线程
LetterThread letterThread = new LetterThread();
letterThread.setName("letterThread");
letterThread.start();
}
}
运行结果:
从结果我们可以看出,两个线程几乎是在同一时刻启动,在同一时刻停止,差不多都运行了 5 秒
。所以我们可以说他们是同时在执行的,即都在这 5 秒里完成了自己的任务。
大家还需要注意一点:虽然在代码中 numberThread.start(); 写在了 letterThread.start(); 的前面,但是不代表 numberThread 就一定先于 letterThread 启动;start() 方法只是告诉 CPU 当前线程需要启动,但是什么时候启动,就由 CPU 来决定了,我们就不能再控制它了。我们唯一能做的就是重写 run() 方法来定义一个线程需要完成什么任务和调用 start() 方法来启动这个线程。
所以前面的代码运行的时候也可能会出现 letterThread 比 numberThread 先启动的情况:
在前面的所有示例代码中,我们都重写了 Thread 类或者 Runabble 接口的 run() 方法。并在 run() 方法中定义这个线程需要完成的任务。但是启动线程时不是直接去调用线程实例的 run() 方法,而是通过调用 start() 方法来启动线程。需要注意的是,调用了 start() 方法后,线程可能不会立即执行,它需要等待 cpu 来调度。cpu 在处理该线程的任务时,其实就是执行我们定义的 run() 方法。
如果我们直接在代码中调用线程实例的 run() 方法,是没有多线程的效果的,run() 方法直接就在 main 线程中执行了,与执行一个普通方法没有什么区别。
所以我们应该调用线程实例的 start() 方法来启动一个线程。而不是直接调用 run() 方法。
下面我们还是拿前面的交替打印字母和数字的例子做一个举例: 使用 start() 方法启动两个线程的代码与前面一致,就不在写了。我们将直接调用 run() 方法的代码做一个简单例子:
Test01.java:
public class Test01 {
public static void main(String[] args) {
NumberThread numberThread = new NumberThread();
numberThread.setName("numberThread");
//调用 run() 方法
numberThread.run();
LetterThread letterThread = new LetterThread();
letterThread.setName("letterThread");
//调用 run() 方法
letterThread.run();
}
}
从上图中我们可以看出以下两点: (1)输出数字的线程的名字不是我们设置的 numberThread ,而是 main 线程;输出字母的线程的名字也不是我们设置的 letterThread ,而是 main 线程。所以印证了我们前面的说法:如果直接调用线程实例对象的 run() 方法不会启动一个新的线程,而是直接在 main(主)线程中执行 run() 方法。 (2)从上图中我们可以看出,这段代码大约执行了 10 秒,
本文就到此结束了,主要介绍了如下几个知识点:
示例代码地址:https://github.com/coderllk/threadDemo/tree/main/threadDemoOne
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/156347.html原文链接:https://javaforall.cn