当前位置:   article > 正文

java 对象锁_Java的对象锁

java 对象锁

内置锁

Java提供了一种内置的锁机制来支持原子性和可见性:同步代码块(Synchronized Block)。同步代码块包括两部分:一个是作为锁的对象引用,一个是锁保护的代码块。每一个Java对象都可以用做一个实现同步的锁,这种锁被称为内置锁(Intrinsic Lock)或者监视器锁(Monitor Lock)。线程进入同步代码块之前会自动获得锁,并且在退出同步代码块时(正常返回,或者是异常退出)会自动释放锁。

同步代码块以关键字synchronized修饰,例如:

synchronized(锁对象引用){

//锁保护的代码块

}

如果synchronized修饰的是对象的方法,那么被修饰的方法体就是同步代码块,锁的对象引用就是被修饰的方法所在的对象。

public class SyncTest {

public synchronized void method() {

//方法体就是同步代码块

}

}

如果synchronized修饰的是静态方法,那么被修饰的方法体就是同步代码块,锁的对象引用就是被修饰的方法所在的Class对象。

public class SyncTest {

public static synchronized void method() {

//方法体就是同步代码块

}

}

内置锁的特性

互斥:同一时间最多只有一个线程能够持有这种锁。

线程尝试获取一个被其它线程持有的内置锁,线程必须等待(自旋)或者阻塞(自旋策略失效),并且因为请求内置锁被阻塞的线程不能被中断。

可重入:如果某个线程试图获取一个已经由它持有的内置锁,那么这个请求就会成功。

实现原理:为每个所关联一个获取计数值和一个所有者线程。当计数值为0,表示这个锁没有被任何线程持有。当线程请求一个未被持有的锁,JVM将所有者线程设置为请求线程,并且将计数值置为1。如果同一个线程再次请求这个锁,计数值递增,退出同步代码块计数值将递减。如果计数值为0,这个锁将被释放。

synchronized的原理

当声明 synchronized 代码块时,编译而成的字节码将包含 monitorenter 和 monitorexit 指令。这两种指令均会消耗操作数栈上的一个引用类型的元素(也就是 synchronized 关键字括号里的引用),作为所要加锁和解锁的锁对象。

自旋

因为监视器锁实现的同步是互斥同步,互斥导致的Java 线程的阻塞以及唤醒,都是依靠操作系统来完成的。举例来说,对于符合 posix 接口的操作系统(如 macOS 和绝大部分的 Linux),上述操作是通过 pthread 的互斥锁(mutex)来实现的。这些操作将涉及系统调用,需要从操作系统的用户态切换至内核态,这些操作给操作系统的并发性能带来了很大的开销。

自旋的实现原理就是,如果线程请求获取监视器锁失败,并不立刻阻塞线程,而是让线程执行一个忙循环(自旋)。自旋之后再次尝试获取锁。如果获取锁失败,这个过程会循环一定次数,超过某个阀值,如果还是获取不到锁,才阻塞线程。自旋可以通过-XX:+UserSpinning参数来开启,自旋的次数通过-XX:PreBlockSpin来更改(默认是10)。

自旋虽然避免了线程切换的损耗,但是需要占用处理器时间。自旋的效果取决于锁被占用的时间,如果锁被占用的时间很短,自旋等待的效果就会很好,反之,自旋只会白白消耗处理器资源,带来性能上的损耗。

JDK1.6引入了自适应的自旋锁。

锁优化

自旋锁

见自旋小节

重量级锁

重量级锁是 JVM中传统的锁实现。在这种状态下,Java 虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。

轻量级锁

轻量级锁的目标是在没有多线程的竞争下,减少重量级锁使用的操作系统互斥量产生的性能消耗。

原理

轻量级锁的实现依赖对象头的标记字段。Java的对象头被设计为能够根据对象的状态复用自己的储存空间,对象头的标记字段有2bit用于储存锁状态,不同的锁状态对应的对象头的内容及状态之间转换如下图:

5fbe42932f81

对象头标记字段的锁状态的转化

加锁过程

当进行加锁操作时,JVM会判断是否已经是重量级锁。如果不是,它会在当前线程的当前栈桢中划出一块空间,作为该锁的锁记录(Lock Record),并且将锁对象的标记字段复制到该锁记录中。

然后,JVM会尝试用 CAS(compare-and-swap)操作将锁对象的标记字段替换为锁记录的指针。如果操作成功,那么这个线程就获取了这个对象的锁,并且标记字段的锁标志位转变为“00”,表示该锁处于轻量级锁定状态。

如果标记字段替换操作失败,JVM会先检查对象的标记字段是否指向当前线程的栈帧,如果是表明当前线程已经持有了该对象的锁。否则说明这个对象的锁已经被其它线程所持有了,这时,轻量级锁膨胀为重量级锁,锁的标记字段的锁标志位变为“10”,标记字段存储的就是指向重量级锁(互斥量)的指针,后面的线程要进入阻塞状态。

解锁过程

轻量级锁的解锁过程也要通过CAS操作来进行。如果锁对象的标记字段仍然指向线程的锁记录,JVM尝试用CAS操作将锁对象的标记字段替换为锁记录中的复制过来的标记字段。如果CAS操作成功,则释放了锁。否则说明有其他线程尝试获取过该对象的锁,那么在释放锁的同时,唤醒被挂起的线程。

性能分析

轻量级锁提升性能的依据是:对于绝大部分的锁,在整个同步期间都是不存在竞争的。如果没有竞争,轻量级锁使用CAS操作避免了重量级锁使用互斥量的开销。如果存在竞争,除了重量级锁的互斥量开销,还带来了CAS操作的开销,性能反而比重量级锁差。

偏向锁

偏向锁的目的也是消除无竞争条件下的同步原语。偏向锁会偏向于第一个获取到它的线程,如果在获取到锁之后的过程中,没有发生锁竞争,那么持有偏向锁的线程将永远不需要再进行同步。相比轻量级锁,偏向锁能够消除轻量级锁多次加锁的CAS操作。

加锁过程

具体来说,在线程进行加锁时,如果该锁对象支持偏向锁,那么 JVM会通过 CAS 操作,将当前线程的ID记录在锁对象的标记字段之中,并且将标记字段的锁标志位置为“01”,即偏向模式。

如果操作成功,在接下来的运行过程中,每当有线程请求这把锁,Java 虚拟机只需判断锁对象标记字段中:最后三位是否为 101,是否包含当前线程的ID,以及 epoch 值是否和锁对象的类的 epoch 值相同。如果都满足,那么当前线程持有该偏向锁,可以直接返回。

当有另外的线程尝试获取这个锁时,偏向模式宣告结束。如果当前对存于未锁定状态,撤销偏向恢复至未锁定(标记字段的锁标志位为“01”)。如果处于锁定状态,则升级为轻量级锁(标记字段的锁标志位为“00”),后续的同步操作便按照轻量级锁的规则来进行。

偏向锁失效

如果某一类锁对象的总撤销数超过了一个阈值(对应JVM参数-XX:BiasedLockingBulkRebiasThreshold,默认为 20),那么 JVM会宣布这个类的偏向锁失效。

如果总撤销数超过另一个阈值(对应 JVM参数 -XX:BiasedLockingBulkRevokeThreshold,默认值为 40),那么 Java 虚拟机会认为这个类已经不再适合偏向锁。此时,Java 虚拟机会撤销该类实例的偏向锁,并且在之后的加锁过程中直接为该类实例设置轻量级锁。

其它优化手段

锁消除

JVM通过逃逸分析,如果判断出在一段代码中,堆上的所有数据都不会逃逸出去从而被其它线程访问到,就可以把他们当做栈上数据看待,同步加锁就不需要进行。

锁粗化

如果一系列的连续操作都是对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中(如StringBuffer的连续多次append操作),JVM会将加锁同步的范围扩展到这个操作系列的外部。

参考

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/292078
推荐阅读
相关标签
  

闽ICP备14008679号