浅谈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 任务交给它,这样线程才有意义,没有具体任务的线程是无意义的,然后启动该线程对象即可。

原文发布于微信公众号 - Java大联盟(javaunion)

原文发表时间:2019-04-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券