线程的概念
前言: 一个程序运行起来,就会对应一个进程,例如,启动一个 Java 程序,就会创建一个 Java 进程。进程也被称为系统分配资源的基本单位。
一个进程可以包含一个线程,也可以包含多个线程(不能包含0个线程);线程是系统调度执行的基本单位,也被称为轻量级(Lignt Weight Process)
创建线程
关于线程的创建,各位亲们,请详见博主的另一篇博客《Java 中创建线程(Thread)的五种方法》
使用多线程的原因
主要原因
- 单核CPU的发展遇到了瓶颈,要想提高算力,就需要多核CPU,而并发编程能更充分利用多核CPU资源,将计算逻辑分配到多个处理器核心上,显著减少程序的运行时间,并且随着更多处理器核心的加入而变得更加高效
- 有些任务场景需要"等待IO",为了让等待IO的时间能够去做一些其他的工作,也需要用到并发编程。
理解: 就比如一些复杂的业务逻辑,当你在京东App上购物时,你购买一个东西时就涉及到了一笔订单的创建:插入订单数据,生成订单快照,发送邮件通知卖家和记录货品销售数量等。作为用户的你,从确认订购开始,就要等待上述的操作全部完成才能看到订购成功的结果。
解决: 使用多线程技术,将比如生成订单快照,发送邮件等数据一致性不强的操作派发给其它的线程,就可以显著缩短响应时间,从而提示用户购物体验
次要原因: 虽然多进程也能实现并发编程,但是线程比进程更轻量,同一个进程的线程之间,共享了资源 ( 内存+文件描述符表等)
体现如下:
1.创建线程比创建进程更快;
2.销毁线程比销毁进程更快;
3.调度线程比调度进程更快;
进程的状态
Java 线程的状态
状态名称 | 说明 |
---|---|
NEW | Thread 对象有了,但还没调用 start(),系统内部的线程还未创建 |
RUNNABLE | 就绪状态:指的是这个线程“随叫随到” 以下 2 种情况: 1. 这个线程正在 CPU 执行 2. 这个线程虽然没在 CPU 执行,但是随时可以调度到 CPU 上执行 |
TERMINATED | 线程已经终止了,内核中的线程已经销毁了,但是 Thread 对象还在 |
WAITING | 阻塞:死等进入的阻塞 |
TIMED_WAITING | 阻塞:在进行带有超时时间的等待 |
BLOCKED | 阻塞:进行锁竞争的时候产生的阻塞 |
注:只要线程出现WAITING
,TIMED_WAITING
, BLOCKED
中的任意一个状态,都是阻塞,只是产生这几个状态的原因不一样
作图理解:
启动线程
线程对象在初始化完成之后,调用 start() 方法就可以启动。
线程 start() 方法的含义是:当前线程同步告知 Java 虚拟机,只要线程规划器空闲,应立即启动调用 start() 方法的线程。
调用 start 方法,才真的在操作系统的底层创建出一个线程
中断/终止线程
中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。
终止线程,在Java中 都只是"提醒,建议",真正要不要终止,还得线程本体来进行决定的!!
t 线程,正在执行,其他线程,只能提醒一下t是不是要终止了,t 收到这样的提醒之后,也还是得自己决定的。终止就好比其他线程对该线程打了一个招呼
停止线程的常见方式有以下2种:
- 通过共享的标记来进行沟通
- 调用 interrupt() 方法来通知
示例1: 使用自定义的变量来作为标志位
核心思路:让需要终止的线程的入口方法尽快执行结束(跳出循环,还是尽快return都可以)
代码示例:
package Thread;/*** Created with IntelliJ IDEA.* Description:* User: fly(逐梦者)* Date: 2024-03-29* Time: 10:44*/
public class Demo10 {private static boolean isRunning = true;public static void main(String[] args) {Thread t = new Thread(()-> {while (isRunning){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t 线程已经结束了");});t.start();try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}// 3s 之后,主线程修改 isRunning 的值,从而通知 t 结束System.out.println("控制 t 线程结束");isRunning = false;}
}
这里的isRunning
不能设置为局部变量,只能设置为静态成员变量
原因: 涉及到变量捕获,作为lambda或者匿名内部类,都能捕获到外面一层作用域中的变量名,就可以使用的。
但是变量捕获,有一个前置条件,就是要求变量得是final(常量)或者“事实" final",isRunning
变量涉及到修改操作,变量捕获失败,javac 无法编译通过,编译器会给你爆红提示报错;
设置为静态成员变量就不是变量捕获了,而是内部类访问外部类的成员,此时 lambda 本质上就是一个匿名内部类,实现了函数式接口
上述代码的缺点: 有些情况下,main 线程是无法及时把 t 线程终止掉
代码运行结果:
示例2: 使用 Thread.interrupted() 代替自定 义标志位
刚才是定义了一个boolean变量,实际上Thread 里面内置了一个,使用内置的标志位,功能要更强大
代码示例:
package Thread;/*** Created with IntelliJ IDEA.* Description:* User: fly(逐梦者)* Date: 2024-03-29* Time: 11:19*/
public class Demo11 {public static void main(String[] args) {Thread t = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {
// throw new RuntimeException(e);e.printStackTrace();}}});t.start();try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}t.interrupt();}
}
Thread.currentThread()
=> 是个static方法,这个方法,能够获取到当前线程,可以能够获取到 t 这个引用
isInterrupted()
=> 线程内置的标志位,boolean 类型变量,true 表示线程要终止了,false表示线程要继续执行
t.interrupt()
=> 这个方法,相当于示例1的设置 boolean 值为 true,并且除了能设置boolean值,还可以唤醒 sleep等阻塞的方法。即使正在sleep(10s),刚休眠1s按照示例1/第一种写法,必须再等待9s,才能让线程结束(sleep结束了,才能继续进行循环判定); 示例2/第二种写法,则立即就会让 sleep抛出一个InterruptedException异常,不会再等待,立即就唤醒了
当使用 Interrupt 方法之后,此时,要不要结束,都是t线程自己决定的!!!
代码运行结果:
现象: sleep 在抛出异常后,没有立即终止,而是继续循环打印。
原因: 出现这个现象,还是 sleep在搞鬼;
如果代码没有sleep,确实是直接修改了标志位就完了;
如果有 sleep,并且是触发 Interrupt 的时候,线程正在 sleep,sleep 被唤醒的同时,就会清除刚才的标志位(又改回false );
之所以要改回来,就是把控制权,转交给作为程序员的我们.
如果想立即结束,直接在 catch 语句中加个break, 即可。至于稍后结束,还是不想结束,稍微修改下代码就行了
相关代码:
Thread t = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {
// throw new RuntimeException(e);
// e.printStackTrace();break;}}});t.start();
运行结果:
线程等待
存在问题的代码:
public static void main(String[] args) {Thread t = new Thread(()->{System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}});t.start();for (int i = 0; i < 3; i++) {System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("main end");
}
运行结果:
分析原因
根本原因: 多个线程,在系统中的调度顺序,是无序的(抢占式执行),作为程序猿的我们,期望程序的结果是稳定的,不应该是"随机"的
希望在随机的体系上,加入一些控制,让结果变的不那么随机
问题: 多个线程什么时候被调度执行,不确定 => 多个线程谁先执行结束,不确定
解决: 通过线程等待(join),来确定线程结束的先后顺序
join 相关函数
函数返回值 | 方法及其作用 | 个人理解 |
---|---|---|
void | join() Waits for this thread to die. | 这个join无参数版本 (很少使用) 死等,不见不散 只要 t 不结束,join 就会一直等待下去 |
void | join(long millis) Waits at most millis milliseconds for this thread to die. | (常用)等待 N 毫秒 比如,写了等待10ms,如果10ms之内, t 线程结束了,直接返回 如果10ms 到了,t 还没结束,不等了,代码继续往下执行了 |
void | join(long millis, int nanos) Waits at most millis milliseconds plus nanos nanoseconds for this thread to die. | 等待 N 毫秒 又 M 纳秒 (几乎不用) |
具体实现: 由于上述代码中, main 和 t 线程,之间的结束顺序是不确定的,如果希望让代码里面的t能够先结束, main后结束,就可以在main 中使用线程等待(join)
代码如下:
package Thread;/*** Created with IntelliJ IDEA.* Description:* User: fly(逐梦者)* Date: 2024-03-30* Time: 2:06*/
public class Demo12 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {for (int i = 0; i < 3; i++) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();for (int i = 0; i < 3; i++) {System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}t.join();System.out.println("main end");}
}
运行结果:
main 中调用上述 join 方法,有以下两种可能:
1.如果t线程此时已经结束了,此时join 就会立即返回 => 没涉及到任何阻塞操作直接往下执行了
2.如果t线程此时还没结束,此时 join 就会 阻塞 等待,一直等待到t线程结束之后, join才能解除阻塞,继续执行 => 这就确保了 main 线程—定是后结束
阻塞: 该线程暂时不参与cpu调度执行,解除阻塞继续执行,线程重新参与到cpu调度了
注意:可怜的main线程 -> main 一般情况都不会被其它线程 join,即是不会被等待的 ~~ 批准:main 你什么时候才能摆脱舔狗的身份啊!!!
线程安全
批注:(#^.^#) 浅谈一下,接下来的博客会详细的讲讲多线程带来的线程安全
线程安全问题的原因:
1.抢占式执行,随机调度
2.多个线程同时修改同一个变量
3.修改操作不是原子的
4.内存可见性
5.指令重排序
解决线程安全问题: 加锁(synchronized)