JavaEE 初阶篇-深入了解多线程安全问题(指令重排序、解决内存可见性与等待通知机制)

这篇具有很好参考价值的文章主要介绍了JavaEE 初阶篇-深入了解多线程安全问题(指令重排序、解决内存可见性与等待通知机制)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

🔥博客主页: 【小扳_-CSDN博客】
❤感谢大家点赞👍收藏⭐评论✍

JavaEE 初阶篇-深入了解多线程安全问题(指令重排序、解决内存可见性与等待通知机制),JavaEE 初级篇,java,开发语言,java-ee

JavaEE 初阶篇-深入了解多线程安全问题(指令重排序、解决内存可见性与等待通知机制),JavaEE 初级篇,java,开发语言,java-ee

文章目录

        1.0 指令重排序概述

        1.1 指令重排序主要分为两种类型

        1.2 指令重排序所引发的问题

        2.0 内存可见性概述

        2.1 导致内存可见性问题主要涉及两个方面

        2.2 解决内存可见性问题

        2.2.1 使用 volatile 关键字

        2.2.2 使用 synchronized 关键字

        3.0 线程的等待通知机制概述

        3.1 等待 - wait()

        3.2 通知 - notity()

        3.3 通知所有 - notifyAll()


        1.0 指令重排序概述

        指令重排序是指编译器或处理器为了提高性能,在不改变程序执行结果的前提下,可以对指令序列进行重新排序的优化技术。这种优化技术可以使得计算机在执行指令时更高效地利用计算资源,提高程序的执行效率。

        1.1 指令重排序主要分为两种类型

        1)编译器重排序:编译器在生成目标代码时会对源代码中的指令进行优化和重排,以提高程序的执行效率。编译器重排序时在编译阶段完成的,目的是生成更高效率的机器代码。

        2)处理器重排序:处理器在执行指令也可以对指令进行重排序,以最大程度地利用处理器的流水线和多核等特性。目的提高指令的执行效率。

        1.2 指令重排序所引发的问题

        虽然指令重排序可以提高程序的执行效率但是在多线程编程中可能会引发内存可见性问题。由于指令重排序可能导致共享变量的读写顺序与代码中的顺序不一致,当多个线程同时访问共享变量时,可能会出现数据不一致的情况。

        2.0 内存可见性概述

        在多线程编程中,由于线程之间的执行是并发的,每个线程有自己的工作内存,共享变量存储在主内存中,线程在执行过程中会将共享变量从主内存中拷贝到自己的工作内存中进行操作,操作完成后再将结果写回主内存。这里的工作内存指的是:寄存器或者是缓存。

        2.1 导致内存可见性问题主要涉及两个方面

        1)多线程并发操作抢占式执行导致内存可见性:如果一个现车给修改了共享变量的值,但其他线程无法立即看到这个修改之后的共享变量,就会导致数据不一致的情况。

        2)指令重排序导致内存可见性:由于编译器和处理器可以对指令进行重排序优化,可能会导致共享变量的读写顺序与代码中的顺序不一致,从而影响了线程对共享变量的可见性。

代码如下:

public class demo1 {

    public static int count = 0;
    public static void main(String[] args) {

        Thread t1 = new Thread(()->{
            while (count == 0){

            };
            System.out.println("线程 t1 结束");
        });

        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("输出:");
            count = scanner.nextInt();
        });

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

    }
}

        t1 在启动线程之后,只要 count == 0 这个条件满足时,就会进入循环;t2 启动线程要求输出一个值并且将该值赋值给 count 。

        预想过程:只要输出一个非 0 的值时,那么 count 不为 0 了,t1 线程中的循环就会退出,因此会输出 ”线程 t1 结束“ 这句话。最后程序结束。

运行结果:

JavaEE 初阶篇-深入了解多线程安全问题(指令重排序、解决内存可见性与等待通知机制),JavaEE 初级篇,java,开发语言,java-ee

        输出 1 之后,按理来说,count 此时应该赋值为 1 了,那么 t1 中的循环应该要结束了并且得输出一段话。但是,看到结果,即使输出了 1 之后,t1 还在循环中。

原因如下:

        由于 t1 循环中的代码块里面是没有任何代码,无需任何操作,在 CPU 中主要执行两条指令:load 将内存中的 count 加载到寄存器中;cmp 将 count 与 0 之间进行比较。

        因为 cpm 执行这条指令直接在寄存器中操作,而 load 需要将内存的数据加载到寄存器中,这个操作的速度就比 cmp 的速度慢很多很多了。所以编译器重排序在生成目标代码时对源代码中的指令进行优化重排,将 count 变量存储到寄存器或者缓存中,目的为了提高执行效率。然而,t2 线程对 count 进行重新赋值后,将重新赋值后的 count 写回到主存中,但是 t1 线程是没有看到重新赋值后的 count 变量。因为对于 t1 线程来说,count 变量已经”固定“在工作内存中,没有重新加载主存中的 count 变量,而是反复读取自己工作内存中的 count == 0 这个变量。

        总而言之,指令重排序导致了内存可见性问题。

        2.2 解决内存可见性问题

        主要有两个方法:使用 volatile 关键字、使用 synchronized 关键字。

        2.2.1 使用 volatile 关键字

        volatile 关键字可以确保被修饰的变量对所有线程可见,禁止指令重排序。

代码如下:

当给 count 加上 volatile 关键时,编译器或者处理器就不会对指令重排序了

import java.util.Scanner;

public class demo1 {

    public static volatile int count = 0;
    public static void main(String[] args) {

        Thread t1 = new Thread(()->{
            while (count == 0){

            };
            System.out.println("线程 t1 结束");
        });

        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("输出:");
            count = scanner.nextInt();
        });

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

    }
}

运行结果:

JavaEE 初阶篇-深入了解多线程安全问题(指令重排序、解决内存可见性与等待通知机制),JavaEE 初级篇,java,开发语言,java-ee

        当输出 1 回车之后,count 就会重新赋值为 1 。从而 t1 中的循环退出,输出打印之后,整个进程就结束了。

        2.2.2 使用 synchronized 关键字

        可以确保同一时刻只有一个线程可以访问共享变量,同时保证了线程间的数据一致性。

代码如下:

import java.util.Scanner;

public class demo1 {

    public static int count = 0;
    public static void main(String[] args) {
    Object o = new Object();
        Thread t1 = new Thread(()->{
            synchronized (o){
                System.out.println("线程 t1 开始");
                while (count == 0){
                    try {
                        o.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }

                };

                System.out.println("线程 t1 结束");
            }
        });

        Thread t2 = new Thread(()->{
                System.out.println("输出:");
                Scanner scanner = new Scanner(System.in);
                synchronized (o){
                    count = scanner.nextInt();
                    o.notify();
                }
        });

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

    }
}

运行结果:

JavaEE 初阶篇-深入了解多线程安全问题(指令重排序、解决内存可见性与等待通知机制),JavaEE 初级篇,java,开发语言,java-ee

        t1 线程在进入循环前会先获取对象 o 的锁,并在循环体中通过 o.wait() 释放锁并等待唤醒。当 t2 线程修改了 count 的值后,会再次获取对象 o 的锁并调用 o.notify() 唤醒 t1 线程,从而解除等待状态,保证了内存可见性和线程间的通信。

        

        3.0 线程的等待通知机制概述

        线程的等待通知机制是多线程编程中常用的一种同步机制,用于实现线程间的协作和通信。

        3.1 等待 - wait()

        线程调用对象的 wait() 方法时,会释放对象的锁并且同时进入等待状态,直到其他线程调用相同对象的 notify() 或者 notifyAll() 方法来唤醒它。在等待的过程中,线程会一直处于阻塞状态。 

        3.2 通知 - notity()

        线程调用对象的 notify() 方法时,会唤醒等待在该对象上的一个线程,若有多个等待唤醒的线程时,具体唤醒的线程是不确定的,使其从等待状态转为就绪状态,被唤醒的线程会尝试重新获取对象的锁,并继续执行。

        3.3 通知所有 - notifyAll()

        线程调用对象的 notifyAll() 方法时,会唤醒所有等待在该对象上的线程,使它们从等待状态转为就绪状态。被唤醒的线程会竞争对象的锁,只有一个线程能够获取锁并继续执行,其他线程会再次进入等待状态。

举个例子:

public class demo2 {
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Thread t1 = new Thread(()->{
            synchronized (lock){
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("正在执行 t1 线程");
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (lock){
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("正在执行 t2 线程");
            }
        });
        Thread t3 = new Thread(()->{
            synchronized (lock){
                lock.notify();
                lock.notify();
                System.out.println("正在执行 t3 线程");
            }
        });
        t1.start();
        t2.start();
        Thread.sleep(1000);
        t3.start();
    }
}

        t1 ,t2 线程都在阻塞状态,等待 t3 线程通知,但是 t3 线程还没释放锁,所以 t1 ,t2 线程继续阻塞状态。直到 t3 线程释放锁之后,t1,t2 线程就可以竞争获取锁,假设 t1 获取锁之后,执行完代码,释放锁,t1 线程结束。再到 t2 线程获取锁,执行完代码释放锁,t2 线程也结束。因此线程的先后顺序:t3 线程一定是最早结束的,接着到 t1 或者 t2 线程随机其中的一个线程。

运行结果:

JavaEE 初阶篇-深入了解多线程安全问题(指令重排序、解决内存可见性与等待通知机制),JavaEE 初级篇,java,开发语言,java-ee

补充:

        等待通知机制通常需要搭配 synchronized 关键字来确保线程安全。在Java中, wait()、notiyf() 和 notiyfAll() 方法必须在同步代码块或同步方法中调用,即在获取对象锁的情况下使用,以避免出现并发访问的问题。

JavaEE 初阶篇-深入了解多线程安全问题(指令重排序、解决内存可见性与等待通知机制),JavaEE 初级篇,java,开发语言,java-ee文章来源地址https://www.toymoban.com/news/detail-851794.html

到了这里,关于JavaEE 初阶篇-深入了解多线程安全问题(指令重排序、解决内存可见性与等待通知机制)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • JavaEE 初阶篇-深入了解 I/O 高级流(缓冲流、交换流、数据流和序列化流)

    🔥博客主页: 【 小扳_-CSDN博客】 ❤感谢大家点赞👍收藏⭐评论✍ 文章目录         1.0 缓冲流概述         1.1 缓冲流的工作原理         1.2 使用缓冲流的步骤         1.3 字节缓冲流于字符缓冲流的区别         1.4 字节缓冲流的实例         1.5 字符缓冲流的实例

    2024年04月29日
    浏览(52)
  • JavaEE 初阶篇-线程安全的集合类、多线程环境使用 ArrayList、队列、哈希表(HashMap 、ConCurrentHashMap 、HashTable 的区别)

    🔥博客主页: 【 小扳_-CSDN博客】 ❤感谢大家点赞👍收藏⭐评论✍   文章目录         1.0 线程安全的集合类         1.2 线程安全的集合类 - Vector         1.3 线程安全的集合类 - Stack         1.4 线程安全的集合类 - HashTable         2.0 多线程环境使用 ArrayList        

    2024年04月25日
    浏览(51)
  • JavaEE 初阶篇-生产者与消费者模型(线程通信)

    🔥博客主页: 【 小扳_-CSDN博客】 ❤感谢大家点赞👍收藏⭐评论✍   文章目录         1.0 生产者与消费者模型概述         2.0 在生产者与消费者模型中涉及的关键概念         2.1 缓冲区         2.2 生产者         2.3 消费者         2.4 同步机制         2.5 线程间通

    2024年04月28日
    浏览(42)
  • JavaEE 初阶篇-深入了解 CAS 机制与12种锁的特征(如乐观锁和悲观锁、轻量级锁与重量级锁、自旋锁与挂起等待锁、可重入锁与不可重入锁等等)

    🔥博客主页: 【 小扳_-CSDN博客】 ❤感谢大家点赞👍收藏⭐评论✍ 文章目录         1.0 乐观锁与悲观锁概述         1.1 悲观锁(Pessimistic Locking)         1.2 乐观锁(Optimistic Locking)         1.3 区别与适用场景         2.0 轻量级锁与重量级锁概述         2.1 真正加

    2024年04月16日
    浏览(35)
  • 【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序

    目录 第一个问题:什么是线程安全问题? 第二个问题:为什么会出现线程安全问题?  第三个问题:如何解决多线程安全问题?  第四个问题:产生线程不安全的原因有哪些?  第五个问题:内存可见性问题及解决方案  第六个问题:指令重排序问题? 线程安全就是多线程

    2024年02月01日
    浏览(65)
  • 【JavaEE初阶】 线程安全

    线程安全是多线程编程是的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且准确的执行,不会出现数据污染等意外情况。上述是百度百科给出的一个概念解释。换言之,线程安全就是某

    2024年02月08日
    浏览(52)
  • 【JavaEE初阶】 线程安全的集合类

    原来的集合类, 大部分都不是线程安全的. Vector, Stack, HashTable, 是线程安全的(不建议用), 其他的集合类不是线程安全的. 为什么不建议使用呢? 因为我们在使用的时候,这些类就会自动的加锁,虽然编译器会自动优化为没有锁竞争的线程进行锁消除的优化,但是呢万一编译器没

    2024年02月08日
    浏览(35)
  • javaEE初阶——多线程(九)——JUC常见的类以及线程安全的集合类

    T04BF 👋专栏: 算法|JAVA|MySQL|C语言 🫵 小比特 大梦想 此篇文章与大家分享多线程专题的最后一篇文章:关于JUC常见的类以及线程安全的集合类 如果有不足的或者错误的请您指出! 3.1Callable接口 Callable和Runnable一样,都是用来描述一个任务的 但是区别在于 ,用Callable描述的任务是有

    2024年04月25日
    浏览(43)
  • 【JavaEE】多线程之线程安全(synchronized篇),死锁问题

    线程安全问题 观察线程不安全 线程安全问题的原因  从原子性入手解决线程安全问题 ——synchronized synchronized的使用方法  synchronized的互斥性和可重入性 死锁 死锁的三个典型情况  死锁的四个必要条件  破除死锁 在前面的章节中,我们也了解到多线程为我们的程序带来了

    2024年02月01日
    浏览(57)
  • 【JavaEE初阶】了解JVM

    JVM启动的时候,会申请到一整个很大的内存区域.JVM是一个应用程序,要从操作系统里申请内存.JVM就根据需要,把空间分为几个部分,每个部分各自有不同的功能.具体划分如下: Native Method Stacks(本地方法栈) :native表示是JVM内部的C++代码.就是给调用native方法(JVM内部的方法)准备的栈空

    2024年02月13日
    浏览(59)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包