主内存与工作内存
Java内存模型的主要目标是定义程序中哥哥变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量与Java编程中所说的变量有所区别,它包括了实例字段、静态字段、和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然不存在竞争问题。
Java内存模型规定了所有的变量都存储在主内存(Main Memory)中。每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
内存间交互操作
即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步会主内存之类的实现细节。Java内存模型中定义了以下8中操作来完成,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的(对于double、long类型的变量来说,load、store、read和write操作在某些平台上允许有例外)。
- lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,它把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传递送到主内存中,以便随后的write操作使用
- write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量值放入主内存的变量中。
如果要把一个变量从主内存赋值到工作内存,那就要顺序地执行 read 和 load 操作。
如果要把变量从工作内存同步回主内存,就要顺序地执行 store 和 write 操作。
⚠️Java内存模型只要求上述两个操作必须按顺序执行,而没有保证是连续执行,也就是说,read和load之间、store和write之间是可以插入其他指令的。
除此之外,Java内存模型还规定了在执行上述8种基本操作时必须满足如下规则:
- 不允许 read 和 load 、store 和 write 操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。
- 不允许一个线程丢弃它的最近的 assign 操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
- 不允许一个线程无原因地(没有发生任何assign操作)把数据从线程的工作内存同步回主内存中。
- 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,就是对一个变量实施 use、store 操作之前,必须先执行过了 assign 和 load 操作。
- 一个变量在同一个时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会解锁。
- 如果对一个变量执行 lock 操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量值。
- 如果一个变量事先没有被 lock 操作锁定,那就不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定住的变量。
- 对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中(执行 store、write)
对于volatile型变量的特殊规则
当变量定义为 volatile之后,它将具备两种特性:
第一个是保证此变量对所有线程的可见行,具体是当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成。
由于Java里面的运算并非原子操作,导致 volatile 变量的运算在并发下一样是不安全的。
例如:public static volatile int race = 0;多线程执行 race++;
通过javap反编译代码后会发现 race++被拆解成了由4条字节码指令构成的,从字节码层面上很容易就分析出并发失败的原因,当getstatic指令把 race 的值取到操作栈顶时,volatile关键字宝成了race的值在此时是正确的,但是在执行 iconst_1、iadd这些指令的时候,其他线程可能已经把race的值加大了,而在操作栈顶的值就变成了过期的数据,所以putstatic指令执行后就可能把较小的race值同步回主内存之中。
public void volatileAdd();
Code:
0: getstatic #2 // Field race:I
3: iconst_1
4: iadd
5: putstatic #2 // Field race:I
8: return

第二个是volatile变量禁止指令重排序的优化,普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序于程序代码的执行顺序一致。
对于double和long类型变量的特殊规则
允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,也就是说允许虚拟机选择可以不保证64位类型数据的load、store、read、和write这4个操作的原子性,这点就是所谓的 long 和 double 的非原子性协定。
如果由多个线程共享一个并未声明为volatile的 long 或 double 类型的变量,并且同时对他们进行读取和修改操作,那么某些线程可能会读取到一个即非原值,也不是其他线程修改值的代表了“半个变量”的数值。
原子性、可见性、有序性
原子性(Atomicity):由Java内存模型来直接保证的原子性变量操作包括 read、load、assign、use、store、write,可以认为基本数据类型的访问读写是具备原子性的。
如果需要更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把这种操作开发给用户使用,但是却提供了相关的字节码指令 monitorenter 和 monitorexit来隐式的使用这两个操作,这两个字节码指令反应到Java上就是同步快语句了,synchronized关键字。因此在synchronized之间的操作也具备了原子性。
可见性(Visibility):可见性就是当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型通过在变量修改后将心智同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的。volatile的特殊规则保证了新值能够立即同步到主内存,以及每次使用前立即从主内存刷新。可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。
除此之外还有synchronized和final,同步快的可见性是由“对一个变量执行unlock操作之前,不惜把变量同步回主内存中(执行 store、write操作)”这条规则获得的,而final关键字的可见性是指:被final修饰的字段在狗仔起中一旦初始化,并且构造器没有把this的引用传递出去(this引用逃逸是一件很危险的事情,其他线程由可能通过这个引用访问到“初始化一半的对象”),哪其他线程中就能看见final字段的值。
有序性(Ordering):Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile 关键字本身包含了禁止指令重排序的语义,synchronized则是一个变量在同一个时刻只允许一条线程对其进行lock操作,这条规则决定了持有同一个锁的两个同步快只能串行地进入。
先行发生原则
- 程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行于书写后面的操作,准确的说,应该是控制流顺序而不是程序代码顺序,因为考虑到分支循环等结构。
- 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作,这里必须强调是同一个锁,这里的“后面”是指时间上的先后顺序。
- volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”是指时间上的先后顺序。
- 线程启动规则:thread 对象的 start()方法先行发生于此线程的每一个动作。
- 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过thread.join()方法结束、thread.isAlive()的返回值等手段检测到线程已经终止执行。
- 线程中断规则:对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted()方法检测到是否由中断发生。
- 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
- 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论
Java语言无需任何同步手段保障就能成立的先行发生规则就只有上面这些,
Java线程的实现
实现线程主要有 3 种方式:使用内核线程实现、使用用户线程实现和使用用户线程加轻量级进程混合实现。
- 使用内核线程实现
内核线程(Kernel-Level Thread,KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。
程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口----轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常意义上所讲的线程。
Java线程状态转换
- 新建(NEW):创建后尚未启动的线程处于这种状态
- 运行(Runable):Runable包括了操作系统线程状态中的 Running 和 Ready ,也就是处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间。
- 无限期等待(Waiting):处于这种状态的线程不会被分配CPU执行时间,他们要等待其他线程显式地唤醒,一下方法会让线程陷入无限期的等待状态:
- 没有设置Timeout参数的Object.wait()方法。
- 没有设置Timeout参数的Thread.join()方法。
- LockSupport.park()方法。
- 阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是:
- “阻塞状态”在等待着获取到一个排他锁,这个时间将在另外一个线程放弃这个锁的时候发生;
- “等待状态”则是在登台一段时间,或者唤醒动作发生,在程序等待进入同步区域的时候,线程将进入这种状态。
- 结束(Terminated):已终止线程的线程状态,线程已经结束执行。
锁
自旋锁与自适应自旋
自旋锁在jdk1.4.2引入,参数为 -XX:+UseSpinning参数来开启,在JDK1.6中就已经改为了默认开启。自旋等待不能代替阻塞,自旋等待虽然避免了线程开销,但是要占用处理器时间。如果锁占用时间短,那么自旋等待的效果非常好,反之会浪费处理器资源,所以Java规定了自旋的默认次数,默认为10此,参数为 -XX:PreBlockSpin。
JDK1.6中也引入了自适应的自旋锁,意味着自旋的时间不在固定,
- 而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机会认为这次自旋由可能再次成功,
- 如果对于某个锁,自旋很少成功获得过,拿在以后要获取这个锁时可能将省略掉自旋过程
锁消除
锁消除是指在虚拟机即时编译器在运行时,对代码要求同步,但是检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判断依据来源于逃逸分析的数据支持,判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它当作栈上数据对待,认为他们时线程私有的,同步加锁自然就无须进行。
例如:一段代码中并没有任务有地方加了锁,String类型的数据去累加操作,虚拟机会把它编译成stringbuffer的形式去append,那么这个stringbuffer其实是加了锁的,jdk1.5之后就换成stringbuilder来append,所以类似stringbuffer这种,就可以被消除掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。
锁粗化
如果虚拟机探测到由这样遗传零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。
轻量级锁
“轻量级” 是相对于使用操作系统互斥量来实现的串通锁而言的,因此传统的锁就称为了“重量级”锁。需要强调的是,轻量级锁并不是用来代替重量级锁的,本意是在没有多线程竞争的前提下,减少传统的重量级锁对操作系统互斥量产生的性能消耗。
理解轻量级锁,必须从Hotspot虚拟机的对象(对象头部分)的内存布局开始介绍。对象头分为两部分信息,第一部分用于存储自身的运行时数据,如hashcode,GC分代年龄等,官方称为“Mark Word”,是实现轻量级锁和偏向锁的关键,另外一部分用于存储指向方法去对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分用于存储数组长度。
HotSpot 虚拟机对象头 Mark Word
存储内容 | 标识位 | 状态 |
对象哈希吗、对象分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
空,不需要记录信息 | 11 | GC标记 |
偏向线程ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
轻量级锁加锁
在代码进入同步快的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录的空间(Lock Record),用于存储锁对象目前的 Mark word的拷贝
然后,虚拟机将使用CAS操作尝试将对象的Mark word更新为指向Lock Record的指针。如果成功,表示这个线程拥有了该对象的锁,并且对象的Mark word的锁标志位最后两位将转变为“00”,即表示此对象处于轻量级锁定状态。
如果更新失败,虚拟机首先去检查对象的Mark word是否指向当前线程的栈帧,如果是当前线程已经拥有了这个对象的锁,那就可以进入同步快继续执行,否则说明这个锁对象被其他线程抢占了,如果由两条以上的线程竞争同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,锁标志的状态变为“10”,Mark word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
轻量级锁解锁
上面描述的是轻量级锁的加锁过程,它的解锁过程也是通过CAS操作来进行的,如果对象的Mark word仍然指向着线程的锁记录,如果替换成功,整个同步过程就完成,如果替换失败,说明由其他线程尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程。
轻量级锁能提升性能的依据是,对于绝大部分的锁,在整个同步周期内都是不存在竞争的,轻量级锁使用CAS操作避免了使用互斥量的开销,但是如果存在锁竞争,除了互斥的开销,还额外的发生了CAS操作,因此在竞争下,轻量级锁会比传统的重量级锁更慢。
偏向锁
它的意思是这个锁会偏向第一个获得它的线程,如果在接下来的过程中,该锁没有被其他线程获取,持有偏向锁的线程永远不需要在进行同步。
偏向锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式,同时使用CAS操作把获取到的这个锁的线程的ID记录在对象的 Mark word 之中,如果操作成功,持有偏向锁的线程以后每次进入这个锁的同步快,虚拟机都不在进行同步,
当由另外的线程获取这个锁时,偏向模式宣布结束,根据锁对象目前是否处于锁定状态,撤销偏向(Revoke Bias)后恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态,后续就像轻量级锁那样执行。