Appearance
JUC 多线程基础
本文主要介绍Java中关于多线程编程的一些基础知识。
1. 概念解析
1.1 进程与线程
在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。
每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。(进程是资源分配的最小单位)。
某些进程内部还需要同时执行多个子任务。例如,我们在使用Word时,Word可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程。
同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)。
进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。

因为同一个应用程序,既可以有多个进程,也可以有多个线程,因此,实现多任务的方法,有以下几种:
- 多进程模式(每个进程只有一个线程):
多线程模式(一个进程有多个线程):
多进程+多线程模式(复杂度最高):

和多线程相比,多进程的缺点在于:
- 创建进程比创建线程开销大,尤其是在Windows系统上;
- 进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。
而多进程的优点在于:
多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。
Java语言内置了多线程支持:**一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动多个线程。**此外,JVM还有负责垃圾回收的其他工作线程等。因此,对于大多数Java程序来说,多任务,实际上是说如何使用多线程实现多任务。
1.2 并行与并发
并发是指具有同时处理多个活动的能力,这里的同时应理解为宏观上的同时,在微观上仍然为序列执行。

并行是指在物理上同时处理多个活动,即在微观上也是同时处理的

2. 创建新线程
2.1 实现Runnable接口
- 新建任务类实现
Runnable接口,在run()方法中实现具体任务; - 新建
Thread类,传入我们的任务类实例,调用start()方法启动线程;
java
// 任务类实现Runnable接口
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i=1;i<=100;i++){
System.out.println("MyRunnable: " + i);
}
}
}
public class TestRunnable {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable(); //实例化
Thread thread = new Thread(myRunnable); //凭借Thread类的start()方法启动另一个线程
thread.start();
//主线程要做的事
for (int i=1;i<=100;i++){
System.out.println("main: " + i);
}
}
}结果:


2.2 继承Thread类
- 继承
Thread类,重写run()方法; - 实例化
Thread子类,并调用start()方法启动线程;
java
// 继承Thread类,重写run()方法
class MyThread extends Thread{
@Override
public void run() {
for (int i=1;i<=100;i++){
System.out.println("MyThread: " + i);
}
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread(); //实例化
myThread.start(); //启动线程
//主线程要做的事
for (int i=1;i<=100;i++){
System.out.println("main: " + i);
}
}
}结果:


2.3 两种方式的区别
对比上述两种方式,我们发现Runnable方法甚至还需要用Thread类的start()方法启动线程,比Thread方法更复杂,那我们为什么还需要Runnable接口呢?存在必有理,我们知道Java只支持单继承,假如某一个类继承了另一个类,但是又要实现多线程,那么它还能继承Thread类吗?显然不能,所以此时Runnable接口就派上用处了。
3. 线程的状态
3.1 操作系统中进程的状态
在操作系统中,进程有五种状态:
- 新建态(New-State):对应于进程刚刚被创建时没有被提交的状态,并等待系统完成创建进程的所有必要信息;
- 就绪(可运行)态(Ready-State):当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行,进程这时的状态称为就绪状态;
- 运行态(Running-State):进程已获得CPU,其程序正在执行;
- 阻塞态(Blocked-State):正在执行的进程由于发生某事件而暂时无法继续执行时,便放弃处理机而处于暂停状态,亦即进程的执行受到阻塞,把这种暂停状态称为阻塞状态;
- 终止态(Exit-State):进程已结束运行;

本节讨论线程状态,可是为什么这里讨论的是进程状态呢?线程是CPU调度的最小单位,所以这里的进程状态就类似于线程状态。
3.2 Java中线程的状态
在Java中,线程有六种状态,线程的状态由枚举类State定义(已去掉注释):
java
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}复制复制失败复制成功线程状态图如下:

NEW:线程已被创建,但还未启动,即还没有执行start()方法,此时状态为NEW;RUNNABLE:该状态包含了操作系统层面上的可运行(就绪)状态、运行状态和阻塞状态;BLOCKED:当一个线程试图获取一个内部的对象锁,而这个锁目前被其他线程占有,该线程就会被阻塞;WAITING:当线程等待一个事件发生时,就会进入WAITING状态,例如,调用join()时就会进入WAITING状态,实际上,BLOCKED和WAITING状态并无太大区别;TIMED WAITING:如果等待有时间限制,则会进入TIMED WAITING状态,即限时等待状态,例如,当调用Thread.sleep(long mills);TERMINATED:当run()方法正常退出,线程自然终止或者由于一个没有捕获的异常终止了run()方法,线程意外终止;
3.3 代码演示
本节用代码演示Java 线程的六种状态:
java
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1"){
@Override
public void run() {
//演示NEW状态
}
};
Thread t2 = new Thread("t2"){
@Override
public void run() {
while (true){
//演示RUNNABLE状态
}
}
};
t2.start();
Thread t3 = new Thread("t3"){
@Override
public void run() {
try {
//演示WAITING状态
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t3.start();
Thread t4 = new Thread("t4"){
@Override
public void run() {
synchronized (MultiThreadTest03.class){
try {
//演示TIMED WAITING状态
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t4.start();
Thread t5 = new Thread("t5"){
@Override
public void run() {
synchronized (MultiThreadTest03.class){
try {
//演示BLOCKED状态
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t5.start();
Thread t6 = new Thread("t6"){
@Override
public void run() {
//演示TERMINATED状态
}
};
t6.start();
Thread.sleep(200);
System.out.println(t1.getName()+"--"+t1.getState());
System.out.println(t2.getName()+"--"+t2.getState());
System.out.println(t3.getName()+"--"+t3.getState());
System.out.println(t4.getName()+"--"+t4.getState());
System.out.println(t5.getName()+"--"+t5.getState());
System.out.println(t6.getName()+"--"+t6.getState());
}结果:

4. 线程常用方法
4.1 常用方法列表
查看Thread的源码,其常用的方法列表如下:
| 方法声明 | 解释 |
|---|---|
long getId() | 返回线程的唯一标识符ID |
String getName() | 返回线程名 |
void setName(String name) | 设置线程名 |
int getPriority() | 返回线程优先级 |
void setPriority(int newPriority) | 设置线程优先级,优先级为1-10的整数 |
Thread.State getState() | 返回线程状态 |
void run() | 运行run()方法 |
void start() | 启动线程,执行run()方法 |
boolean isAlive() | 判断线程是否存活 |
void interrupt() | 中断线程 |
void join() | 等待线程结束 |
void join(long millis) | 等待线程结束,最多等待mills毫秒 |
void setDaemon(boolean on) | 设置线程是否为守护线程 |
static boolean interrupted() | **(静态方法)**判断线程是否被中断 |
static void sleep(long millis) | **(静态方法)**线程休眠 |
static Thread currentThread() | **(静态方法)**获取当前线程 |
static void yield() | **(静态方法)**当前线程让出CPU的使用权 |
4.2 常用方法详解
4.2.1 run()与start()
如果直接调用run()方法,则相当于调用一个普通的方法,并没有启动一个新线程;只有调用start()方法才能启动一个新线程,并且在新线程中运行run()方法。
直接调用run()方法:
java
public static void main(String[] args) {
Thread t1 = new Thread(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"--"+"正在运行");
}
};
t1.run();
System.out.println(Thread.currentThread().getName()+"--"+"正在运行");
}
调用start()方法:
java
public static void main(String[] args) {
Thread t1 = new Thread(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"--"+"正在运行");
}
};
t1.start();
System.out.println(Thread.currentThread().getName()+"--"+"正在运行");
}
4.2.2 sleep()与yield()
当调用sleep()方法时,当前线程会从Running状态进入到Timed Waiting状态。当线程A睡眠后,其他线程可以调用线程A的interruot()方法唤醒线程A,这时sleep()方法会抛出InterruotedException异常;睡眠结束后的线程进入Runnable状态,未必会立刻得到执行。
调用yield()方法会让当前线程从Running状态进入Runnable状态,然后调度执行其他线程,具体的实现依赖于操作系统的任务调度器。
4.2.3 join()
如果在线程A中调用线程B的join()方法,则线程A会等待线程B运行结束后再继续运行。
如果有参数join(long millis),表示线程A最多等待mills毫秒,如果超过了这个时间,则继续运行。
4.2.4 interrupt()
如果打断处于阻塞状态(调用了sleep()、join()等方法)的线程,则打断标记为false;
如果打断正常运行的线程,则打断标记为true,并且被打断的线程并不会停止运行,我们可以根据打断标记来终止被打断的线程。
应用:利用线程A优雅地终止线程B(两阶段终止模式)
例子:现在有一个监控线程,每隔一秒执行一次监控记录。若在正常运行时期被其他线程打断,则打断标记为true,则终止线程;如果在睡眠期间被打断,则捕获异常,手动设置打断标记,再一次循环时根据打断标记终止线程。

java
import java.time.LocalDateTime;
public class MultiThreadTest02 {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination tpt = new TwoPhaseTermination();
tpt.start();
Thread.sleep(5500);
tpt.stop();
}
}
class TwoPhaseTermination{
private Thread monitor; //监控线程
//启动监控线程
public void start(){
monitor =
new Thread(
() -> {
while (true) {
Thread currentThread = Thread.currentThread();
if (currentThread.isInterrupted()) {
System.out.println(LocalDateTime.now().toString()+" 线程终止,执行终止任务");
break;
}
try {
Thread.sleep(1000);
System.out.println(LocalDateTime.now().toString()+" 执行监控记录");
} catch (InterruptedException e) {
//在睡眠状态被打断,手动设置中断标志
monitor.interrupt();
e.printStackTrace();
}
}
});
monitor.start();
}
//停止监控线程
public void stop(){
monitor.interrupt();
}
}结果:

4.2.5 setDaemon()
可以通过调用t.setDaemon(true)将线程t转换为守护线程。守护线程的唯一用途就是为其他线程提供服务。当只剩下守护线程时,虚拟机就会退出,因为如果只剩下守护线程,就没必要继续运行程序了。