Java如何解决可见性和有序性的问题?
首先需要了解,为什么会有「可见性」和「时序性」问题,然后我们来看Java是如何解决这两个问题的。
「可见性」和「时序性」问题
导致「可见性」和「时序性」问题的原因有如下几个:
抢占式任务执行:现代CPU执行多任务方式是「抢占式」,它的总控制权在操作系统手中,操作系统会轮流给需要CPU执行的任务分配执行时间片,超过时间后,操作系统会剥夺当前任务的 CPU 使用权,把它排在队列的最后,最后分配时间片……
存储速度差异:各存储执行速度的不同,离CPU越近,存储速度越快,相对的容量就越小。执行程序所需要的数据不可能一次性全部都加载到寄存器中,所以有load与store的过程,影响了所谓的「可见性」
指令重排:大多数现代微处理器都会采用将指令乱序执行(out-of-order execution,简称OoOE或OOE)的方法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待。通过乱序执行的技术,处理器可以大大提高执行效率。除了处理器,常见的Java运行时环境的JIT编译器也会做指令重排序操作,即生成的机器指令与字节码指令顺序不一致。
解决方法
解决思路很简单,就是把多线程强制单线程执行。
解决方法无非两种:
内存屏障
锁
先看下JVM的内存模型,我们基于这个模型来简单说明下
内存屏障
内存屏障在Java中通过volatile关键字体现。volatile会在适当的地方添加下面四种内存屏障。
LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
内存屏障只保证可见性,不保证时序性。也就是说内存屏障只是解决了线程A修改的内容能立刻被线程B读到。
锁
Java中锁按性质分可以分悲观锁和乐观锁。悲观锁基于锁指令实现,乐观锁基于CAS实现。
通过monitorenter和monitorexit两个指令实现悲观锁,这两个指令之间的指令不得重排,且独占。假设线程A和线程B同时执行一段代码,线程A先通过monitorenter获取到了锁,那么在线程A执行monitorexit之前,线程B都只能等待。
CAS即CompareAndSet,Java通过自旋以及CPU层级的指令实现。具体可参考JUC实现。假设有一个变量c,初始值为3。线程A和线程B同时修改这个变量,A,B都同时获取到了变量c的值,A首先进行修改,将值改成了4。B尝试修改,但是发现c的值现在是4而不是3,所以进行自旋等待,然后重新执行修改操作,将4改成了5。
ThreadLocal
最后说下ThreadLocal。ThreadLocal即本地线程变量,也就是将公共的变量直接拿到线程内使用,其中的修改对外不影响。谈不上解决了「可见性」和「时序性」。只是保证了当前线程内的修改不影响其它线程,其它线程的修改也不影响当前线程。
线程安全有三大特性:原子性,可见性,有序性,只有三大特性都满足的时候才能保证线程安全,三大特性详细描述如下:
1,原子性:通常是指代码执行的效果,要么全部执行成功,要么全部失败;
2,可见性:线程中的本地内存中的变量值,应该立即同步到主内存中,让其他线程可见;
3,有序性:保证代码执行的串行性;
java中的原子性操作主要是使用sun.misc.Unsafe包下的compareAndSwap方法实现,这是sun包下native方法,使用c++代码实现,在CPU层级来 保证底层指令的原子性,或者使用加锁的方式!
但是CAS也会存在问题,具体参见我的另一个回答,这不做 详细讨论!
可见性无法保证主要是因为,内存的存取速度跟CPU相比存在差距,所以在CPU和主内存之间,引入了缓存(线程本地内存)的概念,来增加CPU 的计算性能,副作用就是导致线程操作共享数据时,无法保证中间数据是最新的(有可能还在别的线程的本地内存汇总)!
有序性无法保证是因为基于性能的考虑,编译器和处理器会对操作指令进行重排序,在单线程之中不存在问题,但如果是多线程就可能存在 判断错误的情况!
编译器的重排序也是规则的,java中先天的有序性组成了happens-before原则,如果happens-before原则不能推导出指令的执行次序,则指令 就不是有序的,happens-before八大原则为:程序次序规则(单线程中的顺序性,多线程无法保证),锁定规则(同一个锁先解锁,后加锁) ,volatile变量规则(先写后读),传递规则(类似A早于B,B早于C,则A早于C),线程启动原则(start方法是线程最先执行的方法),线程 中断规则,线程终结规则,对象终结规则!happens-before八大原则规定了指令执行的有序性
由此可以看出,线程不安全的原因十有八九都是追求性能惹的祸!
通常代码在满足三大特性的时候,就能保证线程安全,java中保证线程安全的方式有很多,包括加锁和不加锁,下面来逐一谈下:
1,加锁:比如synchronized(JDK中自带的关键字,JMM规定获取锁的时候,必须清空工作内存中的变量值,保证需要 获取变量的时候,只能从主内存中获取;释放锁的时候,必须把最新值写入到主内存中,这样来保证数据的可见性和原子性)!
还有基于AQS实现的reentrantLock,ReentrantReadWriteLock等都是加锁!
2,不加锁:
①,使用volatile+CAS操作,volatile使用内存屏障来保证变量的可见性和指令有序性,CAS保证原子性,最终实现线程安全,在jdk中Atomic 打头的几个类,都是用了这种方式,实现线程安全!如下图:
②,使用ThreadLocal,每个线程都维护一份数据到自己的本地内存中,相当于没有共享资源的竞争,所以也不会有线程安全问题;
后面会有更多的JAVA开发技术或者遇到的面试问题进行持续分享,希望能帮到更多的朋友,敬请关注。。
Java学到什么程度才能叫精通?
精通:透彻理解并能熟练掌握
看了精通的意思,可能很多人都不敢说自己真的精通Java!原因有2点:
- 精通这个词是不能乱用的,因为行业里总有你不会的。想想在自己的工作中,你没有问过他人Java相关问题吗?我相信工作中肯定都问过!
- 学无止境,何来精通?Java作为一门编程语言,它也在不断的变化,比如说从Java9-Java10,这不都是在不断的变化吗?
学无止境!
学习并不是一蹴而就的,在工作中我们肯定会遇到问题,这就需要我们去认真的学习,毕竟不学就不会,升职加薪神马的都说枉然!我们随意看看Java开发工程师的任职要求,就会发现要求会的还是蛮多的!
来看看这两个招聘信息,其实就涵盖了大部分Java开发工程师需要掌握的技能。
- Java开源框架:spring、springmvc、mybatis、hibernate等等
- 熟悉常见的数据库,并且有基本的应用能力,比如说:MySQL、oracle等
- 熟悉Eclipse、Tomcat、JDK、SVN运行环境的配置;
- 熟悉Maven的使用,理解Maven的原理与使用技巧
如何自学Java?
有多少小伙伴想自学Java或者是正在学Java的路上?你有一整套的学习方法吗?如果没有,这个肯定能帮到你!
分享Java学习线路图这个学习线路图把每个知识点都涵盖进去了,可以查看大图!下面说分阶段的,配合视频学习!
第一阶段:
第二阶段
第三阶段:
第四阶段
第五阶段
希望能帮助到你呦。
很多人都会问:到底Java学到什么程度才能叫精通?
我只想说,码农千千万,你凭什么说自己精通Java?
自学Java的人不少,科班出身的也很多,但是到什么程度才有资格说自己精通Java?个人觉得至少需要经历以下几个阶段:重视代码品质,精益求精,这是技术开发的本质,也是程序员的立足之本
对处于还没工作或者工作1年左右这个阶段的人来说,看书是比较好的提升方式,推荐《Java编程思想》、《effective java》等。
需要注意的是,在看书的过程中一定要思考能否运用书中的经验来改善自己写的代码,运用到实处,比如看到final和static,想想自己代码中用的是否合理。
此外,还可以看些常用的开源框架,下面知识图谱可以借鉴:
打好基础后,主动跳出舒适区,不断扩大自己的技术视野
这一步比较难,因为很多人会被公司的技术和业务牵着走。比如公司当前的业务规模决定了技术上不需要分布式技术,很多人就不会去思考规模大了之后如何利用分布式技术来解决问题。这种时候,除了寻找更大规模的业务外,要时刻保持跳出当前层级和环境来思考的习惯。
比如,你只用了关系型数据库,有没有想过数据一直产生,到达TB级别的时候该如何快速检索与保存呢?
高性能架构技术栈
一切的一切,都要用到你的实战中去
有了互联网热点技术,一个真实的互联网项目可以让你把所学的热点技术由点到线,由线到面,将所有技术聚集到实战的环境,最终将技术点和项目实战真正变成自己的本领;享学课堂的老师们已经完成了B2C商城真实项目的设计与开发,课堂手把手带大家分析B2C项目的每一行源行及设计规范,让你对Nginx、 Redis、Docker、Mq等技术学而致用,用而不忘,具备独立的架构设计与开发能力。
高并发电商系统开发实战
扩展了知识宽度,还要加强自己的技术深度
很多人信奉“不去造轮子”,看似性价比很高的一句话,却不知道这句话是有前提的,那就是“你得知道轮子的内部结构”,否则就是自我麻痹。
接下来需要选择某一个或几个方面深入研究下去,构建自己的核心竞争力。一个有技术深度的程序员才是有灵魂的。而大厂也愿意为有趣的灵魂买单,这个时候看的就不仅仅是工资水平,更多的在于晋升和发展前景的广阔性。
技术的突破需要依赖业务场景的需求和自身刻意的规划学习,二者缺一不可。但前者是机会,往往不是自己能完全把控的,如果在工作中没有实践场景,最好的办法就是系统的学习与梳理,待机会来临时才能一展身手。
PS:
需要清晰系统图的可以关注我的主页!
希望我这些分享可以帮助在这个行业发展的朋友和童鞋们,在论坛博客等地方少花些时间找资料,把有限的时间,真正花在学习上,我的很多文章都有分享各种架构资料,相信对于已经工作和遇到技术瓶颈或者写博客码友,在我的主页一定都有你需要的内容。