线程安全问题与解决方案

本文简介:
用多线程开发的人都知道,在多线程的开发过程中有可能会出现线程安全问题(专业术语叫内存可见性问题),但并不一定每次都会出现。出现这样的情况,也会另开发者头皮发麻,无从下手,接下来我们会慢慢深入,揭开多线程的神秘面纱。

本文主要介绍了Java多线程开发的优势,使用该技术可能会出现的一些内存不可见问题以及相应的解决措施。通过本文,读者将学习到如下几块知识:

为什么需要多线程技术(多线程的优势)
使用多线程技术带来的一些问题
产生内存不可见问题的条件
产生内存不可见问题的原因
我们该如何分析会不会产生内存可见问题及出现问题之后的解决方式
下面进入正文

为什么需要多线程技术(多线程的优势)
线程是Java语言中不可或缺的重要部分,它们能使复杂的异步代码变得简单,简化复杂系统的开发;能充分发挥多处理器系统的强大计算能力。

充分利用硬件资源。由于线程是CPU的基本调度单位,所以如果是单线程,那么最多只能同时在一个处理器上运行,意味着其他的CPU资源都将被浪费。而多线程可以同时在多个处理器上运行,只要各个线程间的通信设计正确,那么多线程将能充分利用处理器的资源。
结构优雅。多线程程序能将代码量巨大,复杂的程序分成一个个简单的功能模块,每块实现复杂程序的一部分单一功能,这将会使得程序的建模,测试更加方便,结构更加清晰,更加优雅。
简化异步处理。为了避免阻塞,单线程应用程序必须使用非阻塞I/O,这样的I/O复杂性远远高于同步I/O,并且容易出错。
使用多线程技术带来的一些问题
线程安全( 内存可见性问题):由于统一进程下的多个线程是共享同样的地址空间和数据的,又由于线程执行顺序的不可预知性,一个线程可能会修改其他线程正在使用的变量,这一方面是给数据共享带来了便利;另一方面,如果处理不当,会产生脏读,幻读等问题,好在Java提供了一系列的同步机制来帮助解决这一问题,例如内置锁。
活跃性问题。可能会发生长时间的等待锁,甚至是死锁。
性能问题。 线程的频繁调度切换会浪费资源,同步机制会导致内存缓冲区的数据无效,以及增加同步流量。
产生内存不可见问题(线程安全问题)的条件
产生内存不可见的条件有俩个
1. 多个线程
2. 存在共享变量
当多个线程操作了共享变量时,就有可能会产生线程安全问题(内存不可见问题)。

产生内存不可见问题的原因
产生内存不可见的问题有三个原因
1. 没有保证代码的原子性
2. 没有保证代码的可见性
3. 没有保证代码的有序性
1.1 没有保证原子性产生内存不可见的究极原因 ?
由于现代的CPU是多核的,可以实现并行,所以读书的时候我一直带着这样的疑问?
多核会不会同时操作同一内存下的数据:举例 代码 i++;多核处理器会不会同时执行这行代码?答案是不会同时执行。
请参考
1.2 没有保证代码的可见性产生内存不可见的究极原因?
可见性定义:一个线程的写对另一个线程立即可知。
现代的处理器都有读写缓冲区,读缓冲区异步从主存中读取数据,写缓冲区临时保存向内存写入的数据。有了写缓冲区可以保证指令流水线持续进行,它可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟。同时以异步的方式刷新写缓冲区,以及合并写缓冲区中对同一个地址的多次写,减少对内存总线的占用。

假设线程A,B,分别在俩个处理器上执行,初始时a=0 ,线程A先执行 a=1, 线程B 再执行 a=2,线程3去读变量a,最后a会是几呢? 答案 0 1 2 都有可能。
产生0的原因:线程A先执行 a=1,然后放到写缓冲区, 线程B 再执行 a=2,然后又放到写缓冲区,假设俩个写缓冲区还没又刷新的时候 线程3去读变量a,此时线程3读取到变量a就为0 。
产生1的原因:线程A先执行 a=1,然后放到写缓冲区, 线程B 再执行 a=2,然后又放到写缓冲区,假设线程A所在处理器的写缓冲区已刷新,线程B还没又刷新的时候, 线程3去读变量a,此时线程3读取到变量a就为1 。
产生2的原因:线程A先执行 a=1,然后放到写缓冲区, 线程B 再执行 a=2,然后又放到写缓冲区,假设线程B所在处理器的写缓冲区已刷新,线程A还没又刷新的时候, 线程3去读变量a,此时线程3读取到变量a就为2 。
综上所诉,没有保证代码的可见性产生内存不可见的究极原因是:写缓冲区的存在,且以异步的方式刷新缓冲。
1.3 没有保证代码的有序性产生内存不可见的究极原因?
编译器和处理器为了优化程序的性能会进行从排序,所以有序性会产生内存不可见问题。

我们该如何分析会不会产生内存可见问题及出现问题之后的解决方式
如何分析我们的代码会不会产生内存可见性问题
Java虚拟机提供了Java内存模型来分析会不会产生内存可见性问题,Java内存模型也提供了一系列规则(happens-before等等)来辅助程序员更好的分析会不会产生内存可见行问题。

出现问题之后的解决方式
产生线程安全问题的原因有三个,解决方式无非就是 1.保证代码的原子性 2.保证代码的可见性 3.保证代码的有序性。
JVM 提供了synchronized 和volatile 关键字来解决问线程安全问题
Synchronized 可以保证代码的原子性、保证代码的可见性、保证代码的有序性。
volatile 可以保证代码的可见性、保证代码的有序性但不能保证代码的原子性。
所以Synchronized可以完全解决线程安全问题,而volatile不可以。

总结
为什么本篇要这么写呢?因为读完Java并发编程书籍之后,我觉得非常的混乱,读完之后感觉没有收获,就想写点东西做个总结,可是又无从下手,不知道如何引入到Java内存模型和synchronized 、volatile 关键字。为什么无从下手呢,后来发现是对Java内存模型和synchronized 、volatile 理解不够到位。

Java内存模型可以帮助我们理解我们的代码会不会存在内存可见性问题。
synchronized 、volatile可以帮助我们解决内存可见性问题。前者是分析会不会产生问题,后者是产生问题之后的解决。
————————————————
版权声明:本文为CSDN博主「Sprint01」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/SprintMan/article/details/79641180

猜你喜欢

转载自www.cnblogs.com/xianshiwang/p/11443914.html