(一)阅读指导
1.本文站在设计者角色进行思考。
2.知其然,知其所以然。
3.没有完美东西,请遵循应用场景。
请原谅我用大量篇幅来娓娓道来,只为了以上三点!
(二)发展历史(设计者角度)
注:以下谓语 ‘我‘ 仅仅代表Java设计者(虽然是不正确的理解)
1. 在我发布了Java1.5之前,当时的业务场景是单核市场,我们并没有预料到Java会发展的如此庞大。当时我们考虑到所谓的并发编程,我们利用的是原语级别的指令(),当然这时会阻塞其他的线程,因为我们称之为同步代码块。我们用synochrized关键词来实现,当然我们Java初级了解到可以锁方法或者对象,在javac编辑后会在同步代码块前后生成monitorenter和monitorexit字节码指令,如果reference类型参数指明锁定对象,那么这个对象的引用作为reference。当然引用为空我们可以认为修饰的是(实例或者类)方法。
2. 我们团队迎来的重大改变,可能是为了和C#一绝高下。我么们决定把1.X版本修改为X版本,算命先生说要该名字才能转运(开玩笑),首先使用我们Java的程序猿提出好多不足,我们来听听,好让我们Java更加完美。多线程高并发下的我们适应多核时代,用户发现多种应用场景使用synochrized感觉有点沉。比如,一个共享变量,我们对其读远大于写。我们可以不用阻塞线程来完成,阻塞线程的synochrized太重了,我们需要替代它,并且在某些应用场景下,可以实现它的优点。
3. volatile extends synochrized`s advantage,我们要继承synochrized的优点,比如:可见性,原子性,有序性。因为synochrized是同步代码块,一个线程在操作此资源时,其他需要此资源的线程都会被挂起,只允许一个线程对其lock操作,线程间串行执行,当然不会有先线程问题,不过随之而来的,加锁,释放锁,上下文切换,调度延迟等都是导致synochrized比较重的原因。于是乎,我们能不能设计一个比较轻的锁,在某些乐观的情况下,比如读远多于写,我们就可以用一个其他的东西来代替这个大家伙。
4. 我们设计一个东西,既然这个变量我们不用重量级锁,那么我们告诉jvm,这个变量我们感觉它会变,但是我们不想用锁,请你照顾一下,我们给这个变量修饰为 volatile(易变的),此时这个变量就是VIP,你说我是VIP不对比普通变量怎么体现我的VIP身份,好,我给你说所普通用户是保存它的。我们要有一个知识储备,在多核系统中每个cpu都有它的cache,但是主内存就有一个。为了解决缓存一致性,我们就必须保证我们cache和主内存是一致的,如果非要和Java内存区域联系起来,那么栈是工作内存,堆是主内存。
5. 普通用户我们不会去关心他的正确性,取变量就会直接在(你可以理解为堆)主内存(Java内存模型规定所有变量都储存在主内存)中copy变量到自己的工作内存(私有的,你可以理解为栈),所有的操作(读取,赋值)在工作内存进行。我们规定,有8个原子操作来说明一个变量如何从主内存拷贝到工作内存,如何从工作内存同步回主内存这个操作,8种操作我们简化为4种,提前剧透8种为(lock,unlock,read,load,use,assign,store,write)浅淡的理解为这4种(read,write,lock.unlock);
(三)你来设计volatile尽可能实现 原子性 、可见性、有序性
注:double和long这种占两个变量槽的暂时不考虑(几率很小)
1. 什么是原子性?,我写一行Java代码是原子性吗?i++,显然不是,他在底层汇编有四个操作,分别是:取i的值、取常量1、i+1操作、i+1复制给i,一共四部,当然不意为一条字节码指令就是原子操作了,一条字节码指令解释器执行时要运行许多行代码才能实现它的语义。原子性粗浅的理解为在这次从主内存到工作内存复制的变量时正确的。很显然,我们只能保证取i的值是原子性的。(原子性失败)
2. 我们实现可见性吧,我们在2.5说到普通用户的读取共享变量,对于我们volatile修饰的变量我们目标是保证此变量对所有线程可见,也就是一个线程修改了这个变量的值,新值立马就会知道。在此我们声明两个前提:on the one hand is 运算结果不依赖当前变量的值,或者能够确保只有单一的线程修改变量的值(i++),on another hand is 变量不需要与其他的状态变量共同参与不变约束,这个我理解不是很深刻,为了避免误人子弟,贴出这个参考代码(摘抄《深入理解Java虚拟机第三版》代码清单12-3)
// 被volatile修饰的布尔类型,用来做控制逻辑代码
volatile boolean shutdownRequested;
public void shutdown(){
shutdownRequested = true;
}
public void doWork(){
while(!shutdownRequested){
// 执行逻辑代码
}
}
3. 我们可以这样实现,即假设我们不优化代码,我们就认为这个变量是不稳定因素,我们在工作内存中不长时间保存这个变量,我们每次use之前必须load,每次load的时候之后必须use,这样就能保证了我们每次用到的值是主内存最新的,可是怎么保证可见性呐?我修改的值怎么让别人快速看到呐 ?我们规定每次store的之后就是writer,每次writer之前就要store,这样能保证我们操作完变量立马刷新到内存中。但是使用了volatile修饰的变量在并发下也不是安全的,从硬件来看,我们仅仅得到了最新的值,并不保证运算操作是安全的。
4. 我们解决重排序问题。首先我们明白为啥重排序?我们可以粗浅认为我们写的代码被编译为字节码,字节码会到机器码,那么重排序优化是机器级的优化,提前执行的是这个语句对应的汇编代码被提前执行,因为我们知道一行Java代码要被解析为很多汇编代码。首先我们了解到 synochrized 是通过保证共享变量只能被一个lock来保证串行话,保证了线程之前的安全。jdk1.5我们做了改变,可以让你用双锁检测实现单例。
5. 我们在volatile修饰的变量赋值后多执行一个“ lock addl $0x0,(%esp) ”操作,看不懂的小伙伴没关系哈,我也看不懂!这个操作相当于一个内存屏障(不是垃圾回收器的那个内存屏障),这个指令重排序时不能把后面的指令重排序到内存屏障之前的位置,这个字节码指令显然是个加0操作,为啥不用nop(空操作),因为IA32手册规定lock前缀不允许配合nop使用。lock前缀作用是将本处理器的缓存写入内存,可以引起别的cpu保存的本地变量无效,也就是其他的本地变量失效只能从主内存再次读,此时也保证了内存的可见性。同时我们修改变量值的话就保证了我们的变量已经正确获取和赋值了,也就是保证了有序性。
总结
1.可见性是靠 load 和 use 、assign和store相关联
2.有序性在此基础上做了 空加操作,为了保证赋值前后的有序性
参考书籍:《深入理解Java虚拟机第三版》
参考网站: volatile百度百科
附录:volatile 实现 i ++
请多多指出错误之处,不甚感激!