首先我们需要知道synchronized这个重量级锁的底层原理。synchronized是一种对象锁,它锁的对象是某个类的对象实例或某个类。在JVM中,每个类实例对象的对象头中都有一个monitor关键字,也就是所谓的管程,获得一个锁就等于获得了一个对象的管程,而每个对象只有一个管程,没有获得锁的线程会被操作系统阻塞,如下阻塞的线程会放入到管程的一个Entrylist集合。而我们要知道,管程是操作系统所有的,所以使用它的成本是很大的,所以我们需要对synchronized进行优化。
使用场景:如果一个对象虽然有多线程访问,但所现场访问的时间是错开的(也就是没有竞争,有竞争轻量级锁会升级为重量级锁),那么可以使用轻量级锁来优化。轻量级锁对使用者是透明的,即语法仍然是synchronized,例如下面案例:
static final Object obj=new Object();
public static void method1(){
synchronized(obj){
//同步块A
method2();
}
}
public static void method2(){
synchronized(obj){
//同步块B
}
}
CAS是一种乐观锁机制,也被称为无锁机制。全称: Compare-And-Swap。它是并发编程中的一种原子操作,通常用于多线程环境下实现同步和线程安全。CAS操作通过比较内存中的值与期望值是否相等来确定是否执行交换操作。如果相等,则执行交换操作,否则不执行。
4. 如果交换成功,对象头中存储了锁记录地址和状态00,表示该线程给对象加锁
7. 当退出synchronized代码块(解锁)锁记录的值不为null,这时使用CAS将Mark Word的值恢复给对象头
如果在尝试轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为了重量级锁。
static Object obj=new Object()
public static void method1(){
}
重量级锁竞争时,还可以采用自旋来进行优化,如果当前线程自旋成功(即这时候持有锁的现场已经退出了同步块,释放了锁),此时当前现场就可以避免阻塞(前面我们所到,当一个现场申请Monitor锁时,若发现owner非空,就会进入EntryList进行阻塞,自旋优化的目的就是,若线程发现owner不为空,就会原地进行一定数量的自旋而不直接进入EntryList进行阻塞,如果自旋期间Monitor锁被释放了,自旋的线程就可以获得Monitor锁,这样就避免了阻塞-即避免了上下文切换)。
自旋重试成功的情况
自旋重试失败的情况
轻量级锁在没有竞争的时候,每次重入仍然需要执行CAS操作。Java 6引入了偏向锁来做进一步优化:只有一次使用CAS将线程ID设置到对象头的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不重新CAS。以后只要不发生竞争,这个对象就归线程所有。
一个对象创建时:
-XX:BiasedLockingStartupDelay=0
来禁用延迟
注意:
- hashcode会禁用一个对象的偏向锁,这是因为hashcode被调用后,线程ID在对象头中就没有多余的位置存储线程ID了,所以就会让偏向锁失效
- 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
- 调用wait/notify会使偏向锁失效(因为这个机制只有重量级锁中才有,所以偏向锁会升级为重量级锁)
如果对象虽然被多个线程访问,但没有竞争(即一种时间错开的访问),这时偏向了现场1的对象仍有机会偏向线程2,重偏向会重置对象的线程ID。当偏向锁失效的阈值超过20次后,jvm会觉得,我是不是偏向错了,于是会给这些对象加锁时重新偏向至加锁线程(这是对偏向锁情况失效的优化)。
案例场景
1. 创建一个user类
2. 初始化一个集合
3. 线程t0给这个集合循环添加30个user对象,然后分别给每个对象加锁,此时每个user对象头的信息都会显示偏向现线程t0
4. 然后线程t2再给集合中的30个对象再次加锁,会发现前20个对象,偏向锁会被撤销,会使用轻量级锁。而后10个对象由于jvm进行了批量重偏向,所以对象user类的对象重偏向到了t2
当偏向锁失效超过40次后(说明有很多现场会访问该对象),jvm会觉得,自己确实偏向错了,根本不应该偏向。于是整个类对象都会变得不可偏向,新建的对象也是不可偏向的。
案例场景
1. 创建一个user类
2. 初始化一个集合
3. 线程t0给这个集合循环添加40个user对象,然后分别给每个对象加锁,此时每个user对象头的信息都会显示偏向现线程t0
4. 然后线程t2再给集合中的30个对象再次加锁,会发现前19个对象,偏向锁会被撤销,会使用轻量级锁。而后11个对象由于jvm进行了批量重偏向,所以对象user类的对象重偏向到了t2
5. 然后线程t3重新给这40个对象加锁,会发现前19个对象由于t2撤销了重偏向所以前面19个对象还是撤销重偏向状态,,而后面出现批量重偏向撤销,而从20个对象开始的对象时偏向t2线程所以t3同样会进行批量撤销重定向操作,一直到第40个对象时已经有39次撤销操作了,所以user类以后所有对象会被设置为不可重偏向
首先我们使用JMH对下面代码进行一个基准测试:
//总共做几轮测试
@Fork(1)
//采用吞吐量的模式
@BenchmarkMode(Mode.AverageTime)
//执行预热的次数
@Warmup(iterations = 3)
//正式测试的次数
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class jvmtestMain {
static int x=0;
@Benchmark
public void a()throws Exception {
x++;
}
@Benchmark
public void b() throws Exception{
Object o=new Object();
synchronized (o){
x++;
}
}
}
打包运行
java -jar benchmarks.jar
最后结果发现加锁的b和没加锁的a性能(score)几乎差不多(按道理来说加锁会对程序性能有很大影响),这是因为JIT的存在,它会对我们字节码进行进一步优化,JIT会发现局部变量o不会逃逸出b方法的作用域(逃逸分析),即它是线程私有的不会出现并发安全问题,所以JIT对取消对变量o加锁。这中JIT的优化行为就称为锁消除,我们可以通过-XX: -Eliminatelocks
来关闭JVM进行锁消除优化。