【JavaEE初阶】 线程安全

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

🌴线程安全的概念

线程安全是多线程编程是的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且准确的执行,不会出现数据污染等意外情况。上述是百度百科给出的一个概念解释。换言之,线程安全就是某个函数在并发环境中调用时,能够处理好多个线程之间的共享变量,是程序能够正确执行完毕。也就是说我们想要确保在多线程访问的时候,我们的程序还能够按照我们的预期的行为去执行,那么就是线程安全了。

我们可以这样认为:

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该得到的结果,则说这个程序是线程安全的。

🌳观察线程不安全

现在有以下代码,调用两个线程,两个线程对同一个对象的同一元素进行加加操作,该元素初始值为0,每个线程加50000次,我们观察最终结果

代码如下:

 class Count {
    public int count = 0;
    void increase() {
        count++;
    }
}
public class Counter {
    public static void main(String[] args) throws InterruptedException {
        final Count count = new Count();
        //搞两个线程,分别对count进行++操作,每一个线程加50000次
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count.count);
    }
}

我们预期的结果是count = 100000,但是我们运行后发现,结果无法预料

下面是博主运行多次的结果展示
【JavaEE初阶】 线程安全,JavaEE初阶,java-ee,安全,java,多线程,计算机操作系统,开发语言
我们可以观察到每次的运行结果都不相同,这是什么原因造成的呢?

🎄线程不安全的原因

🚩修改共享数据

上面的线程不安全的代码中, 涉及到多个线程针对同一变量count.count 变量进行修改.

此时这个 coun.count 是一个多个线程都能访问到的 "共享数据“
【JavaEE初阶】 线程安全,JavaEE初阶,java-ee,安全,java,多线程,计算机操作系统,开发语言

count.count 这个变量就是在堆上. 因此可以被多个线程共享访问

要考虑线程安全问题,就需要先考虑Java并发的三大基本特性:原子性、可见性以及有序性

📌原子性

原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行

就好比转账,从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。2个操作必须全部完成

再比如下面这个卖票的例子,就不具备原子性
【JavaEE初阶】 线程安全,JavaEE初阶,java-ee,安全,java,多线程,计算机操作系统,开发语言

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。

那我们应该如何解决这个问题呢?

是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。

有时也把这个现象叫做同步互斥,表示操作是互相排斥的

注意:一条 java 语句不一定是原子的,也不一定只是一条指令

比如我们上述不安全的代码 count ++,其实是由三步操作组成的:

  1. 从内存把数据读到 CPU
  2. 进行数据更新
  3. 把数据写回到 CPU

所以在多线程中,有可能一个线程还没自增完,可能才执行到第二步(进行数据更新),另一个线程就已经读取了值,导致结果错误。那如果我们能保证自增操作是一个原子性的操作,那么就能保证其他线程读取到的一定是自增后的数据。

那么不保证原子性会给多线程带来什么问题?

如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。(这点也和线程的抢占式调度密切相关. 如果线程不是 “抢占” 的, 就算没有原子性, 也问题不大)

📌 可见性

当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。简单理解为:一个线程对共享变量值的修改,能够及时地被其他线程看到

若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题。

可见性就与操作系统内存模型有关系了,这里博主为大家介绍以下Java 内存模型 (JMM)

JMM 为Java虚拟机规范中定义了Java内存模型.

目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果.

【JavaEE初阶】 线程安全,JavaEE初阶,java-ee,安全,java,多线程,计算机操作系统,开发语言
模型说明:

  • 线程之间的共享变量存在 主内存 (Main Memory).

  • 每一个线程都有自己的 “工作内存” (Working Memory) .

  • 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.

  • 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存

由于每个线程有自己的工作内存, 这些工作内存中的内容相当于同一个共享变量的 “副本”. 此时修改线程1 的工作内存中的值, 线程2 的工作内存不一定会及时变化.

如下所示:

  1. 初始情况下, 两个线程的工作内存内容一致
    【JavaEE初阶】 线程安全,JavaEE初阶,java-ee,安全,java,多线程,计算机操作系统,开发语言
  2. 一旦线程1 修改了 a 的值, 此时主内存不一定能及时同步. 对应的线程2 的工作内存的 a 的值也不一定能及时同步
    【JavaEE初阶】 线程安全,JavaEE初阶,java-ee,安全,java,多线程,计算机操作系统,开发语言
    这个时候代码中就容易出现问题,就如上述不安全的代码一样

这时候我相信有一部分人就有这样的疑问了

  1. 为啥整这么多内存?

实际并没有这么多 “内存”. 这只是 Java 规范中的一个术语, 是属于 “抽象” 的叫法.
所谓的 “主内存” 才是真正硬件角度的 “内存”. 而所谓的 “工作内存”, 则是指 CPU的寄存器和高速缓存

  1. 为啥要这么麻烦的拷来拷去?

因为 CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级, 也就是几千倍, 上万倍).

比如某个代码中要连续 10 次读取某个变量的值, 如果 10 次都从内存读, 速度是很慢的. 但是如果只是第一次从内存读, 读到的结果缓存到 CPU 的某个寄存器中, 那么后 9 次读数据就不必直接访问内存了. 效率就大大提高了

然后我们是不是又会有新问题了,既然访问寄存器速度这么快, 还要内存干啥?

答案 :就是一个字: ,寄存器太贵了
【JavaEE初阶】 线程安全,JavaEE初阶,java-ee,安全,java,多线程,计算机操作系统,开发语言

  • 值的一提的是, 快和慢都是相对的. CPU 访问寄存器速度远远快于内存, 但是内存的访问速度又远远快于硬盘.

  • 对应的, CPU 的价格最贵, 内存次之, 硬盘最便宜

📌代码顺序性

程序执行的顺序按照代码的先后顺序执行,在多线程编程时就得考虑这个问题

如果一个线程的话,代码的顺序就为从上到下,依次执行,而当为多线程时就不一样了。

当多个线程同时共享,同一个全局变量或静态变量(即局部变量不会),做写的操作时,可能会发生数据冲突问题,也就是线程安全问题。但是做读操作是不会发生数据冲突问题。

关于代码顺序性不得不说一个概念:代码重排序

何为代码重排序呢?

比如有段代码是这样的:

  1. 去前台取下 U 盘

  2. 去教室写 10 分钟作业

  3. 去前台取下快递

如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序

编译器对于指令重排序的前提是 “保持逻辑不发生变化”. 这一点在单线程环境下比较容易判断, 但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价

重排序是一个比较复杂的话题, 涉及到 CPU 以及编译器的一些底层工作原理, 此处不做过多赘述了

🌲解决之前的线程不安全问题

我们使用关键字synchronized进行加锁操作

 class Count {
    public int count = 0;
    //加锁
     synchronized void increase() {
        count++;
    }
}
public class Counter {
    public static void main(String[] args) throws InterruptedException {
        final Count count = new Count();
        //搞两个线程,分别对count进行++操作,每一个线程加50000次
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count.count);
    }
}

我们来看一下代码运行结果:
【JavaEE初阶】 线程安全,JavaEE初阶,java-ee,安全,java,多线程,计算机操作系统,开发语言
我们发现结果正确,说明上述的线程不安全问题解决了

在我们改善后代码里涉及到了一个新的知识synchronized进行加锁操作,该操作博主会在下一篇博客进行详细讲解

⭕总结

关于《【JavaEE初阶】 线程安全》就讲解到这儿,感谢大家的支持,欢迎各位留言交流以及批评指正,如果文章对您有帮助或者觉得作者写的还不错可以点一下关注,点赞,收藏支持一下!文章来源地址https://www.toymoban.com/news/detail-715289.html

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

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

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

相关文章

  • JavaEE初阶:Java线程的状态

    目录 获取当前线程引用 休眠当前线程  线程的状态 1.NEW               2.TERMINATED  3.RUNNABLE 4.WAITING 5.TIMED_WAITING 6.BLOCKED 多线程的意义 单线程  多线程 这个方法返回当前线程的引用。但是我们会对static有疑惑,这其实是一个静态方法,更好的说法是这是一个 类方法, 调用这

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

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

    2024年04月25日
    浏览(43)
  • 【Java EE 初阶】TCP协议的安全效率机制

    目录 1.应用层协议 2.传输层协议 3.UDP协议格式 4.TCP协议格式 5.TCP的安全效率机制 1.确认应答机制 2.超时重传机制 但是,主机A未收到B发来的确认应答,也可能是因为ACK丢失了; 3.连接管理机制 ​编辑 面试题:会不会有可能变成三次挥手? 面试题:第二个FIN丢包了如何处理?

    2024年02月09日
    浏览(44)
  • 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 指令重排序概述         1.1 指令重排序主要分为两种类型         1.2 指令重排序所引发的问题         2.0 内存可见性概述         2.1 导致内存可见性问题主要涉及两个方面      

    2024年04月15日
    浏览(49)
  • JavaEE初阶:多线程 - 编程

    我们在之前认识了什么是多进程,今天我们来了解线程。 一个线程就是一个 \\\"执行流\\\". 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 \\\"同时\\\" 执行 着多份代码. 引入 进程 这个概念,主要是为了解决并发编程这样的问题。因为cpu进入了多核心的时代,要想进一步

    2024年02月12日
    浏览(36)
  • javaee初阶———多线程(三)

    T04BF 👋专栏: 算法|JAVA|MySQL|C语言 🫵 小比特 大梦想 此篇文章与大家分享多线程专题第三篇,关于 线程安全 方面的内容 如果有不足的或者错误的请您指出! 我们在前面说过,线程之间是抢占式执行的,这样产生的随机性,使得程序的执行顺序变得不一致,就会使得程序产生不同的结

    2024年04月16日
    浏览(38)
  • 多线程(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日
    浏览(36)
  • 多线程(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日
    浏览(53)
  • 多线程(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日
    浏览(39)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包