文章目录
本章将会介绍 Java 多线程编程的相关方面,包括创建、启动线程、控制线程,以及多线程的同步操作。并会介绍如何利用 Java 内建支持的线程池来提高多线程的性能。
线程概述
几乎所有的操作系统都支持同时运行的多个任务,一个任务通常就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程。
线程和进程
所有运行中的任务通常对应一条进程(process)。当一个程序进入内存中运行,即变成一个进程。进程是处于运行中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。
进程包括如下三个特征:
- 独立性
- 动态性:进程与程序的区别在于,程序只是一个静态的指令集,而进程是一个正在系统中活动的指令集合。进程拥有自己的生命周期和不同的状态,这些概念在程序中是不具备的。
- 并发行:多个进程可以在单处理器上并发进行,多个进程之间不会相互影响。
线程(Thread)也被称为轻量级进程,线程是进程的执行单元。线程在进程中是独立的、并发的执行流,每条线程也是相互独立的。线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须拥有一个父进程。
线程是独立运行的,它并不知道进程中是否还有其他线程存在。线程的执行是抢占式的,也就是说,当前运行的线程在任何时候都可能被挂起,以便另外一个线程可以运行。
多线程的优势
- 进程间不能共享内存,但线程之间共享内存非常容易
- 系统创建进程需要为进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现任务并发比多进程的效率高
- Java 语言内置多线程功能支持
线程的创建和启动
Java 的 Thread 类代表线程,所有线程对象都必须是 Thread 类或其子类的实例。每条线程的作用是完成一定的任务,实际上就是执行一段程序流(一段顺序执行的代码)。Java 使用 run 方法来封装这样一段程序流。
继承 Thread 类创建线程类
通过继承 Thread 类来创建并且启动多线程的步骤:
- 定义 Thread 类的子类,并重写该类的 run 方法,该 run 方法的方法体就代表线程需要完成的任务。
- 创建 Thread 子类的实例,即创建了线程对象
- 同线程对象的 start 方法来启动该线程
public class FirstThread extends Thread {
private int i;
//重写run()方法,方法体就是线程的执行体
public void run(){
for (; i < 100; i++) {
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
//调用Thread的currentThread方法获取当前线程
System.out.println(Thread.currentThread().getName() + " " + i);
if(i == 20){
//创建并启动第一条线程
new FirstThread().start();
new FirstThread().start();
}
}
}
}
运行结果:
Thread-0 97
Thread-0 98
Thread-0 99
Thread-1 0
Thread-1 1
多条线程之间无法共享线程类的实例变量
使用 Runnable 接口或创建线程类
实现 Runnable 接口来创建并启动多线程的步骤如下:
- 定义 Runnable 接口的实现类,并重写该接口的 run() 方法
- 创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 来创建 Thread 对象,该 Thread 对象才是真正的线程对象。
- 使用 start() 来启动该线程
public class SecondThread implements Runnable {
private int i;
@Override
public void run() {
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if(i == 20){
SecondThread st = new SecondThread();
new Thread(st, "新线程1").start();
new Thread(st, "新线程2").start();
}
}
}
}
运行结果:
新线程1 14
新线程1 15
新线程1 16
新线程1 17
新线程1 18
新线程1 19
采用 Runnable 接口的方式来创建多条线程是可以共享线程类的实例属性的。
两种方式创建线程的对比
通过继承 Threa 类和 实现 Runnable 接口都可以实现多线程,但两种方式存在一定的差别
采用实现 Runnable 接口的方式的多线程:
- 线程类只是实现了 Runnable 接口,还可以继承其他类
- 这中方式下,可以多个线程共享一个 target 对象,所以非常适合多个相同线程来处理同一份资源的情况
采用继承 Thread 类的方式的多线程:
- 因为线程已经继承了 Thread 类,所以不能再继承其他的父类
- 编写简单
线程的生命周期
当线程被创建并启动以后,它既不是已启动就进入执行状态,也不是一直处于执行状态,在线程的生命周期中,它要经过新建(new)、就绪、运行、阻塞和死亡五种状态。
新建和就绪状态
当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时它与其它 Java 对象一样,仅仅由 Java 虚拟机为其分配了内存,并初始化了其成员你变量。当程序调用了 start() 方法之后,该线程处于就绪状态,Java 虚拟机会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有开始运行,它只是表示该线程可以运行了。
运行和阻塞状态
如果处于就绪状态的线程获取了 CPU,开始执行 run 方法的线程执行体,则该线程处于运行状态。
发生如下情况,系统将会进入阻塞状态:
- 线程调用 sleep 方法主动放弃所占用的处理器资源
- 线程在等待某个通知
- 线程调用了一个阻塞式 IO 方法,在该方法返回之前,该线程被阻塞
- 程序调用了线程的 suspend 方法将该线程挂起
- 线程试图调用一个同步监视器,但该同步监视器正在被其他线程所占有
当正在执行的线程被阻塞之后,其他线程就获得执行的机会了。被阻塞的线程会在合适的时候进入就绪状态。
线程死亡
- run() 方法执行完成,线程正常结束
- 线程抛出一个未捕获的 Exception 或 Error
- 直接调用该线程的 stop() 方法来结束该线程
当主线程结束后,其他线程不受影响
控制线程
join 线程
Thread 提供了让一个线程等待另一个线程完成的方法:join() 。当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到被 join 方法加入的 join 线程完成为止。
public class JoinThread extends Thread {
public JoinThread(String name){
//提供一个有参数的构造器,用于设置该线程的名字
super(name);
}
//重写run方法
public void run(){
for (int i = 0; i < 100; i++) {
System.out.println(getName() + " " + i);
}
}
/*
* 当主线程循环变量i等于20的时候,启动了名为“被join的线程”,该线程不会和main线程并发执行,
* 而是main线程必须等到该线程执行结束后才开始向下执行。
* 在名为“被join的线程”的线程执行时,实际上只有两条子线程并发执行,而主线程处于等待状态下
* */
public static void main(String[] args) throws Exception{
//启动子线程
new JoinThread("新线程").start();
for (int i = 0; i < 100; i++){
if(i == 20){
JoinThread jt = new JoinThread("被join的线程");
jt.start();
//main线程调用了jt线程的join方法,main线程必须等jt线程结束才会向下执行
jt.join();
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
}
join 有三种重载的形式:
- join():等待被 join() 的线程执行完成
- join(long millis):等待被 join 的线程最长为 millis 毫秒
- join(long millis, int nanos):等待被 join 的线程最长 millis 毫秒加上 nanos 微秒
后台线程
有一种线程,它是在后台运行的,它的任务是为其他线程提供服务,这种线程被称为“后台线程”,又称“守护线程”。JVM 的垃圾回收线程就是典型的后台线程。
后台线程有个特征:如果所有的前台线程死亡,后台线程会自动死亡。
调用 Thread 对象的 setDeamon(true) 方法可将指定线程设置成后台线程。
线程睡眠: sleep
如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,可以调用 Thread 类的静态 sleep 方法。
线程让步:yield
yield() 方法只是让当前正在执行的线程暂停,但他不会阻塞该线程,他只是将该线程转为就绪状态。实际上,当某个线程调用了 yield 方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程高的就绪状态的线程才会获得执行机会。
sleep 方法和 yield 方法区别如下:
- sleep 方法暂停线程之后,会给其他线程执行机会,不会理会其他线程的优先级。但 yield 方法只会给优先级相同或更高的线程机会。
- sleep 方法会将线程转为阻塞状态。而 yield 不会将线程转入阻塞状态,他只是强制当前线程进入就绪状态。
改变线程优先级
每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,反之得到较少的执行机会。
每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main 线程具有普通优先级。
Thread 提供了 setPriority(int newPriority) 和 getPriority() 方法来设置和返回线程的优先级。
线程的同步
当使用多个线程来访问同一个数据时,非常容易出现线程安全问题。