首先了解一下JMM中定义的内存操做:java
一个线程操做数据时候都是从主内存(堆内存)读取到本身工做内存(线程私有的数据区域)中再进行操做。对于硬件内存来讲,并无工做内存和主内存的区分,这都是java内存模型划分出来的,它只是一种抽象的概念,是一组规则,并非实际存在的。Java内存模型中定义了八种同步操做:安全
1.lock(锁定):做用于主内存的变量,把一个变量标记为一条线程独占状态多线程
2.unlock(解锁):做用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才能够被其余线程锁定jvm
3.read(读取):做用于主内存的变量,把一个变量值从主内存传输到线程的工做内存中,以便随后的load动做使用 性能
4.load(载入):做用于工做内存的变量,它把read操做从主内存中获得的变量值放入工做内存的变量副本中优化
5.use(使用):做用于工做内存的变量,把工做内存中的一个变量值传递给执行引擎spa
6.assign(赋值):做用于工做内存的变量,它把一个从执行引擎接收到的值赋给工做内存的变量操作系统
7.store(存储):做用于工做内存的变量,把工做内存中的一个变量的值传送到主内存中,以便随后的write的操做 插件
8.write(写入):做用于工做内存的变量,它把store操做从工做内存中的一个变量的值传送到主内存的变量中线程
若是要把一个变量从主内存中复制到工做内存中,就须要按顺序地执行read和load操做, 若是把变量从工做内存中同步到主内存中,就须要按顺序地执行store和write操做。但Java内存模型只要求上述操做必须按顺序执行,而没有保证必须是连续执行。
同步规则:
对一个变量执行unlock操做以前,必须先把此变量同步到主内存中(执行store和write操做)
Synchronized:
synchronized是jvm内置的同步锁,它是隐式锁,不须要咱们本身手动释放锁。
每个java对象中都有一个内部对象Monitor。synchronized就是经过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操做系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低(jdk1.6以后进行了优化)。
当咱们在代码中使用了synchronized以后,能够在字节码文件看到MONITORENTER和MONITOREXIT。Idea中安装了ByteCode Viewer插件就能够查看字节码,选中编译完的class文件
java虚拟机中ObjectMonitor的定义:(虚拟机C++代码片断)
加锁的过程:
Monitor.Enter和Monitor.Exit就是做用在JMM中定义的内存操做中的lock和unlock上面。而后从上面的同步规则中能够知道一个变量在同一时刻只容许一条线程对其进行lock操做,lock操做的时候会清空工做内存,从新去主内存load最新的数据。Unlock操做则会执行store和write操做将工做内存中的数据写回主内存。这也就是为何咱们用了Synchronized关键字以后就可以实现线程安全。
Java对象内存结构:
对象在内存中存储的结构由三部分组成:对象头,主要是一些标记信息MarkWord,好比hashcode,锁状态这些;实例数据,就是真实的数据;对齐填充,要求对象大小8字节的整数倍,若是不是就填充补齐。
MarkWord:锁状态标记就在这里面,以32位jvm为例,64位也是这些东西,只是占的大小不同
无锁状态:前25位记录的是hashcode,后四位是对象分代年龄,而后是不是偏向锁标记
偏向锁状态:前23位是偏向的线程ID
轻量级锁:前30位指向线程栈中锁记录的指针
重量级锁:前30位指向重量级锁Monitor的指针
JVM内置锁优化升级
JDK1.6版本以后对synchronized的实现进行了各类优化,自旋锁、偏向锁和轻量级锁
并默认开启偏向锁
开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking
偏向锁
偏向锁是Java 6以后加入的新锁,它是一种针对加锁操做的优化手段,通过研究发现,在大多数状况下,锁不只不存在多线程竞争,并且老是由同一线程屡次得到,所以为了减小同一线程获取锁(会涉及到一些CAS操做,耗时)的代价而引入偏向锁。偏向锁的核心思想是,若是一个线程得到了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需 再作任何同步操做,即获取锁的过程,这样就省去了大量有关锁申请的操做,从 而也就提供程序的性能。因此,对于没有锁竞争的场合,偏向锁有很好的优化效 果,毕竟极有可能连续屡次是同一个线程申请相同的锁。可是对于锁竞争比较激 烈的场合,偏向锁就失效了,由于这样场合极有可能每次申请锁的线程都是不相 同的,所以这种场合下不该该使用偏向锁,不然会得不偿失,须要注意的是,偏向锁失败后,并不会当即膨胀为重量级锁,而是先升级为轻量级锁。
轻量级锁
假若偏向锁失败,虚拟机并不会当即升级为重量级锁,它还会尝试使用一种 称为轻量级锁的优化手段(1.6以后加入的),此时Mark Word 的结构也变为轻量 级锁的结构。轻量级锁可以提高程序性能的依据是“对绝大部分的锁,在整个同 步周期内都不存在竞争”,注意这是经验数据。须要了解的是,轻量级锁所适应 的场景是线程交替执行同步块的场合,若是存在同一时间访问同一锁的场合,就会致使轻量级锁膨胀为重量级锁。这个时候也就是上面的Monitor.Enter和Monitor.Exit。锁的升级过程是不可逆的。
自旋锁
虚拟机为了不线程真实地在操做系统层面挂起,会进行一项称为自旋锁的优化手段。它是一个过渡,每一次升级以前先进行自旋,好比经过必定的自旋以后发现仍是偏向锁锁的场景那么就不进行锁的升级。这是基于在大多数状况下,线程持有锁的时间都不会太长,若是直接挂起操做系统层面的线程可能会得不偿失,毕竟操做系统实 现线程之间的切换时须要从用户态转换到核心态,这个状态之间的转换须要相对 比较长的时间,时间成本相对较高,所以自旋锁会假设在不久未来,当前的线程 能够得到锁,所以虚拟机会让当前想要获取锁的线程作几个空循环(这也是称为 自旋的缘由),通常不会过久,多是50个循环或100循环,在通过若干次循环后,若是获得锁,就顺利进入临界区。若是还不能得到锁,那就会将线程在操做 系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是能够提高效率的。
整个过程以下图