Java之路:多线程

一、进程与线程

进程:进程就是程序执行中的程序。它是一个动态概念。

当我们下载一个QQ程序(腾讯公司出品的一款即时聊天软件)时,程序是静态不变的,而当我们开启3个QQ窗口聊天时,实际上就是开启了三个QQ进程。

由此可见,一个程序是可以对应多个进程的。每一个进程都有自己独立的一组系统资源(包括处理机、内存等)。在进程的概念中,每一个进程的内部数据和状态都是完全独立的

进程是操作系统的资源分配单位,创建并执行一个进程的系统开销是比较大的。相比而言,线程是进程的一个执行路径

多线程(Multithread)指的是在单个进程中同时运行多个不同的线程,执行不同的任务。多线程意味着一个程序的多行语句块并发执行。

同属一个进程的多个线程是共享一块内存空间和一组系统资源,属于线程本身的数据通常只有的寄存器和堆栈内的数据。 所以,当进程生产一个线程,或者在各个线程之间切换时,负担要比进程小得多,正因如此,线程也被称为轻负荷进程(light-weight process)

如下图所示:

在这里插入图片描述

进程的调度是带资源调度的,而线程的调度是不带资源的。

就如同参加接力赛跑一样,对于进程来说,它们就是背着书包(资源)跑,故此运动员(进程)在交接资源时,比较慢。而对于线程来说,它就好比就是轻装上阵,线程间的切换是便捷的。

在一般情况下,程序的某些部分同特定的事件或资源联系在一起,同时又不想为它而暂停程序其他部分的执行,在这种情况下,就可以考虑创建一个线程,令它与那个事件或资源关联到一起,并让它独立于主程序运行。通过使用线程,可以避免用户在运行程序和得到结果之间的停顿,还可以让一些任务(如打印任务)在后台运行,而用户则在前台继续完成一些其他的工作。再例如,在Android程序开发中,多线程成了必不可少的一项重要技术。利用多线程技术,可以使编程人员方便地开发出能同时处理多个任务的功能强大的应用程序。

需要注意的是,多线程“同时”执行是给人的一种感觉,而实际上在线程之间轮换执行,只不过多个线程之间切换的延迟足够短,给人的感觉好像是在同时执行一样。

所谓的线程(Thread)是指程序的运行流程,“多线程”的机制则是指可以同时执行运行多个程序块,可克服传统程序语言所无法解决的问题。

例如,有些循环可能运行的时间比较长,此时便可让一个线程来做这个循环,另一个线程做其他事情,比如与用户交互。

下面来看一个单一线程的运行流程:

package com.xy.thread;

class TestThread extends Thread {
	public void run() {
		for(int i = 0; i < 5; i++) {
			System.out.println("TestThread在运行!");
		}
	}
}
public class ThreadDemo1 {
	public static void main(String[] args) {
		new TestThread().run();
		// 循环输出
		for(int i = 0; i < 5; i++) {
			System.out.println("main线程在运行!");
		}
	}
}

【结果】
在这里插入图片描述

二、通过继承Thread类实现多线程

Thread类存放于java.lang类库里。

java.lang包中提供常用的类、接口、一般异常、系统等编程语言的核心内容,如基本数据类型、基本数学函数、字符串处理、线程、异常处理类等,系统默认加载这个包,所以我们可以直接使用Thread类。

在Thread类中定义了run()方法,要想实现多线程,必须覆写Thread类的run方法。然后使用该类的对象调用start()方法,从而来激活一个新的线程。 也就是说要使一个类可激活线程,必须按照下面的语法来编写:

class 类名称 extends Thread  //从Thread类扩展出子类
{
	// 属性...
	// 方法...
	修饰符 run() // 覆写Thread类里的run()方法
	{
		// 程序代码;// 激活的线程将从run方法开始执行
	}
}

下面看个例子:

package com.xy.thread;

class TestThread extends Thread {
	public void run() {
		for(int i = 0; i < 5; i++) {
			System.out.println("TestThread在运行!");
			try {
				Thread.sleep(1000); // 睡眠1000毫秒
			}
			catch(InterruptedException e) {
			// Thread.sleep()和Object.wait()都可以抛出此类 异常
				e.printStackTrace();
			}
		}
	}
}
public class ThreadDemo1 {
	public static void main(String[] args) {
		new TestThread().start(); // 激活一个线程
		// 循环输出
		for(int i = 0; i < 5; i++) {
			System.out.println("main线程在运行!");
			try {
				Thread.sleep(1000); // 睡眠1000毫秒
			}
			catch(InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

【结果】
在这里插入图片描述

三、通过Runnable接口实现多线程

在Java中不允许多继承,即一个子类只能有一个父类,因此如果一个类已经继承了其他类,那么这个类就不能再用继承Thread类。此时,如果一个其他类的子类又想采用多线程技术,那么这时就要用到Runnable接口,来创建线程。接口可以实现多继承。
通过实现Runnable接口实现多线程的语法如下:

class 类名称 implements Runnable {   //实现Runnable接口
	// 属性...
	// 方法...
	public void run() {   //实现Runnable接口里的run方法
                //激活的线程将从run方法开始运行
	// 程序代码...
	}
}

需要注意的是,激活一个新线程同样使用Thread类的start方法。

package com.xy.thread;


class ThreadDemo2 implements Runnable {
	public void run() {
		for(int i = 0; i < 5; i++) {
			System.out.println("TestThread在运行!");
			try {
				Thread.sleep(1000); // 睡眠1000毫秒
			}
			catch(InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}
public class RunnableThread {
	public static void main(String[] args) {
		ThreadDemo2 t = new ThreadDemo2();
		new Thread(t).start(); // 激活一个线程
		// 循环输出
		for(int i = 0; i < 5; i++) {
			System.out.println("main线程在运行!");
			try {
				Thread.sleep(1000); // 睡眠1000毫秒
			}
			catch(InterruptedException e) {	
				// Thread.sleep()和Object.wait()都可以抛出此类 异常
				e.printStackTrace();
			}
		}
	}
}

【结果】
在这里插入图片描述

查找JDK文档就可以发现,在Runnable接口中只有一个run方法,如下表所示:
在这里插入图片描述

在Runnable接口并没有start方法,所以一个类即使实现了Runnable接口,也需用Thread类中的start方法来启动多线程。

对这一点,通过查找JDK文档中的Thread类可以看到,在Thread类之中有这样一个构造方法。

public Thread( Runnable target );

由此构造方法可以看到,可以将一个Runnable接口(或其子类)的实例化对象作为参数去实例化Thread类对象。在实际的开发中,建议尽可能的使用Runnable接口去实现多线程机制。

四、两种多线程实现机制的比较

在这里插入图片描述

通过查询Thread API文档可得,Thread类实现Runnable接口。即在本质上,Thread类是Runnable接口的一个子类。
接口是功能的集合,也就是说,只要实现了Runnable接口,就具备了可执行的功能,其中run()方法的实现就是可执行的表现。

下面是一个模拟铁路售票系统的范例,实现4个售票点发售某日某次列车的车票20张,一个售票点用一个线程来表示。下面,首先用继承Thread类来实现上述功能:

package com.xy.thread;
class ThreadDemo3 extends Thread {
	private int tickets = 5;
	public void run() {
		while(tickets > 0) {
			System.out.println(Thread.currentThread().getName() + 
					"出售票" + tickets);
			tickets--;
		}
	}
}
public class ThreadDemo1 {
	public static void main(String[] args) {
		ThreadDemo3 t = new ThreadDemo3();
		// 一个线程对象只能启动一次
		t.start();
		t.start();
		t.start();
		t.start();
	}
}

【结果】
在这里插入图片描述
从运行结果可以看到,程序运行时出现了异常,之后却只有一个线程在运行。这说明了一个类继承了Thread类之后,这个类的实例化对象无论调用多少次start方法,结果都只有一个线程在运行。一个线程对象只能启动一次。

下面修改以上程序,记main方法中产生4个线程:

package com.xy.thread;
class ThreadDemo3 extends Thread {
	private int tickets = 5;
	public void run() {
		while(tickets > 0) {
			System.out.println(Thread.currentThread().getName() + 
					"出售票" + tickets);
			tickets--;
		}
	}
}
public class ThreadDemo1 {
	public static void main(String[] args) {
		new ThreadDemo3().start();
		new ThreadDemo3().start();
		new ThreadDemo3().start();
		new ThreadDemo3().start();
	}
}

【结果】
在这里插入图片描述
以上程序,分别使用new TestThread().start()并发了4个线程对象。从这部分输出结果中可以看出,这4个线程对象各自占有自己的资源,例如,这4个线程的每个线程都有自己的数据tickets,我们的本意是一共有5张票,每个线程模拟一个售票窗口,一起把这5张票卖完,但从运行的结果可以看出,每个线程都卖了5张票,这样就卖出了4×5=20张票,这不是我们所需要的。因此,用Thread实际上很难达到资源共享的目的,但是可以通过静态变量达到资源共享,例如,可将tickets设置为static类型的:

package com.xy.thread;
class ThreadDemo3 extends Thread {
	private static int tickets = 5;	// 将票设置为静态的
	public void run() {
		while(tickets > 0) {
			System.out.println(Thread.currentThread().getName() + 
					"出售票" + tickets);
			tickets--;
		}
	}
}
public class ThreadDemo1 {
	public static void main(String[] args) {
		new ThreadDemo3().start();
		new ThreadDemo3().start();
		new ThreadDemo3().start();
		new ThreadDemo3().start();
	}
}

【结果】
在这里插入图片描述

下面是实现Runnable接口来完成卖票:

package com.xy.thread;
class ThreadDemo4 implements Runnable {
	private int tickets = 5;
	public void run() {
		while(tickets > 0) {
			System.out.println(Thread.currentThread().getName() + 
					"出售票" + tickets);
			tickets--;
		}
	}
}
public class RunnableDemo2 {
	public static void main(String[] args) {
		ThreadDemo4 t = new ThreadDemo4(); 
		new Thread(t).start();
		new Thread(t).start();
		new Thread(t).start();
		new Thread(t).start();
	}
}

【结果】
在这里插入图片描述

从程序的输出结果来看,尽管启动了4个线程对象,但结果都是操纵同一个资源(即tickets=5),这4个线程一起协同把这5张票卖完了,实现了资源共享的目的。

可见,实现Runnable接口相对于继承Thread类来说,有如下几个显著的优势:
(1)避免由于Java的单继承特性带来的局限。
(2)可以使多个线程共享相同的资源,以达到资源共享的目的。

如果多运行几次本程序,就会发现,程序的运行结果不唯一,事实上,就是产生了与时间有关的错误。这是“共享”付出的代价,比如在上面的运行结果中,第5张票就被线程0、线程2和线程3卖了3次,出现“一票多卖”的现象,这是当tickets=1时,线程0、线程2和线程3都同时看见了,满足条件tickets > 0,当第一个线程就把票卖出去了,tickets理应减1(参加第16行),当它还没有来得及更新,当前的线程的运行时间片就到了,必须推出CPU,让其他线程执行,而其他线程看到的tickets依然是旧状态(tickets=1),所以,依次也把那张已经卖出去的票再次“卖”出去了。事实上,在多线程运行环境中,tickets属于典型的临界资源(Critical resource),而run()方法就属于临界区(Critical Section)。

多个进程中涉及到同一个临界资源的临界区称为相关临界区。

五、线程的状态

每个Java程序都有一个默认的主线程,对于Java应用程序,主线程是main方法执行的线程;对于Applet程序,主线程是指挥浏览器加载并执行Java Applet程序的线程。要想实现多线程,必须在主线程中创建新的线程对象。

线程具有5种状态,即创建、就绪、运行、阻塞、终止

线程状态的转移与转移原因之间的关系如下图所示:
在这里插入图片描述
线程状态可知:
在这里插入图片描述

下面这个程序演示线程的生命周期:;

package com.xy.thread;

import java.util.Scanner;

public class ThreadStatus implements Runnable {
	public void run() {
		System.out.println("处于运行状态!");
		Scanner in = new Scanner(System.in);
		System.out.println("等待I/O,处于阻塞状态!");
		System.out.println("请输入字符串:");
		in.next();	// next()方法扫描in输入的字符串
		in.close();	// 扫描器结束,系统不再等待I/O,线程重新进入就绪状态
		System.out.println("结束阻塞状态,重新进入就绪状态,然后进入运行状态!");
		try {
			Thread.sleep(1000);
		}
		catch(InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("线程进入死亡状态!");
	}
	public static void main(String[] args) {
		Thread thread = new Thread(new ThreadStatus());
		System.out.println("处于创建状态!");
		thread.start();
		System.out.println("处于就绪状态!");
	}
}

【结果】
在这里插入图片描述

六、操作线程的一些方法

在这里插入图片描述

在Thread类中,可以通过getName方法取得线程的名称,通过setName方法设置线程的名称。线程的名称一般在启动线程前设置,但也允许为已经运行的线程设置名称。允许两个Thread对象有相同的名称,但为了清晰,应尽量避免这种情况的发生。如果程序并没有为线程制定名称,系统会自动为线程分配名称,此外,Thread类中currentThread()也是个常用的方法,它是个静态方法,该方法的返回值是执行该方法的线程实例。

(1)获得线程的名字

package com.xy.thread;

public class GetThreadName extends Thread {
	public void run() {
		for(int i = 0; i < 3; i++) {
			print();
			try {
				Thread.sleep(1000); // 睡眠1000毫秒
			}
			catch(InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	public void print() {
		// 获得运行此代码的线程的引用
		Thread t = Thread.currentThread();
		String name = t.getName();
		System.out.println("name = " + name);
	}
	public static void main(String[] args) {
		GetThreadName gtn = new GetThreadName();
		gtn.start();
		for(int i = 0; i < 3; i++) {
			gtn.print();
			try {
				Thread.sleep(1000); // 睡眠1000毫秒
			}
			catch(InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

【结果】
在这里插入图片描述
因为main()方法也是一个线程,实际上在命令行中运行java命令时,就启动了一个JVM的进程,默认情况下,此进程会产生多个线程,如main方法线程,垃圾回收线程等。所以产生的结果中会有main。

(2)设置与获取线程名

package com.xy.thread;

public class GetSetThreadName implements Runnable {
	public void run() {
		Thread thread = Thread.currentThread();
		System.out.println("执行这条语句的线程名字:" + thread.getName());
	}
	public static void main(String[] args) {
		Thread t = new Thread(new GetSetThreadName());
		t.setName("线程范例");
		t.start();
	}
}

【结果】
在这里插入图片描述

(3)判断线程是否启动

package com.xy.thread;

public class StartThreadDemo extends Thread {
	public void run() {
		for(int i = 0; i < 5; i++) {
			print();
		}
	}
	public void print() {
		// 获得运行此代码的线程的引用
		Thread t = Thread.currentThread();
		String name = t.getName();
		System.out.println("name = " + name);
	}
	public static void main(String[] args) {
		StartThreadDemo st = new StartThreadDemo();
		st.setName("StartThreadDemo");
		System.out.println("调用start()方法之前,st.isAlive() = " + st.isAlive());
		st.start();
		System.out.println("调用start()方法之时,st.isAlive() = " + st.isAlive());
		for(int i = 0; i < 5; i++) {
			st.print();
		}
		// 以下输出不确定,有时true有时false
		System.out.println("main()方法结束时,st.isAlive() = " + st.isAlive());
	}
}

【结果】
在这里插入图片描述

(4)守护线程与setDaemon方法

JVM(Java虚拟机)中线程分为两种,用户线程和守护线程用户线程也称之为前台线程(一般线程)。对Java程序来说,只要还有一个用户线程在运行,这个进程就不会结束。守护线程(daemon)也称之为后台线程。顾名思义,守护线程就是守护其他线程的线程,它是指用户程序在运行时后台提供的一种通用服务的线程。

例如,对于JVM来说,其中垃圾回收的线程就是一个守护线程。这类线程并不是用户线程不可或缺的部分,只是用于提供服务的“服务线程”。当线程中只剩下守护线程时JVM就会退出,反之,如果还有任何用户线程在,JVM都不会退出。
查看Thread源码可以知道这么一句话:

private boolean daemon = false;

这就意味着,默认创建的线程,都属于普通的用户线程。只有调用setDaemon(true) 之后,才能转成守护线程。

package com.xy.thread;

class ThreadDemo5 implements Runnable {
	public void run() {
		for(int i = 0; true; ++i) {
			System.out.println(i + " " + 
					Thread.currentThread().getName() + "is running!");
		}
	}
}
public class ThreadDaemon {
	public static void main(String[] args) {
		ThreadDemo5 t = new ThreadDemo5();
		Thread tt = new Thread(t);	
		tt.setDaemon(true); // 设置守护线程,一定要在start()之前
		tt.start();
		try {
			Thread.sleep(1000); // 睡眠1000毫秒
		}
		catch(InterruptedException e) {
			e.printStackTrace();
		}
	}
}

【结果】
在这里插入图片描述
创建了一个无限循环的线程(将for循环退出的条件设置为true,即永远都满足for循环条件),但因为它是守护线程,因此整个进程在主线程main结束时就随之终止运行了。 这验证了进程中在只有守护线程运行时,进程就会结束的说法。

需要注意的是,设置为某个线程为守护线程时,一定要在start()方法调用之前设置,也就是说一个线程启动之前设置其属性。

(5)线程的联合

一个线程A在占有CUP资源期间,可以让其他线程调用join()和本线程联合。一旦线程A在占有CUP资源期间联合B线程,那么A线程将立刻挂起(suspend),直到它所联合的线程B执行完毕,A线程再重新排队等待CUP资源,以便恢复执行。

例如,B.join()方法阻塞调用此方法的A线程(calling thread),直到线程B完成,此线程再继续。通常用于在main()主线程内,等待其他线程完成再结束main()主线程。

package com.xy.thread;

class ThreadDemo6 implements Runnable {
	public void run() {
		int x = 0;
		for(int i = 0; i < 5; ++i) {
			try {
				Thread.sleep(1000); // 睡眠1000毫秒
			}
			catch(InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + "--->>" + i);
			x++;
		}
	}
}
public class ThreadJoin {
	public static void main(String[] args) {
		ThreadDemo6 t = new ThreadDemo6();
		Thread tt = new Thread(t);
		tt.start();
		int x = 0;
		for(int i = 0; i < 5; i++) {
			if(x == 3) {
				try {
					tt.join(); 	// 强制运行完tt线程后,再运行后面的程序
				}
				catch(Exception e) {
					e.printStackTrace();
				}
			}
			System.out.println("main Thread" + x);
			x++;
		}
	}
}

【结果】
在这里插入图片描述
main程序中,当x==3时,main线程被挂起,直到tt线程运行完毕。

由此可见,tt线程和main线程由两个交替执行的线程合并为顺序执行的线程,就好象tt和main是一个线程,也就是说tt线程中的代码不执行完,main线程中的代码就只能一直等待。

查看JDK文档可以发现,除了无参数的join方法外,还有两个带参数的join方法,分别是join( long millis ) 和join( long millis, int nanos ),它们的作用是指定最长等待时间,前者精确到毫秒,后者精确到纳秒,意思是如果超过了指定时间,合并的线程还没有结束,就直接分开。

(6)线程的中断
在Java多线程编程中,经常会遇到需要中止线程的情况。例如,启动多个线程在数据库中搜索,如果有一个线程返回了需要的搜索结果,则其他线程就可以取消了。

在实施中断线程过程中,有三个函数比较常用的成员方法:

(1)Thread.interrupt():来设置中断状态为true,当一个线程运行时,另一个线程可以调用另外一个线程对应的interrupt()方法来中断它。
(2)Thread.isInterrupted():来获取线程的中断状态。
(3)Thread.interrupted():这是一个静态方法,用来获取中断状态(),并清除中断状态,其获取的是清除之前的值,也就是说连续两次调用此方法,第二次一定会返回false。

package com.xy.thread;

public class SleepInterrupt implements Runnable {
	public void run() {
		try {
			System.out.println("在run()方法中-这个线程休眠10秒!");
			Thread.sleep(10000);
			System.out.println("在run()方法中-继续运行!");
		}
		catch(InterruptedException e) {
			System.out.println("在run()方法中-线程中断!");
			return;
		}
		System.out.println("在run()方法中-休眠之后继续完成!");
		System.out.println("在run()方法中-正常退出!");
	}
	public static void main(String[] args) {
		SleepInterrupt si = new SleepInterrupt();
		Thread t = new Thread(si);
		t.start();
		try {
			Thread.sleep(2000);
		}
		catch(InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("在main方法中-中断其他线程!");
		t.interrupt();
		System.out.println("在main方法中-退出!");
	}
}

【结果】
在这里插入图片描述

在main中调用了sleep方法(Thread.sleep(2000);),将线程休眠2秒,这样做是为了保证run方法中的内容能够多执行一段时间。
之后,调用线程t的interrupt()方法,将t线程中断。使t线程产生一个InterruptedException异常,从而退出休眠状态。

需要注意的是,调用interrupt()方法并不会使正在执行的线程停止执行,它只对调用wait、join、sleep等方法或由于I/O操作等原因受阻的线程产生影响,使其退出暂停执行的状态(详见JDK文档)。换句话说,它对正在运行的线程是不起作用的,只有对阻塞的线程有效。

当然,正在执行的线程可以通过isInterrupted()方法判断某个线程(包括自己)是否处于中断状态,以决定是否需要执行某些操作,如下:

package com.xy.thread;

public class SleepInterruptDemo {
	public static void main(String[] args) {
		Thread t = Thread.currentThread();
		System.out.println("A: t.isInterrupted() = " + t.isInterrupted());
		t.interrupt();
		System.out.println("B: t.isInterrupted() = " + t.isInterrupted());
		System.out.println("C: t.isInterrupted() = " + t.isInterrupted());
		try {
			Thread.sleep(2000);
			System.out.println("线程没有被中断!");
		}
		catch(InterruptedException e) {
			System.out.println("线程被中断!");
		}
		// 因为sleep抛出了异常,所以它清除了中断标志
		System.out.println("D: t.isInterrupted() = " + t.isInterrupted());
	}
}

【结果】
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_43555323/article/details/84962812