从零开始学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类的方式则需要手动实现线程池的功能。

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

相关推荐
CodeAmaz2 分钟前
Spring编程式事务详解
java·数据库·spring
没有bug.的程序员4 分钟前
微服务基础设施清单:必须、应该、可以、无需的四级分类指南
java·jvm·微服务·云原生·容器·架构
武子康7 分钟前
Java-204 RabbitMQ Connection/Channel 工作流程:AMQP 发布消费、抓包帧结构与常见坑
java·分布式·消息队列·rabbitmq·ruby·java-activemq
郑州光合科技余经理8 分钟前
海外国际版同城服务系统开发:PHP技术栈
java·大数据·开发语言·前端·人工智能·架构·php
appearappear18 分钟前
Mac 上重新安装了Cursor 2.2.30,重新配置 springboot 过程记录
java·spring boot·后端
CryptoRzz27 分钟前
日本股票 API 对接实战指南(实时行情与 IPO 专题)
java·开发语言·python·区块链·maven
程序员水自流29 分钟前
MySQL数据库自带系统数据库功能介绍
java·数据库·mysql·oracle
谷哥的小弟34 分钟前
Spring Framework源码解析——RequestContext
java·后端·spring·框架·源码
天远Date Lab39 分钟前
Java微服务实战:聚合型“全能小微企业报告”接口的调用与数据清洗
java·大数据·python·微服务
lizz3144 分钟前
C++操作符重载深度解析
java·c++·算法