一、概述

1、多任务

当我们打开电脑,可以一边打开qq音乐听歌,一边打开浏览器浏览网页,还算可以上qq聊天。电脑是同时可以执行多个任务的,

CPU执行代码都是一条一条顺序执行的,但是,即使是单核cpu,也可以同时运行多个任务。因为操作系统执行多任务实际上就是让CPU对多个任务轮流交替执行。

例如,假设我们有语文、数学、英语3门作业要做,每个作业需要30分钟。我们把这3门作业看成是3个任务,可以做1分钟语文作业,再做1分钟数学作业,再做1分钟英语作业:

这样轮流做下去,在某些人眼里看来,做作业的速度就非常快,看上去就像同时在做3门作业一样

类似的,操作系统轮流让多个任务交替执行,例如,让浏览器执行0.001秒,让QQ执行0.001秒,再让音乐播放器执行0.001秒,在人看来,CPU就是在同时执行多个任务。

2、进程和线程

计算中,把一个任务称为一个进程,如上面的qq是一个进程,浏览器也是一个进程,每个子任务称作一个线程,比如qq聊天打字的同时也可以接收消息,就是两个子任务即两个线程。

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

操作系统调度的最小任务单位其实不是进程,而是线程。常用的Windows、Linux等操作系统都采用抢占式多任务,如何调度线程完全由操作系统决定,程序自己不能决定什么时候执行,以及执行多长时间。

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

  • 使用多进程
  • 使用单进程多线程
  • 使用多进程+多线程

具体采用哪种方式,要考虑到进程和线程的特点。

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

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

而多进程的优点在于:

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

3、多线程

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

因此,对于大多数Java程序来说,我们说多任务,实际上是说如何使用多线程实现多任务。

和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。

Java多线程编程的特点又在于:

  • 多线程模型是Java程序最基本的并发模型;
  • 后续读写网络、数据库、Web开发等都依赖Java多线程模型。

4、用户线程 守护线程

Java中线程分为用户线程和守护线程两种。用户线程是用户自定义的线程,当主线程停止用户线程不会停止,守护线程当进程不存在或者主线程停止,守护线程也会停止,通过setDaemon(true)将一个线程设置为守护线程

5、什么是JUC

在 Java 中, 线程部分是一个重点, 本篇文章说的 J UC 也是关于线程的。 J UC 就是 java.util . concurrent 工具包的简称。 这是一个处理线程的工具包, JDK 1 . 5 开始出现的。

6、串行、并发、并行

6.1、概念

串行:串行是一次只能取得一个任务,并执行这个任务

并发:指一个处理器同时处理多个任务。(不是真正的同时,而是看来是同时,因为cpu要在多个程序间切换)

多线程篇1:java创建多线程以及线程状态_等待状态

并行:指多个处理器或者是多核的处理器同时处理多个不同的任务。并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。

多线程篇1:java创建多线程以及线程状态_等待状态_02

并行,是每个cpu运行一个程序。

6.2、案例

1、并发,就像一个人(cpu)喂2个孩子(程序),轮换着每人喂一口,表面上两个孩子都在吃饭。并行,就是2个人喂2个孩子,两个孩子也同时在吃饭。

2、多个人同时做一件事 ,多个人同时做不同的事

二、多线程创建

1、继承Thread类


public class Test { public static void main(String[] args) { new MyThread().start(); for (int i = 0; i < 100; i++) { String log = String.format("线程%s(属于线程组%s)打印%d",Thread.currentThread().getName(), Thread.currentThread().getThreadGroup().getName(),i); System.out.println(log); } } } class MyThread extends Thread{ @Override public void run() { for (int i = 0; i < 10000; i++) { System.out.println(Thread.currentThread().getName()); } } }


2、实现Runbable接口


public class Test {
public static void main(String[] args) {
//也可以直接使用lamda表达式
Thread thread = new Thread(new MyThread(),"myThread");
thread.start();
for (int i = 0; i < 100; i++) {
String log = String.format("线程%s(属于线程组%s)打印%d",Thread.currentThread().getName(),
Thread.currentThread().getThreadGroup().getName(),i);
System.out.println(log);
}
}
}
class MyThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}


Thread类常用的方法

currentThread():静态⽅法,返回对当前正在执⾏的线程对象的引⽤;

start():开始执⾏线程的⽅法,java虚拟机会调⽤线程内的run()⽅法;

yield():yield在英语⾥有放弃的意思,同样,这⾥的yield()指的是当前线程愿 意让出对当前处理器的占⽤。这⾥需要注意的是,就算当前线程调⽤了yield() ⽅法,程序在调度的时候,也还有可能继续运⾏这个线程的;

sleep():静态⽅法,使当前线程睡眠⼀段时间;

Thread.setPriority(int n) // 1~10, 默认值5 可以对线程设定优先级 ,优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。

两种方式比较:

由于Java“单继承,多实现”的特性,Runnable接⼝使⽤起来⽐Thread更灵活。

Runnable接⼝出现更符合⾯向对象,将线程单独进⾏对象的封装。

Runnable接⼝出现,降低了线程对象和线程任务的耦合性。 如果使⽤线程时不需要使⽤Thread类的诸多⽅法,显然使⽤Runnable接⼝更 为轻量。

所以,我们通常优先使⽤“实现 Runnable 接⼝”这种⽅式来⾃定义线程类

三、线程状态

1、线程状态概述

在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:

  • New:新创建的线程,尚未执行;
  • Runnable:运行中的线程,正在执行run()方法的Java代码;
  • Blocked:运行中的线程,因为某些操作被阻塞而挂起;
  • Waiting:运行中的线程,因为某些操作在等待中;
  • Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
  • Terminated:线程已终止,因为run()方法执行完毕。

其实java中关于线程状态在thread类是有一个枚举的,如下


public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}


用一个状态转移图表示如下:

┌─────────────┐
│ New │
└─────────────┘


┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
┌─────────────┐ ┌─────────────┐
││ Runnable │ │ Blocked ││
└─────────────┘ └─────────────┘
│┌─────────────┐ ┌─────────────┐│
│ Waiting │ │Timed Waiting│
│└─────────────┘ └─────────────┘│
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─


┌─────────────┐
│ Terminated │
└─────────────┘

当线程启动后,它可以在​​Runnable​​​、​​Blocked​​​、​​Waiting​​​和​​Timed Waiting​​​这几个状态之间切换,直到最后变成​​Terminated​​状态,线程终止。

线程终止的原因有:

  • 线程正常终止:​​run()​​​方法执行到​​return​​语句返回;
  • 线程意外终止:​​run()​​方法因为未捕获的异常导致线程终止;
  • 对某个线程的​​Thread​​​实例调用​​stop()​​方法强制终止(强烈不推荐使用)。

一个线程还可以等待另一个线程直到其运行结束。例如,​​main​​​线程在启动​​t​​​线程后,可以通过​​t.join()​​​等待​​t​​线程结束后再继续运行:

public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("hello");
});
System.out.println("start");
t.start();
t.join();
System.out.println("end");
}
}

当​​main​​​线程对线程对象​​t​​​调用​​join()​​​方法时,主线程将等待变量​​t​​​表示的线程运行结束,即​​join​​​就是指等待该线程结束,然后才继续往下执行自身线程。所以,上述代码打印顺序可以肯定是​​main​​​线程先打印​​start​​​,​​t​​​线程再打印​​hello​​​,​​main​​​线程最后再打印​​end​​。

如果​​t​​​线程已经结束,对实例​​t​​​调用​​join()​​​会立刻返回。此外,​​join(long)​​的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。

2、具体转换

线程之间的具体转成如下表示

多线程篇1:java创建多线程以及线程状态_jvm_03

NEW

处于NEW状态的线程此时尚未启动。这⾥的尚未启动指的是还没调⽤Thread实例 的start()⽅法


private void testStateNew() {
Thread thread = new Thread(() -> {});
System.out.println(thread.getState()); // 输出 NEW
}


从上⾯可以看出,只是创建了线程⽽并没有调⽤start()⽅法,此时线程处于NEW状 态。

关于start()的两个引申问题

1. 反复调⽤同⼀个线程的start()⽅法是否可⾏?

2. 假如⼀个线程执⾏完毕(此时处于TERMINATED状态),再次调⽤这个线程 的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) {

}
}
}


我们可以看到,在start()内部,这⾥有⼀个threadStatus的变量。如果它不等于0, 调⽤start()是会直接抛出异常的。

我们接着往下看,有⼀个native的 start0() ⽅法。这个⽅法⾥并没有对 threadStatus的处理。到了这⾥我们仿佛就拿这个threadStatus没辙了,我们通过 debug的⽅式再看⼀下:


@Test
public void testStartMethod() {
Thread thread = new Thread(() -> {});
thread.start(); // 第⼀次调⽤
thread.start(); // 第⼆次调⽤
}


我是在start()⽅法内部的最开始打的断点,叙述下在我这⾥打断点看到的结果: 第⼀次调⽤时threadStatus的值是0。 第⼆次调⽤时threadStatus的值不为0。 查看当前线程状态的源码:


// Thread.getState⽅法源码:
public State getState() {
// get current thread state
return sun.misc.VM.toThreadState(threadStatus);
}
// sun.misc.VM 源码:
public static State toThreadState(int var0) {
if ((var0 & 4) != 0) {
return State.RUNNABLE;
} else if ((var0 & 1024) != 0) {
return State.BLOCKED;
} else if ((var0 & 16) != 0) {
return State.WAITING;
} else if ((var0 & 32) != 0) {
return State.TIMED_WAITING;
} else if ((var0 & 2) != 0) {
return State.TERMINATED;
} else {
return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
}
}


两个问题的答案都是不可⾏,在调⽤⼀次start()之后,threadStatus的值会改 变(threadStatus !=0),此时再次调⽤start()⽅法会抛出 IllegalThreadStateException异常。 ⽐如,threadStatus为2代表当前线程状态为TERMINATED。

RUNNABLE

表示当前线程正在运⾏中。处于RUNNABLE状态的线程在Java虚拟机中运⾏,也 有可能在等待其他系统资源(⽐如I/O)。

Java线程的RUNNABLE状态其实是包括了传统操作系统线程的ready和 running两个状态的。

BLOCKED

阻塞状态。处于BLOCKED状态的线程正等待锁的释放以进⼊同步区。 我们⽤BLOCKED状态举个⽣活中的例⼦:

假如今天你下班后准备去⻝堂吃饭。你来到⻝堂仅有的⼀个窗⼝,发现前⾯ 已经有个⼈在窗⼝前了,此时你必须得等前⾯的⼈从窗⼝离开才⾏。 假设你是线程t2,你前⾯的那个⼈是线程t1。此时t1占有了锁(⻝堂唯⼀的 窗⼝),t2正在等待锁的释放,所以此时t2就处于BLOCKED状态。

WAITING

等待状态。处于等待状态的线程变成RUNNABLE状态需要其他线程唤醒。 调⽤如下3个⽅法会使线程进⼊等待状态: Object.wait():使当前线程处于等待状态直到另⼀个线程唤醒它;

Thread.join():等待线程执⾏完毕,底层调⽤的是Object实例的wait⽅法;

LockSupport.park():除⾮获得调⽤许可,否则禁⽤当前线程进⾏线程调度。

你等了好⼏分钟现在终于轮到你了,突然你们有⼀个“不懂事”的经理突然来 了。你看到他你就有⼀种不祥的预感,果然,他是来找你的。 他把你拉到⼀旁叫你待会⼉再吃饭,说他下午要去作报告,赶紧来找你了解 ⼀下项⽬的情况。你⼼⾥虽然有⼀万个不愿意但是你还是从⻝堂窗⼝⾛开 了。 此时,假设你还是线程t2,你的经理是线程t1。虽然你此时都占有锁(窗 ⼝)了,“不速之客”来了你还是得释放掉锁。此时你t2的状态就是 WAITING。然后经理t1获得锁,进⼊RUNNABLE状态。 要是经理t1不主动唤醒你t2(notify、notifyAll..),可以说你t2只能⼀直等待 了。

TIMED_WAITING

超时等待状态。线程等待⼀个具体的时间,时间到后会被⾃动唤醒。 调⽤如下⽅法会使线程进⼊超时等待状

1、Thread.sleep(long millis):使当前线程睡眠指定时间

2、Object.wait(long timeout):线程休眠指定时间,等待期间可以通过 notify()/notifyAll()唤醒;

3、Thread.join(long millis):等待当前线程最多执⾏millis毫秒,如果millis为0,则 会⼀直执⾏;

4、LockSupport.parkNanos(long nanos): 除⾮获得调⽤许可,否则禁⽤当前线 程进⾏线程调度指定时间

5、LockSupport.parkUntil(long deadline):同上,也是禁⽌线程进⾏调度指定时 间;

到了第⼆天中午,⼜到了饭点,你还是到了窗⼝前。 突然间想起你的同事叫你等他⼀起,他说让你等他⼗分钟他改个bug。 好吧,你说那你就等等吧,你就离开了窗⼝。很快⼗分钟过去了,你⻅他还 没来,你想都等了这么久了还不来,那你还是先去吃饭好了。 这时你还是线程t1,你改bug的同事是线程t2。t2让t1等待了指定时间,t1先 主动释放了锁。此时t1等待期间就属于TIMED_WATING状态。 t1等待10分钟后,就⾃动唤醒,拥有了去争夺锁的资格。

TERMINATED

终⽌状态。此时线程已执⾏完毕。

四 、线程中断

当执行一个很耗时的任务时,比如下载文件,用户随时可能取消下载,当前取消下载,我们应在服务端中断当前下载文件的线程。

中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行​​run()​​方法,使得自身线程能立刻结束运行。

中断一个线程非常简单,只需要在其他线程中对目标线程调用​​interrupt()​​方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。

public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1); // 暂停1毫秒
t.interrupt(); // 中断t线程
t.join(); // 等待t线程结束
System.out.println("end");
}
}

class MyThread extends Thread {
public void run() {
int n = 0;
while (! isInterrupted()) {
n ++;
System.out.println(n + " hello!");
}
}
}

仔细看上述代码,​​main​​​线程通过调用​​t.interrupt()​​​方法中断​​t​​​线程,但是要注意,​​interrupt()​​​方法仅仅向​​t​​​线程发出了“中断请求”,至于​​t​​​线程是否能立刻响应,要看具体代码。而​​t​​​线程的​​while​​​循环会检测​​isInterrupted()​​​,所以上述代码能正确响应​​interrupt()​​​请求,使得自身立刻结束运行​​run()​​方法。

如果线程处于等待状态,例如,​​t.join()​​​会让​​main​​​线程进入等待状态,此时,如果对​​main​​​线程调用​​interrupt()​​​,​​join()​​​方法会立刻抛出​​InterruptedException​​​,因此,目标线程只要捕获到​​join()​​​方法抛出的​​InterruptedException​​​,就说明有其他线程对其调用了​​interrupt()​​方法,通常情况下该线程应该立刻结束运行。

public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1000);
t.interrupt(); // 中断t线程
t.join(); // 等待t线程结束
System.out.println("end");
}
}

class MyThread extends Thread {
public void run() {
Thread hello = new HelloThread();
hello.start(); // 启动hello线程
try {
hello.join(); // 等待hello线程结束
} catch (InterruptedException e) {
System.out.println("interrupted!");
}
hello.interrupt();
}
}

class HelloThread extends Thread {
public void run() {
int n = 0;
while (!isInterrupted()) {
n++;
System.out.println(n + " hello!");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
}
}

 

​main​​​线程通过调用​​t.interrupt()​​​从而通知​​t​​​线程中断,而此时​​t​​​线程正位于​​hello.join()​​​的等待中,此方法会立刻结束等待并抛出​​InterruptedException​​​。由于我们在​​t​​​线程中捕获了​​InterruptedException​​​,因此,就可以准备结束该线程。在​​t​​​线程结束前,对​​hello​​​线程也进行了​​interrupt()​​​调用通知其中断。如果去掉这一行代码,可以发现​​hello​​线程仍然会继续运行,且JVM不会退出。

另一个常用的中断线程的方法是设置标志位。我们通常会用一个​​running​​​标志位来标识线程是否应该继续运行,在外部线程中,通过把​​HelloThread.running​​​置为​​false​​,就可以让线程结束:

public class Main {
public static void main(String[] args) throws InterruptedException {
HelloThread t = new HelloThread();
t.start();
Thread.sleep(1);
t.running = false; // 标志位置为false
}
}

class HelloThread extends Thread {
public volatile boolean running = true;
public void run() {
int n = 0;
while (running) {
n ++;
System.out.println(n + " hello!");
}
System.out.println("end!");
}
}

注意到​​HelloThread​​​的标志位​​boolean running​​​是一个线程间共享的变量。线程间共享变量需要使用​​volatile​​关键字标记,确保每个线程都能读取到更新后的变量值。

为什么要对线程间共享的变量用关键字​​volatile​​声明?这涉及到Java的内存模型。在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!

┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
Main Memory
│ │
┌───────┐┌───────┐┌───────┐
│ │ var A ││ var B ││ var C │ │
└───────┘└───────┘└───────┘
│ │ ▲ │ ▲ │
─ ─ ─│─│─ ─ ─ ─ ─ ─ ─ ─│─│─ ─ ─
│ │ │ │
┌ ─ ─ ┼ ┼ ─ ─ ┐ ┌ ─ ─ ┼ ┼ ─ ─ ┐
▼ │ ▼ │
│ ┌───────┐ │ │ ┌───────┐ │
│ var A │ │ var C │
│ └───────┘ │ │ └───────┘ │
Thread 1 Thread 2
└ ─ ─ ─ ─ ─ ─ ┘ └ ─ ─ ─ ─ ─ ─ ┘

这会导致如果一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。例如,主内存的变量​​a = true​​​,线程1执行​​a = false​​​时,它在此刻仅仅是把变量​​a​​​的副本变成了​​false​​​,主内存的变量​​a​​​还是​​true​​​,在JVM把修改后的​​a​​​回写到主内存之前,其他线程读取到的​​a​​​的值仍然是​​true​​,这就造成了多线程之间共享的变量不一致。

因此,​​volatile​​关键字的目的是告诉虚拟机:

  • 每次访问变量时,总是获取主内存的最新值;
  • 每次修改变量后,立刻回写到主内存。

​volatile​​关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。

如果我们去掉​​volatile​​​关键字,运行上述程序,发现效果和带​​volatile​​差不多,这是因为在x86的架构下,JVM回写主内存的速度非常快,但是,换成ARM的架构,就会有显著的延迟。

小结

对目标线程调用​​interrupt()​​​方法可以请求中断一个线程,目标线程通过检测​​isInterrupted()​​​标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到​​InterruptedException​​;

目标线程检测到​​isInterrupted()​​​为​​true​​​或者捕获了​​InterruptedException​​都应该立刻结束自身线程;

通过标志位判断需要正确使用​​volatile​​关键字;

​volatile​​关键字解决了共享变量在线程间的可见性问题;

五、守护线程

Java程序入口就是由JVM启动​​main​​​线程,​​main​​线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。

如果有一个线程没有退出,JVM进程就不会退出。所以,必须保证所有线程都能及时结束。

但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程

class TimerThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println(LocalTime.now());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
}
}

如果这个线程不结束,JVM进程就无法结束。问题是,由谁负责结束这个线程?

然而这类线程经常没有负责人来负责结束它们。但是,当其他线程结束时,JVM进程又必须要结束,怎么办?

答案是使用守护线程(Daemon Thread)。

守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。

因此,JVM退出时,不必关心守护线程是否已结束。

如何创建守护线程呢?方法和普通线程一样,只是在调用​​start()​​​方法前,调用​​setDaemon(true)​​把该线程标记为守护线程:

Thread t = new MyThread();
t.setDaemon(true);
t.start();

在守护线程中,编写代码要注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。

小结

守护线程是为其他线程服务的线程;

所有非守护线程都执行完毕后,虚拟机退出;

守护线程不能持有需要关闭的资源(如打开文件等);

参考

​https://www.liaoxuefeng.com/wiki/1252599548343744/1306580767211554​