底层原理
Java程序的编译与运行
- 最开始,我们编写的Java代码,是*.java文件
- 在变异(javac命令)后,从刚才的*.java文件会变成一个新的Java字节码文件(*.class)
- JVM会执行刚才生成的字节码文件(*.class),并把字节码文件转化为机器指令
- 机器指令可以直接在CPU上执行运行,也就是最终的程序执行
JVM实现会带来不同的“翻译”,不同的CPU平台的机器指令有千差万别,无法保证并发安全的效果一致。
1、JVM内存结构、Java内存模型、Java对象模型
这三个是截然不同的概念,下面会一一进行讲解
1.1、JVM内存结构
JVM内存结构,和Java虚拟机运行时区域有关
- 堆:整个运行区最大的,new创建的实例对象,都存储在堆中,运行时动态分配
- 虚拟机栈:对象的引用,存储在java栈中,编译期就确定了大小
- 方法区:储存static静态变量,常量信息等,还有永久引用(static修饰的class),运行时常量池也是存储在方法区
- 本地方法栈:Native方法
- 程序计数器:保存当前线程执行的字节码的行号数,协助上下文切换
1.2、Java内存模型
Java内存模型,和Java的并发编程有关
1.3、Java对象模型
Java对象模型,和Java对象在虚拟机中的表现形式有关
- Java对象自身的存储模型
- JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该Java类
- 当我们在Java代码中,使用了new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据
2、JMM是什么
2.1、为什么需要JMM(Java内存模型)
C语言不存在内存模型的概念,很多操作依赖处理器,不同的处理器处理的结果不一样。无法保证并发安全。JMM是一个标准规范,让多线程运行的结果可预期。
JMM是一组规范,需要各个JVM的实现来遵守JMM规范,以便于开发者可以利用这些规范,更方便的开发多线程程序
如果没有JMM内存模型来规范,那么JVM可能根据不同规则的重排序之后,导致不同的虚拟机上的运行结果的不一样,那是很大的问题
2.2、关键字的原理
帮助我们实现了内存栅栏,简单理解就是主内存和线程内存之间的同步。我们只需要使用关键字就可以达到效果,而不用自己去实现内存栅栏。
volatile、synchronize、Lock等的原理都是JMM
3、重排序
代码的实际执行顺序和代码在Java文件中的顺序不一致,代码指令并不是严格按照代码语句顺序执行的,他们的顺序被改变了,这就是重排序。
重排序的好处:提高运行速度和处理速度
/** * 演示重排序 * 直达达到某个条件才停止,测试小概率事件 */ public class OutOfOrderExcution { private static int x = 0, y = 0; private static int a = 0, b = 0; public static void main(String[] args) throws InterruptedException { int i = 0; for (;;){ i++; x=0; y=0; a=0; b=0; CountDownLatch latch = new CountDownLatch(1); Thread one = new Thread(new Runnable() { @Override public void run() { try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } a = 1; x = b; } }); Thread two = new Thread(new Runnable() { @Override public void run() { try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } b = 1; y = a; } }); one.start(); two.start(); latch.countDown(); one.join(); two.join(); if (x == 1 && y == 1){ // if (x == 0 && y == 0){ System.out.println("i="+i+", x="+x+", y="+y); break; }else { System.out.println("i="+i+", x="+x+", y="+y); } } } }
这4行代码的执行顺序界定了最终x和y的结果,一共有3种情况:
- a=1;x=b(0);b=1;y=a(1),最终结果是x=0,y=1
- b=1;y=a(0);a=1;x=b(1),最终结果是x=1,y=0
- b=1;a=1;x=b(1);y=a(1),最终结果是x=1,y=1
一定只有这3中情况吗?会不会出现第四种情况呢?
为什么出现了0,0的情况呢?那是因为重排序发生了,4行代码的执行顺序为:y=a;a=1;x=b;b=1;
内存中“重排序”:线程A的修改线程B却看不到,引出可见性问题。修改后没有及时写回主存造成的现象。
4、可见性
4.1、什么是可见性问题
代码演示
可以使用volatile关键字解决这个问题,他会在修改本地缓存后刷新到主内存中,这样其他线程获取的就是修改后的值
4.2、为什么会出现可见性
- 因为cpu有多层缓存,越靠近cpu的容量越小,但是速度越快。
- 线程中的可见性是由于多级缓存造成的
- 每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待刷入到主存中。所以会导致有些核心读取到一个过期的值。
4.3、JMM的抽象:主内存和本地内存
- Java作为高级语言,屏蔽了这些底层细节,用JMM定义了一套读写内存数据的规范,虽然我们不再关心一级缓存和二级缓存的问题,但是,JMM抽象了主内存和本地内存的概念。
- 这里说的本地内存并不是真的是一块给每个线程分配的内存,而是,JMM的一个抽象,是对于寄存器、一级缓存、二级缓存的抽象
JMM有以下规定:
- 所有的变量都是存储在主内存中,同时每个线程也是由自己独立的工作内存,工作内存中的变量内存是主内存中的拷贝
- 线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量,然后再同步到主内存中
- 主内存是多个线程共享的,但线程之间不共享工作内存,如何线程间需要通信,必须借助主内存中转来完成
所有的共享变量存在于主内存中,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题。
4.4、Happens-Before原则
4.4.1、什么是Happens-Before
- Happens-Before是用来解决可见性问题的:在时间上,动作A发生字董总B之前,B保证能看见A,这就是Happens-Before
- 两个操作可以用Happens-Before来确定他们的执行顺序:如果一个操作Happens-Before于另一个操作,那么我们说第一个操作对于第二个操作是可见的
- Happens-Beforea就是想要说明一个可见性的问题
4.4.2、什么不是Happens-Before
- 两个线程没有相互配合的机制,所以代码X和Y执行结果并不能保证总被别人看到,这就不是Happens-Before
4.4.3、Happens-Before规则有哪些
- 单线程原则
- 锁操作(synchronize和Lock)
- volatile变量
- 线程启动
- 线程join
- 传递性
- 中断
- 构造方法
- 工具类
近朱者赤:给一个变量加了volatile,不仅改变量被影响,读取变量之前的操作也是可见的,可以实现轻量级同步
4.5、volatile关键字
- volatile是一种同步机制,比synchronize或者Lock相关类更轻量,因为使用volatile并不会发送上下文切换等开销很大的行为
- 如果一个变量被修饰为volatile,那么JVM就知道这个变量可能被并发修改
- 因为开销小,相应的能力也小,虽说volatile也是用来同步线程安全的,但是volatile做不到synchronize那样的原子保护,volatile仅在很有限的场景下才能发挥作用
4.5.1、volatile不适用于a++操作
/** * 不适用于volatile */ public class NoVolatile implements Runnable{ volatile int a; AtomicInteger readA = new AtomicInteger(); @Override public void run() { for (int i = 0; i < 10000; i++) { readA.incrementAndGet(); a++; } } public static void main(String[] args) throws InterruptedException { NoVolatile noVolatile = new NoVolatile(); Thread thread1 = new Thread(noVolatile); Thread thread2 = new Thread(noVolatile); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(noVolatile.a); System.out.println(noVolatile.readA); } }
4.5.2、适用的场景
- 适合场景1:boolean flag,如果一个共享变量自始至终只被各个线程赋值,而没有其他操作,那么可以使用volatile代替synchronize或者代替原子变量操作。因为赋值操作是原子操作
/** * 适用于volatile */ public class UseVolatile1 implements Runnable{ volatile boolean done = false; AtomicInteger readA = new AtomicInteger(); @Override public void run() { for (int i = 0; i < 10000; i++) { readA.incrementAndGet(); setDone(); } } private void setDone() { done = true; } public static void main(String[] args) throws InterruptedException { UseVolatile1 noVolatile = new UseVolatile1(); Thread thread1 = new Thread(noVolatile); Thread thread2 = new Thread(noVolatile); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(noVolatile.done); System.out.println(noVolatile.readA); } }
/** * 适用于volatile */ public class NoVolatile2 implements Runnable{ volatile boolean done = false; AtomicInteger readA = new AtomicInteger(); @Override public void run() { for (int i = 0; i < 10000; i++) { readA.incrementAndGet(); flipDone(); } } private void flipDone() { done = !done; } public static void main(String[] args) throws InterruptedException { NoVolatile2 noVolatile = new NoVolatile2(); Thread thread1 = new Thread(noVolatile); Thread thread2 = new Thread(noVolatile); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(noVolatile.done); System.out.println(noVolatile.readA); } }
改变值不依赖他原来的值,就可以适用于volatile关键字
- 适用场合2:作为刷新触发器
4.5.3、volatile的两点作用
- 可见性:读一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一个volatile属性会立即刷入到主内存。
- 禁止指令重排序:解决单例双重锁乱序问题
4.5.4、volatile和synchronize的关系
- volatile可以看作是轻量版的synchronize,如果一个共享变量从始至终只被各个线程赋值,而没有其他的操作,那么就可以使用volatile来代替synchronize关键字,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全
4.5.5、小结
- volatile修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程对此属性进行了修改,其他线程可以立即得到修改后的值,比如,boolean flag;作为触发器,实现轻量级同步
- volatile属性的读写操作都是无锁的,他不能代替synchronize,因为他没有提供原子性和互斥性。因为无所,不需要花费时间在获取锁和释放锁上,所以说它是低成本的
- volatile只能作用于属性,我们用volatile修饰属性,这样jvm就不会对这个属性进行重排序
- volatile提供了可见性,任何一个线程对其修改将立马对其他线程可见,volatile属性不会被线程缓存,始终都是从主存中读取
- volatile提供了happens-before保证,对volatile变量v的写入happens-before所有其他线程后续对v的读操作
- volatile可以使得long和double的赋值是原子的
4.6、能保证可见性的措施
- 除了volatile可以让变量保证可见性外,synchronize、Lock、并发集合、Thread.join()和Thread.start()等都可以保证可见性
- 具体可以参考happens-before
4.7、对synchronize可见性的正确理解
- synchronize不仅保证了原子性,还保证了可见性
- synchronize不仅让被保护的代码安全,还近朱者赤(让被锁住之前的代码也可见)
5、原子性
5.1、什么是原子性
- 一系列的操作,要么全部都执行成功,要么全部不执行,不会出现执行一般的情况,是不可能分割的
5.2、Java中原子操作有哪些
- 除了long和double之外的基础类型(int,byte,short,char,float,boolean)的操作
- 所有引用reference的赋值操作,不管是32位的机器还是64位的机器
- java.concurrent.Atomic.*包中所有类的原子操作
5.3、long和double的原子性
官方文档对于64位的值的写入,可以分为两个32位的操作进行写入、读取错误、使用volatile解决
32位机器上,long和double不是原子操作,在64位机器上,long和double是原子操作
实际开发中,商用java虚拟机不会出现这种情况,因为已经对这个做过处理
5.4、原子性 + 原子性 != 原子性
- 两个原子操作组合在一起,并不能保证整体依然具有原子性
6、常见问题
6.1、单例模式8中写法,单例和并发的关系
单例模式可以节省内存和计算保证结果正确,方便管理
/** * 1.饿汉式(静态常量) (可用) * 类加载的时候就会实例化 */ public class Singleton1 { private final static Singleton1 INSTANCE = new Singleton1(); private Singleton1(){} public static Singleton1 getINSTANCE() { return INSTANCE; } } /** * 2.饿汉式(静态代码块) (可用) * 类加载的时候就会实例化 */ public class Singleton2 { private final static Singleton2 INSTANCE; static { INSTANCE = new Singleton2(); } private Singleton2(){} public static Singleton2 getINSTANCE() { return INSTANCE; } } /** * 3.懒汉式(线程不安全) (不可用) */ public class Singleton3 { private static Singleton3 INSTANCE; private Singleton3(){} public static Singleton3 getINSTANCE() { //如果两个线程同时进入,判断都为null,那么下面创建就会多次 if (INSTANCE == null){ INSTANCE = new Singleton3(); } return INSTANCE; } } /** * 4.懒汉式(线程安全) (不推荐) */ public class Singleton4 { private static Singleton4 INSTANCE; private Singleton4(){} public synchronized static Singleton4 getINSTANCE() { //synchronized保护,那么两个线程无法同时进入,导致多次创建的问题。但是效率太低 if (INSTANCE == null){ INSTANCE = new Singleton4(); } return INSTANCE; } } /** * 5.懒汉式(线程不安全) (不推荐) */ public class Singleton5 { private static Singleton5 INSTANCE; private Singleton5(){} public static Singleton5 getINSTANCE() { if (INSTANCE == null){ //如果两个线程都到这里了,虽然创建上锁了,但是依旧会多次创建 synchronized (Singleton5.class) { INSTANCE = new Singleton5(); } } return INSTANCE; } } /** * 6.懒汉式(线程安全) (推荐) * 双重检查 推荐面试使用 */ public class Singleton6 { //新建对象实际上有3个步骤 /** * 1.创建一个空的对象 * 2.调用对象的构造方法 * 3.将实例赋值给引用 * 如果CPU做了重排序,那么就会发生NPE */ private volatile static Singleton6 INSTANCE; private Singleton6(){ } public static Singleton6 getINSTANCE() { if (INSTANCE == null){ synchronized (Singleton6.class) { if (INSTANCE == null) { INSTANCE = new Singleton6(); } } } return INSTANCE; } } /** * 7.静态内部类 (可用) */ public class Singleton7 { public Integer a; private Singleton7(){ } //内部类只有在 private static class SingletonInstance{ private static final Singleton7 INSTANCE = new Singleton7(); } public static Singleton7 getINSTANCE() { return SingletonInstance.INSTANCE; } } /** * 8.枚举单例 (可用) 防止反序列化 * 这个类最简单 */ public enum Singleton8 { INSTANCE; private void whatever() { } }
最好使用枚举
6.2、单例模式面试常见问题
- 饿汉式缺点:如果不适用这个实例,也会加载,会有浪费的问题
- 懒汉式的缺点:容易写成线程不安全
- 为什么要用双重锁?不用线程不安全吗?不使用线程不安全的,
- 为什么双重锁检查要用volatile?因为new为三个操作,创建空对象,构造方法,赋值。如果顺序无法保证,就会有空指针问题。因为添加了volatile,对象被创建,其他线程也一定可以查看到,可见性问题
- 使用哪种实现方式最好?枚举
6.3、什么是Java内存模型
为啥要有->JVM内存结构、Java内存模型、Java对象模型的区别->Java内存模型是一个规范,三个点->可见性对内存的抽象->happens-before原则->volatile关键字->适用场合->和synchronize的关系->原子性(哪些操作是原子的)
6.4、volatile和synchronize的异同
volatile可以看作是轻量级的synchronize,volatile无法实现原子性
6.5、什么是原子操作?Java有哪些是原子操作?生成对象使不是原子操作?
生成对象不是原子操作,他是三个步骤:1.新建一个空的person对象 2.吧这个对象的地址指向p 3.执行person的构造函数。这期间的2,3步操作顺序需要看是否被重排序
6.6、什么是内存可见性
层层缓存导致
6.7、64位是不是原子操作
Java并没有说他们是原子的,所以可能会出现错位的情况,但是商用虚拟机中做了操作,不需要我们来关心这个