前言

大家好,上一篇我们梳理了进程的底层原理,本篇继续深入讲解进程的轻量级执行单元——线程,把进程与线程的核心区别、线程基础概念、Java线程入门一次性讲透,为后续多线程内容打下基础。

线程的引入

为什么要引入线程?

引入多个线程(轻量级进程)是为了实现并发编程。

早期并发编程依赖进程实现,但进程的开销极大:
1.创建/销毁/调度进程,都需要申请和回收系统资源(进程是资源分配的基本单位),时间成本很高。
2.进程切换时,需要完整保存/恢复进程上下文,效率低。

线程的定义

线程是CPU调度和执行的最小单位,是进程内的独立执行流。

1.线程不能独立存在,必须依附于进程。
2.每个进程至少有一个线程(主线程),也可以创建多个线程。
3.线程创建、销毁、调度的开销远小于进程,效率更高。

线程的资源分配特点

对于同一进程中的每个线程:

  1. 共享资源:共享进程的PID、内存指针、文件描述符表等系统资源。
  2. 独立资源:每个线程有独立的运行栈、状态、优先级、上下文、记账信息…,供操作系统调度使用。

线程特点

1.每个线程都可以独立地去cpu上调度执行。
2.同一进程的多个线程之间,共用同一份内存空间和文件资源。

创建线程时不需要重新申请资源,直接复用之前已经分配给进程的资源,省去了资源分配的开销,创建/销毁效率更高。

线程的优势

  1. 调度更轻量,切换开销远小于进程。
  2. 同一进程内的线程共享资源,创建时无需重复申请资源,效率更高。
  3. 支持多线程并发执行,充分利用多核CPU性能。
  4. 同一进程内的线程可以直接通信。

进程与线程的区别

核心区别

对比维度 进程 线程
资源分配 进程是资源分配的基本单位,拥有独立的内存、文件等系统资源 线程不拥有独立资源,共享所属进程的全部资源
调度执行 进程是资源容器,真正被调度执行的是进程内的线程 线程是CPU 调度和执行的基本单位,由操作系统直接调度
开销 创建、销毁、切换开销大 创建、销毁、切换开销极小,效率更高
独立性 进程间完全独立,一个进程崩溃不会影响其他进程 同一进程内的线程相互影响,一个线程异常可能导致整个进程崩溃

进程与线程的关系

这里用一个通俗比喻帮助理解:进程 = 公司,线程 = 公司里的员工。公司拥有独立的场地、设备、资金等资源,彼此相互独立;员工隶属于同一家公司,共享公司所有资源。

  1. 进程包含线程,一个进程里面可以有一个线程,也可以有多个线程;但一个线程只能属于一个进程。
  2. 进程和线程,都是用来实现并发编程场景的,但线程比进程更轻量,更高效。

Java中线程基础

Java线程的本质

运行一个Java程序,本质就是启动一个Java进程,这个进程里至少会有一个线程—主线程。主线程是系统默认创建的,其入口方法为main方法。

  1. 主线程负责创建其他子线程,也必须最后执行完毕,负责程序的关闭动作。
  2. Java中的Thread类,是操作系统线程API的封装,new Thread() 会创建Java线程对象,调用start() 方法才会真正创建内核线程。

start() 与 run() 的区别

  1. run() : 只是线程的执行逻辑,表明该线程要执行的任务是什么 ,直接调用就只是普通方法执行,不会创建新线程。
  2. start() : 会调用操作系统的native API,真正创建内核线程,实现并发执行,再间接调用run() 方法。

代码示例

package thread;

//创建一个类。继承自Thread,重写run
class MyThread1 extends Thread{

	@Override
	public void run() {
		// 这个方法就是线程的入口方法
		while(true) {
			System.out.println("hello thread");
			try {
				//sleep是Thread的静态方法(类方法)
				//在Java中,static作用和“静态”没有任何关系,它只是用来修饰类属性的
				//与类属性相对的是实例属性
				//sleep方法会抛出一个受查异常,必须显示处理
				//此处必须try-catch,不能throws,因为这是重写的父类的run方法,父类的run、没有throws,子类方法也就不能有throws
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO 自动生成的 catch 块
				e.printStackTrace();
			}
		}
	}
	
}
//每个线程,都是一个独立执行的逻辑(独立的执行流)
//操作系统对于多个线程的调度顺序是不确定的,“随机”的(不是概率均等的随机),而是取决于操作系统对于线程调度的模块(调度器)的具体实现
public class Demo{
	//main这个线程是JVM自动创建的,和其他线程相比,没啥特殊的
	//一个Java进程中,至少会有一个main线程
	public static void main(String[] args) throws InterruptedException {
		// TODO 自动生成的方法存根
		Thread thread=new MyThread1();
		//start和run都是Thread的成员
		//run只是描述了线程的入口(线程要做什么任务)
		//start则是真正调用了系统API,在系统中创建出线程,让线程再调用run
		thread.start();
		//thread.run();---若调用run,而不是start,此时代码不会创建出新线程,只有一个主线程。run()方法一直循环。
		while(true) {
			System.out.println("hello main");
			Thread.sleep(1000);
		}
		//此时的并发执行是并行+并发
		//并行:多核+真正同时执行,也就是多个任务在同一时刻同时执行,必须多核CPU才能实现
		//并发:单核+交替执行,也就是多个任务在同一时间段内交替执行,单核CPU也能实现
	}

}

调用start()的运行结果:
在这里插入图片描述

调用run()的运行结果:
在这里插入图片描述

线程的创建

继承Thread类,重写run方法

//创建一个类。继承自Thread,重写run
class MyThread1 extends Thread{

	@Override
	public void run() {
		while(true) {
			System.out.println("hello thread");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO 自动生成的 catch 块
				e.printStackTrace();
			}
		}
	}
	
}

public class Demo{
	public static void main(String[] args) throws InterruptedException {
		// TODO 自动生成的方法存根
		Thread thread=new MyThread1();
		thread.start();
		while(true) {
			System.out.println("hello main");
			Thread.sleep(1000);
		}
	}

}

实现Runnable接口,重写run方法

package thread;
//程序的入口:main
//线程的入口:run

//创建一个类,实现Runnable接口,重写run
class MyRunnable implements Runnable{

	@Override
	public void run() {
		while(true) {
			System.out.println("hello thread");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	
}

public class Demo {
	
	public static void main(String[] args) throws InterruptedException {
		// TODO 自动生成的方法存根
		//Java就愿意写向上转型,能向上转型就向上转型
		Runnable runnable=new MyRunnable();
		//Runnable表示的是一个“可以运行的任务”,这个任务可以交给线程来负责执行,也可以交给其他的实体来执行
		Thread thread=new Thread(runnable);
		thread.start();//通过这个操作,调用系统API来完成创建线程的工作
		
		while(true) {
			System.out.println("hello main");
			Thread.sleep(1000);
		}
	}

}


/*
 * 封装本质上是让调用者不用了解类实现的细节,降低了学习和使用的成本
 * 多态则是在封装的基础上更进一步,不需要知道当前是啥类,更不必说类的实现细节了
 * */

使用直接继承Thread类和实现Runnable接口的写法之间的区别:解耦合。

把执行的任务本身提取出来,就是解耦合,此时就可以随时把代码改成使用其他方式来执行这个任务。

继承Thread类,使用匿名内部类

package thread;

public class Demo {

	public static void main(String[] args) {
		// TODO 自动生成的方法存根
		//匿名内部类的写法
		//先创建一个新的类,类名不知道,只知道这个类是Thread的子类,同时又把这个子类的实例给创建出来了
		//new Thread () { ...} → 自动继承 Thread
		//这个类是一次性的
		Thread thread=new Thread() {

			@Override
			public void run() {
				while(true) {
					System.out.println("hello thread");
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
			
		};
		thread.start();

		while(true) {
			System.out.println("hello main");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO 自动生成的 catch 块
				e.printStackTrace();
			}
		}
	}

}

实现Runnable接口,使用匿名内部类

package thread;

public class Demo {

	public static void main(String[] args) {
		// TODO 自动生成的方法存根
		/*Runnable runnable=new Runnable() {
			
			@Override
			public void run() {
				// TODO 自动生成的方法存根
				while(true) {
					System.out.println("hello thread");
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		};
		Thread thread=new Thread(runnable);
		thread.start();*/
		Thread thread=new Thread(new Runnable() {
			
			@Override
			public void run() {
				// TODO 自动生成的方法存根
				while(true) {
					System.out.println("hello thread");
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		});
		thread.start();
		while(true) {
			System.out.println("hello main");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO 自动生成的 catch 块
				e.printStackTrace();
			}
		}
	}

}

基于lambda表达式

package thread;

public class Demo4 {

	public static void main(String[] args) {
		// TODO 自动生成的方法存根
		//lambda表达式,本质上是一个匿名函数,主要用来实现“回调函数”的作用
		//匿名函数:没有名字的函数,用一次就完了
		Thread thread=new Thread(()->{
			while(true) {
				System.out.println("hello thread");
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		});
		thread.setDaemon(true);//设置为后台线程
		thread.start();
		while(true) {
			System.out.println("hello main");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

线程的优缺点

优点

  1. 可以将耗时任务放到后台执行,提升程序响应速度。
  2. 充分利用 CPU 空闲时间,提高 CPU 利用率。
  3. 充分发挥多核处理器的性能,实现并行执行。
  4. 同一进程内的线程通信简单,无需复杂的进程间通信机制。

缺点

  1. 线程过多会导致频繁上下文切换,消耗 CPU 性能。
  2. 线程需要独立的栈空间,过多线程会增加内存开销。
  3. 多线程并发容易引发线程安全问题、死锁问题,调试难度更高。
  4. 一个线程异常可能影响整个进程的运行稳定性。

全文总结

  1. 进程是资源分配的基本单位,线程是 CPU 调度和执行的基本单位。
  2. 线程依附于进程存在,共享进程资源,开销远小于进程。
  3. Java 线程通过Thread类实现,start()才是开启线程的正确方式,start () 创建线程,run () 定义任务。

🔗 系列文章导航

本篇是「Java并发编程系列」的连载内容,点击链接查看完整系列:

🔹 上一篇:Java并发编程(一)|进程:并发编程的“幕后大管家”
🔹 下一篇:Java并发编程(三)|线程API:如何让你的线程“听话执行”

👉专栏合集:Java并发编程专栏合集

更多推荐