java多线程面试常见问题

0 问题

  • 1.synchronized 和ReentrantLock 底层实现&重入机制
  • 2.锁的四种状态和升级过程
  • 3.CAS 是什么,如何解决ABA问题
  • 4.volatile的可见性和指令重排是如何实现的
  • 5.java 一个对象创建的过程
  • 6.对象在内存布局,Object o=new Object()在内存中占了多少字节
  • 7.DCL单例为什么要加volatile
  • 8.Java中的对象都是在堆上分配的吗?

    如果你能够回答以上问题,后面文章你可以不用看了

1 CAS

CAS(compare and swap) 比较和更新,就是在无锁状态下可以保证多个线程对一个值得更新。
ABA是一个潜在问题,可能会产生问题,解决办法也比较简单就是加标记或者加个version。
在这里插入图片描述
CAS 底层是如何实现的,怎么保证更新原子性的? hostspot源码 底层使用lock cmpxchgl 指令,是硬件级别锁可以保证原子性。

   AtomicInteger atomicInteger=new AtomicInteger();
   atomicInteger.incrementAndGet();
     
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    //比较和更新,如果和原始值一样就更新,可能会有ABA问题
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

//C/C++实现
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);


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

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

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;
}

2 java 对象内存布局

接下来实验室在64位java 虚拟机上,使用下面命令看下jvm 默认参数。
java -XX:+PrintCommandLineFlags -version


-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
java version "1.8.0_291"
Java(TM) SE Runtime Environment (build 1.8.0_291-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.291-b10, mixed mode

UseCompressedOops:普通对象指针压缩,oops: ordinary object pointer ,类里面的引用变量
UseCompressedClassPointers:类指针压缩
JVM默认是开启类指针压缩和普通对象指针压缩,开启原因是为了减少内存空间开销。关闭压缩指针-XX:-UseCompressedClassPointers -XX:-UseCompressedOops,也就是将加号变成减号。

使用open jdk 工具来分析,maven 依赖下面给出。

      <!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.16</version>
        </dependency>

public class LayoutDemo {
    public static void main(String[] args) {
        Object o=new Object();
        Object [] arr=new Object[10];
        System.out.println( ClassLayout.parseInstance(o).toPrintable() );
        System.out.println( ClassLayout.parseInstance(arr).toPrintable() );
    }
}

//普通java 对象
java.lang.Object object internals:

OFF  SZ   TYPE DESCRIPTION               VALUE
  //对象头-标记位 8个字节
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  //对象头-类指针,压缩后4个字节,不压缩就是8个字节
  8   4        (object header: class)    0xf80001e5
  // 对齐
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

//数组对象
[Ljava.lang.Object; object internals:
OFF  SZ               TYPE DESCRIPTION               VALUE
 //对象头-标记位 8个字节
  0   8                    (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
 //对象头-类指针
  8   4                    (object header: class)    0xf800234d
  //数组长度 四个字节
 12   4                    (array length)            10
 // 对齐
 12   4                    (alignment/padding gap)   
 16  40   java.lang.Object Object;.<elements>        N/A
Instance size: 56 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

从上面实验可以得出java 内存布局如下图所示,主要分为三个部分:对象头、数据部分、内存对齐。1)对象头中标记为固定8个字节,对象头中类指针在64位虚拟机情况要看有没有开启对应的压缩指针(默认是开启的)开启了占4个字节否则8个字节,对于数组的对象头会对一个数组长度部分占4个字节。2)对象实例数据部分,涉及引用对象部分也要看有没有开启相应的压缩指针
在这里插入图片描述
对象头中64位,记录了锁升级过程,如下图所示。无锁->偏向锁->轻量级锁(自旋锁)->重量级锁。第一次上锁上偏向锁,有竞争先上轻量级锁,竞争过于激烈就会升级为重量级锁。
在这里插入图片描述

3 synchronized

synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:1)修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;2) 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象; 3) 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;4) 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

synchronized 在JVM 层面也做了很多优化,下面分析下其底层实现原理 ,从编译后的字节码可以看到加了监视器 MONITORENTER,MONITOREXIT,然后jvm在执行的过程进行相应的锁升级。

public class SynchronizeDemo {
    public static void main(String[] args) {
        Object o = new Object();
        
        synchronized (o) {
            System.out.println("test");
        }
    }
**编译后的字节码实现,加了MONITORENTER,MONITOREXIT**
    MONITORENTER
   L0
    LINENUMBER 8 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "test"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L6
    LINENUMBER 9 L6
    ALOAD 2
    MONITOREXIT

更底层实现lock cmpxchg 实现,可以通过-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,具体参考打印出汇编。总结下实现过程,synchronized->MONITORENTER,MONITOREXIT->执行过程锁升级->lock cmpxchg。

4 ReentrantLock

5 volatile 关键字作用原理

volatile 是java一个关键字,其主要有两个作用1)**保证线程可见性 :cpu 缓存MESI 协议。2)禁止指令重排,通过内存屏障禁止指定重排,**典型的案例:DCl 单例模式。下面就这个两个点展开说明一下。

什么是cpu 缓存MESI 协议,MESI(Modified Exclusive Shared Or Invalid)(也称为伊利诺斯协议,是因为该协议由伊利诺斯州立大学提出)是一种广泛使用的支持写回策略的缓存一致性协议,它了为了解决多核CPU 多级缓存一致性问题提出的。如下图所示,下面是X86 CPU的Cache结构图,从图中可以看出一个最简单的双核心 CPU,它有三级缓存,第一级 Cache 是指令和数据分开的,第二级 Cache 是独立于 CPU 核心的,第三级 Cache 是所有 CPU 核心共享的。值得说明是,缓存单位是按照缓存行设计的,一般大小为64个字节,CPU可以保证缓存行的一致性,linux 可以用如下命令查看缓存行大小。

cat /sys/devices/system/cpu/cpu1/cache/index0/coherency_line_size

在这里插入图片描述
这种 cpu缓存结构,会带来如下问题 1)多个核心CPU 1-2级别缓存一致性问题 2.cpu 的3级缓存和内存、显存等之间一致性问题。如下图所示给出了一个不一致例子,x值一初始值是200,在某一时刻cpu1修改x值为100,这个时cpu1 缓存值和内存值以及cpu2 缓存值是不一致的。
要解决缓存一致性问题,首先要解决的是多个 CPU 核心之间的数据传播问题,也就是写传播;最常见的一种解决方案呢,叫作总线嗅探(Bus Snooping)。其次还要解决就是事务的串行化(Transaction Serialization),事务串行化是说,我们在一个 CPU 核心里面的读取和写入,在其他的节点看起来,顺序是一样的,主要靠硬件锁机制。基于总线嗅探机制,其实还可以分成很多种不同的缓存一致性协议。不过其中最常用的,就是今天我们要讲的 MESI 协议。和很多现代的 CPU 技术一样,MESI 协议也是在 Pentium 时代,被引入到 Intel CPU 中的。

在这里插入图片描述

MESI 协议定义了 4 种基本状态:M、E、S、I,即修改(Modified)、独占(Exclusive)、共享(Shared)和无效(Invalid)。下面我结合示意图,给你解释一下这四种状态。

  • M:Modified,当共享变量被某个cpu修改后,那么该cpu中的共享变量状态为【被修改】状态。其他读取了该变量的cpu中的状态为【无效】状态。
  • E:Exclusive,当共享变量第一次被某一个cpu读取进缓存后,该变量的状态就被标记为【独占】状态,也就是说,当前cpu中的变量和主存中的完全一致。当其他cpu从主存中载入该变量数据时,此时该变量状态变为【共享】状态。
  • S:Shared,共享变量被多个cpu载入时,变量状态变为【共享】状态。当cpu对该变量进行修改后,此时该变量在当前cpu的状态变为【被修改】状态,其他cpu中的状态变为【无效】状态。
  • I:Invalid,当其他cpu修改了共享变量后,其他读取了该共享变量的cpu中的状态全部变为【无效】状态。【无效】状态的数据再下次使用之前时必须要从主存同步最新数据过来的。

什么是指令重排呢?
可以简单立理解就是实际执行的顺序可以与代码的顺序不一致,可能会乱序执行。但是值得说明的无论怎么排序,要满足 as-if-serial语义,单线程的运行结果不能改变。可以简单理解为指令重排序不会影响单线程,可能会影响多线程。这里也顺便提下happens-before语义,从JDK 5开始,Java使用新的JSR-133内存模型,提供了happens-before 原则,用来指定两个操作的之前的顺序,提供跨线程的可见性。两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行,happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。
在这里插入图片描述
为什么要指令重排?
提高程序执行性能。

public class Single {
    // 加上 volatile
    private  static Single single;

    private Single() {
    }

    public static Single getInstance() {
        if (single == null) {
            synchronized (Single.class) {
                if (single == null) {
                    // 这个地方可能有问题
                    single = new Single();
                }
            }
        }
        return single;
    }
}


如下面代码所示,3和4 在指令顺序是可以重排的,如果3和4对调,那么这个时候在并发场景,另一个线程看到single是没有初始化的对象。

 LINENUMBER 14 L8
   1. NEW com/hsc/java/practice/layout/Single
    2. DUP
    3. INVOKESPECIAL com/hsc/java/practice/layout/Single.<init> ()V
    4. PUTSTATIC com/hsc/java/practice/layout/Single.single : Lcom/hsc/java/practice/layout/Single;

那么这个问题怎么解决呢?加下volatile,可以实现禁止instance指令重排,那么volatile怎么实现禁止指令重排?要回答这个问题我们先看内存屏障或者内存栅栏,屏障两边的指令不可重排,内存屏障的原语sfence、lfence、mfence等系统源语。在JVM 层面,JSR内存屏障:LoadLoad,StoreStore,LoadStore,StoreLoad。LoadLoad 就是load1和load2 不能换顺序。

在这里插入图片描述
对于volatile 修饰的变量,JVM在读写场景会加上相应屏障,禁止指令重排。
在这里插入图片描述
对于java 虚拟机层面使用 lock addl 指令。

  inline void _OrderAccess_fence() {
    // Always use locked addl since mfence is sometimes expensive
    __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
  }

参考文献
[1] https://time.geekbang.org/column/article/109874
[2] https://time.geekbang.org/column/article/376711
[3] https://www.cnblogs.com/z00377750/p/9180644.html
[4]https://www.cnblogs.com/ITPower/p/13580691.html
[5]https://blog.csdn.net/byhook/article/details/87971081

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值