【Java基础教程】(四十二)多线程篇 · 上:多进程与多线程、并发与并行的关系,多线程的实现方式、线程流转状态、常用操作方法解析~

Java基础教程之多线程 · 上

  • 🔹本节学习目标
  • 1️⃣ 线程与进程
      • 🔍关于多进程、多线程、并发与并行之间的概念关系?
  • 2️⃣ 多线程实现
    • 2.1 继承 Thread 类
    • 2.2 实现 Runnable 接口
    • 2.3 多线程两种实现方式的区别
    • 2.4 利用 Callable 接口实现多线程
    • 2.5 线程的状态
  • 3️⃣ 多线程常用操作方法
    • 3.1 线程的命名与取得
    • 3.2 线程的休眠
    • 3.3 线程优先级
    • 3.4 线程等待
    • 3.5 线程让出执行权
    • 3.6 其他线程操作
  • 🌾 总结

在这里插入图片描述

🔹本节学习目标

  • 理解进程与线程的区别;
  • 掌握Java 中多线程的两种实现方式及区别;
  • 掌握线程的基本操作方法;

1️⃣ 线程与进程

进程是程序的一次动态执行过程,它经历了从代码加载、执行到执行完毕的一个完整过程,这个过程也是进程本身从产生、发展到最终消亡的过程。多进程操作系统能同时运行多个进程(程序), 由于CPU具备分时机制,所以每个进程都能循环获得自己的 CPU 时间片。由于 CPU 执行速度非常快,使得所有程序好像是在 “同时” 运行一样。

线程和进程一样,都是实现并发的一个基本单位。线程是比进程更小的执行单位,线程是在进程的基础之上进行的进一步划分。多线程是实现并发机制的一种有效手段。所谓多线程是指一个进程在执行过程中可以产生多个线程,这些线程可以同时存在、同时运行。 一个进程可能包含多个同时执行的线程,如下图所示。

所有的线程一定要依附于进程才能够存在,那么进程一旦消失了,线程也一定会消失。而 Java 是为数不多的支持多线程的开发语言之一。

🔍关于多进程、多线程、并发与并行之间的概念关系?

多进程、多线程、并发与并行是计算机领域中常用的术语,它们描述了不同的程序执行方式和任务处理方式。

多进程(Multiprocessing):多进程指的是在操作系统层面上同时运行多个独立的进程。每个进程都有自己的内存空间和系统资源,并且通过进程间通信(IPC)来实现数据的交换和共享。多进程能够使得多个任务并行执行,每个进程分别处理自己的任务,互不干扰。多进程适合于利用多核或分布式系统来提高整体系统的吞吐量和处理能力。

多线程(Multithreading):多线程指的是在单个进程内部创建多个轻量级的执行单位(线程)。这些线程共享进程的内存空间和系统资源,可以方便快速地进行通信和共享数据。不同线程之间可以并发地执行不同的任务,有效利用了多核处理器的优势。多线程适用于需要同时处理多个任务、要求高响应性和资源共享的场景。

并发(Concurrency):并发指的是多个任务在时间上交替执行。在并发编程中,任务可能按照任意顺序进行切换,通过时间片轮转等技术来平衡任务的执行。并发编程关注的是任务之间对共享资源的正确访问和同步,以避免竞态条件等问题。

并行(Parallelism):并行指的是将一个大任务划分成多个子任务,并且同时执行这些子任务,从而加速整体任务的完成速度。并行编程利用了多核处理器或者分布式系统的能力来实现任务的同时执行,提高了整体计算能力。在并行编程中,需要考虑数据共享与通信的问题以及正确处理并行任务的调度和负载均衡。

简而言之,多进程与多线程都可以实现并发的效果,但多进程是通过同时运行不同的进程,在操作系统层面上进行并行处理;而多线程是在单个进程内部运行多个线程,通过在用户空间内进行并发执行。并发指的是多个任务按照某种顺序快速交替执行,而并行指的是同时执行多个任务以提升整体性能。

2️⃣ 多线程实现

在 Java 中,如果要想实现多线程的程序,就必须依靠一个线程的主体类(就好比主类的概念一样, 表示的是一个线程的主类)。但是这个线程的主体类在定义时也需要有一些特殊的要求,即此类需要继承 Thread 类或实现 Runnable(Callable) 接口来完成定义。

JDK从最早开始定义多线程支持时,只有两种实现要求:要么继承Thread类,要么实现 Runnable 接口,而在JDK 1.5开始又提供了一个新的线程接口:Callable

从实际的开发角度而言,很明显,使用接口定义的线程类会更加合理,因为使用继承 Thread类的方式实现会带来单继承局限。

2.1 继承 Thread 类

java.lang.Thread 是一个负责线程操作的类,任何类只需要继承 Thread 类就可以成为一个线程的主类。但是既然是主类就必须有它的使用方法,而线程启动的 主方法需要覆写 Thread 类中的 run()方法实现,线程主体类的定义格式如下。

class 类名称 extends Thread{		//继承Thread类
	属性… ;	//类中定义属性
	方法… ;	//类中定义方法
	public void run(){	//覆写Thread类中的run0方法,此方法是线程的主体
		...
	}
}
//	范例 1: 定义一个线程操作类
class MyThread extends Thread {	//这就是一个多线程的操作类
	private String name;	//定义类中的属性
	
	public MyThread(String name){	//定义构造方法
		this.name = name;
	}

	@Override
	public void run(){		//覆写run()方法,作为线程的主操作方法
		for (int x=0; x<5; x++){
			System.out.println(this.name + "-->" + x);
		}
	}
}

此程序线程类的功能是进行循环的输出操作,所有的线程与进程是一样的,都必须轮流去抢占资源,所以多线程的执行应该是多个线程彼此交替执行。也就是说,如果直接调用 run()方法,并不能启动多线程,多线程启动的唯一方法就是 Thread 类中的 start()方法:

public void start();

调用此方法时,执行的方法体是 run() 方法定义的代码。

//	范例 2: 启动多线程
public class TestDemo{                                                              //主类
	public static void main(String[] args){
		MyThread mt1 = new MyThread("线程A"); 	// 实例化多线程类对象
		MyThread mt2 = new MyThread("线程B");	// 实例化多线程类对象
		MyThread mt3 = new MyThread("线程C");	// 实例化多线程类对象
		mt1.start();  	//启动多线程
		mt2.start(); 
		mt3.start();
	}
}

程序执行结果:

线程B-->0
线程A-->0
线程A-->1
线程C-->0
线程C-->1
线程A-->2
线程B-->1
线程B-->2
线程B-->3
线程A-->3
线程C-->2
线程A-->4
线程B-->4
线程C-->3
线程C-->4

程序首先实例化了3个线程类对象,然后调用了通过 Thread 类继承而来的 start()方法,进行多线程的启动。通过本程序可以发现所有的线程都是交替运行的。

在范例中使用了 Thread类继承的 start()方法启动多线程,但是最终调用的依然是 run() 方法定义的代码,那么为什么要这么做?为什么不直接调用 run()呢 ?

原因是多线程的操作是需要操作系统支持。为了解释多线程启动调用的问题,下面可以打开 java.lang.Thread 类的 start()源代码来进行观察。

//	范例 3: start()方法的源代码
public synchronized void start(){
	if (threadStatus != 0)
		throw new IllegalThreadStateException();
	group.add(this);
	boolean started = false;
	try {
		start0();
		started = true;
	} finally {
		try {
			if(!started){
				group.threadStartFailed(this);
			}
		} catch (Throwable ignore){
		}
	}
}
private native void start0();

通过程序可以发现在 start() 方法里面要调用一个 start0()方法,而且此方法的结构与抽象方法类似,使用了native声明。在Java的开发里面有一门技术称为Java本地接口 (Java Native Interface, JNI) 技术,这门技术的特点是使用Java调用本机操作系统提供的函数。但是此技术有一个缺点,不能离开特定的操作系统。如果要想能够执行线程,需要操作系统来进行资源分配,所以此操作严格来讲主要是由JVM负责根据不同的操作系统而实现的。即是说使用Thread 类的 start0()方法不仅要启动多线程的执行代码,还要根据不同的操作系统进行资源的分配。

另外需要注意的是,可以发现在 Thread类的 start()方法里面存在一个”IlegalThreadStateException“异常抛出。本方法里面使用了throw抛出异常,照道理讲应该使用 try..catch处理,或者在 start() 方法声明上使用 throws声明,但是此处并未这样处理,

原因是在IlegalThreadStateException异常类继承结构中,可以发现此异常属于 RuntimeException 的子类,这样就可以由用户选择性进行处理。如果某一个线程对象重复进行了启动 ( 同一个线程对象调用多次start()方法),就会抛出此异常。

2.2 实现 Runnable 接口

使用 Thread 类的确可以方便地进行多线程的实现,但是这种方式最大的缺点就是单继承的问题,为此,在 Java 中也可以利用 Runnable接口来实现多线程,而这个接口的定义如下。

@Functionallnterface
public interface Runnable{
	public void run();
}

Runnable 接口中也定义了 run() 方法,所以线程的主类只需要覆写此方法即可。

//	范例 4: 使用 Runnable 实现多线程
class MyThread implements Runnable {               //定义线程主体类
	private String name;                        	//定义类中的属性
	
	public MyThread(String name){                         //定义构造方法
		this.name = name;
	}
	
	@Override
	public void run(){                        	//覆写run()方法
		for (int x=0; x<5; x++){
			System.out.println(this.name + "-->" + x);
		}
	}
}

此程序实现了 Runnable 接口并且正常覆写了 run()方法,但是却会存在一个新的问题:要启动多线程, 一定需要通过 Thread 类中的 start() 方法才可以完成。如果继承了 Thread 类,那么可以直接将 Thread 父类中的 start() 方法继承下来继续使用,而 Runnable 接口并没有提供可以被继承的 start()方法,这时该如何启动多线程呢?

此时可以观察 Thread 类中提供的一个有参构造方法:

public Thread(Runnable target);

此方法可以接收一个 Runnable 接口对象。

//	范例 5: 利用Thread 类启动多线程
public class TestDemo {
	public static void main(String[] args){
		MyThread mt1 = new MyThread("线程A");	// 实例化多线程类对象
		MyThread mt2 = new MyThread("线程B"); 
		MyThread mt3 = new MyThread("线程C"); 
		new Thread(mt1).start();		//利用Thread 启动多线程
		new Thread(mt2).start();
		new Thread(mt3).start();
	}
}

程序执行结果:

线程A-->0
线程B-->0
线程C-->0
线程B-->1
线程A-->1
线程B-->2
线程C-->1
线程B-->3
线程B-->4
线程A-->2
线程C-->2
线程A-->3
线程C-->3
线程C-->4
线程A-->4

此程序首先利用 Thread 类的对象包装了 Runnable 接口对象实例 (new Thread(mt1).start()), 然后利用 Thread 类的 start() 方法就可以实现多线程的启动。

可以发现,在 Runnable 接口声明处使用了“@FunctionalInterface”的 注解,证明Runnable是一个函数式接口,所以对于范例5的操作也可以使用 Lambda 表达式的风格简化编写。

//	范例 6: 使用Lambda表达式实现多线程
public class TestDemo {
public static void main(String[] args){
	String name ="线程对象";
	new Thread(() -> {
		for (int x=0; x<5; x++){
			System.out.println(name + "-->" + x);
	}).start();
}

此程序利用 Lambda表达式直接定义的线程主体实现操作,并且依然依靠 Thread 类的 start() 方法进行启动,这样的做法要比直接使用Runnable 接口的匿名内部类更加方便。

使用 Runnable接口可以有效避免单继承局限问题,所以在实际的开发中,对于多线程的实现首先选择的就是 Runnable 接口。

2.3 多线程两种实现方式的区别

Thread 类 和 Runnable 接口都可以作为同一功能的方式来实现多线程,但从 Java 的实际开发角度来讲,肯定使用 Runnable 接口,因为它可以有效避免单继承的局限。那么除了这些,这两种方式是否还有其他联系呢?

为了解释这两种方式的联系,下面可以观察 Thread 类的定义。

public class Thread extends Object implements Runnable	...	

通过定义可以发现 Thread 类也是 Runnable 接口的子类,这样对于之前利用 Runnable 接口实现的多线程,其类图结构如下图所示。


上图所表现出来的代码设计模式非常类似于代理设计模式,但是它并不是严格意义上的代理设计模式,因为严格来讲代理设计模式中,代理主题能够使用的方法依然是接口中定义的 run ()方法,而此处代理主题调用的是 start() 方法,所以只能说形式上类似于代理设计模式,但本质上还是有差别的。

除了以上联系外,对于 Runnable 接口和 Thread 类还有一个不太好区分的特点:使用 Runnable 接口可以更加方便地表示出数据共享的概念。

//	范例 7: 通过继承 Thread 类实现卖票程序
package com.xiaoshan.demo;

class MyThread extends Thread  {	//线程的主体类 
	private int ticket = 5;		//一共5张票
	
	@Override
	public void run(){		//线程的主方法
		for (int x=0; x<50; x++){	 //循环50次
			if (this.ticket >0){
				System.out.println("卖票, ticket ="+ this.ticket--);
			}
		}
	}
}

public class TestDemo{
	public static void main(String[] args) throws Exception {
		MyThread mtl = new  MyThread(); 	//创建线程对象
		MyThread mt2 = new  MyThread(); 
		MyThread mt3 = new  MyThread(); 
		mtl.start();		//启动线程
		mt2.start();	
		mt3.start();
	}
}

程序执行结果:

卖票, ticket =5
卖票, ticket =4
卖票, ticket =5
卖票, ticket =4
卖票, ticket =3
卖票, ticket =2
卖票, ticket =5
卖票, ticket =1
卖票, ticket =3
卖票, ticket =2
卖票, ticket =1
卖票, ticket =4
卖票, ticket =3
卖票, ticket =2
卖票, ticket =1

此程序定义了3个线程对象,希望3个线程对象同时卖5张车票,而最终的结果是一共买出了15张票,等于每一个线程对象各自卖各自的5张票,这时的内存关系如下图所示。

而下面我们使用第二种实现 Runnable接口的方式来实现此功能:

//	范例 8: 利用Runnable 接口来实现多线程
package com.xiaoshan.demo;

class MyThread implements Runnable { 	//线程的主体类
	private int ticket =5;	//一共5张票
	
	@Override
	public void run(){	//线程的主方法
		for (int x=0; x<50; x++){	//循环50次
			if (this.ticket>0){
				System.out.println("卖票,ticket="+ this.ticket--);
			}
		}
	}
}

public class TestDemo  {
	public static void main(String[] args) throws Exception {
		MyThread mt = new MyThread();                 //创建线程对象
		new Thread(mt).start();          	//启动线程
		new Thread(mt).start();            
		new Thread(mt).start();
	}
}                                

程序执行结果:

卖票,ticket=5
卖票,ticket=3
卖票,ticket=4
卖票,ticket=1
卖票,ticket=2

此程序使用 Runnable 实现了多线程,同时启动了3个线程对象,但是与使用 Thread 操作的卖票范例不同的是,这3个线程对象都占着同一个 Runnable 接口对象的引用,所以实现了数据共享的操作。 程序的内存关系如下图所示。

综上,多线程的继承Thread类、实现Runnable接口 两种实现方式的区别是:

  • 都需要一个线程的主类,而这个类可以实现 Runnable 接口或继承 Thread 类,不管使用何种方式都必须在子类中覆写 run()方法,此方法为线程的主方法;
  • Thread 类是 Runnable 接口的子类,而且使用 Runnable 接口可以避免单继承局限,并且可以更加方便地实现数据共享的概念。

2.4 利用 Callable 接口实现多线程

使用 Runnable 接口实现的多线程可以避免单继承局限,但是 Runnable 接口实现的多线程会存在一个问题: Runnable 接口里面的 run()方法不能返回操作结果。 所以为了解决这样的问题,从 JDK1.5 开始,Java 对于多线程的实现提供了一个新的接口: java.util.concurrent.Callable,此接口定义如下。

@Functionallnterface
public interface Callable<V>{
	public V call() throws Exception;
}

在接口中存在一个 call()方法,而在 call()方法上可以实现线程操作数据的返回,而返回的数据类型由Callable 接口上的泛型类型动态决定。

//	范例 9: 定义一个线程主体类
import java.util.concurrent.Callable;

class MyThread implements Callable<String>{              //多线程主体类
	private int ticket = 5;              //卖票
	
	@Override
	public String call() throws Exception{
		for (int x=0; x<100; x++){
			if (this.ticket >0){            	//还有票可以出售
				System.out.println("卖票,ticket=" + this.ticket--);
			}
		}
		return "票已卖光!";
	}
}

此程序中定义的 call() 方法在操作完成后可以直接返回一个具体的操作数据,本次返回的是一个 String 型数据。

当多线程的主体类定义完成后,要利用 Thread 类启动多线程,但是在 Thread 类中并没有定义任何构造方法可以直接接收 Callable 接口对象实例,并且由于需要接收 call()方法返回值的问题,从 JDK1.5开始, Java 提供了一个 java.util.concurrent.FutureTask<V> 类,此类定义如下。

public class FutureTask<V> extends Object implements RunnableFuture<V> ...

通过定义可以发现此类实现了 RunnableFuture 接口,而 RunnableFuture 接口又同时实现了 FutureRunnable 接口。FutureTask 类继承结构如下图所示。


清楚了 FutureTask 类的继承结构之后,下面再来研究 FutureTask 类的常用方法,方法声明、方法类型及描述如下所示:

  • public FutureTask(Callable<V> callable):构造方法,接收Callable接口实例;
  • public FutureTask(Runnable runnable, V result):构造方法,接收Runnable接口实例,并指定返回结果类型;
  • public V get() throws InterruptedException, ExecutionException:普通方法,取得线程操作结果,此方法为 Future 接口定义。

通过 FutureTask 类继承结构可以发现它是 Runnable 接口的子类,并且 FutureTask 类可以接收 Callable 接口实例,这样依然可以利用Thread 类来实现多线程的启动,而如果要想接收返回结果,利用 Future 接口中的 get() 方法即可。

//	范例 10: 启动多线程
import java.util.concurrent.FutureTask;

public class TestDemo {
	public static void main(String[] args) throws Exception{
		MyThread mt1 = new MyThread();  // 实例化多线程对象
		MyThread mt2 = new MyThread(); // 实例化多线程对象
		FutureTask<String> task1 = new FutureTask<String>(mt1);
		FutureTask<String> task2 = new FutureTask<String>(mt2);
		
		// FutureTask是 Runnable接口子类,所以可以使用Thread类的构造来接收task对象
		new Thread(task1).start();              //启动第一个线程
		new Thread(task2).start();               //启动第二个线程
		//多线程执行完毕后可以取得内容,依靠FutureTask的父接口Future中的get()方法实现 
		System.out.println("A线程的返回结果:"+ task1.get());
		System.out.println("B线程的返回结果:"+ task2.get());
	}
}

程序执行结果:

卖票,ticket=5
卖票,ticket=5
卖票,ticket=4
卖票,ticket=4
卖票,ticket=3
卖票,ticket=3
卖票,ticket=2
卖票,ticket=2
卖票,ticket=1
卖票,ticket=1
A线程的返回结果:票已卖光!
B线程的返回结果:票已卖光!

此程序利用 FutureTask 类实现 Callable 接口的子类包装,由于 FutureTaskRunnable 接口的子类,所以可以利用 Thread 类的 start()方法启动多线程,当线程执行完毕后,可以利用 Future 接口中的 get() 方法返回线程的执行结果。

通过 Callable接口与 Runnable 接口实现的比较,可以发现,Callable接口只是胜在有返回值上。但是Runnable接口是Java最早提供的,也是使用最广泛的接口,所以在进行多线程实现时还是建议优先考虑使用 Runnable 接口。

2.5 线程的状态

要想实现多线程,必须在主线程中创建新的线程对象。任何线程一般都具有5种状态,即创建、就绪、运行、阻塞和销毁。线程转换状态如下图所示。



  1. 创建状态
    在程序中用构造方法创建一个线程对象后,新的线程对象便处于新建状态,此时,它已经有相应的内存空间和其他资源,但还处于不可运行状态。新建一个线程对象可采用 Thread 类的构造方法来实现,例如: Thread thread=new Thread()
  2. 就绪状态
    新建线程对象后,调用该线程的 start()方法就可以启动线程。当线程启动时,线程进入就绪状态。 此时,线程将进入线程队列排队,等待CPU 服务,这表明它已经具备了运行条件。
  3. 运行状态
    当就绪状态的线程被CPU调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的 run()方法。 run() 方法定义了该线程的操作和功能。
  4. 阻塞状态
    一个正在执行的线程在某些特殊情况下,如被人为挂起或需要执行耗时的输入输出操作时,将让出 CPU 并暂时中止自己的执行,进入阻塞状态。在可执行状态下,如果调用 sleep()suspend()wait() 等方法,线程都将进入阻塞状态。阻塞时,线程不能进入排队队列,只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。
  5. 销毁/终止状态
    线程调用 stop() 方法时或 run()方法执行结束后,就处于终止状态。处于终止状态的线程不具有继续运行的能力。

3️⃣ 多线程常用操作方法

Java 除了支持多线程的定义外,也提供了许多多线程操作方法,其中大部分方法都是在 Thread 类中定义的,下面介绍3个主要方法的使用。

3.1 线程的命名与取得

所有线程程序的执行,每一次都是不同的运行结果,因为它会根据自己的情况进行资源抢占,所以要想区分每一个线程,就必须依靠线程的名字。对于线程名字一般而言会在其启动之前进行定义,不建议对已经启动的线程更改名称,或者是为不同的线程设置重名的情况。
如果要想进行线程命名的操作,可以使用 Thread 类的方法,方法声明及描述如下所示:

  • public Thread(Runnable target, String name):构造方法,实例化线程对象,接收 Runnable接口子类对象,同时设置线程名称;
  • public final void setName(String name):普通方法,设置线程名字;
  • public final String getName():普通方法,取得线程名字。

由于多线程的状态不确定,所以线程的名字就成为了唯一的分辨标记,则在定义线程名称时一定要在线程启动前设置名字,而尽量不要重名,且量不要为已经启动的线程修改名字。

由于线程的状态不确定,所以每次可以操作的都是正在执行 run()方法的线程,那么取得当前线程对象的方法为:

public static Thread currentThread();
//	范例 11: 观察线程的命名
package com.xiaoshan.demo;

class MyThread implements Runnable{
	@Override
	public void run(){
		System.out.println(Thread.currentThread().getName());
	}
}

public class TestDemo {
	public static void main(String[] args) throws Exception {
		MyThread mt = new MyThread();
		new Thread(mt,"自己的线程A").start();
		new Thread(mt).start();
		new Thread(mt,"自己的线程B").start();
		new Thread(mt).start();
		new Thread(mt).start();
	}
}

执行结果:

自己的线程A
Thread-2
Thread-1
自己的线程B
Thread-0

通过程序可以发现,当实例化 Thread 类对象时可以自己定义线程名称,也可以采用默认名称进行操作。在 run() 方法中可以使用 currentThread() 取得当前线程对象后再取得具体的线程名字。

//	范例 12: 取得线程名字
package com.xiaoshan.demo;

class MyThread implements Runnable{
	@Override
	public void run(){
		System.out.println(Thread.currentThread().getName());
	}
}

public class TestDemo{
	public static void main(String[] args) throws Exception{
		MyThread mt = new MyThread();
		new Thread(mt,"自己的线程对象").start();
		mt.run();                       //直接调用run()方法,main
	}
}

可能的执行结果:

main
自己的线程对象

此程序首先实例化了 Thread 类对象,然后利用多线程启动 start()间接调用了 run() 方法,同时又在主类中直接利用对象调用了 MyThread 类中的 run() 方法,这样就可以发现主方法本身也属于一个线程。

3.2 线程的休眠

线程的休眠指的是让程序的执行速度变慢一些,在 Thread 类中线程休眠操作方法为:

public static void sleep(long millis) throws InterruptedException

方法设置的休眠时间单位是毫秒 (ms)。

//	范例 13: 观察休眠特点
package com.xiaoshan.demo;

class MyThread implements Runnable{
	@Override
	public void run(){
		for (int x=0; x<5; x++){
			try{
				Thread.sleep(1000);	//每次执行休眠1s
			}catch(InterruptedException e){
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + ", x="+x);
		}
	}
}

public class TestDemo{
	public static void main(String[] args) throws Exception{
		MyThread mt = new MyThread();
		new Thread(mt,"自己的线程对象A").start();
	}
}

可能的执行结果:

自己的线程对象A, x=0
自己的线程对象A, x=1
自己的线程对象A, x=2
自己的线程对象A, x=3
自己的线程对象A, x=4

此程序在每一次线程执行 run() 方法时都会产生1s 左右的延迟后才会进行内容的输出,所以整体代码执行速度有所降低。

3.3 线程优先级

在 Java 的线程操作中,所有的线程在运行前都会保持就绪状态,此时哪个线程的优先级高,哪个线程就有可能会先被执行。线程的优先级如下图所示。

如果要想进行线程优先级的设置,在 Thread 类中提供了支持的方法及常量,如下所示:

  • public static final int MAX_PRIORITY:常量,最高优先级,数值为10;
  • public static final int NORM_PRIORITY:常量,中等优先级,数值为5;
  • public static final int MIN_PRIORITY:常量,最低优先级,数值为1;
  • public final void setPriority(int newPriority):普通方法,设置线程优先级;
  • public final int getPriority():普通方法,取得线程优先级。
//	范例 14: 设置线程优先级
package com.xiaoshan.demo;

class MyThread implements Runnable {
	@Override
	public void run(){
		for (int x=0; x<5; x++){
			try{
				Thread.sleep(100);
			}catch (InterruptedException e){
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+",x="+x);
		}
	}
}

public class TestDemo{
	public static void main(String[] args) throws Exception {
		MyThread mt = new MyThread();
		Thread t1 = new Thread(mt,"自己的线程对象A");
		Thread t2 = new Thread(mt,"自己的线程对象B");
		Thread t3 = new Thread(mt,"自己的线程对象C");
		t3.setPriority(Thread.MAX_PRIORITY);                  //修改一个线程对象的优先级
		t1.start();
		t2.start();
		t3.start();
	}
}

执行结果:

自己的线程对象C,x=0
自己的线程对象A,x=0
自己的线程对象B,x=0
自己的线程对象C,x=1
自己的线程对象A,x=1
自己的线程对象B,x=1
自己的线程对象A,x=2
自己的线程对象B,x=2
自己的线程对象C,x=2
自己的线程对象B,x=3
自己的线程对象A,x=3
自己的线程对象C,x=3
自己的线程对象B,x=4
自己的线程对象C,x=4
自己的线程对象A,x=4

此程序定义了3个线程对象,并在线程对象启动前,利用 setPriority() 方法修改了一个线程的优先级。

3.4 线程等待

join()方法允许一个线程等待另一个线程执行完成。调用join()方法的线程将会阻塞,直到被调用的线程执行完毕。通过使用join()方法,可以保证线程之间的协作和顺序执行。

下面是一个示例代码:

//	范例 15: join()方法的作用
package com.xiaoshan.demo;

class MyRunnable implements Runnable {
	@Override
	public void run() {
    	System.out.println(Thread.currentThread().getName() + " started");
        try {
        	Thread.sleep(2000);
        } catch (InterruptedException e) {
        	System.out.println(Thread.currentThread().getName() + " interrupted");
       	}
        System.out.println(Thread.currentThread().getName() + " completed");
  	}
}
    
public class JoinExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new MyRunnable(), "Thread 1");
        Thread thread2 = new Thread(new MyRunnable(), "Thread 2");
        
        thread1.start();
        try {
            // 在主线程中等待thread1执行完成
            thread1.join();
        } catch (InterruptedException e) {
            System.out.println("Thread 1 interrupted");
        }
        
        thread2.start();
    }
}

执行结果:

Thread 1 started
Thread 1 completed
Thread 2 started
Thread 2 completed

在上述代码中,创建了两个线程:thread1thread2。调用thread1.join()方法使得主线程等待thread1执行完毕再继续执行。这样可以确保thread2thread1执行完成后再开始执行。

通过调用join()方法,我们可以确保线程的顺序执行。在以上示例中,主线程调用了thread1.join()方法等待thread1完成,所以在输出结果中可以看到thread1的执行先于thread2

这说明了join()方法的作用是使得一个线程等待其他线程执行完成,从而实现线程间的协作和顺序执行。在需要确保某个线程执行完毕后再进行下一步操作时,可以使用join()方法来实现。

3.5 线程让出执行权

yield()方法允许当前线程让出CPU的执行时间,给其他具有相同优先级的线程执行的机会。调用yield()方法后,当前线程将进入就绪状态,重新参与调度。注意,yield()方法只是提供一种暗示,不能保证被立即执行。

下面是一个示例代码:

//	范例 16: yield()方法的作用
package com.xiaoshan.demo;

class MyRunnable implements Runnable {
	@Override
	public void run() {
		for (int i = 1; i <= 5; i++) {
			System.out.println(Thread.currentThread().getName() + ": " + i);
			// 当i为3时,让出CPU执行时间
			if(i == 3){
				Thread.yield();
			}
		}
	}
}

public class YieldExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new MyRunnable(), "Thread 1");
        Thread thread2 = new Thread(new MyRunnable(), "Thread 2");
        
        thread1.start();
        thread2.start();
    }
}

输出结果:

Thread 1: 1
Thread 1: 2
Thread 1: 3
Thread 2: 1
Thread 2: 2
Thread 2: 3
Thread 1: 4
Thread 1: 5
Thread 2: 4
Thread 2: 5

在上述代码中,创建了两个线程:thread1thread2。在每个线程的执行过程中,当循环变量i等于3时,调用Thread.yield()方法让出CPU执行时间。这样可以使得两个线程之间更平衡地共享CPU资源。

通过调用yield()方法,我们可以让线程主动让出CPU执行时间。在以上示例中,当thread1thread2的循环变量i等于3时,它们分别调用了Thread.yield()方法,从而使得两个线程交替执行。这样可以更好地利用CPU资源,避免某个线程长时间独占CPU。

需要注意的是,yield()方法只是一种暗示,不能保证被立即执行或者产生特定的效果。具体的调度行为取决于操作系统和JVM的实现。

3.6 其他线程操作

除了以上方法,线程中还支持几个其他的操作:

  • suspend()方法:暂时挂起线程;
  • resume()方法:恢复挂起的线程;
  • stop()方法:停止线程。

但是对于线程中 suspend()resume()stop() 3个方法并不推荐使用,它们也已经被慢慢废除掉了,主要原因是这3个方法在操作时会产生死锁的问题。

打开 Thread类的源代码,从中可以发现 suspend()resume()stop() 方法的声明上都加入了一条 “@Deprecated“的注释,这属于Annotation 的语法,表示此操作不建议使用。所以一旦使用了这些方法将出现警告信息。

既然以上3个方法不推荐使用,那么该如何停止一个线程的执行呢?在多线程的开发中可以通过设置标志位的方式停止一个线程的运行。

//	范例 17: 停止线程运行
package com.xiaoshan.demo;

class MyThread implements Runnable { 
	private boolean flag = true;	//定义标志位属性
	
	public void run(){	//覆写run()方法
		int i=0;
		while(this.flag){	//循环输出
			while(true){
				System.out.println(Thread.currentThread().getName() + "运行,i=" + (i++));     //输出当前线程名称
			}
		}
	}
	
	public void stop(){		//编写停止方法
		this.flag = false;	//修改标志位
	}
}

public class StopDemo {
	public static void main(String[] args){ 
		MyThread my = new MyThread();      	//实例化 Runnable 接口对象
		Thread t = new Thread(my, "线程");	//建立线程对象
		t.start();	//启动线程
		my.stop();	//线程停止,修改标志位
	}
}

此程序一旦调用 stop()方法就会将 MyThread 类中的 flag 变量设置为 false, 这样 run()方法就会停止运行,这种停止方式是开发中比较推荐的。

🌾 总结

本文主要介绍了多线程编程中的关键概念以及常用的操作方法。首先,我们了解了线程与进程之间的关系,明白了多进程、多线程、并发和并行之间的区别。接着,我们探讨了多线程的实现方式,包括继承Thread类、实现 Runnable接口和利用 Callable接口实现多线程,并对它们进行了比较和分析。然后,我们详细讲解了线程的操作状态,帮助读者理解线程在不同状态间的转换情况。最后,我们介绍了多线程常用的操作方法,如线程的命名与获取、线程的休眠、线程优先级、线程等待和线程让出执行权等。

通过本文的学习,读者可以对多线程编程有一个更全面的认识。了解线程与进程的关系,能够选择适合的多线程实现方式。理解线程的操作状态,使得对线程的运行状态具有更好的掌握。熟悉多线程的常用操作方法,可以对线程进行更精确的控制,提高程序的性能和并发处理能力。

总而言之,多线程编程是一项重要且常用的技术。了解多线程的相关概念和操作方法,能够使程序员在开发过程中更好地利用多核处理器和并发处理来提高系统的性能和响应能力。同时,也需要注意合理使用多线程,避免一些常见的线程安全问题和性能瓶颈。

温习回顾上一篇(点击跳转)
《【Java基础教程】(四十一)常用类库篇 · 第十一讲:国际化支持类——解析 Locale 类与 ResourceBundle 类对国际化编程的支持~》

继续阅读下一篇(点击跳转)
《【Java基础教程】(四十三)多线程篇 · 下:深入剖析Java多线程编程:同步、死锁及经典案例——生产者与消费者,探究sleep()与wait()的差异》

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
xiaoxingxing的头像xiaoxingxing管理团队
上一篇 2023年8月9日
下一篇 2023年8月9日

相关推荐