操作系统
从操作系统层面看,有三种方式可以实现锁:pthread_mutex_lock
,spin
和信号量
Java层面
synchronized
在java中,synchronized可以用来做同步处理,一般的写法是synchronized(o){同步代码}
。那么synchronized效率如何呢?
其实synchronized的效率是会发生变化的,它一般会从偏向锁->轻量锁->重量锁变化。
一般情况下,如果这个对象的是可偏向,那么对象第一次被线程持有的时候,锁状态是偏向锁。如果没有其他线程访问这把锁,而且当前线程再次加锁,那么这个对象的锁状态依然是偏向锁。
当有多个线程都交替访问这把锁时,锁的状态会膨胀为轻量锁。
一旦发生资源竞争,这个锁就会膨胀为重量锁。如果这个对象是不可偏向的,那么锁的状态会从轻量锁开始膨胀
随着锁的变化,效率会变得越来越低。当synchronized膨胀为重量锁时,底层会使用pthread_mutex_lock
实现互斥。
那么为什么使用了pthread_mutex_lock
就会变成重量锁呢?
因为pthread_mutex_lock
抢锁时,如果抢不到锁,会调用sleep
指令,而sleep
属于系统指令,当CPU处理RING3
状态下时,没有权限执行sleep
,这时,就需要升级到RING0
,也就是CPU从用户态1升级到内核态2。而这是一个重量级操作,所以当使用pthread_mutex_lock
实现锁时,就是重量锁了。
发生了系统调用的锁就是重量锁
比如sleep就属于系统调用,使用系统调用需要CPU从RING3升级到RING0
最优实践:
当我们在代码里写synchronized(o){}
时,o一定不要调用super.hashCode()
方法,就是object提供的那个native的hashCode()
ObjectL的hashCode返回222时,可以看到对象头中没有存储hashcode
当ObjectL的hashCode调用super.hashCode时,可以看到对象头发生了变化
对象头
锁的状态是存储在对象头中的。引申一下对象的组成:
- 对象头
- 实例数据(不一定有)
- 对齐填充(不一定有)
一个对象的大小一定是8byte的整数倍。对象头的大小一般是12byte,如果是一个空的Object,那么这个对象还会有4字节的对齐填充。对象头的12byte中,有8byte是markword,还有4byte的类型指针3当对象处于不同的状态时,8byte的markword中存的东西是不一样的。下面引用一下子路大神关于对象头存储内容的说明图片:
对象的hash值是存放在对象头中的,而且,一开始对象头中并没有hash值,当程序调用object.hashCode()
时,才会计算hash值,然后存放在对象头中。当对象头中存放hash值后,就没有地方存储对象的偏向锁的线程id了。所以,如果一个对象的对象头中如果存储了hash值,那么就没空间存储偏向线程id了,这时,这个对象就不可偏向了,如果使用这个对象做锁,那么锁从一开始就是一个轻量锁。
synchronized的状态
synchronized的状态是会变化的,最优的情况下,会从偏向锁开始,逐步膨胀到轻量锁,最后膨胀为重量锁。最后,在极其苛刻的情况下,可以退化。synchronized的状态是存储在对象头中的。
自旋锁:
操作系统层面pthread_spin
reentrantlock 在jvm层面是自旋的,当高并发时,但是在os层面上不是,因为会park