万网网站空间服务范围及费用,工业设计手绘,做网站各个流程,建设新网站目录 前情回顾
1. 中断一个线程
1.1 中断的API
1.2 小结
2. 等待一个线程 2.1 等待的API
3. 线程的状态
3.1 贯彻线程的所有状态
3.2 线程状态和状态转移的意义
4. 多线程带来的的风险-线程安全 (重点)
4.1 观察线程不安全
4.2 线程安全的概念
4.3 线程不安全的原因…目录 前情回顾
1. 中断一个线程
1.1 中断的API
1.2 小结
2. 等待一个线程 2.1 等待的API
3. 线程的状态
3.1 贯彻线程的所有状态
3.2 线程状态和状态转移的意义
4. 多线程带来的的风险-线程安全 (重点)
4.1 观察线程不安全
4.2 线程安全的概念
4.3 线程不安全的原因
4.3.1 修改共享数据
4.3.2 原子性
4.3.3 可见性
4.3.4 代码顺序性
4.4 解决之前的线程不安全问题 前情回顾
操作系统、进程和线程_木子斤欠木同的博客-CSDN博客
深入浅出Java的多线程编程——第一篇_木子斤欠木同的博客-CSDN博客
让我们来回顾一下第一篇多线程的内容
1. 多线程
1线程的概念
2进程和线程的区别
3Java代码如何创建线程 ①继承Thread重写run ②实现Runnable接口重写run将该实现类作为参数传给Thread的构造方法 ③继承Thread匿名内部类 ④实现Runnable匿名内部类 ⑤lambda表达式 2. Thread的常用属性
start方法真正从系统这里创建一个线程新的线程将会执行run方法。
run方法表示线程的入口方法是啥线程启动起来要执行哪些逻辑run方法不是让程序猿调用的要交给系统去自动调用【换个角度理解我们可以把线程的run方法理解为main方法都是系统去自动调用】 1. 中断一个线程 李四一旦进到工作状态他就会按照行动指南上的步骤去进行工作不完成是不会结束的。但有时我们需要增加一些机制例如老板突然来电话了说转账的对方是个骗子需要赶紧停止转账那张三该如何通知李四停止呢这就涉及到我们的停止线程的方式了。 本质上来说让一个线程终止办法就一种让该线程的入口方法执行完毕也就是让run跑完
目前常见的有以下两种方式
通过共享的标记来进行沟通调用 interrupt() 方法来通知【记住只是通知而已】
示例1使用自定义的变量来作为标志位
需要给标志位上加 volatile 关键字(这个关键字的功能后面介绍). public static volatile boolean isQuit false;public static void main(String[] args) throws InterruptedException {Thread t new Thread(()-{while (!isQuit){System.out.println(Thread.currentThread().getName() : 别管我我忙着转正);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() : 啊险些误了大事);});System.out.println(Thread.currentThread().getName() : 让李四开始转账);t.start();Thread.sleep(10*1000);System.out.println(Thread.currentThread().getName() : 老板来电话了得赶紧通知李四对方是个骗子);isQuit true;} 这里的 public static volatile boolean isQuit false; 为什么需要加static因为main函数被static修饰所以在main内部用到的成员变量要加static 示例2使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位。
1.1 中断的API
Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记。
方法说明pubilc void interrupt()中断对象关联的线程如果线程正在阻塞会把线程唤醒则以异常的方式通知然后吧标志位设置为truepublic static boolean interrupted()判断当前线程的中断标志位是否设置调用后清除标志位public boolean isInterrupted()判断对象关联的线程的标志位是否设置调用后不清除标志位
使用 thread 对象的 interrupted() 方法通知线程结束.
public class Thread4 {public static void main(String[] args) {Thread t new Thread(() - {while(!Thread.currentThread().isInterrupted()){System.out.println(hello t);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t.start();try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}t.interrupt();}
} 我们可以发现调用t.interrupt方法的时候线程并没有真的结束而是打印了个异常信息又继续执行了 1.2 小结
interrupt方法的作用
1设置标志位为true
2如果该线程正在阻塞中比如执行了sleep此时就会把阻塞状态唤醒通过抛出异常的方式让sleep立即结束
注意一个非常重要的问题当sleep被唤醒的时候只有sleep被唤醒才会重置标志位sleep自动地把isInterrupted标志位给清空了true - false这导致下次循环循环仍然可以继续执行了~~
有的开关是按下之后就按下去了
有的开关是按下去之后自动弹起sleep就属于这种 一种极端的情况 如果设置interrupt的时候恰好sleep刚醒这个时候赶巧了执行到下一轮循环条件就直接结束了。但是这种概率非常低毕竟sleep的时间已经占据了整个循环体的99.999%的时间了 如果需要结束循环就得在catch中搞个break
public class Thread4 {public static void main(String[] args) {Thread t new Thread(() - {while(!Thread.currentThread().isInterrupted()){System.out.println(hello t);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();break;}}});t.start();try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}t.interrupt();}
}小结一下
如果sleep执行的时候看到这个标志位是falsesleep正常进行休眠操作
如果当前的标志位为true
sleep无论是刚刚执行还是已经执行了一般都会触发两件事
1立即抛出异常
2清空标志位为false
再下次循环到sleep
由于当前标志位本身是false就啥也不干~~
总结到这里就有小伙伴有疑问了为什么sleep要清空标志位呢
目的就是为了让线程自身能够对于线程何时结束有一个更明确的控制~~
当前interrupt方法效果不是让线程立即结束而是告诉他你该结束了至于他是否真的要立即结束还是等会结束都是由它本线程的代码来控制~~interrupt只是通知而非“命令” 有的朋友就好奇了我如果不加sleep这些能令线程阻塞的代码那是不是就能让该线程直接结束呢对的是可以但是工作中没人会这么写代码一个不可控的线程是多么可怕 这里就可以又引出一个问题java为啥不强制制定“命令结束”的操作呢
只要调用interrupt就立即结束
答主要是设定成这种对线程来说非常不友好~线程t何时结束一定是t自己要最清楚交给t自身来决定比较好 2. 等待一个线程
有时我们需要等待一个线程完成它的工作后才能进行自己的下一步工作。例如张三只有等李四转账成功才决定是否存钱这时我们需要一个方法明确等待线程的结束。
public class Thread5 {public static void main(String[] args) throws InterruptedException {Runnable run () - {for(int i 0;i 3;i){try {System.out.println(Thread.currentThread().getName() : 我正在工作);Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() : 我的工作结束了);};Thread t1 new Thread(run,李四);Thread t2 new Thread(run,张三);System.out.println(李四先工作);t1.start();t1.join();System.out.println(李四做完了张三开始工作!);t2.start();t2.join();System.out.println(王五做完了张三开始工作!);System.out.println(两人都工作结束了);}
}在main线程中调用t1.join()表示main线程要等t1线程跑完main线程才可以继续执行。 1main线程调用t1.join()的时候如果t1还在运行此时main线程阻塞知道t执行完毕t1的run执行完了main线程才从阻塞中解除才继续执行。 2main线程调用t.join()的时候如果t已经结束了此时join就不会阻塞会立即执行下去。 如果把两个join方法注释掉就会CPU的抢占式调用的典型例子
public class Thread5 {public static void main(String[] args) throws InterruptedException {Runnable run () - {for(int i 0;i 3;i){try {System.out.println(Thread.currentThread().getName() : 我正在工作);Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() : 我的工作结束了);};Thread t1 new Thread(run,李四);Thread t2 new Thread(run,张三);System.out.println(李四先工作);t1.start();
// t1.join();System.out.println(李四做完了张三开始工作!);t2.start();
// t2.join();System.out.println(王五做完了张三开始工作!);System.out.println(两人都工作结束了);}
} 2.1 等待的API
方法说明public void join()等待线程结束public void join(long millis)等待线程结束最多等 millis 毫秒public void join(long millis, int nanos)同理但可以更高精度 3. 线程的状态
3.1 贯彻线程的所有状态
线程的状态是一个枚举类型 Thread.State
public class Thread6 {public static void main(String[] args) {for (Thread.State state:Thread.State.values()) {System.out.println(state);}}
} NEW: 安排了工作, 还未开始行动RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.BLOCKED: 这几个都表示排队等着其他事情WAITING: 这几个都表示排队等着其他事情TIMED_WAITING: 这几个都表示排队等着其他事情TERMINATED: 工作完成了.
3.2 线程状态和状态转移的意义 大家不要被这个状态转移图吓到我们重点是要理解状态的意义以及各个状态的具体意思。 举个栗子 刚把李四、王五找来还是给他们在安排任务没让他们行动起来就是 NEW 状态 当李四、王五开始去窗口排队等待服务就进入到 RUNNABLE 状态。该状态并不表示已经被银行工。 作人员开始接待排在队伍中也是属于该状态即可被服务的状态是否开始服务则看调度器的调度; 当李四、王五因为一些事情需要去忙例如需要填写信息、回家取证件、发呆一会等等时进入 BLOCKED 、 WATING 、 TIMED_WAITING 状态至于这些状态的细分我们以后再详解 如果李四、王五已经忙完为 TERMINATED 状态。所以之前我们学过的 isAlive() 方法可以认为是处于不是 NEW 和 TERMINATED 的状态都是活着的。 4. 多线程带来的的风险-线程安全 (重点)
本质是因为线程之间的调度顺序的不确定性
4.1 观察线程不安全
public class Thread7 {static int count 0;public static void main(String[] args) {Thread t1 new Thread(() - {for(int i 0;i 5000;i){count;}});Thread t2 new Thread(() - {for(int i 0;i 5000;i){count;}});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(count);}} 由于当前这两线程调度的顺序是无序的~~ 你也不知道这两线程自增的过程中到底经历了什么 有多少次是“顺序执行”有多少次是“交错执行”不知道 得到的结果是啥也就是不确定的 这里就引出了一个问题出现bug之后得到的结果一定是 10000或者结果一定是 5000 CPU调用是以原语为单位的
4.2 线程安全的概念
想给出一个线程安全的确切定义是复杂的但我们可以这样认为如果多线程环境下代码运行的结果是符合我们预期的即在单线程环境应该的结果则说这个程序是线程安全的。
4.3 线程不安全的原因
4.3.1 修改共享数据
上面的线程不安全的代码中, 涉及到多个线程针对 counter.count 变量进行修改. 此时这个 count 是一个多个线程都能访问到的 共享数据 count 这个变量就是在堆上. 因此可以被多个线程共享访问. 4.3.2 原子性 什么是原子性 我们把一段代码想象成一个房间每个线程就是要进入这个房间的人。如果没有任何机制保证A进入房间之后还没有出来B 是不是也可以进入房间打断 A 在房间里的隐私。这个就是不具备原子性的。 那我们应该如何解决这个问题呢是不是只要给房间加一把锁A 进去就把门锁上其他人是不是就进不来了。这样就保证了这段代码的原子性了。有时也把这个现象叫做同步互斥表示操作是互相排斥的。
一条 java 语句不一定是原子的也不一定只是一条指令
比如刚才我们看到的 n其实是由三步操作组成的
从内存把数据读到 CPU进行数据更新把数据写回到 CPU
不保证原子性会给多线程带来什么问题 如果一个线程正在对一个变量操作中途其他线程插入进来了如果这个操作被打断了结果就可能是错误的。 这点也和线程的抢占式调度密切相关. 如果线程不是 抢占 的, 就算没有原子性, 也问题不大。 4.3.3 可见性
可见性指一个线程对共享变量值的修改能够及时地被其他线程看到。
Java 内存模型 (JMM): Java虚拟机规范中定义了Java内存模型. 目的是屏蔽掉各种硬件和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致的并发效果. 线程之间的共享变量存在 主内存 (Main Memory).每一个线程都有自己的 工作内存 (Working Memory) .当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.
由于每个线程有自己的工作内存, 这些工作内存中的内容相当于同一个共享变量的 副本. 此时修改线程1 的工作内存中的值, 线程2 的工作内存不一定会及时变化.
1) 初始情况下, 两个线程的工作内存内容一致. 2) 一旦线程1 修改了 a 的值, 此时主内存不一定能及时同步. 对应的线程2 的工作内存的 a 的值也不一定能及时同步. 这个时候代码中就容易出现问题.
此时引入了两个问题:
为啥要整这么多内存?为啥要这么麻烦的拷来拷去?
1) 为啥整这么多内存? 实际并没有这么多 内存. 这只是 Java 规范中的一个术语, 是属于 抽象 的叫法. 所谓的 主内存 才是真正硬件角度的 内存. 而所谓的 工作内存, 则是指 CPU 的寄存器和高速缓存.
2) 为啥要这么麻烦的拷来拷去? 因为 CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级, 也就是几千倍, 上万倍) 比如某个代码中要连续 10 次读取某个变量的值, 如果 10 次都从内存读, 速度是很慢的. 但是如果只是第一次从内存读, 读到的结果缓存到 CPU 的某个寄存器中, 那么后 9 次读数据就不必直接访问内存了效率就大大提高了 那么接下来问题又来了, 既然访问寄存器速度这么快, 还要内存干啥?? 答案就是一个字: 贵 值的一提的是, 快和慢都是相对的. CPU 访问寄存器速度远远快于内存, 但是内存的访问速度又远远快于硬盘。 对应的, CPU 的价格最贵, 内存次之, 硬盘最便宜。 4.3.4 代码顺序性
什么是代码重排序 一段代码是这样的
去前台取下 U 盘去教室写 10 分钟作业去前台取下快递
如果是在单线程情况下JVM、CPU指令集会对其进行优化比如按 1-3-2的方式执行也是没问题可以少跑一次前台这种叫做指令重排序。 编译器对于指令重排序的前提是 保持逻辑不发生变化. 这一点在单线程环境下比较容易判断, 但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价. 重排序是一个比较复杂的话题, 涉及到 CPU 以及编译器的一些底层工作原理, 此处不做过多讨论 4.4 解决之前的线程不安全问题
public class Thread7 {static int count 0;public static void main(String[] args) {Object o new Object();Thread t1 new Thread(() - {synchronized (o) {for(int i 0;i 5000;i){count;}}});Thread t2 new Thread(() - {synchronized (o) {for(int i 0;i 5000;i){count;}}});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(count);}}