致力于最高效的Java学习
关注线程是程序开发中非常重要的一个技能点,无论你使用哪种语言都是绕不开的,作为一名程序猿,线程是你必须要掌握的,但是线程的概念不太好理解,尤其对于初学者来讲更是如此,今天我试图用更加通俗易懂的方式来为你讲解线程,一起来看看。
要搞清楚线程的概念,必须先搞清楚进程,什么是进程?百度百科的解释是:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。这种官方解释准确但是不好理解,如何理解进程呢?简单来讲,计算机上正在运行的一个应用程序就是一个进程,比如我打开 IDEA 写代码就是一个进程,打开微信聊天就是一个进程等等,但是这里需要注意一个关键字:正在运行的,也就是说进程是一个动态概念,必须是正在运行的某个应用程序才能称得上是进程。
如果我在计算机上安装了 IDEA 应用程序,但是并没有启动它,那么就没有进程这一概念,所以进程是动态的,不是永久的,有创建有销毁,运行某个应用程序就表示创建了对应的进程,关闭该程序则表示进程销毁,如下图所示。
一个应用程序可以包含一个进程,也可以由多个进程组成。那么什么是线程呢?线程是进程的基本单位,一个进程由一个或者多个线程组成,搞清楚这个关系之后,我们可以明确线程就是程序执行的最小单元。
线程和进程一样,也是动态概念,有创建有销毁,存在只是暂时的,不是永久性的。进程与线程的区别在于进程在运行时拥有独立的内存空间,也就是说每个进程所占用的内存都是独立的。而多个线程是共享内存空间的,但是每个线程的执行是相互独立的,线程必须依赖于进程才能执行,单独的线程是无法执行的,由进程来控制多个线程的执行,没有进程就不存在线程。
那么什么是多线程呢?一个进程中同时有多个线程在执行就是多线程,有一个很简单的方法来判断程序是单线程还是多线程:把程序的整个运行流程画出来,如果是一条回路则是单线程,如果是两条或两条以上的回路则是多线程,比如下面这段代码。
public class Test {
public static void main(String[] args) {
Test2 test2 = new Test2();
test2.test();
for (int i=0;i<100;i++){
System.out.println("++++++++++++Test");
}
}
}
class Test2{
public void test(){
for (int i=0;i<100;i++){
System.out.println("------Test2------");
}
}
}
这段代码是单线程,这个线程就是 main 方法,按照上述所讲的方式来画出这段程序的运行流程,如下图所示。
只有一条回路,即单线程,两个不同的业务逻辑需要按顺序来排队执行,执行完 Test2 的循环之后,才能执行 Test 的循环,如果是两个线程,应该是什么样的回路呢?如下图所示。
可以看到,两个线程即表示两个不同的业务逻辑是同时在向下执行的,不需要谁等待谁,那么上述的多线程代码如何来实现呢?Java 中实现多线程有两种方式。
1、继承 Thread 类
具体的操作是创建一个类,让其继承 Thread,什么是 Thread?Thread 是由 Java 提供给开发者的一个父类,用来描述线程,我们知道线程是一个概念,Thread 就是 Java 来描述这个概念的类,Thread 的实例化对象就是某个具体的线程,Thread 的源码如下图所示。
可以看到 Thread 类实现了 Runnable 接口,Runnable 接口的定义非常简单,只有一个抽象方法 run,如下图所示。
这个接口的作用是什么呢?我们知道 Java 中的接口表示某种功能,一个类实现了某个接口就表示该类具备了某种功能,所以 Runnable 接口是描述什么功能的呢?这里首先要搞清楚线程和任务的概念,一句话来解释:线程是执行任务的具体对象。
我们说过线程是程序执行的最小单元,每个线程都是在执行某个任务,多个任务汇总之后就是一个完整的程序执行,所以线程存在的意义就是要执行某个任务,任务不是一个实体,只是一种描述,线程是落实这种描述的实体。比如现在需要找个人帮你取快递,这就是一个任务,交给张三完成,张三就是执行任务的实体,即张三是线程对象,取快递是任务。
搞清楚线程和任务的关系之后再来看 Runnable 接口,可以简单理解为让某个类具备执行任务的功能,所以 Thread 类需要实现该接口,它的实例化对象就具备了执行任务的功能。Thread 类中定义了各种线程相关方法,如启动线程,线程调度等等,所以我们自定义的类只需要继承 Thread 类就拥有了线程的各种操作方法,具体实现如下所示。
class Test2 extends Thread{
@Override
public void run() {
for (int i=0;i<100;i++){
System.out.println("------Test2------");
}
}
}
这里对 run 方法进行了重写,因为不同的线程对象需要执行不同的业务,而 Thread 类只是提供了一个模版,需要其子类根据具体需求完成重写,线程类创建完成就可以使用了,如下所示。
public class Test {
public static void main(String[] args) {
Test2 test2 = new Test2();
test2.start();
for (int i=0;i<100;i++){
System.out.println("++++++++++++Test");
}
}
}
class Test2 extends Thread{
@Override
public void run() {
for (int i=0;i<100;i++){
System.out.println("------Test2------");
}
}
}
分析一下 Test 的 main 方法,main 方式是这段程序的主线程,我们在主线程中创建了一个子线程对象 test2,然后通过 start 方法启动子线程,这样主线程和子线程就开始争夺 CPU 资源,所呈现的结果就是两个 for 循环在交替执行,如下图所示。
2、实现 Runnable 接口
相比较于第一种方法,实现 Runnable 接口实质上是将方法一的实现进行了拆分,什么意思呢?我们说过线程需要和任务结合起来,由线程来执行任务,具体到 Java 程序中,Runnable 接口表示可执行任务,Thread 类本身就实现了该接口,即已经把任务绑定给了线程。而第二种方法的意思是单独创建一个任务对象,和线程分开,分别实例化任务对象和线程对象,再把任务交给线程,具体实现如下所示。
public class Test {
public static void main(String[] args) {
Test2 test2 = new Test2();
Thread thread = new Thread(test2);
thread.start();
for (int i=0;i<100;i++){
System.out.println("++++++++++++Test");
}
}
}
class Test2 implements Runnable {
@Override
public void run() {
for (int i=0;i<100;i++){
System.out.println("------Test2------");
}
}
}
此时的 test2 对象不是线程,只是描述线程要执行的任务,所以需要额外创建线程对象 thread,把 test2 任务交给它,这样线程才有意义,没有具体任务的线程是无意义的,然后启动该线程对象即可。
【GitChat达人课】SpringMVC实战手册
【框架合集】MyBatis教程汇总
【框架合集】Spring教程汇总
【框架整合】SSM教程
【十套项目源码】
【BAT面试真题】
你在看吗?