【Java系列】详解多线程(三)—— 线程安全(下篇)

这篇具有很好参考价值的文章主要介绍了【Java系列】详解多线程(三)—— 线程安全(下篇)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

个人主页:兜里有颗棉花糖
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 兜里有颗棉花糖 原创
收录于专栏【Java系列专栏】【JaveEE学习专栏】
本专栏旨在分享学习Java的一点学习心得,欢迎大家在评论区交流讨论💌
【Java系列】详解多线程(三)—— 线程安全(下篇),Java系列,JavaEE学习专栏,java,安全,多线程,java-ee

一、内存可见性

我们先来看一下什么是内存可见性问题,通过一段代码来进行演示:

import java.util.Scanner;
public class Demo13 {
    public static int isQuit = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while(isQuit == 0) {
                ;
            }
            System.out.println("t1线程执行结束!!!");
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入isQuit的值:");
            isQuit = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

我们期望的是上述代码中如果我们输入的值是0的话那么t1线程结束,如果输入的值是非0的话,t1线程将继续无限循环下去,可是实际的程序运行结果如下图(运行结果显示t1线程并没有结束而是继续无限循环下去):
【Java系列】详解多线程(三)—— 线程安全(下篇),Java系列,JavaEE学习专栏,java,安全,多线程,java-ee
我们通过jconsole.exe程序来看看程序运行中的t1线程的状态,如下图:
【Java系列】详解多线程(三)—— 线程安全(下篇),Java系列,JavaEE学习专栏,java,安全,多线程,java-ee
上图中的Thread-0线程就是代码中的t1线程,所以可以发现t1线程()依然是在执行执行过程中。
【Java系列】详解多线程(三)—— 线程安全(下篇),Java系列,JavaEE学习专栏,java,安全,多线程,java-ee
所以上述代码终究还是出现了bug,出现了线程安全问题,具体来说是内存可见性引起的线程安全问题。

我们首先要直到程序在编译运行的时候,java编译器和jvm会对我们写的代码进行优化,即我们的代码在实际执行的时候java编译器和jvm会在原有代码的代码逻辑不变的情况下可能会对我们编写的源代码进行修改以提高代码的执行效率。
编译器优化本质上是依靠代码来对我们写的代码进行智能的判断并进行优化,这个优化在绝大部分的情况都是很好的,都能够保证代码逻辑不变的基础上提高我们代码的执行效率,但是如果是在多线程的情况下编译器优化对我们代码做出的修改很有可能就是错误的(使程序中原有的程序发生改变)。

上述代码中的while判断站在指令的角度其实是有两个指令操作的:load(读取内存)、条件判断(条件成立则代码继续跳转到一个地方继续执行、条件不成立则代码就会跳转到另外一个地方去执行)。
另外寄存器的操作速度是极快的,而都内存操作速度就非常慢了(1次都读内存操作相当于10000次寄存器操作),所以两个操作的时间差值是极大的。
由于寄存器和读取内存之间的速度差异是非常大的,所以编译器就会对代码做出优化:即直接把load(读取内存)的操作给优化掉了,优化掉load之后,只执行第一次load操作,后续将不再执行load操作,而是直接拿寄存器中的数据进行比较判断了。但是代码中是有两个线程的,t2线程对isQuit的值进行了修改,遗憾的是t1线程由于省略了后续的load操作则无法感知到isQuit的值已经被修改了(简单来说就是t1线程无法感知到t2线程的修改),所以就出现了上述的内存可见性问题。

二、volatile关键字

Java中引入了volatile关键字来解决上述代码中的内存可见性问题。

通过volatile来修改一个变量,之后编译器就知道volatile修饰的这个变量是易变的,不再按照编译器优化的方式(即忽略后续的读内存,而直接读取寄存器中的数据),所以就能够保证t1线程在循环过程中始终能够读取到内存中的数据。
代码修改后如下:
【Java系列】详解多线程(三)—— 线程安全(下篇),Java系列,JavaEE学习专栏,java,安全,多线程,java-ee此时我们再来看代码的执行结果:【Java系列】详解多线程(三)—— 线程安全(下篇),Java系列,JavaEE学习专栏,java,安全,多线程,java-ee
volatile关键字可以保证内存可见性(禁止某一变量的读操作被优化到读寄存器中)

另外,编译器什么时候对我们的代码进行优化有时候的确是挺叫人头疼的,我们如果对上述代码进行稍微改动一下的话就不会触发编译器优化了,如下图:
【Java系列】详解多线程(三)—— 线程安全(下篇),Java系列,JavaEE学习专栏,java,安全,多线程,java-ee
上述代码中,我们加了sleep让线程t1休眠了10mm之后,此时就会影响到while循环的执行速度,当执行速度变慢之后编译器就不打算对上述代码进行优化了,此时我们不加上volatile修饰isQuit变量的话,线程t1是能够该知道线程t2通过修改isQuit变量引起的内存变化的,所以代码最终运行结果如下(和加上volatile修饰变量的运行结果是一样的):
【Java系列】详解多线程(三)—— 线程安全(下篇),Java系列,JavaEE学习专栏,java,安全,多线程,java-ee

Java内存模型图(JMM)

【Java系列】详解多线程(三)—— 线程安全(下篇),Java系列,JavaEE学习专栏,java,安全,多线程,java-ee

  • 注意:上图中的工作内存并不是冯诺依曼体系中的内存,而是cpu的寄存器和cpu的缓存在这里统称为工作内存。
  • 上图中的主内存就是我们通常说的内存。

补充一点:为什么Java官方不把工作内存称为cpu中的寄存器和cpu的缓存呢?这主要是因为Java语言是一个可以跨平台的语言,为了支持不同的操作系统,支持不同的硬件设备(比如CPU)。
CPU的架构是有很多种的,比如X86(intel、amd)、arm(苹果手机的m1芯片m2芯片)、mips。不同的CPU架构之间的差异其实挺大的,有的CPU甚至没有缓存、有的只有一级缓存。因此官方就引入了工作内存这个术语来进行表述,同时也可以通过这个术语来屏蔽硬件方面的相关信息。

综上,编译器优化、Java内存模型、多线程等都可能会引发内存可见性问题。而volatile关键字可以保证内存可见性。

synchronized能否保证内存可见性

关于synchronized能够保证内存可见性的问题是由争议的,Java官方并没有明确给这个问题设定答案,一方面也不能通过代码去很好的验证说明这个问题。
我们对上文的代码进行修改来进行举例,代码如下:

//去掉 flag 的 volatile
//t1线程循环内部加上synchronized关键字进行修饰, 并借助counter对象进行加锁
static class Counter {
    public int flag = 0;
}
public static void main(String[] args) {
    Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        while (true) {
            synchronized (counter) {
                if (counter.flag != 0) {
                    break;
               }
           }
            // do nothing
       }
        System.out.println("循环结束!");
   });
    Thread t2 = new Thread(() -> {
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入一个整数:");
        counter.flag = scanner.nextInt();
   });
    t1.start();
    t2.start();
}

运行结果如下:
【Java系列】详解多线程(三)—— 线程安全(下篇),Java系列,JavaEE学习专栏,java,安全,多线程,java-ee

上述代码并没有触发编译器优化,原因可能是因为加锁操作让t1的while循环变慢了,也可能是因为synchronized保证了内存可见性。但是究竟是哪种原因我们并没有一个很好的方法来进行能判断。
关于这个问题最好持有一个保留意见比较好。

三、wait和notify

我们知道,多线程的调度是随机的,线程与线程之间是抢占式执行的,因此线程之间的执行顺序我们是很难预料到的。但是作为开发者很多时候我们希望多个线程可以按照我们规定的执行顺序去进行执行,以便完成线程之间的相互配合工作。

waitnotify是多线程编程中的用来协调线程执行顺序的重要工具。

  • wait(): 让当前线程进入等待状态。
  • notify(): 唤醒在当前对象上等待的线程。
  • wait, notify都是Object类的方法。

【Java系列】详解多线程(三)—— 线程安全(下篇),Java系列,JavaEE学习专栏,java,安全,多线程,java-ee
上图中,wait这里有一个异常(InterruptedException)需要我们处理,这里直接抛出异常即可。
wait引起线程阻塞之后可以使用interrupt方法把线程唤醒,即打断当前线程的阻塞状态。

我们通过一段代码进行举例:

【Java系列】详解多线程(三)—— 线程安全(下篇),Java系列,JavaEE学习专栏,java,安全,多线程,java-ee

wait在执行的时候会做三件事情:

  • 第一步:解锁操作:object.wait会尝试对object对象进行解锁操作。
  • 第二步:线程进入阻塞等待状态。
  • 当被其它线程唤醒之后就会尝试重新加锁,加锁成功之后wait就执行完毕了,然后继续执行其它代码逻辑即可。

在 Java 中,每个对象都有一个监视器锁(monitor)。线程只有在获得了对象的监视器锁后才能执行 wait()、notify()、notifyAll() 等方法。如果线程尝试在未持有该对象的监视器锁时调用这些方法,就会抛出 IllegalMonitorStateException 异常。

代码改正之后如下图:
【Java系列】详解多线程(三)—— 线程安全(下篇),Java系列,JavaEE学习专栏,java,安全,多线程,java-ee
上图代码之所以什么都没有打印出来是因为代码执行到wait的时候线程出现了阻塞。我们可以通过jconsole.exe程序来看到:【Java系列】详解多线程(三)—— 线程安全(下篇),Java系列,JavaEE学习专栏,java,安全,多线程,java-ee
代码中的wait会一直阻塞等待到其它线程进行notify。

使用notify方法唤醒线程

这里我们通过一段代码进行举例(使用notify方法唤醒线程):

public class Demo16 {
    private static Object object = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (object) {
                while(true){
                    System.out.println("t1 wait 开始!!!");
                    try {
                        object.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("t1 wait 结束!!!");
                }
            }
        });
        t1.start();
        Thread t2 = new Thread(() -> {
            while(true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (object) {
                    System.out.println("t2 notify开始!!!");
                    object.notify();
                    System.out.println("t2 notify结束!!!");
                }
            }
        });
        t2.start();
    }
}

运行结果如下:
【Java系列】详解多线程(三)—— 线程安全(下篇),Java系列,JavaEE学习专栏,java,安全,多线程,java-ee

代码解释:在代码中,有两个线程 t1 和 t2。线程 t1 在获取 object 对象的监视器锁后,调用了 object.wait() 进入等待状态。线程 t2 在执行的过程中每隔一秒就获取 object 对象的监视器锁,然后调用 object.notify() 方法来唤醒等待中的线程。

上述代码注意事项如下:

  • 要想让notify唤醒wait,就需要保证wait和notify使用的是同一个对象调用的。
  • 这一点需要注意:wait和notify都需要放到synchronized之内,即使notify不涉及到解锁操作,但是java强制要求notify必须要放到synchronized内部。(系统中的原生API并没有这样的要求)
  • 如果进行notify操作的时候,另一个线程并没有处于wait状态,此时notify的操作并没有任何的副作用。
  • 倘若在某个场景中有n个线程正在wait,外加一个线程负责notify操作,那么此时调用一次notify只能唤醒一个线程,但是唤醒的线程是随机的。当然这里java提供了另外一个方法:notifyAll,此方法可以唤醒所有处于wait中的线程
  • 如果我们想唤醒某个指定的线程,就可以让不同的线程使用不同的对象来进行wait操作,像唤醒谁就可以使用对应的对象来notify。

四、wait和sleep之间的区别

  • sleep达到一定时间之后就会被唤醒,也可以被interrupt提前唤醒。
  • wait默认下会进行“死等”,直到其它线程对其进行notify唤醒(此唤醒相当于顺理成章的唤醒,唤醒之后继续执行其它任务);而sleep是能够被interrupt提前唤醒的(此唤醒相当于告知该线程要结束了,线程需要进入收尾工作)。
  • 协调多个线程之间的执行顺序,优先考虑使用wait、notify;而不是sleep。
  • wait是用于线程之间通信的,而sleep只是让某一线程阻塞一段时间。
  • wait 需要搭配synchronized;使用sleep不需要。
  • wait是Object的方法;sleep是 Thread静态方法

好了,本文到这里就结束了,希望友友们可以支持一下一键三连哈。嗯,就到这里吧,再见啦!!!

【Java系列】详解多线程(三)—— 线程安全(下篇),Java系列,JavaEE学习专栏,java,安全,多线程,java-ee文章来源地址https://www.toymoban.com/news/detail-762940.html

到了这里,关于【Java系列】详解多线程(三)—— 线程安全(下篇)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

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

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

相关文章

  • 【Java系列】详解多线程(一)

    个人主页:兜里有颗棉花糖 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 兜里有颗棉花糖 原创 收录于专栏【Java系列专栏】【JaveEE学习专栏】 本专栏旨在分享学习Java的一点学习心得,欢迎大家在评论区交流讨论💌 在引入多线程之前, 我们先来看一下进程是为了干什么的,

    2024年02月05日
    浏览(27)
  • JavaEE:多线程(2):线程状态,线程安全

    目录 线程状态 线程安全 线程不安全 加锁 互斥性 可重入  死锁 死锁的解决方法  Java标准库中线程安全类 内存可见性引起的线程安全问题 等待和通知机制 线程饿死 wait notify 就绪:线程随时可以去CPU上执行,也包含在CPU上执行的线程 阻塞:这个线程暂时不方便去CPU上执行

    2024年01月23日
    浏览(34)
  • JavaEE(系列14) -- 多线程(Callable)

    Callable 是一个 interface . 相当于把线程封装了一个 \\\"返回值\\\". 方便程序猿借助多线程的方式计算结果. 代码示例 : 创建线程计算 1 + 2 + 3 + ... + 1000, 不使用 Callable 版本 思路 : 创建一个类Result,包含 sum 表示最终结果, lock 表示线程同步使用的锁对象. main 方法中先创建 Result 实例,

    2024年02月06日
    浏览(27)
  • 多线程(JavaEE初阶系列4)

    目录 前言: 1.单例模式 1.1饿汉模式 1.2懒汉模式 1.3结合线程安全下的单例模式 1.4单例模式总结 2.阻塞式队列 2.1什么是阻塞队列 2.2生产者消费者模型 2.2.1 上下游模块之间进行“解耦合” 2.2.2削峰填谷 2.3阻塞队列的实现 结束语: 在上节中小编主要与大家分享了多线程中遇到

    2024年02月15日
    浏览(41)
  • 多线程(JavaEE初阶系列7)

    目录 前言: 1.常见的锁策略 1.1乐观锁和悲观锁 1.2轻量级锁和重量级锁 1.3自旋锁和挂起等待锁 1.4互斥锁与读写锁 1.5可重入锁与不可重入锁 1.6公平锁与非公平锁 2.CAS 2.1什么是CAS 2.2自旋锁的实现 2.3原子类 3.synchronized 3.1synchronized的原理以及基本特点 3.2偏向锁 3.3轻量级锁 3.4重

    2024年02月14日
    浏览(26)
  • 多线程(JavaEE初阶系列2)

    目录 前言: 1.什么是线程 2.为什么要有线程 3.进程与线程的区别与联系 4.Java的线程和操作系统线程的关系 5.多线程编程示例 6.创建线程 6.1继承Thread类  6.2实现Runnable接口 6.3继承Thread,使用匿名内部类 6.4实现Runnable接口,使用匿名内部类 6.5lambda表达式创建Runnable子类对象 7.

    2024年02月15日
    浏览(28)
  • JavaEE之多线程编程:4. 线程安全(重点!!!)

    下面我们来举个例子: 我们大家都知道,在单线程中,以下的代码100%是正确的。 但是,两个线程,并发的进行上述循环,此时逻辑可能就出现问题了。 上述这样的情况就是非常典型的线程安全问题。这种情况就是bug!! 只要实际结果和预期的结果不符合,就一定是bug。 想

    2024年01月25日
    浏览(31)
  • JavaEE(系列8) -- 多线程案例(单例模式)

    目录 1. 设计模式 2. 单例模式 -- 饿汉模式 3. 单例模式 -- 懒汉模式  4. 单例模式(懒汉模式-多线程)  什么是设计模式?         设计模式好比象棋中的 \\\"棋谱\\\". 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有一些固定的套路. 按照套路来走局势就不会吃亏

    2024年02月05日
    浏览(29)
  • 【JavaEE多线程】线程安全、锁机制及线程间通信

    如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。 线程安全问题的原因 [根本原因]多个线程之间的调度顺序是“随机”的,操作系统使用“抢占式”执行的策略来调度线程 多个 线程同时 修改同一个 遍历,容易

    2024年04月16日
    浏览(34)
  • [JAVAee]线程安全

    目录 线程安全的理解 线程不安全的原因 ①非原子性 ②可见性 ③代码重排序 体会何为不安全的线程  保证线程安全 一个代码在多线程的环境下就很容易出现错误.   线程安全是什么呢?通俗的来讲,线程安全就是在多线程的环境下,代码的结果是 符合我们预期 的,就可以称这个

    2024年02月16日
    浏览(29)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包