目录
在上一篇中进程http://t.csdnimg.cn/Qrat7已经讲解了有关进程的相关知识,这一篇我们就接着往下讲有关线程的知识。
一.线程
1.概念
线程是操作系统进运算调度的最小单位,是操作系统调度执行的基本单位。线程被包含在进程中,一个进程可以有多个线程。
2.特点
1.轻量级:线程的创建和切换开销比进程要小。
2.并发执行:一个进程中的多个线程可以并发执行,提高了并发性。
3.资源共享:在同一个进程中的所有线程共享进程的资源。如内存空间、文件资源等。
在同一个进程中,采用多线程的方式可以提高效率,但如果线程的数量过多,比如超出了CPU核心数目,此时就好无法微观上完成所有线程的“并行”执行,线程之间需要进行“竞争”,反而会是效率降低。所以,要想提高效率,需要充分利用多核心,进行“并行执行”
3.线程的创建
在java中,创建线程有以下几种方式:
在java中,Thread是在java.lang中,在使用的时候,不需要进行导包。
1.继承Thread类
通过继承Thread来重写run方法
class MyThread extends Thread{
@Override
public void run(){
System.out.println("通过继承Thread来创建线程");
}
}
public class Main {
public static void main(String[] args) {
MyThread t=new MyThread();//创建线程对象
t.start();//启动线程
}
}
2.实现Runnable接口创建线程
/**
* MyRunnable 类实现了 Runnable 接口,以便通过多线程方式执行特定任务。
* 该类无构造方法注释,因为 Runnable 接口的实现不需要特定的初始化逻辑。
*/
class MyRunnable implements Runnable{
/**
* 当线程执行此方法时,它将打印一条消息。
* 该方法无参数和返回值,符合 Runnable 接口的定义。
*/
@Override
public void run(){
System.out.println("通过实现Runnable来创建线程");
}
}
/**
* Main1 类包含程序的入口点。
* 该类启动一个新线程,该线程执行 MyRunnable 类的实例。
*/
public class Main1 {
/**
* 程序入口点。
* @param args 命令行参数,本程序中未使用。
*/
public static void main(String[] args) {
/* 创建一个新的线程,并将 MyRunnable 实例作为目标。 */
Thread t=new Thread(new MyRunnable());
/* 启动线程,开始执行 run 方法。 */
t.start();
}
}
3.匿名内部类创建Thread子类对象
public class Main2 {
public static void main(String[] args) {
Thread t=new Thread(){
@Override
public void run() {
System.out.println("通过实现匿名内部类来创建线程");
}
};
t.start();
}
}
4. 匿名内部类创建Runnable子类对象
/**
* 程序入口主方法。
* 创建一个线程并启动它,该线程通过实现Runnable接口来定义其执行逻辑。
* 使用这种方式创建线程,可以避免直接继承Thread类,从而更灵活地复用代码和实现多线程。
*
* @param args 命令行参数,本例中未使用。
*/
public static void main(String[] args) {
// 创建一个Thread对象,传入一个实现了Runnable接口的匿名类实例作为线程的执行体。
Thread t=new Thread(new Runnable() {
/**
* Runnable接口的run方法,定义了线程的执行逻辑。
* 在这里,线程的唯一任务是打印一条信息。
*/
@Override
public void run() {
System.out.println("通过创建Runnable子类对象来创建线程");
}
});
// 调用线程的start方法来启动线程。
// 注意:只有调用start方法后,线程才会开始执行其run方法中的代码。
t.start();
}
5.利用Lambda式来创建线程
public class Main5 {
/**
* 程序的入口点。
* 创建并启动一个线程,该线程通过Lambda表达式执行特定任务。
* 使用Lambda表达式创建线程简化了代码,并且使得线程的创建和启动更加直观。
*
* @param args 命令行参数,本程序中未使用。
*/
public static void main(String[] args) {
// 创建一个新线程,并使用Lambda表达式定义线程的执行逻辑。
Thread t=new Thread(()->{
// 线程执行的逻辑:打印一条消息。
System.out.println("通过Lambda式来创建线程");
});
// 启动线程,使得定义的Lambda表达式开始执行。
t.start();
}
}
一般情况下,比较推荐使用Lambda式来实例化一个线程,能够简化代码。
Thread类及其常见方法
Thead类是JVM用来管理线程的一个类。在java中,每个线程都有一个唯一的Thread对象与之关联。Thread类的对象就是用来描述一个线程的执行流,JVM会将这些Thread对象组织起来,用于线程调度,线程管理。
1.Thread常见的构造方法
方法 | 说明 |
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target,String name) | 使用Runnable对象创建线程对象,并命名 |
在jconsole中,可以查看线程的运行状态。
示例:
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
Thead() :创建线程
这里我们先创建两个线程
当运行程序后,打开jconsole进行查看.
给Thread自定义名字
打开jconsole查看
2.Thread的常见属性
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
- ID是线程的唯一标识,不同线程不会重复
- 名称是各种调试工具所要用到的
- 状态标识线程当前所处的一个情况(NEW、RUNNABLE、BLOCKED、WAITING、TIME WAITING、TERMINATED)
- 优先级高的线程理论上更容易被调度到
- 后台线程:也叫守护线程,JVM会在一个进程的所有非后台线程结束后,才会结束运行。
- 是否存活,可以简单理解为run方法是否已经运行结束了
- 线程中断问题,后面进一步说明
什么是后台线程?
后台线程是一种特殊类型的线程,它的生命周期取决于任何前台线程,当所有的前台线程都结束后,后台线程才会自动退出。与前台线程不同,后台线程不会阻止JVM的退出。
后台线程通常用于执行一些支持性任务,如日志记录、定时任务、连接池维护等。它们不会干扰程序的正常运行,但在必要时可以执行一些必要的工作。
启动线程
在前面,我们只是创建了线程对象,但并没有启动线程。
使用start方法,即可启动线程【线程对象.start()】
public class Mains {
public static void main(String[] args) {
Thread t=new Thread(){
@Override
public void run()
{
while(true) {
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t.start();
}
}
这样,我们就启动了一个线程,当然,可能你会觉得这不就是调用了run方法吗,不可以直接调用,为何多此一举,可以看以下例子:
/**
* 主类Main8用于演示如何创建并运行一个线程。
*/
public class Main8 {
/**
* 程序入口主方法。
* @param args 命令行参数
*/
public static void main(String[] args) throws InterruptedException {
// 创建一个匿名内部类Thread对象,重写run方法来定义线程的执行逻辑
Thread t1=new Thread(){
@Override
public void run() {
// 无限循环,使得线程不断打印消息
while (true) {
System.out.println("这是一个线程");
try {
// 线程休眠500毫秒,控制打印频率
Thread.sleep(500);
} catch (InterruptedException e) {
// 打印异常信息
e.printStackTrace();
}
}
}
};
// 启动线程t1
// 直接调用run
t1.run();
// 主线程打印消息
while (true) {
System.out.println("这是主线程");
Thread.sleep(500);
}
}
}
当我们直接调用run方法时,只会打印线程t1中的内容,但主线程main中的语句并没有被执行到,可以知道,但我们想要的是线程t1和main并发执行,所以可以知道,当我们调用run方法时,并不会启动线程。
在调用run后,我们可以看到主线程一直处于阻塞状态【TIME_WAITING】,这是因为t1线程占用着
资源,main线程需要等t1线程释放之后才能执行。
来看下调用start方法
我们可以看到,主线程和线程t1都在执行,虽然图中他们是有序的,但实际上线程之间的顺序是随机的,即“抢占式执行”。也正是因为线程之间的抢占式执行,才有了后面的线程安全问题。
start和run的区别
start:用start来启动线程,真正实现了多线程运行,无需等待run方法体执行完毕,可以直接继续执行后面的代码。调用start方法时,此时线程处于就绪状态,并没有运行,之后通过调用Thread类调用run方法来完成其运行操作,这里的run称为线程体,包含了线程所要执行的内容,当run方法结束,线程结束。
run:只是类中的一个方法,若直接调用run方法,程序中依旧只有主线程这一个,还是需要顺序执行下去。
总结:调用start方法会启动线程,实现多线程;而调用run方法,没有启动线程,当做普通的方法使用。
中断线程
当主线程main执行完main方法后或者是其他线程在执行完run方法后,线程就会终止。但是我们也可以在此之前进行手动中断线程。
常见的中断方式有两种:
- 通过共享的标记来进行沟通
示例:现有一个线程t,但其执行时间太长了,那么如何进行提前中断线程?想要中断线程,其实就是要让其run方法结束,我们可以在循环处添加一个条件,当条件不成立就停止。
即:
/**
* InterruptMain 类用于演示线程的中断处理。
* 主要通过一个循环运行的线程和一个标志位来展示如何通过中断来结束线程的执行。
*/
public class InterruptMain {
/**
* 标志位用于指示线程是否应该继续运行。
*/
private static boolean flag = false;
/**
* 程序入口点。
* 创建一个线程,该线程会不断打印信息,直到标志位被设置为 true,然后线程退出。
*
* @param args 命令行参数
* @throws InterruptedException 如果主线程的睡眠被中断
*/
public static void main(String[] args) throws InterruptedException {
/**
* 创建一个匿名内部类线程,该线程会不断打印信息,直到接到中断信号。
*/
Thread t = new Thread() {
@Override
public void run() {
while (!flag) {
System.out.println("t1 线程");
try {
/**
* 模拟线程执行间隔,让线程睡眠500毫秒。
* 如果在此期间线程被中断,将抛出 InterruptedException。
*/
Thread.sleep(500);
} catch (InterruptedException e) {
/**
* 如果线程在睡眠期间被中断,抛出 RuntimeException。
* 这里选择将中断异常转换为运行时异常来处理。
*/
throw new RuntimeException(e);
}
}
}
};
/**
* 启动线程 t。
*/
t.start();
System.out.println("main 线程开始");
/**
* 主线程睡眠2秒,为线程 t 的执行提供时间。
* 这里模拟的是主线程在做一些其他操作,然后决定中断线程 t。
*/
Thread.sleep(2000);
/**
* 设置标志位为 true,通知线程 t 应该退出循环。
*/
flag = true;
System.out.println("main 线程结束");
}
}
注意:如果使用Lambda式,那么在定义flag时只能定义为成员变量,不能定义为局部变量,这是因为在Lambda中有个语法规则:变量捕获 ,把当前作用域的变量值在Lambda中复制一份,在变量捕获时有一个限制:【只能捕获final修饰的变量或者变量不能做任何修改】。这里将flag设置为成员变量,就不在变量捕获的作用范围内,属于内部类访问外部类。
- 调用interrupt方法来通知
可以使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位
Thread.currentThread()用来获取到当前线程对象
public class InterruptMain3 {
public static void main(String[] args) throws InterruptedException {
/* 创建一个新线程 */
Thread t = new Thread(() -> {
/* 循环运行直到线程被中断 */
while (!Thread.currentThread().isInterrupted()) {
System.out.println("t1 线程");
try {
/* 线程休眠1秒 */
Thread.sleep(1000);
} catch (InterruptedException e) {
/* 输出异常信息 */
e.printStackTrace();
}
}
});
/* 输出主线程开始信息 */
System.out.println("main 线程开始");
/* 启动新线程 */
t.start();
/* 主线程休眠1秒 */
Thread.sleep(1000);
/* 中断新线程 */
t.interrupt();
/* 输出主线程结束信息 */
System.out.println("main 线程结束");
}
}
在上面中,明明抛出了异常,为什么线程还在执行?
原因在于Thread.sleep()上 ,若线程在休眠时,此时调用interrupt,会终止休眠,将线程强制唤醒,会导致sleep异常,从而在上述结果中抛出异常。
为什么线程还在执行呢?这是因为interrupt做了两件事:
- 若sleep在休眠,会唤醒sleep
- 将线程内部标志位改为true,也就是!Thread.currentThread().isInterrupted()的结果为false
这个跟线程继续执行有什么关系?原因在于sleep被唤醒后,还会将刚设置的标志位重置,设为false,从而导致!Thread.currentThread().isInterrupted()为true。如何避免这个情况?当sleep内部发生异常,我们在捕获到异常后,可以用break/return即可。
示例:
线程等待
线程等待通常是指在一个线程(等待线程)中暂停执行,直到另一个线程(被等待线程)完成其任务或达到某个特定的状态。在线程中, 线程等待是线程同步和通信的重要机制,它们在多线程编程中用于协调不同线程的执行。
方法 | 说明 |
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等millis毫秒 |
public void join(long millis,int nanos) | 同理,但可以更高精度 |
(1)哪个线程调用 线程对象.join()(2),就是这个线程(1)要等线程(2)结束之后,(1)才能结束线程。在(1)等待(2)的这个过程,也叫做阻塞。若(2)线程已经结束,那么也不存在阻塞。
若是串行执行的话,那么(1)就要等(2)结束之后,才能执行,下面例子就是串行执行。
若用不带参数的join(),我们可能在某些情况下会无限期的一直等下去,这显然不是我们所想的。 示例:
这段代码中,我将线程转换为串行执行,当t1线程执行完毕再执行t2线程,但由于我给了无限循环的条件,因此线程t1没有可能结束,所以t2将无限期等待。
/**
* JoinMain 类用于演示线程的 join 方法的使用。
* join 方法用于等待当前线程所管理的线程完成执行。
*/
public class JoinMain {
/**
* 程序入口。
* 创建两个线程 t1 和 t2,t2 通过调用 t1 的 join 方法等待 t1 完成后再执行。
* 主线程等待 t2 完成后结束。
*
* @param args 命令行参数
* @throws InterruptedException 如果当前线程被中断
*/
public static void main(String[] args) throws InterruptedException{
/* 创建线程 t1,该线程无限循环打印信息 */
Thread t1=new Thread(()->{
while (true){
System.out.println("t1 线程正在执行");
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
});
/* 创建线程 t2,该线程首先等待 t1 完成,然后无限循环打印信息 */
Thread t2=new Thread(()->{
try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
while (true){
System.out.println("t2 线程正在执行");
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
});
/* 启动线程 t1 和 t2 */
t1.start();
t2.start();
/* 等待线程 t2 完成 */
t2.join();
System.out.println("main 线程结束");
}
}
当然,我们可以将上述代码无限循环的条件改为有限,即可得到:
二.线程和进程的区别
- 进程包含线程,一个进程里面可以有多个线程,但不能没有进程。
- 进程是系统资源分配的基本单位,线程是系统调度执行的基本单位。
- 同一个进程里的线程之间,共享同一份系统资源(如硬盘、内存、网络带宽等...)尤其是“内存资源”,即代码中定义的变量或对象,在编程中,多个线程是可以使用同一份变量的。
- 多进程在创建/销毁时,开销比较大,而多线程在创建/销毁时,开销要比进程低。
- 线程是当下实现并发编程的主流,通过多线程,可以充分利用好多核CPU。
- 多个线程之间可能存在线程安全问题,一个线程抛出异常,有可能将其它线程带走。
- 多个进程之间一般不会相互影响,一个进行崩溃,不会影响其它进程(即“进程的隔离性”)。
三.总结
本篇主要介绍了什么是线程,以及线程在java中是如何创建的,Thread类的概念以及其中常见的方法如何使用,如何启动线程、中断线程和线程等待,介绍了进程和线程之间的区别。
- 线程:是操作系统调度执行的基本单位,具有轻量级、并发执行、资源共享等特点
- 在java中,创建线程有5种方式: 1、继承Thread类来创建线程;2、通过实现Runnable接口来创建线程;3、通过实现Thread匿名内部类来创建线程;4、实现Runnable匿名内部类来创建线程;5、通过Lambda式来创建线程(推荐使用,简化)。
- 当实例化一个线程对象后,启动线程需要调用start(),实现多线程并发执行。而run()方法只是类中一个普通的方法,用来执行线程体中的内容,并不能启动线程。
- 在中断线程中,有两种方式来中断线程:1、通过共享的标记进行沟通来中断线程;2、通过调用interrupt方法来进行通知。可以使用Thread.interrupted()或者Thread.currentThread().isInterrupted()来代替自定义标记位。其中Thread.currentThread()是用来获取当前的线程对象.
- 线程等待:线程等待是实现线程同步和通信的重要机制,用于协同不同的线程执行。在(1)哪个线程中调用(2)【线程对象.join()】,(1)就要等(2)执行完才能结束或者执行。在串行执行中,那么就是要等到(2)执行完(1)才能执行;在并发执行中,那么就是等(2)执行完之后,(1)才能结束。等待这个线程结束的过程叫做阻塞.
- 进程与线程的区别:
- 进程包含线程,一个进程可以有多个线程。
- 同一个进程中所有的线程共享同一份资源(硬盘、内存资源、网络带宽),最重要的是内存资源,在编程的时候,多个线程之间可以共享同一个变量。
- 创建/销毁进程的开销大,但创建/销毁线程的开销比较低。
- 线程与线程之间可能存放线程安全问题,一个线程如果崩了,可能会连带着所有的线程一起崩。
- 多个进程之间,一个进程崩了,不会影响其它的进程,即“进程的隔离性”。
以上就是本篇所有内容~
若有不足,欢迎指正~