面试永远逃不过的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 更好。


最后留下几个问题思考


简述锁升级过程?

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

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

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

目录
相关文章
|
2月前
|
存储 安全 Java
面试高频:Synchronized 原理,建议收藏备用 !
本文详解Synchronized原理,包括其作用、使用方式、底层实现及锁升级机制。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
面试高频:Synchronized 原理,建议收藏备用 !
|
6月前
|
缓存 安全 算法
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
67 0
|
3月前
|
存储 安全 Java
面试题:再谈Synchronized实现原理!
面试题:再谈Synchronized实现原理!
|
5月前
|
存储 安全 Java
【多线程面试题十七】、如果不使用synchronized和Lock,如何保证线程安全?
这篇文章探讨了在不使用`synchronized`和`Lock`的情况下保证线程安全的方法,包括使用`volatile`关键字、原子变量、线程本地存储(`ThreadLocal`)以及设计不可变对象。
|
5月前
|
Java
【多线程面试题十五】、synchronized可以修饰静态方法和静态代码块吗?
这篇文章讨论了Java中的`synchronized`关键字是否可以修饰静态方法和静态代码块,指出`synchronized`可以修饰静态方法,创建一个类全局锁,但不能修饰静态代码块。
|
5月前
|
Java 调度
【多线程面试题十四】、说一说synchronized的底层实现原理
这篇文章解释了Java中的`synchronized`关键字的底层实现原理,包括它在代码块和方法同步中的实现方式,以及通过`monitorenter`和`monitorexit`指令以及`ACC_SYNCHRONIZED`访问标志来控制线程同步和锁的获取与释放。
|
5月前
|
Java
【多线程面试题十三】、说一说synchronized与Lock的区别
这篇文章讨论了Java中`synchronized`和`Lock`接口在多线程编程中的区别,包括它们在实现、使用、锁的释放、超时设置、锁状态查询以及锁的属性等方面的不同点。
|
6月前
|
安全 Java
Java面试题:解释synchronized关键字在Java内存模型中的语义
Java面试题:解释synchronized关键字在Java内存模型中的语义
53 1
|
6月前
|
安全 Java API
Java面试题:解释synchronized关键字在Java中的作用,并讨论其使用场景和限制。
Java面试题:解释synchronized关键字在Java中的作用,并讨论其使用场景和限制。
51 0
|
6月前
|
存储 安全 Java
Java面试题:请解释Java内存模型,并说明如何在多线程环境下使用synchronized关键字实现同步,阐述ConcurrentHashMap与HashMap的区别,以及它如何在并发环境中提高性能
Java面试题:请解释Java内存模型,并说明如何在多线程环境下使用synchronized关键字实现同步,阐述ConcurrentHashMap与HashMap的区别,以及它如何在并发环境中提高性能
55 0

热门文章

最新文章