当我们打开电脑,可以一边打开qq音乐听歌,一边打开浏览器浏览网页,还算可以上qq聊天。电脑是同时可以执行多个任务的,
CPU执行代码都是一条一条顺序执行的,但是,即使是单核cpu,也可以同时运行多个任务。因为操作系统执行多任务实际上就是让CPU对多个任务轮流交替执行。
例如,假设我们有语文、数学、英语3门作业要做,每个作业需要30分钟。我们把这3门作业看成是3个任务,可以做1分钟语文作业,再做1分钟数学作业,再做1分钟英语作业:
这样轮流做下去,在某些人眼里看来,做作业的速度就非常快,看上去就像同时在做3门作业一样
类似的,操作系统轮流让多个任务交替执行,例如,让浏览器执行0.001秒,让QQ执行0.001秒,再让音乐播放器执行0.001秒,在人看来,CPU就是在同时执行多个任务。
计算中,把一个任务称为一个进程,如上面的qq是一个进程,浏览器也是一个进程,每个子任务称作一个线程,比如qq聊天打字的同时也可以接收消息,就是两个子任务即两个线程。
进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程
操作系统调度的最小任务单位其实不是进程,而是线程。常用的Windows、Linux等操作系统都采用抢占式多任务,如何调度线程完全由操作系统决定,程序自己不能决定什么时候执行,以及执行多长时间。
因为同一个应用程序,既可以有多个进程,也可以有多个线程,因此,实现多任务的方法,有以下几种:
具体采用哪种方式,要考虑到进程和线程的特点。
和多线程相比,多进程的缺点在于:
而多进程的优点在于:
多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。
Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程等。
因此,对于大多数Java程序来说,我们说多任务,实际上是说如何使用多线程实现多任务。
和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。
Java多线程编程的特点又在于:
Java中线程分为用户线程和守护线程两种。用户线程是用户自定义的线程,当主线程停止用户线程不会停止,守护线程当进程不存在或者主线程停止,守护线程也会停止,通过setDaemon(true)将一个线程设置为守护线程
在 Java 中, 线程部分是一个重点, 本篇文章说的 J UC 也是关于线程的。 J UC 就是 java.util . concurrent 工具包的简称。 这是一个处理线程的工具包, JDK 1 . 5 开始出现的。
串行:串行是一次只能取得一个任务,并执行这个任务
并发:指一个处理器同时处理多个任务。(不是真正的同时,而是看来是同时,因为cpu要在多个程序间切换)
并行:指多个处理器或者是多核的处理器同时处理多个不同的任务。并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。
并行,是每个cpu运行一个程序。
1、并发,就像一个人(cpu)喂2个孩子(程序),轮换着每人喂一口,表面上两个孩子都在吃饭。并行,就是2个人喂2个孩子,两个孩子也同时在吃饭。
2、多个人同时做一件事 ,多个人同时做不同的事
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()); } } }
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 接⼝”这种⽅式来⾃定义线程类
在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:
其实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)
的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。
线程之间的具体转成如下表示
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