一、概述
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要在多个程序间切换)
并行:指多个处理器或者是多核的处理器同时处理多个不同的任务。并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。
并行,是每个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接口
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类是有一个枚举的,如下
用一个状态转移图表示如下:
当线程启动后,它可以在Runnable
、Blocked
、Waiting
和Timed Waiting
这几个状态之间切换,直到最后变成Terminated
状态,线程终止。
线程终止的原因有:
- 线程正常终止:
run()
方法执行到return
语句返回; - 线程意外终止:
run()
方法因为未捕获的异常导致线程终止; - 对某个线程的
Thread
实例调用stop()
方法强制终止(强烈不推荐使用)。
一个线程还可以等待另一个线程直到其运行结束。例如,main
线程在启动t
线程后,可以通过t.join()
等待t
线程结束后再继续运行:
当main
线程对线程对象t
调用join()
方法时,主线程将等待变量t
表示的线程运行结束,即join
就是指等待该线程结束,然后才继续往下执行自身线程。所以,上述代码打印顺序可以肯定是main
线程先打印start
,t
线程再打印hello
,main
线程最后再打印end
。
如果t
线程已经结束,对实例t
调用join()
会立刻返回。此外,join(long)
的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。
2、具体转换
线程之间的具体转成如下表示
NEW
处于NEW状态的线程此时尚未启动。这⾥的尚未启动指的是还没调⽤Thread实例 的start()⽅法
从上⾯可以看出,只是创建了线程⽽并没有调⽤start()⽅法,此时线程处于NEW状 态。
关于start()的两个引申问题
1. 反复调⽤同⼀个线程的start()⽅法是否可⾏?
2. 假如⼀个线程执⾏完毕(此时处于TERMINATED状态),再次调⽤这个线程 的start()⽅法是否可⾏?
我们可以看到,在start()内部,这⾥有⼀个threadStatus的变量。如果它不等于0, 调⽤start()是会直接抛出异常的。
我们接着往下看,有⼀个native的 start0() ⽅法。这个⽅法⾥并没有对 threadStatus的处理。到了这⾥我们仿佛就拿这个threadStatus没辙了,我们通过 debug的⽅式再看⼀下:
我是在start()⽅法内部的最开始打的断点,叙述下在我这⾥打断点看到的结果: 第⼀次调⽤时threadStatus的值是0。 第⼆次调⽤时threadStatus的值不为0。 查看当前线程状态的源码:
两个问题的答案都是不可⾏,在调⽤⼀次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状态,如果是,就立刻结束运行。
仔细看上述代码,main
线程通过调用t.interrupt()
方法中断t
线程,但是要注意,interrupt()
方法仅仅向t
线程发出了“中断请求”,至于t
线程是否能立刻响应,要看具体代码。而t
线程的while
循环会检测isInterrupted()
,所以上述代码能正确响应interrupt()
请求,使得自身立刻结束运行run()
方法。
如果线程处于等待状态,例如,t.join()
会让main
线程进入等待状态,此时,如果对main
线程调用interrupt()
,join()
方法会立刻抛出InterruptedException
,因此,目标线程只要捕获到join()
方法抛出的InterruptedException
,就说明有其他线程对其调用了interrupt()
方法,通常情况下该线程应该立刻结束运行。
main
线程通过调用t.interrupt()
从而通知t
线程中断,而此时t
线程正位于hello.join()
的等待中,此方法会立刻结束等待并抛出InterruptedException
。由于我们在t
线程中捕获了InterruptedException
,因此,就可以准备结束该线程。在t
线程结束前,对hello
线程也进行了interrupt()
调用通知其中断。如果去掉这一行代码,可以发现hello
线程仍然会继续运行,且JVM不会退出。
另一个常用的中断线程的方法是设置标志位。我们通常会用一个running
标志位来标识线程是否应该继续运行,在外部线程中,通过把HelloThread.running
置为false
,就可以让线程结束:
注意到HelloThread
的标志位boolean running
是一个线程间共享的变量。线程间共享变量需要使用volatile
关键字标记,确保每个线程都能读取到更新后的变量值。
为什么要对线程间共享的变量用关键字volatile
声明?这涉及到Java的内存模型。在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!
这会导致如果一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。例如,主内存的变量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进程就不会退出。所以,必须保证所有线程都能及时结束。
但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程
如果这个线程不结束,JVM进程就无法结束。问题是,由谁负责结束这个线程?
然而这类线程经常没有负责人来负责结束它们。但是,当其他线程结束时,JVM进程又必须要结束,怎么办?
答案是使用守护线程(Daemon Thread)。
守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
因此,JVM退出时,不必关心守护线程是否已结束。
如何创建守护线程呢?方法和普通线程一样,只是在调用start()
方法前,调用setDaemon(true)
把该线程标记为守护线程:
在守护线程中,编写代码要注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。
小结
守护线程是为其他线程服务的线程;
所有非守护线程都执行完毕后,虚拟机退出;
守护线程不能持有需要关闭的资源(如打开文件等);
参考
https://www.liaoxuefeng.com/wiki/1252599548343744/1306580767211554