摘要:一前言我们知道在多线程的场景下,线程安全是必须要着重考虑的。变量可用于提供线程安全,但是只能应用于非常有限的一组用例多个变量之间或者某个变量的当前值与修改后值之间没有约束。
一、前言
我们知道在多线程的场景下,线程安全是必须要着重考虑的。Java语言包含两种内在的同步机制:同步块(synchronize关键字)和 volatile 变量。但是其中 Volatile 变量虽然使用简单,有时候开销也比较低,但是同时它的同步性较差,而且其使用也更容易出错。下面我们先使用一个例子来展示下volatile有可能出现线程不安全的情况:
public class ShareDataVolatile { //同时创建十个线程,每个线程自增100次 //主程序等待3秒让所有线程全部运行完毕后输出最后的count值 //使用volatile修饰计数变量count public volatile static int count=0; public static void main(String[] args){ final ShareDataVolatile data = new ShareDataVolatile(); for(int i=0;i<10;i++){ new Thread( new Runnable(){ public void run(){ try{ Thread.sleep(1); }catch(InterruptedException e){ e.printStackTrace(); } for(int j=0;j<100;j++){ data.addCount(); } System.out.print(count+" "); } } ).start(); } try{ Thread.sleep(3000); }catch(InterruptedException e){ e.printStackTrace(); } System.out.println(); System.out.print("count="+count); } public void addCount(){ count++; } }
运行结果:
200 200 416 585 755 742 513 513 501 855
count=855
多次运行结果最后的count都不是预计的1000,这说明使用volatile变量并不能保证线程安全。
锁提供了两种主要特性:互斥(mutual exclusion)和可见性(visibility)。
互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。
Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值。Volatile 变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。因此,多带带使用 volatile 还不足以实现计数器、互斥锁或任何具有与多个变量相关的不变式(Invariants)的类(例如 “start <=end”)。
所以例子中虽然增量操作(count++)看上去类似一个多带带操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能对组合操作提供必须的原子特性。实现正确的操作需要使 count 的值在操作期间保持不变,而 volatile 变量无法实现这点。
在 java 垃圾回收整理一文中,描述了jvm运行时刻内存的分配。其中有一个内存区域是jvm虚拟机栈,每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。下面一幅图描述这写交互:
read and load 从主存复制变量到当前工作内存
use and assign 执行代码,改变共享变量值
store and write 用工作内存数据刷新主存相关内容
其中use and assign 可以多次出现,但是这一些操作并不是原子性,也就是 在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的,例如:
假如线程1,线程2 在进行ead,load 操作中,发现主内存中count的值都是5,那么都会加载这个最新的值四、Volatile的优势与使用条件
在线程1堆count进行修改之后,会write到主内存中,主内存中的count变量就会变为6
线程2由于已经进行read,load操作,在进行运算之后,也会更新主内存count的变量值为6
导致两个线程及时用volatile关键字修改之后,还是会存在并发的情况。
看了上面的,大家可能已经对volatile表示十分失望,不打算使用它了,然后volatile的存在肯定有它存在的意义:
1.简易性:在某些情形下,使用 volatile 变量要比使用相应的锁简单得多。
2.性能:某些情况下,volatile 变量同步机制的性能要优于锁。
对 JVM 内在的操作而言,我们难以抽象地比较 volatile 和 synchronized 的开销。但是大部分情况下,在目前大多数的处理器架构上,volatile 读操作开销非常低 —— 几乎和非 volatile 读操作一样。而 volatile 写操作的开销要比非 volatile 写操作多很多,因为要保证可见性需要实现内存界定(Memory Fence),即便如此,volatile 的总开销仍然要比锁获取低。
volatile 操作不会像锁一样造成阻塞,因此,在能够安全使用 volatile 的情况下,volatile 可以提供一些优于锁的可伸缩特性。如果读操作的次数要远远超过写操作,与锁相比,volatile 变量通常能够减少同步的性能开销。
所以我们需要明确可以使用volatile的条件有两点:
1.对变量的写操作不依赖于当前值。五、结束语
2.该变量没有包含在具有其他变量的不变式中。
与锁相比,Volatile 变量是一种非常简单但同时又非常脆弱的同步机制,它在某些情况下将提供优于锁的性能和伸缩性。如果严格遵循 volatile 的使用条件 —— 即变量真正独立于其他变量和自己以前的值 —— 在某些情况下可以使用 volatile 代替 synchronized 来简化代码。然而,使用 volatile 的代码往往比使用锁的代码更加容易出错。
另外如果不是很在意性能方面,并且希望实现简洁明了的技术器功能,可以参考我博客内的另一篇介绍AtomicInteger类的文章,该类可以实现原子性操作,从而保证线程安全:http://blog.csdn.net/roy_70/a...
参考文章:
Java 理论与实践: 正确使用 Volatile 变量:
https://www.ibm.com/developer...
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/73042.html
摘要:本文对多线程基础知识进行梳理,主要包括多线程的基本使用,对象及变量的并发访问,线程间通信,的使用,定时器,单例模式,以及线程状态与线程组。源码采用构建,多线程这部分源码位于模块中。通知可能等待该对象的对象锁的其他线程。 本文对多线程基础知识进行梳理,主要包括多线程的基本使用,对象及变量的并发访问,线程间通信,lock的使用,定时器,单例模式,以及线程状态与线程组。 写在前面 花了一周时...
摘要:使用双检查机制来实现多线程环境中的延迟加载单例设计模式。类主要负责日期的转换与格式化,但在多线程环境中,使用此类容易造成数据转换及处理的不准确,因为类并不是线程安全的。 立即加载就是使用类的时候已经将对象创建完毕,常见的实现办法就是直接new实例化。而立即加载从中文的语境来看,有着急、急迫的含义,所以也称为饿汉模式。 package com.zxf.demo.singleton_0; ...
摘要:起因及介绍在处理原始对账文件的时候,我将数据归类后批量存入相应的表中。结论事务只能管着开启事务的线程,其他子线程出了问题都感知不到,所以在多线程环境操作要慎重。高频容易搞死服务器,低频会阻塞自身程序。重试次数和超时时间根据业务情况设置。 起因及介绍 在处理原始对账文件的时候,我将数据归类后批量存入相应的表中。在持久化的时候,用了parallelStream(),想着同时存入很多表这样可...
摘要:的多线程机制可弥补抛出未检查的异常,将终止线程执行,此时会错误的认为任务都取消了。如果想要不保留,则需要设置,此时最小的就是线程池最大的线程数。 提供Executor的工厂类showImg(https://segmentfault.com/img/bVbj3Ei?w=2890&h=1480); 忽略了自定义的ThreadFactory、callable和unconfigurable相关...
摘要:在一个进程内部,要同时干多件事,就需要同时运行多个子任务,我们把进程内的这些子任务称为线程。总结一下,多任务的实现方式有三种多进程模式多线程模式多进程多线程模式线程是最小的执行单元,而进程由至少一个线程组成。 进程与线程 很多同学都听说过,现代操作系统比如Mac OS X,UNIX,Linux,Windows等,都是支持多任务的操作系统。 什么叫多任务呢?简单地说,就是操作系统可以同时...
阅读 3538·2021-11-22 15:22
阅读 3328·2019-08-30 15:54
阅读 2724·2019-08-30 15:53
阅读 783·2019-08-29 11:22
阅读 3529·2019-08-29 11:14
阅读 2073·2019-08-26 13:46
阅读 2209·2019-08-26 13:24
阅读 2277·2019-08-26 12:22