从零开始学Java之线程简介及创建方式

作者 :孙玉昌,昵称【一一哥 】,另外【壹壹哥】也是我哦

千锋教育高级教研员、CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者

前言

在前面的几篇文章中,壹哥 给大家讲解了Java中的IO流,至此我们就把Java中特别难啃的一块骨头给啃了下来,开心😄。但是,你知道的,骨头肯定不会只有一块,Java中还有另一块很难啃的骨头,这就是Java的线程!所以接下来,壹哥会再利用几篇文章,给大家讲解线程、多线程、线程池以及线程安全等相关的内容,这一块的内容可以说比IO流还要重要哦,所以请大家再坚持一下,熬过这一阶段就苦尽甘来了。

------------------------------前戏已做完,精彩即开始----------------------------

全文大约【4700】 字,不说废话,只讲可以让你学到技术、明白原理的纯干货!本文带有丰富的案例及配图视频,让你更好地理解和运用文中的技术概念,并可以给你带来具有足够启迪的思考......

配套开源项目资料

Github: github.com/SunLtd/Lear...

Gitee: gitee.com/sunyiyi/Lea...

一. 线程简介

1. 进程的概念

壹哥在一开始给大家介绍Java的时候,其实就跟大家说过,Java是一种多线程的编程语言,那么到底什么是线程呢?在搞懂线程的概念之前,我们得先知道什么是进程。

所谓的进程(Process),是指一个正在运行中的程序实例,是操作系统对程序执行过程的抽象。 举个例子,你电脑上安装了一个QQ,这个QQ就是一个程序,如果你把QQ启动运行起来了,正在运行的这个QQ程序就是一个进程。但是一个QQ程序有可能同时占有多个进程,这就看开发这个软件时是怎么设置的了。

在一个进程中,程序执行时需要的所有资源都会被分配给该进程,例如内存、CPU时间片等。每个进程都是独立的、互不干扰的,彼此之间不能直接共享数据,需要通过操作系统提供的IPC机制进行通信。另外每个进程都有自己独立的地址空间,内部包含了程序的代码、数据、堆栈等信息。在现代操作系统中,一个计算机可以同时运行多个进程,并且不同的进程之间可以通过操作系统的调度机制分时使用CPU资源,让我们看起来多个任务之间好像是同时进行的。

总之一句话,进程就是一个正在运行的程序!

2. 线程的概念

2.1 线程

那么线程又是什么呢?

在计算机程序中,一条线程(Thread)指的是进程中一个单一顺序的控制流,在一个进程中可以存在多个并发的线程,多个线程之间并行执行着不同的任务。我们可以把线程理解成是一个轻量级的进程,它是程序执行时的最小单位,每个线程都有自己的程序计数器、栈和本地变量等信息。

在Java中,线程是Java的基本执行单元,包括用户线程和守护线程两种。用户线程是指在应用程序中创建的线程,而守护线程则是指在JVM中创建的线程,主要用于提供后台服务,如垃圾回收线程。

2.2 多线程

另外我们在开发时,经常会提到多线程的概念,即多个线程同时工作的意思。多线程是Java支持的最基本的并发模型,网络、数据库、Web等的开发都需要依赖多线程模型,利用这种多线程模型来实现多任务。我们知道,计算机中的CPU执行代码时是一条一条顺序执行的,但即使是单核CPU,也可以同时运行多个任务。这是因为操作系统可以让CPU把多个任务轮流交替执行,从而实现了多任务。而多线程模型却让我们能够以一种特别的方式来实现多任务,使得程序员可以编写出更高效的程序充分利用CPU,且所占用的资源开销也较小。

实际上,Java本身就实现了对多线程的支持。一个Java程序就是一个JVM进程,一个JVM进程会用一个主线程来执行main()方法,而在main()方法的内部,我们又可以启动多个线程。此外,JVM还有负责垃圾回收等其他任务的工作线程。

多线程相对于单线程的特点在于:多线程经常需要读写共享数据,并进行同步,保证共享数据的安全性。比如我们很常见的视频播放,会有一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不能同步。

所以在多线程编程中,确保线程的安全和同步,避免出现竞态条件和死锁等问题就是开发时的重难点。我们可以使用synchronized关键字来实现同步,或者使用Lock和Condition接口提供的方法来进行线程间的通信和同步。此外,Java还提供了一些保障线程安全的类和方法,例如Atomic类、ConcurrentHashMap类、CopyOnWriteArrayList类等。总之,多线程编程的复杂度更高,调试更困难。

3. 进程与线程的关系、区别

3.1 进程与线程的关系

通过上面内容的描述,我们可以知道进程与线程的关系:

  • 进程包括了操作系统分配的内存空间,一个进程可以包含一个或多个线程;
  • 线程不能独立存在,它必须是进程的一部分;
  • 进程一直运行,直到其内部所有的非守护线程都结束运行后才会结束。

关于两者的关系,我们可以参考下图:

3.2 进程与线程的区别

进程(Process)和线程(Thread)是计算机中的两个基本概念,二者都可以执行代码,但它们之间也有以下区别:

  • 资源占用:一个进程可以拥有多个线程,每个线程都共享该进程的内存空间和系统资源,而每个进程都有自己的内存空间和系统资源,因此创建进程需要更多的系统资源。
  • 上下文切换:由于一个CPU同一时间只能执行一个进程或线程,所以当CPU需要切换到另一个进程或线程时,需要保存当前进程或线程的状态,以便下次切换回来时进行恢复。因为线程共享同一个进程的内存和系统资源,所以线程的上下文切换开销要小于进程。
  • 通信和同步:进程间的通信需要使用进程间通信(IPC)机制,比如管道、消息队列、共享内存等。而线程之间可以直接共享进程的内存空间,因为多个线程可以共享同一进程的资源,所以线程之间的同步机制也相对简单。
  • 稳定性:由于一个进程中的一个线程崩溃可能会导致整个进程的崩溃,因此进程的稳定性相对线程较高。

4. 线程的组成部分

我们要知道线程的概念,还要知道一个线程的主要组成,一个线程主要是由以下部分组成:

  • 线程ID(Thread ID) :每个线程都有一个唯一的ID,我们可以使用Thread.getId()方法获取;
  • 程序计数器(Program Counter) :用于记录线程当前执行的位置,以便下次继续执行;
  • 线程栈(Thread Stack) :用于存储线程执行时的局部变量、方法参数、返回地址等数据,以及调用的方法信息;
  • 堆(Heap) :用于存储线程执行时的对象、数组等数据;
  • 同步器(Synchronizer) :用于线程之间的同步与通信。

除此之外,线程还有一些其他的属性,比如线程状态、优先级、名称等,这些属性信息我们都可以使用相应的方法来获取或修改,如Thread.getState()、Thread.getPriority()、Thread.setName()等方法。

5. 配套视频

与本节内容配套的视频链接如下:

player.bilibili.com/player.html...

二. 创建方式

1. 概述

了解了线程的基本概念之后,接下来我们就可以学习如何创建线程了。在Java中,每个线程对象都是由Thread类实例化得来的,目前Java给我们提供了多种构建线程对象的方式,包括继承Thread类、实现Runnable接口、实现Callable接口、Lambda表达式、使用Executor框架等方式。

2. 继承Thread类

继承Thread类是创建线程的最简单方式,我们只需要继承Thread类并重写run()方法即可,run()方法中包含了该线程的核心执行逻辑,如下所示:

java 复制代码
//自定义线程
class MyThread extends Thread {
    // 当线程启动的时候,会执行run方法中的代码
    @Override
    public void run() {
        System.out.println("当前线程:"+Thread.currentThread());
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
        }
    }
}

然后我们就可以创建MyThread对象并调用start()方法来启动线程了,如下所示:

java 复制代码
public class Demo01 {
    public static void main(String[] args) {
        // 创建线程对象
        MyThread mt = new MyThread();
        // 启动线程
        mt.start();

        // main方法所在的类,属于默认的主线程
        System.out.println("主线程:"+Thread.currentThread());
    }
}

大家注意,其实Java中的类,默认情况下都属于主线程。

另外我们要想启动一个线程,必须调用Thread对象的start()方法才能启动,且 一个线程对象只能调用一次 start() 方法。如果我们查看Thread类的源代码,会看到start()方法内部调用了一个private native void start0()方法,native修饰符表示这个方法是由JVM虚拟机内部的C代码实现的,不是由Java代码实现的。

3. 实现Runnable接口

创建线程的第二种常用方式是通过实现Runnable接口,这种方式避免了Java单继承的限制,可以让我们同时实现多个接口。我们实现Runnable接口后,需要实现run()方法,并在该方法中实现核心业务,代码如下所示:

java 复制代码
/**
 * @author 一一哥Sun
 */
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("创建线程的第二种方式,当前线程:"+Thread.currentThread());
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
        }
    }
}

在实现了Runnable接口之后,接下来我们就要把Thread线程对象创建出来了,如下所示:

java 复制代码
/**
 * @author 一一哥Sun
 */
public class Demo02 {
    public static void main(String[] args) {
        // 创建线程对象,注意这种方式要把Runnale对象作为Thread的参数
        Thread t = new Thread(new MyRunnable());
        // 启动线程
        t.start();  

        // main方法所在的类,属于默认的主线程
        System.out.println("主线程:"+Thread.currentThread());
    }
}

4. Lambda表达式

另外在Java 8中,还引入了一种新的方式来创建线程,也就是使用Lambda表达式来实现Runnable接口的run()方法,代码如下:

java 复制代码
public static void main(String[] args) {
    Thread t = new Thread(() -> {
        System.out.println("start new thread!");
    });
    // 启动新线程
    t.start(); 
}

这种方式简化了线程的创建过程,提高了代码的可读性和可维护性。以后在讲解Java新特性时,壹哥再给大家讲解这种方式的用法。

5. 实现Callable接口

除了可以通过实现Runnable接口来创建线程之外,我们还可以通过实现Callable接口结合Future来创建线程。与Runnable接口不同的是,Callable接口可以返回线程的执行结果,我们可以通过该接口中的call()方法返回执行结果,在调用时通过Future接口来获取到最终的执行结果。比如下面的例子:

java 复制代码
import java.util.concurrent.Callable;

/**
 * @author 一一哥Sun
 */
public class MyCallable implements Callable<String>{
    @Override
    public String call() throws Exception {
        //执行业务方法,可以在该方法中返回结果
        String result="方法的执行结果";
        return result;
    }
}

我们在实现Callable接口时,需要指明call()方法要返回的结果类型,这个类型是通过在<>中定义泛型来实现的。接下来我们看看这种方式又该如何创建出对应的线程对象,代码如下:

java 复制代码
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
 * @author 一一哥Sun
 */
public class Demo03 {
    public static void main(String[] args) {
        try {
            // 创建线程对象,注意这种方式是通过Executors线程池创建出来的
            ExecutorService executorService = Executors.newSingleThreadExecutor();
            //将Callable作为参数传入submit()方法中,得到一个Future对象
            Future<String> future = executorService.submit(new MyCallable());
            // 获取执行结果
            String result = future.get();
            System.out.println("执行结果:"+result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }  
    }
}

在这种方式中,我们用到了Executors、ExecutorService、Future等API,这些API其实是和线程池有关的内容,以后壹哥会再专门讲解线程池,请大家继续关注哦。

6. 使用Executor框架

除了以上创建线程的方式之外,我们还可以利用Executor线程池框架进行线程的创建与管理。所谓的线程池,就是包含了一定数量线程的"集合",当需要执行任务时,线程池中的线程会自动分配任务并执行。线程池技术可以避免频繁地创建和销毁线程对象,提高了程序的性能。

在Java中,Executor就是一种线程池框架,它可以把线程的创建和执行分离开,提高了程序的可扩展性和可维护性,所以我们使用Executor框架就可以简化线程的管理。另外ExecutorService是Java中用于管理线程池的接口,它提供了提交任务、管理任务、管理线程池等方法。下面是利用Executor框架创建线程的案例:

java 复制代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author 一一哥Sun
 */
public class Demo04 {
    public static void main(String[] args) {
        // 通过Executors线程池框架创建线程,在线程池中创建10个线程
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        // 执行线程池中的线程
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("当前线程:"+Thread.currentThread());
            }
        });
		
        //关闭线程池
        executorService.shutdown();
    }
}

shutdown()方法用于关闭线程池。当我们调用shutdown()方法时,线程池会停止接收新的任务,已经提交但未执行的任务会继续执行,直到所有的任务都执行完毕后线程池才会真正的关闭,释放资源。如果我们在调用shutdown()之前,已经调用了submit()方法提交了任务,那么这些任务会被执行,但不会再接受新的任务。

需要注意的是,shutdown()方法只是发送一个关闭请求,它并不会等待线程池中所有的任务都执行完毕。如果需要等待线程池中所有的任务都执行完毕后再关闭线程池,可以调用awaitTermination()方法,该方法会阻塞当前线程,直到线程池中所有的任务都执行完毕或超时。如下所示:

java 复制代码
try {
    executor.shutdown();
    //设置超时时间
    executor.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
    // 处理中断异常
}

在上面的代码中,awaitTermination()方法会等待线程池中所有的任务执行完毕或超时(5秒),如果超时了,线程池也会强制关闭。需要注意的是,如果线程池中的任务执行时间过长,可能会导致awaitTermination()方法永远不会返回,这时可以考虑调用shutdownNow()方法来立即中断任务的执行。

7. 配套视频

与本节内容配套的视频链接如下:

player.bilibili.com/player.html...

------------------------------正片已结束,来根事后烟----------------------------

三. 结语

在今天的这篇文章中,壹哥给大家讲解了进程、线程、多线程的概念,并重点给大家讲解了线程的几种创建方式,大家一定要把这几种创建方式掌握了哦。尤其是继承Thread类与实现Runnable接口,对这两种创建线程方式的区别,大家要清楚:

  • 当继承Thread类时,我们需要重写Thread类的run()方法,并通过调用start()方法来启动线程;
  • 当实现Runnable接口时,需要实现Runnable接口中的run()方法,并通过创建Thread对象,并将其传递给Runnable对象来启动线程;
  • 两种方式的主要区别在于继承Thread类只能单继承,而实现Runnable接口可以避免单继承的限制,适用于多个线程执行相同任务的情况;
  • 此外,实现Runnable接口的方式还可以方便地使用线程池,实现线程的复用;而继承Thread类的方式则需要手动实现线程池的功能。

另外如果你独自学习觉得有很多困难,可以加入壹哥的学习互助群,大家一起交流学习。

相关推荐
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ10 分钟前
idea 弹窗 delete remote branch origin/develop-deploy
java·elasticsearch·intellij-idea
Code成立13 分钟前
《Java核心技术 卷I》用户图形界面鼠标事件
java·开发语言·计算机外设
鸽鸽程序猿38 分钟前
【算法】【优选算法】二分查找算法(下)
java·算法·二分查找算法
遇见你真好。1 小时前
自定义注解进行数据脱敏
java·springboot
NMBG221 小时前
[JAVAEE] 面试题(四) - 多线程下使用ArrayList涉及到的线程安全问题及解决
java·开发语言·面试·java-ee·intellij-idea
王二端茶倒水1 小时前
大龄程序员兼职跑外卖第五周之亲身感悟
前端·后端·程序员
像污秽一样1 小时前
Spring MVC初探
java·spring·mvc
计算机-秋大田1 小时前
基于微信小程序的乡村研学游平台设计与实现,LW+源码+讲解
java·spring boot·微信小程序·小程序·vue
LuckyLay1 小时前
Spring学习笔记_36——@RequestMapping
java·spring boot·笔记·spring·mapping
醉颜凉2 小时前
【NOIP提高组】潜伏者
java·c语言·开发语言·c++·算法