代码执行过程
java代码首先会被javac编译成java字节码,然后通过class loader(类加载器)加载到JVM里,然后通过执行引擎将字节码转为汇编指令在CPU上执行。
volatile
它保证了在多核CPU中,共享变量的可见性。它比synchronized更轻量,在JAVA语言规范第三版中有明确说明,允许线程访问共享变量,但线程应该有排它锁来单独获取这个变量。volatile在某些情况比排它锁更方便,如果某个变量被声明为volatile,那么JVM能保证所有线程看到的该变量值一样。
synchronized
同步锁,在JDK1.6之后它已不再是重量级锁,而是引入了偏向锁,轻量级锁。
- 普通方法,锁的对象是当前实例
- 静态方法,锁的对象是该类
- 方法块,括号内的对象
它加锁是根据进入和退出monitor来确定的,也就是说在我们编译的字节码文件中,对应加了synchronized的对象,会在该同步代码块之前加上monitorenter,结束后会加上monitorexit。
JAVA对象头
如果对象是数组,则占用3个字宽,在32位操作系统中,占用32+32+32
位,在64位中占用64+64+32
位。
他们分别是:
长度 | 内容 | 说明 |
---|---|---|
32/64 | Mark Word | 存储对象的Hashcode和锁信息 |
32/64 | Class MataData Address | 存储到对象的数据指针 |
32 | Array Length | 数组长度 |
Mark Word
锁状态 | 25bit | 4bit | 1bit | 2bit | |
23bit | 2bit | 是否偏向 | 锁标志位 | ||
无锁 | 对象hashcode | 对象分代年龄 | 0 | 01 | |
偏向 | 线程ID | Epoch | 对象分代年龄 | 1 | 01 |
轻量 | 栈中锁记录的指针 | 00 | |||
重量 | 指向互斥量的指针 | 10 | |||
GC标记 | 空 | 11 |
由上面表格可以看出,无锁和偏向的锁标志位都是01。偏向锁一般用于锁竞争小,或者无竞争的情况,如果竞争强烈,偏向锁会影响性能,因为它本身不会释放锁,而是CAS获取偏向锁失败时,偏向锁会在全局安全点时,获得该偏向锁的线程挂起,然后判断该线程是否还在方法体内,如果在那么升级为轻量锁,如果已经退出则释放锁,变成无锁状态,这需要时间开销。
轻量级锁的获取
线程在执行同步代码块之前,JVM会在当前线程的栈帧中创建保存锁记录的空间,并将对象头中的Mark Word复制到锁记录中,然后尝试使用CAS将MarkWord替换为创建的锁记录空间的指针。如果成功获得锁,偏亮升级为轻量。如果失败,自旋获取锁。
轻量级锁解锁
用CAS操作将Mark Word替换回原来的对象头,如果失败锁膨胀为重量锁。
CAS操作
在java中可以通过锁或者循环CAS来实现原子性操作。
循环CAS就是一直尝试,知道成功。CAS的操作其实就是比较,替换。
CAS的三大问题
- ABA问题。意思就是CAS会检测值有没有发生变化,如果一个值从A-B-A值本身没有变化,但实际是由变化过程的,但是CAS会认为没有变化。要感知这种变化,可以再值前面加上版本号,比如:1A-2B-3A
- 循环时间长。意思就是自旋CAS长时间不成功,会造成CPU开销很大
- 只能保证一个共享变量。因为是比较替换,所以只能保证一个共享变量。
锁保证原子操作
也就是说锁可以保证只有获得锁的线程才能够操作锁定的区域。但是除了偏向锁,JVM实现锁的方式都用了循环CAS实现。到底是谁成就了谁?