Java之多线程----二

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;
    }

![在这里插入图片描述](https://img-blog.csdnimg.cn/6455457ff8f949ed92f16c2b230d5601.png)

当前是使用 自己创建的变量 来控制循环 , 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();
    }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mJ6iVcDq-1679129688804)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\1679045832696.png)]

为什么第二次循环不会抛出异常 ?

调用 interrupt , 设置中断标志位 .

sleep 第一次执行 , 清空了 标志位 , 并抛出异常

sleep 第二次执行 , 没有这个中断的标志位了 主线程并非是循环反复设置 , 而是只执行一次

如果 sleep 执行的时候看到这个标志位是 false , sleep 正常进行休眠操作

如果当前标志位为 true : sleep 无论是刚刚执行 还是已经执行了一半 , 都会触发两件事情 : 1 . 立即抛出异常 2 . 清空标志位为 false . 再下次循环 , 到 sleep 由于当前 标志位就是 false ,就 休眠 状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ydZ9hH8P-1679129688805)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\1679046275594.png)]

主线程执行完 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");

    }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dzaECifG-1679129688806)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\1679049632783.png)]

在 t.join 执行的时候 , 如果 t 线程还没结束 , main 线程就会 阻塞等待 (Blocking)

代码走到这一行就停下来 , 当前这个线程不参与 CPU 的调度执行了 别的线程不受影响

t.join :

  1. main 线程调用 t.join 的时候 , 如果 t 还在运行 , 此时 main 线程收到阻塞 , 直到 t 执行完毕 ( t 的 run 执行完了 ) , main 才会从阻塞中接触 , 才继续执行
  2. main 线程调用 t. join 的时候,如果 t 已经结束了 , 此时 join 不会阻塞, 就会立即往下执行

3.线程的状态

​ 操作系统里面的线程 , 自身是有一个状态的 . 但是 Java Thread 是对系统线程的封装 , 把这里的状态又进一步的精细化了

​ NEW 系统中的线程还没创建出来 , 只是有个 Thread 对象

​ TEMINATED 系统中的线程已经执行完了 , Thread 对象还在

​ RUNNABLE 就绪状态 :①正在 CPU 上运行 ②准备好随时可以去 CPU 上运行

​ BLOCKED 表示等待锁出现的状态

​ WAITING 使用 wait 方法出现的状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OJUwYe6h-1679129688806)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\1679050468757.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6npnp2kE-1679129688807)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\1679050478264.png)]

状态的转变 :

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3neTLGmR-1679129688808)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\1679050503395.png)]

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 指令构成 :

  1. load , 把内存中的数据读取到 cpu 寄存器中

  2. add , 就是把寄存器中的值 , 进行 + 1 运算

  3. 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 代码块 , 就会触发 解锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RPIrZ5JK-1679129688808)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\1679122558532.png)]

如果两个线程 , 针对于不同对象加锁 , 此时不会存在锁惊竞争 , 各自获取各自的锁即可 , ( ) 里面的锁对象 , 可以是写作任意一个 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 加锁了 , 而是 给 类对象 加锁

  1. 手动指定一个锁对象

    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 禁止指令重排序 : 指令重排序 , 也是编译器优化的策略 , 调整了代码执行的顺序 , 让程序更高效 , 前提也是保证整体逻辑不变

























































































































































































  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值