面试永远逃不过的synchronized(下)

简介: 面试永远逃不过的synchronized

锁升级


由于synchronized性能问题在JDK1.6前饱受诟病,同时和@author  Doug Lea大神写的目前在JUC下的AQS实现的锁差距太大,synchronized开发人员感觉脸上挂不住,所以在1.6版本进行了大幅改造升级,于是就出现了现在常通说的锁升级或锁膨胀的概念,整体思路就是能不打扰操作系统大哥就不打扰大哥,能在用户态解决的就不经过内核。


升级过程


image.png

无锁(锁对象初始化时)-> 偏向锁(有线程请求锁) -> 轻量级锁(多线程轻度竞争)-> 重量级锁(线程过多或长耗时操作,线程自旋过度消耗cpu);


对象头


验证之前需要补充一点知识,锁的状态是保存在哪?


通过上面分析所有同步监视器都是监视的对应,锁的状态就在对象markword上,它是java对象数据结构中的一部分,对象的markword和java各种类型的锁密切相关;

markword数据的长度在32位和64位的虚拟机(未开启压缩指针.jvm配置参数:UseCompressedOops,compressed--压缩、oop--对象指针)中分别为32bit和64bit,它的最后2bit是锁状态标志位,用来标记当前对象的状态,对象的所处的状态,决定了markword存储的内容,如下表所示:


image.png

对象头包含两个word,mark word为第一个word根据文档可以知他里面包含了锁的信息,hashcode,gc信息等等,klass word为对象头的第二个word主要指向对象的元数据。 64位虚拟机锁对象状态:

image.png

简单来说:

image.png

为什么选取这个过程呢???


JDK开发人员做了大量统计,得出的结论是虽然开发人员加上synchronized来互斥资源访问,但是真正竞争资源的时间几乎没有或者很短暂,也就是说很多的锁是没有必要的。


synchronizer再Hotspot中的源码为synchronizer.cpp如下所示,可以看到BiasedLock和CAS等锁


revoke_and_rebias

void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
 if (UseBiasedLocking) {
    if (!SafepointSynchronize::is_at_safepoint()) {
      BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
      if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
        return;
      }
    } else {
      assert(!attempt_rebias, "can not rebias toward VM thread");
      BiasedLocking::revoke_at_safepoint(obj);
    }
    assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
 }
 slow_enter (obj, lock, THREAD) ;
}
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  markOop mark = obj->mark();
  assert(!mark->has_bias_pattern(), "should not see bias pattern here");
  if (mark->is_neutral()) {
    // Anticipate successful CAS -- the ST of the displaced mark must
    // be visible <= the ST performed by the CAS.
    lock->set_displaced_header(mark);
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
      TEVENT (slow_enter: release stacklock) ;
      return ;
    }
    // Fall through to inflate() ...
  } else
  if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
    assert(lock != mark->locker(), "must not re-lock the same lock");
    assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
    lock->set_displaced_header(NULL);
    return;
  }
#if 0
  // The following optimization isn't particularly useful.
  if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) {
    lock->set_displaced_header (NULL) ;
    return ;
  }
#endif
  // The object header will never be displaced to this lock,
  // so it does not matter what the value is, except that it
  // must be non-zero to avoid looking like a re-entrant lock,
  // and must not look locked either.
  lock->set_displaced_header(markOopDesc::unused_mark());
  ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}


怎么证明存在升级过程


通过对象头就可以知道锁状态,所以可以通过观察对象头来验证,我知道的有两种方式打印出来,一是通过java agent在对象创建后增加代理用ObjectSizeService.sizeOf,一种是OpenJDK提供的JOL来实现。


JOL(Java object layeout)java对象布局,引入maven坐标:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

测试代码:

@Test
public void test_object_layout() {
    Object o = new Object();
    System.out.println(VM.current().details());
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
}

image.png

简单分析下打印内容: 整个对象一共16B,其中对象头(Object header)12B,还有4B是对齐的字节(因为在64位虚拟机上对象的大小必 须是8的倍数),由于这个对象里面没有任何字段,故而对象的实例数据为0B。

image.png


ObjectHeader的12B是什么


这个12B当中分别存储的是什么呢?(不同位数的VM对象头的长度不一 样,我本地的是64bit的vm),openJdk文档中有解释:

mark word为第一个word根据文档可以知他里面包含了锁的信息,hashcode,gc信息等等,klass word为对象头的第二个word主要指向对象的元数据,,根据上述利用JOL打印的对象头信息可以知道一个对象头是12Byte,其中8Byte是mark word 那么剩下的4Byte就是klass word了,和锁相关的就是mark word了,那么接下来重点分析mark word里面信息


码验证偏向锁


@Test
public void test_syn_lock() {
    Object o = new Object();
    synchronized (o){
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
    System.out.println("--------------------------------------");
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
}

image.png

从结果markword截图没有偏向锁,直接变成轻量级锁,这是什么原因呢?这是JDK开发人员故意而为之,因为一般启动时会有很多对象分配、jvm,gc等线程竞争没必要立刻开启偏向锁,默认延迟4秒开启。把上述代码当中加上 睡眠5秒的代码,结果就会不一样了

@Test
public void test_syn_lock() {
  TimeUnit.SECONDS.sleep(5L);
    Object o = new Object();
    synchronized (o){
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
    System.out.println("--------------------------------------");
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
}

image.png

jvm默认延时4s自动开启偏向锁(此时为匿名偏向锁,不指向任务线程),可通过-XX:BiasedLockingStartUpDelay=0取消延时;如果不要偏向锁,可通过-XX:-UseBiasedLocking = false来设置。


验证重量级锁:


@Test
public void test_syn_heavy_lock() throws InterruptedException {
    Object o = new Object();
    //模拟多线程竞争
    for (int i = 0; i < 100; i++) {
        new Thread(()->{
            synchronized (o){
                try {
                    TimeUnit.SECONDS.sleep(1L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
    TimeUnit.SECONDS.sleep(5L);
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
    TimeUnit.SECONDS.sleep(100L);
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
}

image.png

结果可以看出:所有线程结束后已经不存在竞争时并不会变为无锁状态,也就是说锁只能升级,不能降级,竞争比较严重时升级为重量级锁,偏向锁和轻量级锁在用户态维护不需要经过内核态,重量级锁需要切换到内核态(os)进行维护,这也是为什么JDK1.6后synchronized性能大幅提升的本质。


轻量级锁


在锁升级过程中有一个轻量级锁,轻量级锁一般指的就是自旋锁CAS(Compare And Exchange),对java开发者来说这种锁也可以看成无锁,因为在java代码层面没有锁的代码。


CAS因为经常配合循环操作,直到完成为止,所以泛指一类操作,cas(v, a, b) ,变量v,期待值a, 修改值b,可能出现ABA问题,解决办法(版本号 AtomicStampedReference),基础类型简单值不需要版本号。


JDK1.6后大量引入CAS操作,比如原子操作类AtomicXXX, synchronized全是C ++ 实现无法跟踪,所以以AtomicInteger举例CAS,AtomicInteger调用incrementAndGet方法会调用unsafe类compareAndSwapInt方法,再往里跟代码发现无法进入,以为此时应是native方法已经是C++实现了,可以在oracle官网下载Hotspot代码分析,大体思路就是用linux_x86汇编语言的lock和cmpxchg指令, 这几个指令都是在用户态实现,不会经过内核态的切换,所以效率比较高,所以称之为轻量级锁。


下面为CAS典型的JUC包下的AtomicIntegerr核心代码部分,这块代码比synchronized要清晰一点,有C++基础的可以研究下

java:AtomicInteger:

public final int incrementAndGet() {
        for (;;) {// 自旋
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }
public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

Java :Unsafe:

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

jdk8u: unsafe.cpp: cmpxchg = compare and exchange

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

jdk8u: atomic_linux_x86.inline.hpp 93行

is_MP = Multi Processors  多个CPU时处理:

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

底层是通过指令cmpxchgl来实现,如果程序是多核环境下,还会先在cmpxchgl前生成lock指令前缀,反之如果是在单核环境下就不需要生成lock指令前缀。为什么多核要生成lock指令前缀?因为CAS是一个原子操作,原子操作隐射到计算机汇编级别的实现,多核CPU的时候,如果这个操作给到了多个CPU,就破坏了原子性,所以多核环境肯定得先加一个lock指令,不管这个它是以总线锁还是以缓存锁来实现的,单核就不存在这样的问题了。 JVM中除了CAS还有八种原子指令,有兴趣的可以自行学习。


jdk8u: os.hpp is_MP()

static inline bool is_MP() {//判断是否是多核
    // During bootstrap if _processor_count is not yet initialized
    // we claim to be MP as that is safest. If any platform has a
    // stub generator that might be triggered in this phase and for
    // which being declared MP when in fact not, is a problem - then
    // the bootstrap routine for the stub generator needs to check
    // the processor count directly and leave the bootstrap routine
    // in place until called after initialization has ocurred.
    return (_processor_count != 1) || AssumeMP;
  }

jdk8u: atomic_linux_x86.inline.hpp

#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

最后:cmpxchg = cas修改变量值 可以通过C++代码发现,CAS最终是以lock cmpxchg指令来实现的,这两个指令都是汇编指令,对我们java应用开发人员来说可以理解为硬件级别的代码。


使用场景


synchronized  相比如AQS锁使用更简洁不需要显示的获取锁、释放锁,同时又有偏向锁、自旋锁等高性能方式,所以在可能存在资源竞争但是可能性很小或者竞争等待很短时使用synchronized 更好。


最后留下几个问题思考


简述锁升级过程?

自旋锁什么时候升级为重量级锁?

为什么有自旋锁还需要重量级锁?

偏向锁是否一定比自旋锁效率高?

相关文章
|
21天前
|
存储 Java 开发者
面试官:小伙子知道synchronized的优化过程吗?我:嘚吧嘚吧嘚,面试官:出去!
面试官:小伙子知道synchronized的优化过程吗?我:嘚吧嘚吧嘚,面试官:出去!
30 1
|
21天前
|
Java
【面试问题】Synchronized 和 ReentrantLock 区别?
【1月更文挑战第27天】【面试问题】Synchronized 和 ReentrantLock 区别?
|
21天前
|
Java API 调度
大厂高频面试题:ReentrantLock 与 synchronized异同点对比
【5月更文挑战第7天】大厂高频面试题:ReentrantLock 与 synchronized异同点对比
23 0
|
21天前
|
安全 Java 程序员
【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)
【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)
16 0
|
21天前
|
存储 安全 Java
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(下)
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(下)
45 0
|
21天前
|
存储 安全 Java
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(上)
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)
38 0
|
21天前
|
安全 Java
大厂面试题详解:synchronized的偏向锁和自旋锁怎么实现的
字节跳动大厂面试题详解:synchronized的偏向锁和自旋锁怎么实现的
22 0
|
7月前
|
存储 Java
面试~Synchronized 与 锁升级
面试~Synchronized 与 锁升级
39 0
|
11月前
|
存储 安全 Java
【Java面试】说说synchronized和volatile的区别
【Java面试】说说synchronized和volatile的区别
152 0
|
11月前
|
安全 Java 容器
【Java基础】java-android面试Synchronized
synchronized是Java的关键字,可用于同步实例方法、类方法(静态方法)、代码块。 sychronized是非公平线程安全的,具有可见性、有序性,有原子性。