Java线程
- 前言——阅读10-20分钟
- 🎆1.创建和运行线程
- Thread
- Runable
- FutureTask
- 多个线程运行方式
- 📣2.不同操作系统查看进程线程的方法
- windows
- linux
- Java命令行
- 🚀3.java线程运行原理
- 栈与栈帧
- 线程上下文切换(Thread Context Switch)
- 👇4.常见的线程API
- 不推荐的方法
- 🐳5.Runable和Thread的run的区别
- 为什么有 Thread 还要有 Runable
- start和run的区别
- 🍑6.sleep和yield区别
- Sleep
- Yield
- 🌯7.sleep和wait区别
- ✨8.wait和await区别
- 📢9.Join方法
- 为什么需要Join
- 💞10.interrupt
- 打断 sleep,wait,join已阻塞线程
- 🐴11.主线程和守护线程
- 什么是守护线程
- 将线程设置为守护线程
- 🤙12.操作系统线程的5种状态
- 😜13.Java线程的6种状态
- 本章的重点在于掌握
前言——阅读10-20分钟
本文仅对Java的线程使用学习做个小结,不涉及过多底层原理和锁的实现,
🎆1.创建和运行线程
Thread
- run任务创建和start执行都由线程直接操作
// 创建线程对象
Thread t = new Thread() {public void run() {// 要执行的任务};// 启动线程
t.start();
Runable
- 任务由Runable创建
- 任务由Thread线程执行
- 达到解耦目录,执行结果是一样的
Runnable runnable = new Runnable() {public void run(){
// 要执行的任务}
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();
FutureTask
- FutureTask的作用和Runable一样
- FutureTask有返回值, Runable 无返回值
- 可以用FutureTask task 接收线程执行的返回值
- task.get()获取返回值
// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {log.debug("hello");return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);
多个线程运行方式
- 交替执行
- 谁先谁后,不由我们控制,由CPU决定
📣2.不同操作系统查看进程线程的方法
windows
任务管理器可以查看进程和线程数,也可以用来杀死进程
- tasklist 查看进程
- taskkill 杀死进程
linux
- ps -fe 查看所有进程
- ps -fT -p 查看某个进程(PID)的所有线程
- kill 杀死进程
- top 按大写 H 切换是否显示线程
- top -H -p 查看某个进程(PID)的所有线程
Java命令行
- jps 命令查看所有 Java 进程
- jstack 查看某个 Java 进程(PID)的所有线程状态
- jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
jconsole 远程监控配置
- 需要以如下方式运行你的 java 类
java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -
Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 -
Dcom.sun.management.jmxremote.authenticate=是否认证 java类
- 修改 /etc/hosts 文件将 127.0.0.1 映射至主机名
如果要认证访问,还需要做如下步骤
- 复制 jmxremote.password 文件
- 修改 jmxremote.password 和 jmxremote.access 文件的权限为 600 即文件所有者可读写
- 连接时填入 controlRole(用户名),R&D(密码
🚀3.java线程运行原理
栈与栈帧
Java Virtual Machine Stacks (Java 虚拟机栈)
我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
线程上下文切换(Thread Context Switch)
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
- 线程的 cpu 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
- 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch 频繁发生会影响性能
开启IDEA的debugger模式,这里每一行对应一个栈帧
- read方法
- main方法
- 先进后出,先运行的在栈底
一个线程对应一个方法栈
- 线程t1和t2
👇4.常见的线程API
不推荐的方法
🐳5.Runable和Thread的run的区别
- 通过组合的方式调用run,从源码角度实际都是同一个run方法
- 源码中Runable重载run,再传参给Thread,在Thread的中判断 Runable 不空,优先调用 Runable 的方法
- 否则就是Thread的run方法,但其实Runable的run方法的内部还是Thread的run方法
- 直接在Thread重载run方法,则运行时运行重载的方法
为什么有 Thread 还要有 Runable
因为有的场景下我们需要把任务和执行两者分开解耦
- Thread创建run任务并start执行,任务和执行都耦合在Thread对象中
- Runable用来创建任务,以传参的方式将 Runable所带的任务传入Thread,然后让Thread执行就实现了任务和执行的解耦
有人会问什么情况下需要这种分离的玩意?
- 消息队列: 生产者消费者就是执行者,队列里面的就是任务
- 凡是需要创建任务和执行任务分离的场景,都可以尝试使用进行解耦
start和run的区别
- 直接调用 run 是在主线程中执行了 run,没有启动新的线程
- 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
- start并不会马上执行,只是让线程进入就绪态,只有他抢到CPU才能执行
🍑6.sleep和yield区别
Sleep
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行,而是进入 Runnable 就绪状态
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
- 不会释放锁,只跟线程相关
Yield
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
2. 具体的实现依赖于操作系统的任务调度器
🌯7.sleep和wait区别
- 涉及到锁,此篇只涉及线程,在后篇补充
✨8.wait和await区别
- 涉及到锁,此篇只涉及线程,在后篇补充
📢9.Join方法
为什么需要Join
- 因为多线程是并发执行的,每个执行完成的顺序是不能确定的。
- 意思如果有线程调用 t2.join()并运行到t2.join()这个位置会阻塞,直到t2线程执行结束,当前线程才能继续往下执行
private static void test2() throws InterruptedException {Thread t1 = new Thread(() -> {sleep(1);r1 = 10;});Thread t2 = new Thread(() -> {sleep(2);r2 = 20;});long start = System.currentTimeMillis();t1.start();t2.start();t1.join();---- // 运行到此卡住,直到t1执行完t2.join();-----// 运行到此卡住, 直到t2执行完long end = System.currentTimeMillis();log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
💞10.interrupt
interrupt打断线程分两种情况:
- 打断正常运行的线程
- 打断sleep,wait,join 已阻塞线程
打断 sleep,wait,join已阻塞线程
首先声明不使用stop()打断线程的原因
- stop()打断线程会直接杀死线程,不会给线程释放锁dd料理后事的机会
- 资源得不到释放
如下例子就是可以处理后事线程
- sleep,wait,join在这里可以看成是一样的阻塞态
- (当然三者的阻塞肯定在细节上有不同,但是这里只要是阻塞就行了)
class TPTInterrupt {private Thread thread;public void start(){thread = new Thread(() -> {while(true) {Thread current = Thread.currentThread();if(current.isInterrupted()) {log.debug("料理后事");break;}try {Thread.sleep(1000);log.debug("将结果保存");} catch (InterruptedException e) {current.interrupt();}
// 执行监控操作}},"监控线程");thread.start();}public void stop() {thread.interrupt();}}--------------------------------------------------TPTInterrupt t = new TPTInterrupt();
t.start();
Thread.sleep(3500);
log.debug("stop");
t.stop();
主要依赖这三个函数的特点"处理后事"
- 因为sleep,wait,join也会设置打断标记
- 而处理后事的判断条件就是打断标记
- 通过这个标记来手动做return结束线程
- 而不是直接stop杀死进程
🐴11.主线程和守护线程
什么是守护线程
- 默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。
- 有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
比如
- 垃圾回收器线程就是一种守护线程
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求,直接结束
将线程设置为守护线程
Thread t1 = new Thread(() -> {
log.debug("开始运行...");
sleep(2);
log.debug("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);----------//设置为守护线程
t1.start();
🤙12.操作系统线程的5种状态
😜13.Java线程的6种状态
- NEW 线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
- BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换详述
- TERMINATED 当线程代码运行结束
本章的重点在于掌握
- 线程创建
- 线程重要 api,如 start,run,sleep,join,interrupt 等
- 线程状态
- 应用方面
- 异步调用:主线程执行期间,其它线程异步执行耗时操作
- 提高效率:并行计算,缩短运算时间
- 同步等待:join
- 统筹规划:合理使用线程,得到最优效果
- 原理方面
- 线程运行流程:栈、栈帧、上下文切换、程序计数器
- Thread 两种创建方式 的源码
- 模式方面
- 终止模式之两阶段终止
- 6种状态