字节码层面
方法的标志位上会增加 0x0020 [synchronized]
具体使用的是monitorenter和monitorexit
0 aload_0
1 dup
2 astore_1
3 monitorenter
4 aload_1
5 monitorexit
6 goto 14 (+8)
9 astore_2
10 aload_1
11 monitorexit
12 aload_2
13 athrow
14 return
为什么有两个monitorexit,除了正常退出之外,发现异常之后也会退出
public class V9_Synchronized {
void m(){
synchronized (this){//monitorenter
}//monitorexit
}
}
操作系统实现
X86 : lock cmpxchg / xxx
这里也是用lock实现的,但是会有疑问,既然synchronized和volatile底层都是lock,为什么synchronized不能保证有序性?
其实synchronized是可以保证有序性的,但是是只对争抢object锁资源时保证,一旦进入方法内部执行代码,这些代码的有序性是不能保证的
锁升级的过程
偏向锁:
在锁对象的对象头上记录下当前获取到锁的线程的id,该线程如果下次又来获取该锁,就可以直接获取到了
轻量级锁(自旋锁、无锁)
由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会撤销(这个步骤也是十分耗资源的),升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
自旋锁竞争的过程:
每个线程都有自己的线程栈,每个线程在自己的线程栈内部生成一个LR(Lock Record),线程会用自旋的方式,把这个LR放到锁对象的mark word中,就表示争抢到了这把锁,其他线程继续进行CAS自旋。
自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒和两个步骤都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量
重量级锁
如果竞争加剧,竞争加剧:有线程超过10次自旋,-XX:PreBlockSpin,或者自旋线程数超过CPU核数的一半,1.6之后加入了自适应自旋Adapative Self Spinning,JVM自己控制。
升级重量级锁,向操作系统申请资源,Linux mutex,线程挂起,进入等待队列,等待操作系统的调度,然后再映射回用户空间。
identity hash code
如上图,一个对象刚new出来之后,如果有调用过identity hash code,那么mark word中有31位记录着这个hashCode,那如果升级到轻量级锁后,mark word中就会记录指向线程栈中LR的指针,那么这个identity hashcode去哪了呢?
会放到自己的线程栈里,线程栈中有一个LR,LR指向了一个空间(Displaced Mark Word)。这个空间记录了锁升级前的mark word,用来做备份。
那偏向锁怎么办呢?
当一个对象已经计算过identity hash code,它就无法进入偏向锁状态;当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为重量级锁;那什么时候对象会计算identity hash code呢?当然是当你调用未覆盖的Object.hashCode()方法或者System.identityHashCode(Object o)时候了。
重量级锁的实现
ObjectMonitor类里有字段可以记录非加锁状态下的mark word,其中可以存储identity hash code的值。或者简单说就是重量锁可以存下identity hash code。
自旋锁什么情况下会升级为重量级锁?
为什么有自旋锁还需要重量级锁?
自旋是消耗CPU资源的,如果锁的时间长,或者自旋线程多,CPU会被大量消耗。
升级为重量级锁后,那些没有获得锁的线程会放到一个锁池当中进行等待(不需要消耗CPU资源),当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列进行等待CPU资源分配,所以在竞争很激烈的时候,重量级锁会比自旋锁更合适。
偏向锁是否一定比自旋效率高?
不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候还不如直接使用自旋锁效率高。
比如JVM启动过程,会有多线程竞争(明确),所以默认情况启动时不打开偏向锁,过一段时间再打开,默认是4秒。-XX:BiasedLockingStartupDelay=4
偏向锁未启动时,此时对对象加锁,可以看到mark word是001,表示无锁态。
public class V24_TestJol {
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
偏向锁已启动时,可以看到对象的mark word变成了101,101是偏向锁,这里就会有疑问,刚new出来的对象为什么会上把偏向锁?
我们知道偏向锁会有一个线程指针,但是这里并没有记录任何线程的指针,全是0,所以这把锁叫匿名偏向锁。
public class V24_TestJol {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000);
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
匿名偏向锁升级为偏向锁:
public class V24_TestJol {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000);
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
CAS(compareAndSwap/compareAndSet)乐观锁
底层实现
incrementAndGet的底层是执行的一条汇编指令(cmpxchg),CAS操作在cpu的汇编级别就已经直接支持了
LOCK_IF_MP
- 但是这个命令本身不是原子的,比如多颗cpu之间操作某一变量时就无法保证,所以在这个指令前面增加了一个指令LOCK_IF_MP,这里看到有一个LOCK_IF_MP,作用是如果是多核处理器,在指令前加上lock前缀,因为在单核处理器中,是不会存在缓存不一致的问题的,所有线程都在一个CPU核上跑,使用同一个缓存区,也就不存在本地内存与主内存不一致的问题,不会造成可见性问题。然而在多核处理器中,共享内存需要从写缓存中刷新到主内存中去,并遵循缓存一致性协议通知其他处理器更新缓存。
lock的作用
- 在cmpxchg执行期间,锁住内存地址[edx],其他处理器不能访问该内存,保证原子性。
- 将本处理器上写缓存全部强制写回主存中去,也就是写屏障,保证每个线程的本地内存与主存一致
- 禁止cmpxchg与前后任何指令重排序,防止指令重排序
CAS的缺点
ABA问题
ABA问题是指在CAS操作时,其他线程将变量值A改为了B,但是又被改回了A,等到本线程使用期望值A与当前变量进行比较时,发现变量A没有变,于是CAS就将A值进行了交换操作,但是实际上该值已经被其他线程改变过,这与乐观锁的设计思想不符合。
ABA问题的解决思路是每次变量更新的时候把变量的版本号加1,那么A-B-A就会变成A1-B2-A3,只要变量被某一线程修改过,改变量对应的版本号就会发生递增变化,从而解决了ABA问题。在JDK的java.util.concurrent.atomic包中提供了AtomicStampedReference来解决ABA问题,该类的compareAndSet是该类的核心方法,实现如下:
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
我们可以发现,该类检查了当前引用与当前标志是否与预期相同,如果全部相等,才会以原子方式将该引用和该标志的值设为新的更新值,这样CAS操作中的比较就不依赖于变量的值了。
CAS导致自旋消耗
多个线程争夺同一个资源时,如果自旋一直不成功,将会一直占用CPU。
解决方法:破坏掉for死循环,当超过一定时间或者一定次数时,return退出。JDK8新增的LongAddr,和ConcurrentHashMap类似的方法。当多个线程竞争时,将粒度变小,将资源分成多个,减少了竞争压力,减少CPU空转自旋时间。
LongAddr和AtomicLong有什么区别?
AtomicLong相当于多个线程竞争一次修改value的机会,LongAddr把value拆成多个值放到cell数组里,相当于多线程竞争多次修改value的机会,性能自然上升。