当前位置: 首页 > article >正文

【JavaEE】多线程(3)

首先回顾一下线程不安全的原因:

  1. 线程是随机调度,抢占式执行的
  2. 修改共享数据,多个线程修改同一个变量
  3. 多个线程修改共享数据的操作不是原子性,(count++是3个CPU指令,但是赋值操作就是原子性的)
  4. 内存可见性问题
  5. 指令重排序

前三点已做讲解,接下来对最后两点进行讲解

一、内存可见性问题

1.1 引入概念

先来看下面的代码:

public class Demo4 {
    public static int count = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (count == 0) {
                ; //循环体为空
            }
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数");
            count = scanner.nextInt();
        });
        
        t1.start();
        t2.start();
    }
}

上述代码就是t1线程来读count,t2来修改count,以原来的逻辑来看:当把count修改为一个非0的值后,t1线程就会结束

输入之后发现,程序没有任何反应,说明t1线程并没有结束,接下来我们仍然站在指令的角度来解释,t1线程中的循环条件count == 0相当于两个指令

  • load:读取内存中的数据到CPU寄存器
  • cmp:比较寄存器中的数据,条件成立就继续执行循环体中的逻辑,不成立就跳转到另外一个地址执行

当前循环体为空,意味着循环速度很快,由于CPU访问寄存器的速度远大于访问内存的速度,所以load执行消耗的时间远多于cmp,也就是执行一次load,会执行很多次load

t2线程中是我们要手动修改count的,要知道load是计算机执行的指令,肯定比人要快很多,所以在t2修改之前会执行很多次的load,JVM发现每次load执行的结果都一样就会把load操作优化掉,后续再执行到对应的代码就不再真正load,而是直接读取load过的寄存器中的值了

上述优化的初衷是为了让程序执行的速度更快,但在多线程这里反而引起了bug

在上述代码中添加一个IO操作或者阻塞操作,循环速度就会大幅降低,也就不会优化掉load,IO操作是不会被优化的:

public static int count = 0;
Thread t1 = new Thread(() -> {
    while (count == 0) {
        System.out.println("执行IO操作");
    }
});

总结:上述问题本质是编译器/JVM优化引起的,一个线程对共享变量的修改可能不会被其他线程立即看到,导致其他线程读取到的可能是旧值,从而引发线程安全问题。这就是内存可见性问题

那么该如何解决该问题

1.2 volatile 关键字

给变量加上volatile关键字后,编译器就不会触发上述优化

public class Demo {

    public static volatile int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (count == 0) {
            }
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数");
            count = scanner.nextInt();
        });


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

}

注意:volatile只能保证内存可见性,并不能保证操作的原子性

public class Demo {
    private static volatile int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

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

}

二、线程等待通知机制

2.1 引入概念

先看下面一个ATM取钱的场景

此时小新正在取钱,发现ATM机中钱不够,于是就开锁出来,接下来就该其他人去取钱,但有可能小新觉得自己操作不对就又进去,出来之后发现又不对于是又进去,像这样某个线程频繁获取释放锁,以至于其他线程分配不到CPU资源的问题称为"线程饿死"

系统中线程调度是无序的,线程饿死的情况就有可能出现,但注意:这并不是死锁,死锁是卡死,而线程饿死只会卡住一下下

线程等待通知机制可以调整线程的执行顺序来解决这个问题,通过添加判断条件判定当前逻辑是否能够执行,如果不能就wait(主动进行阻塞)就把执行的机会让给别的线程了,避免该线程进行无意义的重试

2.2 wait()方法

wait()做的事情:

  • 使当前执行代码的线程进行等待(把线程放在等待队列中)
  • 释放当前的锁
  • 被唤醒时,重新尝试获取这个锁

可以看到wait()做的事情有释放当前的锁,也就是wait()必须放在synchronized里面,否则会抛出异常:

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        System.out.println("等待之前");
        object.wait();
        System.out.println("等待之后");
    }
}

正确的写法如下:

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        synchronized (object) {
            System.out.println("等待之前");
            object.wait();
            System.out.println("等待之后");
        }
    }
}

当前代码会一直等待下去

wait()结束等待的条件:

  • 其他线程调用该对象的notify()
  • wait等待时间超时(和sleep(1000)效果类似,wait(1000),就是等待1s后如果没有被唤醒就自动唤醒)
  • 其他线程调⽤该等待线程的interrupted⽅法,导致wait抛出InterruptedException异常
     

2.3 notify()方法

notify 方法用来唤醒等待线程的

  • 该⽅法是⽤来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁
  • 如果有多个线程等待,则有线程调度器随机挑选出⼀个呈wait状态的线程(并没有"先来后到")

看如下示例:

public class Demo {
    public static void main(String[] args) {
        Object locker = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t1等待之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1等待之后");
            }
        });

        Thread t2 = new Thread(() -> {
           synchronized (locker) {
               System.out.println("t2等待之前");
               locker.notify();
               System.out.println("t2等待之后");
           }
        });

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

}

打印结果:

注意:t2线程的notify()执行完后,并不会释放锁,而是代码走出synchronized后才会真正把锁释放t1线程拿到锁之后继续执行,因此肯定先打印t2等待之后,后打印t1等待之后

2.4 notifyAll() 方法

notifyAll 可以一次唤醒所有的等待线程

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t1等待之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1等待之后");
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t2等待之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t2等待之后");
            }
        });

        Thread t3 = new Thread(() -> {
           synchronized (locker) {
               System.out.println("t3等待之前");
               locker.notifyAll();
               System.out.println("t3等待之后");
           }
        });

        t1.start();
        t2.start();
        Thread.sleep(1000);
        t3.start();

    }

}

在t3.start()方法之前放一个sleep是为了防止t3先执行了notify,此时t1或t2还没有wait,此时直接notify没有任何效果,也不会抛异常,放一个sleep是为了保证先wait再notify

接下来看代码的执行效果

所有线程都执行结束,如果改为notify方法,再看代码的执行效果

由于notify会随机唤醒一个等待线程,这里唤醒的t1,此时t3没有被唤醒也就不会尝试获取锁,没有锁就不会继续执行接下来的逻辑,所以t3一直处于等待

如果不想让t3一直等下去,就将t3的wait改为带有时间版本的,这样时间一到就会自动被唤醒

2.5 面试题:wait() 和 sleep()的区别

  1. wait必须搭配 synchronized 来使用,否则会抛出IllegalMonitorStateException 异常,而 sleep可以在任何地方使用
  2. wait是Object类的一个普通方法,sleep是Thread类的一个静态方法
  3. 线程可以等 sleep 中的计时结束后主动唤醒,但如果是无参版本的 wait,则需要等其他线程调用 notify 或 notifyAll 来被动唤醒
  4. 调用 sleep 方法线程会进入 TIMED_WAITING 有时限等待状态,而调用无参数的 wait 方法,线程会进入 WAITING 无时限等待状态


http://www.kler.cn/a/413854.html

相关文章:

  • 深度学习模型:卷积神经网络(CNN)
  • idea2024加载flowable6.8.1.36遇到的问题-idea启动flowable问题flowable源码启动问题
  • 如何在Python中进行数学建模?
  • NAT:连接私有与公共网络的关键技术(4/10)
  • YOLOv11融合PIDNet中的PagFM模块及相关改进思路
  • java——spring容器启动流程
  • ComfyUI节点安装笔记
  • Python 中的 lambda 函数介绍
  • element ui select绑定的值是对象的属性时,显示异常.
  • 无人机:智能飞行控制系统技术与算法
  • python的数据统计与处理
  • 【JS】React与Vue的异步编程对比:深度解析与实战案例全面指南
  • 【MySQL】数据库开发技术:内外连接与表的索引穿透深度解析
  • 浅谈人工智能之基于容器云进行文生图大模型搭建
  • 【JavaEE】Spring Web MVC
  • Redis双活切换平台建设
  • React Native Android 和 iOS 开发指南
  • 51c自动驾驶~合集35
  • (vue)启动项目报错The project seems to require pnpm but it‘s not installed
  • 40分钟学 Go 语言高并发:超时控制与取消机制
  • 【多线程-第一天-多线程的技术方案-pthread带参数-桥接-bridge Objective-C语言】
  • OODA循环在网络安全运营平台建设中的应用
  • 【ESP32CAM+Android+C#上位机】ESP32-CAM在STA或AP模式下基于UDP与手机APP或C#上位机进行视频流/图像传输
  • QT5+OpenCV+libdmtx识别datamatrx ECC200二维码
  • 论文概览 |《Cities》2024.11 Vol.154(上)
  • 【tiler】一个数据可视化和地图处理切片的 Python 库