作者 :孙玉昌,昵称【一一哥 】,另外【壹壹哥】也是我哦
千锋教育高级教研员、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类的方式则需要手动实现线程池的功能。
另外如果你独自学习觉得有很多困难,可以加入壹哥的学习互助群,大家一起交流学习。