java
环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8
前言
众所周知,随着计算机处理能力的提升和程序的复杂性增加,多线程编程在程序开发中变得越来越重要,尤其是面对一些高并发的场景,多线程的发挥之地那是广袤无垠。因为利用多线程处理,可以充分利用多核处理器资源,提升程序的并发性和速度,大大提升响应力。本文将以Java语言为例,介绍多线程编程中的线程控制相关知识,这对你理解多线程环境又多了一分胜算。
摘要
本文将详细介绍Java多线程编程中的线程控制相关内容,包括线程创建、线程启动、线程睡眠、线程等待和唤醒等方面。通过对相关案例分享解析,结合实际应用场景案例,分析多线程编程的优缺点,并提供类代码方法介绍和测试用例,帮助大家快速掌握多线程编程的核心概念和技巧,这也是我写多线程篇的初衷,就是希望能够帮助到大家。
概述
想必都不用多言,多线程开发是指在一个程序中启动多个线程,这些线程可以同时执行不同的任务,大白话就是指并发。Java作为一种面向对象的编程语言,其内心提供了丰富的API来支持多线程编程,所以支持并发场景,Java那是非常有硬实力的。通过使用Java提供的线程控制相关类和方法,开发人员可以方便地创建、启动、控制和监视线程的执行。
不言而喻,大家也都知晓,Java多线程编程本身就是Java语言中一个非常重要的特性,它允许开发者创建并行执行的程序,以提高程序的效率和性能。接下来,我就来跟同学们讲解下关于多线程的一些核心概念和控制机制,这也是关乎到后续的案例演示环节,所有这里有必要将一些核心概念预先温热下,避免出现云里雾里的情况出现。大家也可以跟随如下的流程图,先总体了解下该流程的前后顺序。
1、线程创建
在Java中,我们最为熟悉的就是通过以下两种方式来创建线程,其一是继承Thread类,其二是实现Runnable接口;接着我就重点来代码演示下,如下:
-
继承Thread类
阐述:创建一个类继承自
Thread
类,并重写其run
方法。
java
class MyThread extends Thread {
public void run() {
// 线程执行的代码
}
}
MyThread myThread = new MyThread();
-
实现Runnable接口
阐述:创建一个类实现
Runnable
接口,并实现其run
方法;然后可以使用Thread类的构造器来创建线程。
java
class MyRunnable implements Runnable {
public void run() {
// 线程执行的代码
}
}
Thread thread = new Thread(new MyRunnable());
这里,有的同学估计就坐不住啦,有话要补充,bug菌对于那些好学能力强,涉及面广的同学,必须竖个大拇哥,向他们学习。创建线程,确实还有很多方式,这里我就把一些可以普及且不冷门的创建方式普及下,补充如下,至于为啥不重点讲解,我是考虑到大部分看我这篇文章的同学都是初学者或者小白,我总不能一来就打击初学者的积极性与热衷,但是如上两种,是务必需要掌握的,如下的可以作为了解,有精力的同学也可以深入,技多不压身。
-
实现Callable接口:
阐述:Callable接口与Runnable类似,但它可以返回值,并且可以抛出异常。实现Callable接口的类需要实现call()方法。使用FutureTask包装Callable对象,然后将其传递给Thread,使用上也比较常规,也是用的比较多的创建方式之一。
核心示例代码演示如下:
java
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 线程执行的代码,并返回结果
return 1;
}
}
-
使用Executor框架:
阐述:基于Java 5,它引入了基于固定池大小的线程池的Executors类,可以通过Executors类来创建线程池,进而管理线程的创建和执行。 核心示例代码演示如下:
java
// 创建固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
// 提交任务给线程池执行
executorService.submit(new MyRunnable());
// 关闭线程池
executorService.shutdown();
-
使用匿名内部类:
阐述:使用匿名内部类来快速创建线程,这种方式不需要显式地定义类。 核心示例代码演示如下:
java
// 使用匿名内部类实现Runnable接口
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 线程执行的代码
}
});
thread.start();
-
使用Lambda表达式(Java 8及以上):
阐述:利用Lambda表达式可以简化Runnable接口的实现。 核心示例代码演示如下:
java
// 使用Lambda表达式创建线程
Thread thread = new Thread(() -> {
// 此处省略(线程执行的代码)
});
thread.start();
说明一点,针对每种创建线程的方式都有其适用场景,大家可以根据具体需求和偏好选择合适的线程创建方式,没有强制学习掌握哪一种的说法。
2、线程启动
创建线程后,我们需要调用其start()
方法来启动线程。start()
方法会调用线程的run()
方法,从而开始执行线程的代码。
java
thread.start();
具体代码实现演示如下:
既然讲到start()
方法,可能对于有的小伙伴可能对此方法理解的不够深入,这里让我用更通俗的语言来解释下start()
方法:
首先,想象一下,你有一个繁忙的餐厅,而start()
方法就像是告诉服务员开始服务客人的信号。
-
服务员就位:当你告诉服务员开始工作时,他们就会准备好接待客人。在Java中,这相当于为新线程分配所需的资源,比如他们自己的工作空间(线程栈)。
-
点菜上菜 :服务员开始工作后,他们会按照菜单(
run()
方法)来为客人服务。如果服务员是通过一个特殊的团队(实现了Runnable
接口的类)来工作的,那么他们将按照团队的菜单来服务。如果服务员是餐厅自己培养的(继承自Thread
类),那么他们会按照自己的菜单来服务。 -
同时服务多桌客人:一旦服务员开始工作,他们可以同时为多桌客人服务。这就像是Java中的线程,它们可以同时执行多个任务,即使在只有一个CPU的情况下,它们也会快速切换,让每个任务都感觉像是在同时进行。
简单来说,start()
方法就像是给线程一个开始工作的信号,告诉它"好了,现在开始执行你的任务吧!"一旦线程开始工作,它就可以和其他线程一起,同时处理不同的任务。
不知我如上这样讲,大家是否可以理解了。
3、线程睡眠
sleep()
方法可以使当前线程暂停执行指定的时间(以毫秒为单位)。这个方法是Thread
类的静态方法。
java
try {
Thread.sleep(1000); // 线程暂停1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
实际使用演示如下:
源码截图如下:
sleep()源码解读:
这里,我根据个人的理解,给大家剖析下Thread类的sleep()
源码,仅供参考,不喜勿喷。
在Java中,Thread
类的sleep()
方法允许当前正在执行的线程暂停执行指定的时间长度。这个方法是静态的,意味着它不依赖于调用它的对象的状态。下面是对sleep()
方法的简单剖析,以及一些关键点的解释。
1、基本语法:
java
public static void sleep(long millis) throws InterruptedException {
// 实现细节
}
2、参数:
millis
:线程需要暂停的毫秒数。
3、异常:
InterruptedException
:如果当前线程在睡眠时被其他线程中断,将会抛出此异常。
4、实现细节:
-
检查传入时间 :首先,
sleep()
方法会检查传入的millis
参数是否小于0。如果是,它将抛出一个IllegalArgumentException
异常,因为睡眠时间不能是负数。 -
获取当前线程 :
sleep()
方法通过Thread.currentThread()
获取当前正在执行的线程。 -
等待 :然后,它调用当前线程的
wait(long timeout)
方法,传入millis
作为超时时间。这个方法会使当前线程进入等待状态,直到以下任一情况发生:- 指定的超时时间已过。
- 其他线程调用了当前线程的
interrupt()
方法。
-
处理中断 :如果在等待期间发生中断(即调用了
interrupt()
),当前线程的中断状态将被清除,并且抛出InterruptedException
。
5、伪代码示例:
java
public static void sleep(long millis) {
if (millis < 0) {
throw new IllegalArgumentException("Sleep time cannot be negative");
}
Thread currentThread = Thread.currentThread();
currentThread.wait(millis);
}
6、注意事项:
sleep()
方法不会释放任何锁,这意味着如果当前线程持有锁,那么在sleep()
期间,其他线程仍然不能访问被锁定的资源。- 使用
sleep()
时,应该捕获InterruptedException
,以便适当地处理中断情况。 sleep()
方法不应该用于精确的时间控制,因为它可能会受到线程调度和系统负载的影响。
7、示例用法:
java
try {
Thread.sleep(1000); // 线程暂停1秒
} catch (InterruptedException e) {
e.printStackTrace();
// 处理中断异常
}
8、小结: 通过这种方式,sleep()
方法为Java程序提供了一种简单的线程暂停机制,允许开发者在需要时让当前线程暂时让出CPU资源。
4、线程等待
线程可以通过wait()
方法进入等待状态,直到其他线程调用相同对象的notify()
或notifyAll()
方法。这通常用于线程间的同步。
示例用法:
java
synchronized (object) {
while (condition) {
object.wait();
}
// 当条件满足时执行的代码
}
5、线程唤醒
当某个条件满足时,其他线程可以调用对象的notify()
方法唤醒在此对象上等待的单个线程,或者使用notifyAll()
唤醒所有在此对象上等待的线程。
示例用法:
java
synchronized (object) {
object.notify(); // 唤醒在此对象上等待的单个线程
// 或者
object.notifyAll(); // 唤醒所有在此对象上等待的线程
}
6、线程中断
线程可以通过interrupt()
方法来请求中断另一个线程。被请求中断的线程会抛出InterruptedException
,通知线程它已被中断。
示例用法:
java
thread.interrupt();
7、线程终止
线程应该通过正常的退出流程来终止,即执行完run()
方法中的所有代码。强制终止线程是不推荐的做法,因为它可能会导致资源未被正确释放。
8、线程优先级
Java线程有优先级的概念,可以通过setPriority(int priority)
方法设置。优先级高的线程更有可能被CPU调度执行。
9、守护线程
守护线程是一种特殊的线程,当所有非守护线程结束时,守护线程也会自动结束。可以通过setDaemon(true)
方法将线程设置为守护线程。
10、线程同步
线程同步是确保多个线程在访问共享资源时的一致性和线程安全。可以使用synchronized
关键字或java.util.concurrent
包中的锁来实现同步。
11、线程池
Java提供了线程池(ExecutorService
)来管理线程的创建和销毁,提高资源利用率和执行效率。
总而言之,以上就是Java多线程编程中的一些基本控制机制,实际应用中还需要考虑线程安全、死锁、资源竞争等问题,这些我们就日后慢慢再讲,怕干货过多,导致知识点积压无法及时消化,主打就是个循序渐进。
案例分享
在日常Java中,线程创建最先想到也最方便使用的是通过继承Thread
类或实现Runnable
接口两种方式之一,虽然上述我也普及了其他的实现方式,如果你有足够的时间精力,后续还是得必须多掌握几种的,这对你在日常面试中即为热门,基本必考。不过以上都是些题外话,如果掌握的够到位,我们都知道线程的启动和执行是通过调用start()
方法来完成的。
下面是一个简单的示例代码,仅供参考:
java
/**
* @Author bug菌
* @Source 公众号:猿圈奇妙屋
* @Date 2024-04-17 21:44
*/
public class MyThread extends Thread {
public void run() {
// 线程执行的代码逻辑
System.out.println("Thread " + getName() + " is running");
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
代码解读:
在上述的代码中,我先定义了一个名为MyThread
的类,该类继承Thread
类,并重写了run()
方法。在run()
方法中,可以写线程执行的代码逻辑,这里我就暂时直接写了句打印输出。
其次,在main()
函数中,创建一个MyThread
对象myThread
,测试一下,我们就直接调用其start()
方法来启动线程。start()
方法会调用run()
方法,使得线程可以执行由run()方法定义的代码逻辑,即能正常执行。
但是有一点,同学们必须要注意的,线程的具体执行时间由操作系统决定,程序中的代码只是定义了线程的逻辑,并不能保证在什么时候开始执行和执行多长时间。
应用场景案例
那么,究竟那些场景可以派上用场?这点值得我们去考虑。我们或多或少都知道多线程编程广泛应用于需要处理并发任务的场景,例如服务器端程序、图形界面应用程序、游戏开发等。这里给同学们梳理下常见的几个多线程场景。
以下是几个常见的应用场景案例:
-
服务器端并发处理:在Web服务器中,每个请求通常会被分配给一个独立的线程来处理。这样可以提高服务器的并发处理能力,同时保证各个请求之间的独立性。
-
图形界面应用程序:在图形界面应用程序中,通常需要多线程来处理用户界面的更新和响应,以保持程序的流畅性。例如,一个界面线程负责接收用户输入,而另一个线程负责刷新界面显示。
-
游戏开发:游戏开发中通常需要处理大量的并发任务,例如用户输入、物理碰撞检测、AI决策等。通过使用多线程,可以实现游戏的流畅性和响应性。
同样,还有其他的场景,待发现与实现,这就靠你们这些后起之秀来玩转啦。
优缺点分析
事物都讲究辩证看待,别说是一个客观的知识点了,面对多线程,它同样有优点也有缺点,其中它的优点主要体现在以下几个方面:
-
提高程序的并发性:通过多线程编程,可以充分利用多核处理器的资源,提高程序的并发处理能力和性能。
-
提升程序的响应性:通过把耗时的任务放到后台线程中执行,可以保持程序界面的流畅性和响应性。
-
提高程序的可维护性:通过合理地划分任务和模块,可以降低程序的复杂性,提高程序的可维护性和可扩展性。
然而,它的缺点也梳理如下:
-
线程安全问题:多线程编程中需要注意线程安全,避免出现数据竞争和死锁等问题。
-
调试和测试困难:多线程程序的调试和测试比较困难,需要特别注意线程间的同步和通信问题。
-
资源消耗增加:多线程编程会增加系统资源的消耗,包括内存、CPU和IO等方面。
类代码方法介绍
在Java多线程编程中,我们对一些重要的类和方法是必须要去熟悉和掌握的,比如如下,考察下学了几期是否基础有被夯实。
-
Thread
类:Java提供的线程类,通过继承Thread
类可以创建线程对象,重写run()
方法来指定线程执行的任务。 -
Runnable
接口:Java提供的线程接口,通过实现Runnable
接口可以创建线程对象,重写run()方法来指定线程执行的任务。 -
start()
方法:Thread类中的方法,用于启动线程。调用start()
方法后,线程将被放入可运行状态,等待系统调度执行。 -
sleep()
方法:Thread
类中的方法,用于使当前线程进入睡眠状态,暂停一段时间执行。 -
wait()
和notify()
方法:Object
类中的方法,用于线程间的等待和唤醒操作。wait()
方法使当前线程进入等待状态,而notify()
方法用于唤醒等待的线程。
测试用例
测试代码
下面是一个简单且完整的测试用例,用于演示线程控制的基本操作,穿插如上讲解的知识点,主打一个理论与实践相结合:
代码如下,仅供参考:
java
/**
* @Author bug菌
* @Source 公众号:猿圈奇妙屋
* @Date 2024-04-17 21:51
*/
public class MyThreadTest extends Thread {
public void run() {
try {
System.out.println("Thread " + getName() + " is running");
Thread.sleep(1000);
System.out.println("Thread " + getName() + " is done");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
MyThreadTest thread1 = new MyThreadTest();
MyThreadTest thread2 = new MyThreadTest();
thread1.start();
thread2.start();
}
}
在上述代码中,我们是创建了两个线程对象,并调用start方法启动它们。每个线程执行的任务就是打印一段文字,并睡眠1秒钟,然后执行结束。
如下是实际执行结果:
测试结果
针对如上测试代码,这里我本地进行实际测试一波,结果仅供参考,有条件的同学们也可以自己本地实践一下。
测试代码解析
针对如上测试代码,这里我再具体给大家讲下(实际上把每一步都剖析给大家听),希望能够帮助大家理解的更透彻。
如上测试代码我先定义了一个继承自Thread类的MyThreadTest
类。在MyThreadTest
类中,重写run()
方法,在run()
方法中打印当前线程的名称,并让线程休眠1秒钟,然后再打印当前线程的名称。
在main方法中,创建了两个MyThreadTest
对象thread1
和thread2
,并分别调用它们的start方法启动线程。
当运行该程序时,thread1
和thread2
会同时开始执行,由于在run方法中有一个1秒钟的休眠操作,所以不同的运行结果会有一定的随机性,即无法确定哪个线程先执行完毕。
注意,这里的MyThreadTest
类是继承自Thread类的,所以可以通过调用start方法来启动线程。另外,由于run方法是在子线程中执行的,所以可以在run()
方法中编写具体的业务逻辑。
全文小结
本文我以Java开发语言为例,介绍了多线程编程中的线程控制相关知识,诸如多线程的常规概念等。然后通过对源代码的解析和应用场景案例的讲解,帮助大家对多线程编程的基本概念和实践使用有个全面的了解。同时,我也分析了多线程编程的优缺点,并提供了类代码方法介绍和测试用例,帮助大家加深理解和掌握多线程编程的核心内容,相信大家也能够及时吸收并掌握。
总结
总而言之,多线程它是如今软件Java开发中的核心编程技术之一,它不但可以提高程序的并发性,还能提高响应性和可维护性。通过合理地使用线程控制相关的类和方法,开发人员可以轻松地创建、启动和控制线程的执行。然而,多线程编程也存在一些挑战和缺点,需要大家注意线程安全和调试测试等问题,这点我日后也会讲。
至此,只要我们通过不断学习和实践,我们一定可以更好地应用多线程编程,提升软件开发效率和质量。
结尾
希望通过本文的介绍,同学们能够对Java多线程编程中的线程控制有了更深入的理解。多线程编程是一个广阔的领域,还有很多其他方面的知识和技巧等待我们去探索。通过不断学习和实践,我们可以成为高效的多线程编程专家,并为软件开发带来更多的创新和价值。哈哈,目的都是一致的,带大家一起实现财富自由,才是人生理想。
... ...
ok,以上就是我这期的全部内容啦,若想学习更多,你可以持续关注我,我会把这个多线程篇系统性的更新,保证每篇都是实打实的项目实战经验所撰。只要你每天学习一个奇淫小知识,日积月累下去,你一定能成为别人眼中的大佬的!功不唐捐,久久为功!
「赠人玫瑰,手留余香」,咱们下期拜拜~~
本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!