synchronized关键字
synchronized是一种非常传统的阻塞同步方式。用它加锁意味着多线程环境下,当多个线程需要进同步块时,会以串行的方式运行,即没有获得锁的线程会等到获取锁的线程执行完同步块,释放锁资源后才能竞争锁,得到锁后才能继续运行。但是到了JDK1.6进行优化后,就不是那么简单了。
加锁和解锁
synchronized可加三种锁:
- 给普通方法加锁,锁住实例对象
- 给静态方法加锁,锁住类
- 给同步块加锁,锁住括号里配置的对象
synchronized在一个线程获取锁资源后,其他线程处于Blocked状态,等待线程执行完同步部分。
原理
synchronized对应在字节指令上,进入同步体前会有一个monitorenter指令,执行完同步体后会执行monitorexit指令。表示获取到同步资源的锁和释放同步资源的锁。
JDK1.6之后synchronized的优化
优化后,由原来的重量级锁演变为无锁、偏向锁、轻量级锁、重量级锁的升级过程。下面从JVM的角度来解析加锁过程。
对象头
在JVM的对象内存布局中,我们知道对象布局包括三个部分:对象头、实例数据和对齐填充。其中对象头包括Mark word、类型指针(可有可无),若是数组对象,还有一段区域存储数组维度。Mark Word常规情况下存储的是对象的hash码,分代年龄,标志位,是否开启偏向锁。如下图所示:
图中以32bit的对象头举例,标出了各种情况下对象头的存储内容的变化。
偏向锁的加锁和解锁
轻量级锁的加锁和解锁
在获取轻量级锁时,线程栈中会划分一块lock record的空间存储Mark Word的内容,然后尝试CAS操作使得Mark Word内的存储内容修改为指向本地lock record的指针,成功则获取轻量级锁,标志位设置为00。自旋过程中还是获取失败,则将该锁膨胀为重量级锁。Mark Word改为指向互斥量的指针,并且标志位为10.
解锁时,若CAS更新Mark Word失败,说明已经膨胀为重量级锁(因为重量级锁的Mark Word存储的是指向互斥量的指针,CAS预期值为指向本地lock record的指针)。于是,释放锁并唤醒等待互斥量的其他线程。
volatile
一提到volatile,我们就知道它的作用,脱口而出:可见性和禁止指令重排序,对于复合操作无法保证原子性。但是要说明原理,还得从编译器到JVM到操作系统说起。
volatile的内存语义
特性:
可见性
禁止指令重排序
单独读、写操作的原子性,但无法保证如自增这样的复合操作的原子性。
写-读内存语义:
1.写一个volatile变量,需要立即将对该变量的修改刷新到主内存中。
2.读一个volatile变量,会首先将本地内存中的变量置为无效,然后从主内存中读取最新的值
从JVM角度分析原理
java编译器在编译volatile变量的读写操作时,会插入内存屏障,禁止指令重排序,保证内存可见性。
基于保守策略的内存屏障:
- 在volatile写之前插入storestore,禁止写写重排序
- 在volatile写之后插入storeload,禁止写读重排序
- 在volatile读之后插入loadload,禁止读读重排序
- 在volatile读之后插入loadstore,禁止读写重排序
保守策略适应任何平台。volatile变量和普通变量的读写不能重排序是在JSR-133时更新的。
在X86中,由于不存在读写、写写、读读的重排序,所以只需要插入storeload即可。
从底层分析原理
JVM的字节码最后还是会转化为操作系统的指令,在volatile变量的写操作指令前会加上lock前缀,改前缀做了两件事:
- 将缓存的数据刷新到主内存中。
- 通过缓存一致性协议,让其他线程中缓存的该变量的数据无效。