Java面向对象系列[v1.0.0][多线程]

进程和线程

当一个程序运行时,对于操作系统而言它是以进程的形式存在的,如果是用的windows操作系统,打开任务管理器,能够看到进程标签页,在该标签页中能够看到当前正在运行的进程,而每个进程就是一个运行中的程序
当一个程序启动后进入内存运行时,它就成了一个操作系统的进程,对于操作系统而言进程是系统进行资源分配和调度的一个独立单位,而一个进程在运行时,内部可能包含多个顺序执行流,每个顺序执行流就是一个线程

进程特征

  • 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间,在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间
  • 动态性:进程不等于程序,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集和,在进程中有时间的概念,它具有自己的生命周期和各种不同的状态,而这些是程序所不具备的
  • 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响

大部分操作系统都支持多进程并发运行(虽然因为硬件和系统不同并发的策略很不一样),现代的操作系统几乎都支持同时运行多个任务,例如我们经常开着浏览器,开着编译器,同时还开这Word等,这些进程看上去像是在同时工作,但实际上对于一个CPU而言,它在某个时刻只能运行一个进程,CPU不断的在各个进程之间轮换执行,因为它太快了,人感知不到其中的切换,看上去像是在一起执行,假设我们开的程序足够多会发现每个程序都变慢了,也就感知到了它的切换变慢了

并发性concurrency和并行性parallel是两个概念,并行指在同一时刻,有多条指令在多个处理器上同时执行;并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,表面上看似是同事执行的效果

线程特征

多线程扩展了进程的概念,使得同一个进程可以同时并发处理多个任务,线程也被称为轻量级进程,实际上它是进程的执行单元,线程在程序中是独立的、并发的执行流,当进程初始化后,同时它的主线程也被创建了,通常情况下对于应用程序来说,仅要求有一个主线程,进程内可以有多个顺序执行流,这些顺序执行流也就是线程,每个线程是互相独立的。

  • 线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程
  • 线程可以有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源
  • 线程可以独立完成任务,也可以与其他线程共享父进程中的共享变量及部分环境,相互协同来完成进程所要完成的任务
  • 线程是独立运行的,它并不知道进程中的其他线程的存在,其执行方式是抢占式的,也就是说当前运行的线程在任何时候都可能被挂起,以便另外一个线程可以运行
  • 一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行
  • 操作系统无需将多个线程看作多个独立的应用,对多线程实现调度和管理以及资源分配,这些由进程本身负责管理和完成

线程与进程优劣

  • 线程在程序中是独立的、并发的执行流,与进程相比线程之间的隔离程度更小,他们共享内存、文件句柄以及每个进程应有的状态
  • 线程的划分尺度更小,使得多线程程序的并发性更高,进程在执行过程中拥有独立的内存单元,而多个线程是共享内存的,极大的提高了程序的运行效率
  • 线程比进程性能更高,因为同一个进程中的线程有共性,多个线程共享同一个进程的虚拟空间
  • 同一个进程里的线程共享资源,更容易实现线程间相互通信
  • 创建进程必须为该进程分配独立的内存空间,并分配大量的相关资源,但创建线程简单很多,实现多任务并发效率更高
  • Java内置了很多多线程功能的支持,编程更容易,应用更广泛

线程的创建与启动

Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例

继承Thread类创建线程类

  • 定义Thread类的子类,并重写该类的run()方法,该方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体
  • 创建Thread子类的实例,即创建了线程对象
  • 调用线程对象的start()方法来启动该线程
// 通过继承Thread类来创建线程类
public class FirstThread extends Thread
{
	private int i;
	// 重写run方法,run方法的方法体就是线程执行体
	public void run()
	{
		for ( ; i < 100; i++)
		{
			// 当线程类继承Thread类时,直接使用this即可获取当前线程
			// Thread对象的getName()返回当前该线程的名字
			// 因此可以直接调用getName()实例方法返回当前线程的名
			System.out.println(getName() + " " + i);
		}
	}
	public static void main(String[] args)
	{
		for (var i = 0; i < 100; i++)
		{
			// 调用Thread的currentThread类方法获取当前线程对象
			System.out.println(Thread.currentThread().getName() + " " + i);
			if (i == 20)
			{
				// 创建、并启动第一条线程
				new FirstThread().start();
				// 创建、并启动第二条线程
				new FirstThread().start();
			}
		}
	}
}

执行程序的时候,会发现有3个线程,其中两个是代码里创建的,另一个是主线程,程序至少会创建一个主线程,主线程的线程执行体不是由run()方法确定的,而是由main()方法确定的,main()方法的方法体代表主线程的线程执行体
程序可以通过setName(String name)方法为线程设置名字,也可以通过getName()方法返回指定线程的名字,默认情况下主线程名字为main,用户启动的线程名为Thread-0、Thread-1…Thread-N
使用集成Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量

实现Runnable接口创建线程类

  • 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体
  • 创建Runnable实现类的实例,并以此实例作为Thread的target,实际的线程对象依然是Thread实例,该Thread负责执行其target的run()方法
// 创建Runnable实现类的对象
SecondThread st = new SecondThread();
// 以Runnable实现类的对象作为Thread的target来创建Thread对象,即线程对象
new Thread(st);
//也可以在创建Thread对象时为该Thread对象指定一个名字
new Thread(st, "新线程OO");
  • 调用线程对象的start()方法来启动该线程

// 通过实现Runnable接口来创建线程类
public class SecondThread implements Runnable
{
	private int i;
	// run方法同样是线程执行体
	public void run()
	{
		for ( ; i < 100; i++)
		{
			// 当线程类实现Runnable接口时,
			// 如果想获取当前线程,只能用Thread.currentThread()方法。
			System.out.println(Thread.currentThread().getName() + " " + i);
		}
	}

	public static void main(String[] args)
	{
		for (var i = 0; i < 100; i++)
		{
			System.out.println(Thread.currentThread().getName() + " " + i);
			if (i == 20)
			{
				var st = new SecondThread();     // ①
				// 通过new Thread(target, name)方法创建新线程
				new Thread(st, "新线程1").start();
				new Thread(st, "新线程2").start();
			}
		}
	}
}

Runnable接口中只包含一个抽象方法,Java8开始,Runnable接口使用了@FunctionalInterface修饰,也就是说它是个函数式接口,可以使用Lambda表达式创建Runnable对象
执行该代码示例,可以从结果中看出来,两个子线程的i变量是连续的,也就是采用Runnable接口的方式创建的多个线程可以共享线程类的实例变量,这是因为在这种方式下,程序创建的Runnable对象只是线程的target,而多个线程可以共享一个target,所以多个线程可以共享同一个线程类的实例变量

使用Callable和Future创建线程

Java5之后,提供了Callable接口,该接口提供了一个call()方法可以作为线程执行体,但call()方法比run()方法功能更强大

  • call()方法可以有返回值
  • call()方法可以声明抛出异常

Java5之后还提供了Future接口来代表Callable接口里的call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并实现了Runnable接口,可以作为Thread类的target
Future接口里定义了如下几个公共方法来控制它关联的Callable任务:

  • boolean cancel(boolean mayInterruptIfRunning):试图取消该Future里关联的Callable任务

  • V get():返回Callable任务里call()方法的返回值,调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值

  • V get(long timeout, TimeUnit unit):返回Callable任务里call()方法的返回值,该方法让程序最多阻塞timeout和unit指定的时间,如果过了指定时间后Callable任务依然没有返回值,抛出TimeoutException异常

  • boolean isCancelled():如果Callable任务完成前被取消,则返回true

  • boolean isDone():如果Callable任务已经完成,则返回true
    Callable接口有泛型限制,Callable接口里的泛型形参类型与call()方法返回值类型相同并且Callable接口是函数式接口,可以使用Lambda表达式创建Callable对象

  • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值,再创建Callable实现类的实例,Java8开始可以直接使用lambda表达式创建Callable对象

  • 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值

  • 使用FutureTask对象作为Thread对象的target创建并启动新线程

  • 调用FutureTask对象的get()方法来获取子线程执行结束后的返回值

import java.util.concurrent.*;

public class ThirdThread
{
	public static void main(String[] args)
	{
		// 创建Callable对象
		var rt = new ThirdThread();
		// 先使用Lambda表达式创建Callable<Integer>对象
		// 使用FutureTask来包装Callable对象
		FutureTask<Integer> task = new FutureTask<>((Callable<Integer>)() -> {
			var i = 0;
			for ( ; i < 100; i++)
			{
				System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i);
			}
			// call()方法可以有返回值
			return i;
		});
		for (var i = 0; i < 100; i++)
		{
			System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i);
			if (i == 20)
			{
				// 实质还是以Callable对象来创建、并启动线程
				new Thread(task, "有返回值的线程").start();
			}
		}
		try
		{
			// 获取线程返回值
			System.out.println("子线程的返回值:" + task.get());
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
		}
	}
}

程序使用了Lambda表达式直接创建了Callable对象,这样就无需先创建Callable实现类,再创建Callable对象了。

三种方式比较

采用实现Runnable、Callable接口的方式创建多线程的优缺点:

  • 线程只是实现了Runnable接口或Callable接口,还可以继承其他类
  • 在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好的体现了面向对象的思想
  • 编程稍微复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法

采用继承Thread类的方式创建多线程的优缺点:

  • 因为线程类已经继承了Thread类,所以不能继承其他父类
  • 编写简单,如果需要访问当前线程,直接使用this即可获得当前线程
发布了207 篇原创文章 · 获赞 124 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/dawei_yang000000/article/details/105372286