文章目录
参考:黑马程序员
1.Java对象头
以32位虚拟机为例
普通对象的对象头一共有64位前面的32为是Mark Word后面的32位是Klass Word,这个Klass Word是一个指针他指向了这个对象所从属的Class。
MarkWord的32位(普通)包括了25位的hashcode,4位的GC分代年龄,1位标识是否偏向锁,最后两位标识了加锁状态。
2. synchronized工作原理
Monitor被翻译成监视器或管程
当有一个线程进入synchronized代码块中这个obj对象的对象头中MarkWord的头30位会指向Monitor监视器,后两位加锁状态会发生改变然后看一下Owner是否为空,如果为空则会指向进来的线程,说明这个进来的线程已经持有锁,这时候如果其他线程进来也会检查一下Owner是否为空如果为空就会占有这个锁,如果不为空则会被EntryList指向,这个EntryList可以理解为一个阻塞队列,如果Thread2执行结束释放锁,那么阻塞队列的线程会竞争上岗,抢占这个Owner。
3.synchronized工作原理(字节码角度)
来看这一段代码然后我们对他进行一次反编译看看字节码角度synchronized是怎么工作的。
public class Test4 {
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock){
counter++;
}
}
}
4.synchronized优化原理
4.1轻量级锁
使用场景:如果一个对象虽然有多线程访问,但是多线程访问的时间是错开的(也就是说没有竞争),那么可以使用轻量级锁来优化。轻量级锁对使用者来说是透明的,即语法仍然是synchronized。
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
首先每个线程的栈帧里面会有一个锁记录Lock Record,当加锁的时侯会将锁记录里面的ObjectReference指向锁对象,然后通过一个cas操作将锁记录中的lock Record地址和Object的Mark Word进行交换
如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁。
此时如图所示。
在CAS操作过程中有可能出现失败的情况一种是这个锁对象已经被别人使用了。这时候Object原来的Mark Word中的内容已经是锁记录地址和状态00。这时候说明锁存在竞争,进入锁膨胀。
还有一种就是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数。这个例子就是开头的那段代码method2外面被加了一个锁,method2里面又加了一次锁,这就是锁重入。
如图所示当发生锁重入时栈帧里面仍然会创建一个锁记录并把自己的Object Reference指向Object这时候还会寻求执行CAS操作不过当他发现Object里面已经有lock Record地址的时候就会失败这个部分就会被置为空值不过这也没事,因为他知道自己线程栈帧中的其他锁记录已经指向了Object,这个锁重入的锁记录主要目的是计数看看加了几层锁。
当解锁的时候会查看这个锁记录一旦发现里面的值是空,说明有锁重入然后就把他删了,表示重入记录减1。当发现这个锁记录的值不为空,这时使用CAS将Mark Word的值恢复给对象头,成功则解锁成功,失败则说明轻量级锁已经升级为重量级锁,进入重量级锁的解锁过程。
4.2锁膨胀
来看下面这个例子。
Thread-0已经获取了锁对象,这时候Thread-1也想获取这个锁于是他把自己的锁记录中的Object Reference,指向Object,然后他想让自己的锁记录地址和Object的Mark Word做CAS操作,但是这个CAS操作无法成功,已经有其他线程上了轻量级锁有竞争,这时候需要进行锁膨胀,将轻量级锁升级成重量级锁。
这时候Thread-1加轻量级锁失败,进入锁膨胀流程。
- 为Object对象申请Monitor锁,让Object指向重量级锁地址。Object对象的MarkWord改成Monitor地址(前30位)后2位改成10说明一下这是重量级锁。
- 然后自己进入Monitor的EntryList BLOCKED阻塞状态。
- 当Thread-0退出同步块解锁的时候,使用CAS将Mark Word的值恢复给对象头,失败。这时会进入重量级锁流程,即按照Monitor地址找到Monitor对象,设置Owner位null,唤醒EntryList中的BLOCKED线程。
5.自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这个后持锁线程已经释放了锁),这时当前线程就可以避免阻塞。
注意事项:
- java6之后这个自旋是自适应的比如说你这次成功了那么认为自旋成功可能性高,就会多旋几次;反之就少自旋甚至不自旋。
- 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。
6.偏向锁
static final Object obj = new Object();
public static void m1() {
synchronized( obj ) {
// 同步块 A
m2();
}
}
public static void m2() {
synchronized( obj ) {
// 同步块 B
m3();
}
}
//。。。。。。。。。。。。。。
看这一段代码同一个线程m1首先拿到obj对象锁,然后会执行m2,m2这个方法会做一次CAS操作试图拿到obj这个对象,同理后面的m3,m4可能嵌套一大堆在试图获取obj这个对象时都会做一次CAS操作,但其实他们都是一个线程,所以会进入锁重入,这个表明是同一个线程在获取这个obj对象。
只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现
这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。
下面两张图可以生动形象的说明轻量级锁和偏向锁的区别。
6.1偏向状态
Normal状态前面存储了哈希码,分代年龄等等,倒数第三位biased_lock是0,说明没加偏向锁,Biased状态是加了偏向锁的状态,倒数第三位为1,Biased状态前面存储的也不是hashcode了,存储的是thread线程ID。LightWeight Locked是轻量级锁,最后两位是00,前面的是锁记录的地址,Heavyweight Locked是重量级锁最后两位是10,前面存储的是monitor对象的地址。
一个对象创建时,默认都是开启了偏向锁(将来加锁的时候优先考虑加偏向锁)也就是说它的二进制MarkWord最后3位应该是101,但是偏向锁默认是延迟的不会在程序启动时立即生效,如果想避免延迟,可以加VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟。
下面看一个例子我们打印出来一个对象加偏向锁之后的对象头信息。
// 添加虚拟机参数 -XX:BiasedLockingStartupDelay=0
pu