文章目录
Java之多线程----二
1.中断线程
中断这里就是字面意思 , 就是让一个线程停下来 . 线程的终止
本质上说 , 让一个线程终止 , 办法就一种 , 让该线程的 入口方式 执行完毕 ( return 抛出异常)
1.通过共享的标记来进行沟通
给线程中设定一个结束标志 , 把循环条件用一个变量来控制
private static boolean isQuit = false;//lambda 表达式 访问成员变量 不受变量捕获规则的限制
//若用局部变量 lambda 表达式受变量捕获的限制
public static void main(String[] args) {
Thread t = new Thread( () -> {
while (!isQuit) {
System.out.println("hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t 线程终止");
});
t.start();
//主线程中,修改 isQuit
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
isQuit = true;
}
![在这里插入图片描述
当前是使用 自己创建的变量 来控制循环 , Thread 类内置了一个标志位 , 让我们来更方便的实现上述效果~~~
2.调用 interrupt() 方法来通知
Interrupt():
public static void main(String[] args) {
Thread t = new Thread( () -> {
// currentThread 是获取到当前线程实例
// 此处 currentThread 得到的对象就是 t
// isInterrupted 就是 t 对象里自带的一个标志位.
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//e.printStackTrace();
try {
Thread.sleep(2000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
break;
}
}
});
t.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//把 t 内部的标志位设置成 true
t.interrupt();
}
为什么第二次循环不会抛出异常 ?
调用 interrupt , 设置中断标志位 .
sleep 第一次执行 , 清空了 标志位 , 并抛出异常
sleep 第二次执行 , 没有这个中断的标志位了 主线程并非是循环反复设置 , 而是只执行一次
如果 sleep 执行的时候看到这个标志位是 false , sleep 正常进行休眠操作
如果当前标志位为 true : sleep 无论是刚刚执行 还是已经执行了一半 , 都会触发两件事情 : 1 . 立即抛出异常 2 . 清空标志位为 false . 再下次循环 , 到 sleep 由于当前 标志位就是 false ,就 休眠 状态
主线程执行完 interript 就继续往后走 , 不会再执行到 interrupt
sleep 为什么要清空标志位 ???
目的就是为了让线程自身能够对于线程何时结束 , 有一个更明确的控制
当前 interrupt 方法 , 效果 , 不是让线程立即结束 , 而是告诉他 , 你该结束了 , 至于是否要立即结束 , 都是代码来灵活控制的
interrupt 只是通知 , 而不是 “命令”
用代码来控制进程是否结束:
1.无视请求 , 继续执行代码:
Thread t = new Thread(() -> {
while(!Thread.currentThread().isInterrupted()) {
System.out.println("hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//break;
}
}
});
2.立即响应请求,中断程序
Thread t = new Thread(() -> {
while(!Thread.currentThread().isInterrupted()) {
System.out.println("hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTeace():
break;
}
}
});
3.稍后中断
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//e,printStackTrace();
try {
Thread.sleep(2000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
break;
}
}
});
2.等待一个线程
线程之间是并发执行的 , 操作系统对于线程的调度 , 是无序的 . 无法判定两个线程谁先执行结束 , 谁后执行结束
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread( () -> {
System.out.println("hello t");
});
t.start();
System.out.println("hello main");
}
此时 , 先输出 hello main 还是 hello t . 是不确定的
就需要用线程等待来实现 ----join方法
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread( () -> {
System.out.println("hello t");
});
t.start();
t.join();//等待
System.out.println("hello main");
}
在 t.join 执行的时候 , 如果 t 线程还没结束 , main 线程就会 阻塞等待 (Blocking)
代码走到这一行就停下来 , 当前这个线程不参与 CPU 的调度执行了 别的线程不受影响
t.join :
- main 线程调用 t.join 的时候 , 如果 t 还在运行 , 此时 main 线程收到阻塞 , 直到 t 执行完毕 ( t 的 run 执行完了 ) , main 才会从阻塞中接触 , 才继续执行
- main 线程调用 t. join 的时候,如果 t 已经结束了 , 此时 join 不会阻塞, 就会立即往下执行
3.线程的状态
操作系统里面的线程 , 自身是有一个状态的 . 但是 Java Thread 是对系统线程的封装 , 把这里的状态又进一步的精细化了
NEW 系统中的线程还没创建出来 , 只是有个 Thread 对象
TEMINATED 系统中的线程已经执行完了 , Thread 对象还在
RUNNABLE 就绪状态 :①正在 CPU 上运行 ②准备好随时可以去 CPU 上运行
BLOCKED 表示等待锁出现的状态
WAITING 使用 wait 方法出现的状态
状态的转变 :
4.多线程带来的风险(线程安全)
1.线程不安全例子
某个代码 , 在多线程环境下执行 , 会出现 bug ==> 线程不安全
本质上因为 , 线程之间调度顺序是不确定的
//线程不安全
class Counter {
private int count = 0;
public void add() {
count++;
}
public int get() {
return count;
}
}
public class ThredDemo13 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// 两个线程 , 两个线程分别对这个 counter 自增 5w 次
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
t1.start();
t2.start();
//等待两个线程执行结束,看两个结果
t1.join();
t2.join();
System.out.println(counter.get());
}
以上代码 , 是 两个线程针对同一个变量 , 各自自增 5w 次.
运行程序 , 发现 预期结果是 10 w , 实际结果 像是一个随机值 , 每次结果不一样
实际结果和预期结果不相符 , 就是 bug
就是由多线程引起的 bug ==> 线程不安全 / 线程安全问题
但是为什么会出现以上 bug 问题呢 ???
和线程的调度随机密切相关…
count++ 操作 , 本质上就是 三个CPU 指令构成 :
-
load , 把内存中的数据读取到 cpu 寄存器中
-
add , 就是把寄存器中的值 , 进行 + 1 运算
-
save , 把寄存器中的值写回到 内存中
由于 多线程是不确定的 , 实际执行过程中 , 这两个线程中 ++ 操作的实际指令 排列顺序有很多个可能 !!!
线程安全问题 , 全是因为 , 线程的无序调度 , 导致了执行顺序不确定 , 结果就变化了
2.线程不安全的原因
1.抢占式执行
2.多个线程修改同一个变量
3.修改操作 , 不是原子的 ( 不可分割的最小单位 , 称为原子 )
像上述 ++ 操作 , 里面可以拆分成 三个 操作 load , add , save
某个操作 , 对应单个 cpu 指令 , 就是原子的 , 如果这个操作对应多个 CPU 指令 , 大概率不是原子的
4.内存可见性 , 引起线程不安全
5.指令重排序 , 引起的线程不安全
3.如何解决线程不安全
加锁
锁的核心操作有两个 : 1 . 加锁 2 . 解锁
一旦某个线程加锁了之后 , 其他线程也想加锁 , 就不能直接加上 , 就需要阻塞等待 , 一直等到拿到锁的线程释放锁了为止 ( 线程调度是抢占式执行)
Java中如何加锁 :
synchronized : java 中的关键字 , 直接使用这个关键字来实现加锁效果
public void add() {
synchornized (this) {
count++;
}
}
此处使用 代码块 的方式来表示 . 进入 synchronized 修饰的代码块 , 就会触发 加锁 , 出了 synchronized 代码块 , 就会触发 解锁
如果两个线程 , 针对于不同对象加锁 , 此时不会存在锁惊竞争 , 各自获取各自的锁即可 , ( ) 里面的锁对象 , 可以是写作任意一个 Object 对象 (内置类型不行)
此处的 this 就相当于
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
在上述代码中 , 这两个线程是在竞争同一个锁对象 , 就会产生 锁竞争 ( t1 拿到锁 , t2 就要阻塞) , 此时就能保证 count++ 操作就是原子的 , 不受影响了
加锁本质上就是把并发变成了串行的
join 和 synchronized 的区别
join 只是让两个线程完整的进行串行 , 加锁 , 两个线程的某个小部分串行了 , 大部分都是并发的 .
synchronized 的用例 :
1. 直接修饰普通方法
public void add() {
synchorized (this) {
count++;
}
}
synchronized public void add() {
count++;
}
如果直接给方法使用 synchronized 修饰 此时就相当于以 this 为锁对象
2. 修饰静态方法
public static void test2() {
synchronized (Counter.class){
}
}
synchronized public static void test() {
}
如果 synchronized 修饰 静态方法 (static) 此时就不是给 this 加锁了 , 而是 给 类对象 加锁
-
手动指定一个锁对象
private Object locker = new Object(); public void add() { synchronized (locker) { count++; } }
4.内存可见性 引起的线程不安全
1.先写个bug
public static int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (flag == 0) {
//空着
}
System.out.println("循环结束 t1结束");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数:");
flag = scanner.nextInt();
});
t1.start();
t2.start();
}
此时代码编译完毕~~~
预期效果 : t1 通过 flag == 0 作为条件进行循环 初始情况 , 将进入循环
t2 通过控制台输入一个整数 , 一旦用户输入了非 0 的值 , 此时 t1 的循 环就会立即结束 , 从而 t1 线程退出
实际效果 : 输入非 0 的值 后 , t1 线程并没有退出 , 循环没有结束 ,
2.产生此类问题的原因
内存可见性 !!
所谓的内存可见性 , 就是多线程环境下 , 编译器对于代码优化 , 产生了误判 , 从而引起了 bug
3.处理方式
让编译器针对于 这个场景暂停优化 ===> 使用 volatile 关键字
4.volatile 关键字
被 volatile 修饰的变量 , 此时编译器 就会禁止优化能够保证每次都是 从内存 重新读取数据
volatile public static int flag = 0;
加上 volatile 关键字之后 , 此时编译器就能够保证每次都是重新从内存读取 flag 变量的值 , 此时 t2 修改 flag , t1 就可以立即感知到了 , t1 就可以退出了
volatile 不保证原子性
volatile 适用的场景 , 是一个线程读 , 一个线程写的情况
synchronized 则是多个线程写
volatile 的这个效果 , 称为 “保证内存可见性”
volatile 禁止指令重排序 : 指令重排序 , 也是编译器优化的策略 , 调整了代码执行的顺序 , 让程序更高效 , 前提也是保证整体逻辑不变
flag = 0;
加上 volatile 关键字之后 , 此时编译器就能够保证每次都是重新从内存读取 flag 变量的值 , 此时 t2 修改 flag , t1 就可以立即感知到了 , t1 就可以退出了
volatile 不保证原子性
volatile 适用的场景 , 是一个线程读 , 一个线程写的情况
synchronized 则是多个线程写
volatile 的这个效果 , 称为 "保证内存可见性"
volatile 禁止指令重排序 : 指令重排序 , 也是编译器优化的策略 , 调整了代码执行的顺序 , 让程序更高效 , 前提也是保证整体逻辑不变