多线程 3

1. 线程安全

单线程和多线程执行相同的逻辑的代码,如果得到的结果是相同的就是线程安全,如果不相同就是线程不安全

2. 导致线程不安全的原因

  1. 操作系统对线程的调度是随机的
  2. 多线程对同一个变量进行修改
  3. 修改操作不是原子的

原子性 :一个操作要么完整地执行,要么完全不执行,在执行过程中不能被其他线程"插进来" ,可以理解为不可分割的 。

例如一行java代码看着好像是一行代码,但是这行代码变成机械语言的时候,就可能变成了多个 CPU 指令,在执行这些指令的过程中,可能被其他进程插队,程序就可能出现错误,这就是不满足原子性的

  1. 内存可见性

可见性是指一个线程修改共享变量,可以及时被其他线程看见

java 内存模型 :

java 进程中每个线程都有一个工作内存(work memory),这些线程会共享一个主内存(main memory)

  • 当线程修改数据的时候会把主内存中的数据拷贝到工作内存中,修改工作内存的数据后,将数据写回到主内存中
  • 当线程读取数据的时候会把主内存中的数据拷贝到工作内存中,读取工作内存的数据

由于每个线程有⾃⼰的⼯作内存,这些⼯作内存中的内容相当于同⼀个共享变量的"副本"

修改线程1的⼯作内存中的值,线程2的⼯作内存不⼀定会及时变化

这里的主内存指的是 平时说的内存; 工作内存指的是寄存器和缓存

3. 示例1

代码

定义 class Counter

java 复制代码
class Counter {
    public int count = 0;
    public void add(){
        count++;
    }
}

创建出 2 个线程 来完成 10w 次 count 的自增

java 复制代码
public static void main(String[] args) throws InterruptedException {
     Counter c = new Counter();
     Thread t1 = new Thread(() -> {
        for(int i = 1; i <= 50000; i++) {
            c.add();
        }
     });

     Thread t2 = new Thread(() -> {
         for(int i = 1; i <= 50000; i++) {
             c.add();
         }
     });

     t1.start();
     t2.start();

     t1.join();
     t2.join();
     System.out.println("count = " + c.count);

 }

发现并不是预期的结果 count = 100000

再次运行,也不是预期结果,都是 < 10w 的数字

原因

CPU 在执行 count++ 分成了 3 个 指令

  1. 从内存中读取 count 到寄存器
  2. 修改寄存器中的值count + 1
  3. 把内存器中新的count值写回到内存
    这就很可能在 线程 t1 在执行的这三个指令时,被 线程 t2 插队

由于操作系统对线程的调度是随机的,这只是其中的一种被插队可能性

synchronized 关键字

Q :怎么把修改操作打包成一个整体,让它具有原子性 ?

A : synchronized 关键字,也就是锁

锁就像是一个厕所,A 想上厕所,进入厕所把门给锁上(加锁 ),其他人如果也想用这个厕所,进不去只能在外面等待

等 A 上完厕所把厕所上的门的锁打开(解锁),其他人才能使用。

如果一个线程加锁成功, 那么其它线程就不能加锁, 进入堵塞等待的状态, 此时线程的状态就是 BLOCKED(锁竞争)

synchronized会起到互斥效果

  • 进⼊ synchronized 修饰的代码块,相当于加锁
  • 退出 synchronized 修饰的代码块,相当于解锁

只需要在 counter 类的 add 方法加上 snychronized 关键字

java 复制代码
public synchronized void add(){
    count++;
}

加锁后 ------>达到了预期的结果 :

4. 示例 2

代码

定义 2 个线程 t1 、 t2 和 1 个变量 flag

t1 中包含⼀个循环,这个循环以 flag == 0 为循环条件

t2 中从键盘读⼊⼀个整数,并把这个整数赋值给 flag .

当 flag 的值是 0 以外的数字, t1 线程结束

java 复制代码
private 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 sc = new Scanner(System.in);
          System.out.println("输入 0 以外的数字中止线程 t1");
          flag = sc.nextInt();
          System.out.println("t2 线程结束");
      });

      t1.start();
      t2.start();
  }

预期:当⽤⼾输⼊⾮0的值的时候 , t1线程结束。但是 t1线程 没有结束那说明就出现了线程安全问题

原因

内存可见性问题是由于 编译器优化 导致的

在执行 while 循环的时候:

  1. CPU 要从内存中读取数据(flag)到寄存器中
  2. 比较寄存器中的值(flag) 和 0 是否相同,如果相同就继续执行;不相同就跳转到指定位置

从内存中读取数据的指令开销的是 远大于 在寄存器中比较数据的指令开销

虽然在另一个线程中,有修改操作,但是编译器并不知道另一个线程修改的时机 , 在读取用户输入的数据之前,编译器都是读到了相同的值,于是编译器就把从 内存中读数据(flag)到寄存器中 优化成 只从寄存器中读取数据(flag), 即使是 在 t2 线程中修改了内存中数据(flag), t1 线程读取的还是寄存器中的旧的数据(flag)

如果从 JMM 的角度来说 :

线程 t1 的 while 循环判定的是工作内存中的数值,但 t2 线程修改的是主内存的数据,由于工作内存中是主内存中数据的副本,修改主内存不会影响工作内存

volatile 关键字

使用 volatile 关键字修饰某个变量,volatile 关键字只是针对内存可见性,不保证原子性 、 没有互斥的特点
告诉编译器这个变量是可变的,之后编译器针对这个变量就不会进行优化了,就会从内存中读取数据

java 复制代码
private volatile static int flag = 0;

wait & notify

wait & notify 的作用是让多个线程按照一定的顺序来执行代码逻辑

如果需要先执行 线程 A 的 逻辑1 再执行 线程 B 的逻辑2 :线程 A 要先执行 逻辑1 ,但是很可能 线程B 先被调度,线程 B 就先 wait 下,等线程A的 逻辑1 执行完 notify 线程B,线程B再执行逻辑2

wait 和 notify 是 Object 类方法, 必须是在 snychronized 代码块中使用

方法 说明
wait() 让当前线程进入等待状态(没有 notify 就一直等)
wait(long timeout) 超时后解除等待(仍需要获取锁)
notify() 如果有多个等待的线程随机唤醒一个
notifyall() 如果有多个等待的线程唤醒所有

wait()

java 复制代码
public static void main(String[] args) {
    Object obj = new Object();
    Thread t = new Thread(()->{
        synchronized (obj){
            System.out.println("wait 前");
            try {
                obj.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("wait 后");
        }
    });
    t.start();
}

调用 wait 后,线程进入堵塞状态,后面的逻辑不再执行

wait 方法会执行 :

  1. 释放当前对象的锁
  2. 让当线程进入堵塞状态
  3. 被 notify 唤醒,重新获取锁,从阻塞状态恢复到就绪状态

被加锁的对象和调用 wait() 的对象必须是同一个对象

notify()

java 复制代码
public static void main(String[] args) {
        Object obj = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (obj) {
                System.out.println("t1 wait 前");
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1 wait 后");
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (obj) {
            	  System.out.println("t2 notify 前");
            	  
                Scanner sc = new Scanner(System.in);
                System.out.println("输入任意内容触发 notify");
                sc.next();
                
                obj.notify();
                System.out.println("t2 notify 后");
            }
           
        });

        t1.start();
        t2.start();
    }

wait() 和 notify() 是怎么让代码按照一定的顺序来执行的

wait() 和 sleep() 的区别

相同点
1. 都可以让线程堵塞,可以设置堵塞的时间
不同点
1. wait() 设计出来是为了被 notify;sleep() 是为了按照一定的时间进行堵塞
2. wait() 必须搭配锁使用;sleep() 是 Thread 类的静态方法不需要搭配锁
3. wait() 会释放锁再获取锁 ;sleep() 在锁内部休眠时不会释放锁
相关推荐
CoderYanger1 小时前
A.每日一题:856. 括号的分数
java·程序人生·算法·leetcode·面试·职场和发展·学习方法
liangbo72 小时前
01-Java 集合框架全景:从 Collection 到 Map 一张关系网理清
java
xuxigifxfh2 小时前
HJ15 求int型正整数在内存中存储时1的个数
java·开发语言·华为机考
Wokoo72 小时前
自动化测试入门:核心场景、回归测试与 Web 自动化环境搭建
java·开发语言·测试工具
我是人✓2 小时前
Spring IOC注解全解析和Spring整合测试单元
java·后端·spring
智购科技无人售货机工厂店2 小时前
玻璃瓶饮料破损率突然上升,排查发现是取货口缓冲垫老化了~YH
java·开发语言·人工智能·python·eclipse
镜象科技2 小时前
中小学AI心理健康解决方案服务商怎么选?2026年选型参考
android·java·人工智能
阿狗童鞋2 小时前
高并发系统设计实战指南
java·spring boot·spring cloud
Sam_Deep_Thinking2 小时前
CountDownLatch实现原理
java·后端·面试·程序员