java代码编译成字节码,然后被类加载器加载到jvm中,jvm执行,最终转换为汇编指令在cpu上执行,java的并发机制其实依赖的是jvm和cpu的指令。
volatile加在共享变量上,保证所有线程看到这个变量的值是一致的,即valoatile变量相当于加了一个读写锁,是通过原子操作实现的。
实现原理:汇编代码给volatile变量加了lock前缀指令。
lock指令:将当前cpu缓存的volatile变量立刻写回系统内存,并且使其他cpu缓存中的volatile变量数据无效。(是通过硬件的EMSI实现,不懂的朋友可以参考上篇EMSI的文章)
synchronized保证一个方法或者代码块同时只有一个线程执行(不会发生中断和抢占)。
Java中每个对象都可以作为锁(对象头)
具体实现:
普通同步方法:锁是当前实例对象。
静态同步方法:锁是当前类的class文件。
对于配置了synchonized对象的,锁是配置对象。
当进程访问代码块时,必须先得到锁,退出或抛出异常时释放锁。
synchronized是通过jvm实现的,jvm是通过monitorenter和monitorexit指令实现的。
monitorenter获得对象锁,monitorexit释放对象锁。对象锁存在于对象头中。
markword:存储对象的hashcode和锁信息。
ClassMetadataAddress:对象的元数据(包括方法表,构造方法,成员变量。。)
arrayLength:数组的长度(如果是数组的话,如果不是数组没有这个)
锁存在于MarkWord中:
锁状态 | 25bit | 4bit | 1bit是否是偏向锁 | 2bit锁标志位 |
对象的hashcode | 对象分代年龄 | 0 | 01 |
各种锁的MarkWord:
轻量级锁 | 指向 | 栈中锁记录的 | 指针 | 00 |
重量级锁 | 指向互斥 | 量(重量级锁) | 的指针 | 10 |
GC标记 | 11 | |||
偏向锁 | 线程ID | epoch,对象分代年龄 | 1 | 01 |
(2)锁升级
在synchronized中有四种级别的锁:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。
锁只能升级,不能降级。
(1)偏向锁:一个锁由一个对象多次获得,为了代价最低,引入了偏向锁
当一个线程获得了无锁状态的锁,那么就会将自己线程ID通过CAS记录进对象头MarkWord中,从无锁升级为偏向锁。
当线程再次获得了锁对象,就检测一下锁对象的对象头的MardWCASDord中的偏向锁是否是自己的线程ID,如果是直接用,如果不是则用CAS修改为自己的线程ID,然后使用。
如果CAS一次成功则获得偏向锁,如果失败则进行锁升级。
(2)轻量级锁(不会阻塞线程,线程自适应自旋拿锁):
jvm现在当前线程的栈中创建用于存储锁记录的空间,并且将对象头中的MarkWord复制到锁记录中,然后线程会尝试用CAS将对象头中的markword替换为指向锁的指针(指向栈中锁的指针),如果成功,当前线程获得锁,如果失败,表示其他进程在竞争锁,当前线程便使用自旋来获得锁。
如果自旋很多次都没有获得锁,那么就会膨胀成重量级锁。
(3)重量级锁(阻塞线程)
这是一个操作系统的互斥锁(或者是信号量),一旦线程完成了synchronized就会释放锁,其他进程竞争锁。
开销比较大,应对的是高竞争。
没有获得锁的进程会进入阻塞,当有锁时候会被唤醒,这会造成上下文切换,造成较大的性能损失。
当存在跨总线宽度,跨多个缓存行和跨页表时候,处理器不能保证其原子性,但是可以提供总线锁定和缓存锁定来保证原子性。
MESI协议
(1)总线锁定:当一个cpu处理时,其他cpu无法访问这个内存。
(2)缓存锁定:使用缓存一致性MESI,使用缓存行时候,其他cpu缓存中的共享缓存行设置为invalied。