java synchronized关键字

字节码层面

方法的标志位上会增加 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的机会,性能自然上升。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值