前置知识
什么是线程和进程?
进程: 是程序的一次执行,一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。
线程: 进程中的一个执行流(控制单元 / 执行任务),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
线程的优点
- 轻量, 创建一个线程的代价要比进程小的多
- 线程之间的切换, 对比进程, OS 要做的工作小很多
- 线程运行, 占用资源比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速 I/O 操作同时, 可执行其他任务
- 计算密集型应用, 可将计算分解到多个线程中实现, 以便在多处理器系统上运行
- I/O 密集型应用, 可将 I/O 操作重叠, 一个线程等待多个不同的 I/O 操作, 以提高性能.
二者的区别和联系
- 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
- 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
- 进程有自己的内存地址空间, 线程只独享指令流执行的必要资源, 如寄存器和栈
- 线程的创建, 切换, 终止效率更高 .
更轻量的追求
人们不满足于线程的轻量, 因此又有了 “线程池” (ThreadPool) 和 “协程” (Coroutine) .
ThreadPool : 是一种利用池化技术思想来实现线程管理的技术, 主要是为了复用线程.
简单理解就是, 创建了一个容器, 容器里面放的是一定量的线程, 每次使用线程的时候, 不用创建, 直接从容器中取一个线程用, 用完之后不用销毁, 再放到回容器里去,以备下次使用
协程运行在线程之上, 属于是在线程基础之上通过分时复用的方式运行多个协程.
即一个线程包括多个协程, 协程可以当更小的线程取用, 并且协程的状态切换比线程更轻量 .
Java 线程和 OS 线程的关系
线程是 OS 的概念, OS 内核实现了线程这样的机制, 并且对用户层提供了一些 API 以供使用.
Java 标准库中的 Thread 类可以视为是对 OS 提供的 API , 进行了进一步的抽象和封装, 以便使用 .
运行 DEMO
运行代码
import java.util.Random; public class test1 { private static class MyThread extends Thread{ @Override public void run() { Random random = new Random(); while(true) { // 打印线程名称 System.out.println(Thread.currentThread().getName()); try { // 随机停止 0-9 秒 Thread.sleep(random.nextInt(10)); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) { MyThread t1 = new MyThread(); MyThread t2 = new MyThread(); MyThread t3 = new MyThread(); t1.start(); t2.start(); t3.start(); Random random = new Random(); while(true) { // 打印线程名称 System.out.println(Thread.currentThread().getName()); try { // 随机停止 0-9 秒 Thread.sleep(random.nextInt(10)); } catch (InterruptedException e) { e.printStackTrace(); } } } }
运行结果
main Thread-0 Thread-2 Thread-1 Thread-1 Thread-0 main Thread-2 Thread-2 Thread-1 Thread-1 main Thread-0 Thread-0 Thread-0 Thread-1 main Thread-1 Thread-2 Thread-0 ...
从运行结果可以看出, 主线程与子线程之间的运行顺序完全随机 .
线程创建
继承 Thread 类
- 线程类继承 Thread
private static class MyThread extends Thread{ @Override public void run() { System.out.println("线程运行 逻辑"); } }
- 创建线程类的实例
MyThread t = new MyThread(); //此时只是声明了我要创建子线程, 并没有真正的去分配资源啥的
3.调用 start 方法, 才真的在操作系统的底层创建出一个线程
t.start(); //真正给线程分配资源
实现 Runnable 接口
- 实现 Runnable 接口
class MyRunnable implements Runnable { @Override public void run() { System.out.println("线程运行 逻辑"); } }
- 创建 Thread 类实例, 调用 Thread 时构造方法将 Runnable 对象作为 target 参数 .
Thread t = new Thread(new MyRunnable());
- 调用 start 方法, 才真的在操作系统的底层创建出一个线程
t.start();
对比上述两种方式, 若要表示本子线程
- 继承 Thread 类, 直接使用 this 则表示当前线程对象的引用
- 实现 Runnable 接口, this 表示的时 MyRunnable 的引用, 若想表示本子进程, 需要使用 Thread.currentThread()
匿名内部类创建 Thread 子类对象
Thread t1 = new Thread() { @Override public void run() { super.run(); } };
匿名内部类创建 Runnable 子类对象
Thread t2 = new Thread(new Runnable() { @Override public void run() { System.out.println("内部代码逻辑"); } });
lambda 表达式创建 Runnable 子类对象
Thread t3 = new Thread(() -> System.out.println("内部代码逻辑")); Thread t4 = new Thread(() -> { System.out.println("内部代码逻辑"); });
Thread 类常用方法
构造方法
Thread 常见的属性
- ID 是线程的唯一标识, 不会重复
- 优先级高的线程, 理论上 更容易被调用到
- 后台线程的话记住一点: **JVM会在一个进程的所有 非后台线程 结束后, 才会结束运行 **
- 存活代表 run 方法是否运行结束
中断进程
常见两种方式(本质上没什么区别)
- 自定义一个共享标记
- 使用 interrupt() 方法来通知 (相当于系统定义的共享标记)
使用自定义的变量来作为标志位
public class test2 { private static class MyRunnable implements Runnable{ private static boolean isQuit = true; @Override public void run() { while(isQuit) { System.out.println("线程执行"); try { // 线程执行中 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } // 共享标记修改, 需要终止线程 System.out.println("线程终止"); } } public static void main(String[] args) throws InterruptedException { MyRunnable target = new MyRunnable(); Thread t = new Thread(target, "zrj"); t.start(); Thread.sleep(10 * 1000); System.out.println("需要在此刻终止线程的运行!"); target.isQuit = false; } }
使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted()
Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记
public class test3 { private static class MyRunnable implements Runnable{ @Override public void run() { // 如果该标记位没有被设置, 即没有被中断 while(!Thread.interrupted()) { System.out.println("进程执行"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); System.out.println("进程阻塞!"); break; } } // 标记位被设置, 进程被打断 System.out.println("进程终止"); } } public static void main(String[] args) throws InterruptedException { MyRunnable target = new MyRunnable(); Thread t = new Thread(target, "lty"); System.out.println("进程执行!"); t.start(); Thread.sleep(10 * 1000); System.out.println("打断进程!即设置标记位"); t.interrupt(); } }
Thread 收到通知的方式有两种
1.线程因为调用 wait / join / sleep 等方法引起的阻塞, 以异常抛出的方式通知, 清除中断标记
2.如果是内部中断标记被设置, thread 可以通过 两个判断方法来收到通知, 该方式收到通知更及时, 即使线程正在 sleep 也可以马上收到
等待一个线程 - join
public class test4 { public static void main(String[] args) throws InterruptedException { Runnable target = () -> { for (int i=1 ; i<=10; i++) { try { System.out.println("线程执行中!"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + ": 线程执行结束!"); }; Thread t1 = new Thread(target, "zrj"); Thread t2 = new Thread(target, "lty"); // t1 开始执行 t1.start(); // t1 挂起 t1.join(); // t2 开始执行 t2.start(); // t2 挂起 t2.join(); } }
attention : 对于 join 挂起的线程, 如果没有被唤醒的话, 将永久不会被调用执行
获取当前线程引用
休眠当前线程
线程的状态
NEW: 安排了工作, 还未开始行动
RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
BLOCKED: 这几个都表示排队等着其他事情
WAITING: 这几个都表示排队等着其他事情
TIMED_WAITING: 这几个都表示排队等着其他事情
TERMINATED: 工作完成了.
进程状态之间的转换
学校教学课本上的图片展示是这样的 (算是一个简略版本)
给出几个注意的点 :
BLOCKED 表示被锁住状态 ; WAITING 和 TIMED_WAITING 表示等待唤醒状态 .
TIMED_WAITING 线程在等待唤醒, 但设置了时限 ; WAITING 线程没有设置时限 (死等)
Thread.yield() 调用后, 不会改变进程状态, 但会立即让出 CPU, 重新去就绪队列排队 .
多线程 (上) - 学习笔记2:https://developer.aliyun.com/article/1518454