Skip to content

JUC 多线程基础

本文主要介绍Java中关于多线程编程的一些基础知识。

1. 概念解析

1.1 进程与线程

在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。

每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。(进程是资源分配的最小单位)。

某些进程内部还需要同时执行多个子任务。例如,我们在使用Word时,Word可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程。

同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)。

进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。

image-20200818130849617

因为同一个应用程序,既可以有多个进程,也可以有多个线程,因此,实现多任务的方法,有以下几种:

  • 多进程模式(每个进程只有一个线程):

image-20200818130926242

  • 多线程模式(一个进程有多个线程):

    image-20200818130951105

  • 多进程+多线程模式(复杂度最高):

image-20200818130959835

和多线程相比,多进程的缺点在于:

  • 创建进程比创建线程开销大,尤其是在Windows系统上;
  • 进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。

而多进程的优点在于:

多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。

Java语言内置了多线程支持:**一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动多个线程。**此外,JVM还有负责垃圾回收的其他工作线程等。因此,对于大多数Java程序来说,多任务,实际上是说如何使用多线程实现多任务。

1.2 并行与并发

并发是指具有同时处理多个活动的能力,这里的同时应理解为宏观上的同时,在微观上仍然为序列执行。

image-20200818210524915

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

image-20200818211144261

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);
        }
    }
}

结果:

image-20200326213238792

image-20200326213319525

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);
        }
    }
}

结果:

image-20200326213500074

image-20200326213608447

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):进程已结束运行;

image-20200820211100577

本节讨论线程状态,可是为什么这里讨论的是进程状态呢?线程是CPU调度的最小单位,所以这里的进程状态就类似于线程状态。

3.2 Java中线程的状态

在Java中,线程有六种状态,线程的状态由枚举类State定义(已去掉注释):

java
public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}复制复制失败复制成功

线程状态图如下:

image-20200820212001810

  • 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());
}

结果:

image-20200820214311834

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()+"--"+"正在运行");
}

image-20200819213920354

调用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()+"--"+"正在运行");
}

image-20200819214019706

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,则终止线程;如果在睡眠期间被打断,则捕获异常,手动设置打断标记,再一次循环时根据打断标记终止线程。

image-20200820095855819

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();
    }
}

结果:

image-20200820101710993

4.2.5 setDaemon()

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