详解JVM体系结构(多图)

JVM体系结构

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

可能看过上面一段简介以后还是不知道JVM是什么东西,只知道JVM是一套可供java程序运行的环境。
那么jvm在程序中到底扮演者什么地位呢?
在这里插入图片描述
JVM是介于java程序和操作系统之间的一个区域,由于是由C++编写,因此java又可以称为C++ - -,一个java文件编写完成后会被编译成class文件,然后经过类加载器进入运行时数据区。
如图所示,为JVM体系结构简图,由于main方法在运行时会被压入栈底,每运行完一个方法会被弹出,因此垃圾回收不可能存在在栈里,而应该存在于堆中,由于方法区为特殊的堆,因此大部分性能调优都是对于堆的性能调优。

类加载器

类加载器的主要作用是加载类对象,将class文件加载为class类,如图所示:
在这里插入图片描述
通过new的方式可以将class对象实例化为不同的实例化对象,实例化对象可以通过getClass的方式反射获得类对象,因此可以得出类是模板,而对象是具体的,所有的对象都是由同一个类实例化的。类的hashcode恒定,而每个对象各不相同。

测试

public class Animal {
    public static void main(String[] args) {
        //类是模板,对象是具体的

        Animal 喜羊羊 = new Animal();
        Animal 美羊羊 = new Animal();
        Animal 懒羊羊 = new Animal();

        System.out.println(喜羊羊.hashCode());
        System.out.println(美羊羊.hashCode());
        System.out.println(懒羊羊.hashCode());

        Class<? extends Animal> 喜羊羊Class = 喜羊羊.getClass();
        Class<? extends Animal> 美羊羊Class = 喜羊羊.getClass();
        Class<? extends Animal> 懒羊羊Class = 喜羊羊.getClass();

        System.out.println(喜羊羊Class.hashCode());
        System.out.println(美羊羊Class.hashCode());
        System.out.println(懒羊羊Class.hashCode());

    }
}

输出结果
在这里插入图片描述

双亲委派机制

由于类加载器有很多,在程序执行的时候到底执行的是哪个加载器呢?
首先我们来看下加载器的类型

测试

public class Animal {
    public static void main(String[] args) {
        //类是模板,对象是具体的

        Animal 喜羊羊 = new Animal();

        Class<? extends Animal> aClass = 喜羊羊.getClass();

        ClassLoader classLoader = aClass.getClassLoader();
        System.out.println(classLoader);    //应用程序加载器(AppClassLoader)
        ClassLoader parent = classLoader.getParent();
        System.out.println(parent);         //扩展类加载器(ExtClassLoader)  \Java\jre\lib\ext\
        ClassLoader oldParent = parent.getParent();
        System.out.println(oldParent);      //null  不存在或者java获取不到    \Java\jre\lib\rt.jar

    }
}

输出结果
在这里插入图片描述
这里可以看到加载器有

  • 应用程序加载器(AppClassLoader)
  • 扩展类加载器(ExtClassLoader) =>存在于\Java\jre\lib\ext\
  • 根加载器(BootstrapClassLoader) =>存在于\Java\jre\lib\rt.jar
  • 虚拟机自带加载器

双亲委派机制指的是类加载器(AppClassLoader)收到类加载的请求,会将这个请求向上委托给父类加载器去完成,一直向上委托。
应用程序加载器-------扩展类加载器-------根加载器-------启动类加载器
启动类加载器会检查是否可以加载这个类,如果可以加载就结束,使用当前加载器,否则抛出异常,通知子加载器进行加载,我们平时自定义的方法都是运行在应用程序加载器(AppClassLoader)上面的。
启动类加载器-------根加载器-------扩展类加载器-------应用程序加载器
当所有加载器都无法加载会通过native调用操作系统层的本地方法。

沙箱安全机制

Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。

java历史上的一些沙箱模型如下:

  • jdk1.0

在这里插入图片描述

  • jdk 1.1

在这里插入图片描述

  • jdk 1.2

在这里插入图片描述

  • jak1.6

在这里插入图片描述

Native关键词

凡是使用了Native关键字的都是java代码实现不了的范围,会去底层调用C语言的库。主要过程为通过本地方法栈,调用本地方法本地接口,JNI(Java Native Interface)。
JNI的作用主要是为了融合不同的编程语言为java所用,它在内存区域中专门开辟了一块标记区域Native Method Stack用来登记Native方法,在最终执行的时候加载本地方法库中的方法。

常用的有线程类中的start0()方法。

private native void start0();

PC寄存器

每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

方法区

方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等,但是实例对象存在堆内存中,与方法区无关。

static、final、Class、常量池等公共属性都存在方法区中。

栈是一种数据结构,常用来和队列相比较。
栈是先进后出,队列是先进先出。
栈内存主管程序的运行,生命周期和线程同步。线程结束,栈内存也就释放了,不存在垃圾回收的问题。
栈里面存放的有8中基本类型、对象的引用、实例的方法。

栈的示意图如下,每执行完一个方法就会弹出一次,知道所有都弹出,线程结束。
在这里插入图片描述
有时会出现栈溢出的情况,是因为栈空间被占满,抽象示意图如下:

在这里插入图片描述

对应的代码如下:

测试

public class Test {
	//栈溢出测试
    public static void main(String[] args) {
        new Test().a();
    }

    public void a(){
        b();
    }

    public void b(){
        a();
    }
}

结果输出:StackOverflowError(栈溢出)

在这里插入图片描述

对象实例化的过程示意图如下,通对对栈里面的引用对应堆中的对象实例化实体。
在这里插入图片描述

一个JVM中只有一个堆内存,堆内存的大小是可以调节的。
类、方法、常量、变量一般会被存放在堆中,堆中保存了所有引用类型的真实对象。

堆内存中分为三个区域:

  • 新生区
  • 老年区
  • 永久区

如果垃圾回收没有在新生区中被回收就会进入幸存区,称为轻GC,如果多次没被回收则会进入老年区,当老年区接近满的时候则会进行深度清理,称为重GC,如图所示:
在这里插入图片描述

因此GC回收主要发生在新生区和老年区,当内存满值的时候,则会报错(OOM),即堆内存不够。
在JDK8以后永久存储区改名为元空间

新生区

新生区是一个类生成、成长、消亡的地方。
新生区主要分为伊甸园区、幸存1区,幸存2区。
所有对象都是在伊甸园区new出来的。

老年区

当经过新生区还未被杀死的对象会进入老年区。
研究表明,99%的对象都是临时对象,在新生区被kill。

永久区

这个区域是常驻内存的,用来存放JDK自身携带的Class对象,以及interface元数据,存放的是java运行时的一些环境或类信息。这个区域不存在垃圾回收,关闭虚拟机就会释放这个区域的内存。
一个类家在大量第三方jar包、Tomcat部署太多应用,大量动态生成反射类不断地被加载知道内存满了就会出现OOM。

  • jdk6之前:永久代,常量池存在于方法区中。
  • jdk7:永久代,慢慢的退化了,常量池在堆中。(去永久代)
  • jdk8:无永久代,慢慢的退化了,常量池在元空间中。

在jdk1.8下堆中的结构如图所示:
在这里插入图片描述

测试虚拟机试图使用的最大内存和jvm初始化总内存。

默认情况下分配的总内存是电脑内存的4/1,初始化内存为默认为总内存的64/1

测试

public class Test {
    public static void main(String[] args) {
        //返回虚拟机试图使用的最大内存
        long max = Runtime.getRuntime().maxMemory();    //字节 1024*1024
        //返回jvm的初始化总内存
        long total = Runtime.getRuntime().totalMemory();

        //默认情况下分配的总内存是电脑内存的4/1,初始化内存为默认为总内存的64/1
        System.out.println("最大内存为:" + max/(double)1024/1024 + "MB");
        System.out.println("初始化总内存为:" + total/(double)1024/1024 + "MB");

    }
}

结果输出
在这里插入图片描述
这里可以手动设置使用内存大小,如图所示

输入 -Xms1024m -Xmx1024m -XX:+PrintGCDetails

在这里插入图片描述
测试

public class Test {
    public static void main(String[] args) {
        //返回虚拟机试图使用的最大内存
        long max = Runtime.getRuntime().maxMemory();    //字节 1024*1024
        //返回jvm的初始化总内存
        long total = Runtime.getRuntime().totalMemory();

        //默认情况下分配的总内存是电脑内存的4/1,初始化内存为默认为总内存的64/1
        System.out.println("最大内存为:" + max/(double)1024/1024 + "MB");
        System.out.println("初始化总内存为:" + total/(double)1024/1024 + "MB");

    }
}

结果输出
在这里插入图片描述

堆内存调优

当出现OOM时,可采取以下方法调优

  • 尝试扩大堆内存 -Xms1024m -Xmx1024m -XX:+PrintGCDetails 查看结果。
  • 若仍旧出现问题,分析内存,看一下哪里出现问题。

计算可得,年轻代和老年代内存的和等于内存值,因此可以得出,元空间在逻辑上存在,在物理上不存在。

模拟堆内存溢出(OOM)

我们首先设置内存,将内存调小为1M。

修改内存 -Xms1m -Xmx1m -XX:+PrintGCDetails

测试

public class Test {
    public static void main(String[] args) {
        String str = "";
        while (true){
            str += new Random().nextInt(999999999);
        }
    }
}

首先轻GC,年轻代内存占满,然后重GC,老年代内存占满,最后内存溢出,OOM报错。

运行结果

在这里插入图片描述
在这里插入图片描述

使用Jprofiler工具分析OOM

常用的内存快照分析工具可以快速定位内存泄漏问题,如MAT(eclipse集成),Jprofiler等。

MAT、Jprofiler的作用:

  • 分析Dump内存文件,快速定位内存泄漏。
  • 获得堆中的数据。
  • 获得大的对象。

安装步骤如下:

  • 勾选如下设置

在这里插入图片描述

  • 搜索插件安装

在这里插入图片描述

在这里插入图片描述

测试:模拟OOM报错

public class OOMTest {
    byte[] array = new byte[1*1024*1024];

    public static void main(String[] args) {
        ArrayList<OOMTest> list = new ArrayList<>();
        int count = 0;


        try {
            while (true){
                list.add(new OOMTest());
                count = count+1;
            }
        } catch (Error e) {
            System.out.println("count:"+count);
            e.printStackTrace();
        }
    }
}

配置生成Dump文件(-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError)
在这里插入图片描述
在src同级目录下会生成Dump文件
在这里插入图片描述
查看文件,定位报错信息位置
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
Dump参数含义

-Xms :设置初始化内存分配大小
-Xmx:设置最大分配内存
-XX:+PrintGCDetails:打印GC垃圾回收信息
-XX:+HeapDumpOnOutOfMemoryError:OOM Dump文件生成

GC 垃圾回收机制

JVM堆里面可能存在GC的地区有以下几种

  • 新生代
  • 幸存区(from、to,对应前文的幸存0区和幸存1区,交替转换)
  • 老年区

GC:分为轻GC(普通GC)和重GC(全局GC)

程序在运行过程中,会产生大量的内存垃圾(一些没有引用指向的内存对象都属于内存垃圾,因为这些对象已经无法访问,程序用不了它们了,对程序而言它们已经死亡),为了确保程序运行时的性能,java虚拟机在程序运行的过程中不断地进行自动的垃圾回收(GC)。

GC的常用算法

关于 JVM 的 GC 算法主要有下面四种:

1、引用计数算法(Reference counting)
每个对象在创建的时候,就给这个对象绑定一个计数器。每当有一个引用指向该对象时,计数器加一;每当有一个指向它的引用被删除时,计数器减一。这样,当没有引用指向该对象时,该对象死亡,计数器为0,这时就应该对这个对象进行垃圾回收操作。

缺点:会产生内存碎片,每个空间一个计数器浪费资源。

2、复制算法
该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。

优点:没有内存碎片。
缺点:浪费了内存空间,多了一半空间永远是空的(to区)。
复制算法最佳使用场景为对象存活率较低(新生区)

3、标记–清除算法(Mark-Sweep)
为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。
分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。

优点:不需要额外的空间。
缺点:两次扫描严重浪费时间,会产生内存碎片。

4、标记–整理算法
标记-整理法是标记-清除法的一个改进版。同样,在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是将所有存活的对象整理一下,放到另一处空间,然后把剩下的所有对象全部清除。这样就达到了标记-整理的目的。

优点:解决了内存碎片问题。
缺点:整理需要再次消耗一次资源,浪费时间。

GC的常用算法总结

内存效率(时间复杂度):复制算法>标记清除算法>标记整理算法
内存整齐度:复制算法=标记整理算法>标记清除算法
内存利用率:标记整理算法=标记清除算法>复制算法

没有最好的算法,只有最合适的算法,因此GC又被称为分代收集算法。

年轻代:存活率低,一般使用复制算法。
老年代:区域大,存活率高,一般使用标记清除(内存碎片较少时)+标记压缩(内存碎片较多时)混合使用。

JMM

JMM(Java Memory Model的缩写)是一种java内存模型。它类似于缓存一致性协议,用于定义数据读写的规则。
JMM定义了线程工作内存和主内存的一种抽象关系,线程中的共享对象存在于主内存中,每个线程都有一块私有的本地内存。
解决共享对象可见性问题,即线程中修改数据立马同步到主线程中,其它线程复制时可以得到最新的数据:volilate和synchronize关键词。
JMM数据同步模型如下图。
在这里插入图片描述

volatile关键词

volatile是可以保持可见性,不能保证原子性,由于内存屏障,可以保证避免指令重排的现象产生!

可见性

两个线程,同时对单一对象进行操作时,均为从对象中复制一份,然后对复制的对象进行操作,然后合并。
可见性指的是当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,保证修改的数据为最新的数据。

可见性测试

public class VisibilityDemo {
    private static int num = 0;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while(num == 0){

            }
        }).start();

        TimeUnit.SECONDS.sleep(1);
        num = 1;
        System.out.println(num);
    }
}

输出结果

在这里插入图片描述
此时线程陷入死循环,无法感知到main线程已经对数据进行了修改。

public class VisibilityDemo {
    private volatile static int num = 0;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while(num == 0){

            }
        }).start();

        TimeUnit.SECONDS.sleep(1);
        num = 1;
        System.out.println(num);
    }
}

输出结果

在这里插入图片描述
此时可以感知到main线程的变化,退出循环。

非原子性

原子性测试

public class AtomicityDemo {
    private volatile static int num = 0;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                num++;
            }
            System.out.println("T1完成");
        },"T1").start();
        new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                num++;
            }
            System.out.println("T2完成");
        },"T2").start();

        TimeUnit.SECONDS.sleep(1);
        System.out.println(num);
    }
}

输出结果

在这里插入图片描述
Num++不是原子操作,因为其可以分为:读取Num的值,将Num的值+1,写入最新的Num的值。
对于Num++;操作,线程1和线程2都执行一次,最后输出Num的值可能是:1或者2。

输出结果1的解释:当线程1执行Num++;语句时,先是读入Num的值为0,倘若此时让出CPU执行权,线程获得执行,线程2会重新从主内存中,读入Num的值还是0,然后线程2执行+1操作,最后把Num=1刷新到主内存中; 线程2执行完后,线程1已经开始执行,但之前已经读取的Num的值0,所以它还是在0的基础上执行+1操作,也就是还是等于1,并刷新到主内存中。所以最终的结果是1。

解决方案:使用CAS,通过自旋锁解决原子性问题

public class AtomicityDemo {
    private volatile static int num = 0;

    public static void main(String[] args) throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger(0);
        new Thread(()->{
            for (int i = 0; i < 50000; i++) {
//                num++;
                atomicInteger.getAndIncrement();
            }
            System.out.println("T1完成");
        },"T1").start();
        new Thread(()->{
            for (int i = 0; i < 50000; i++) {
//                num++;
                atomicInteger.getAndIncrement();
            }
            System.out.println("T2完成");
        },"T2").start();

        TimeUnit.SECONDS.sleep(1);
//        System.out.println(num);
        System.out.println(atomicInteger.get());
    }
}

输出结果

在这里插入图片描述

CAS应用场景

public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);
//        int andIncrement = atomicInteger.getAndIncrement();

//        public final boolean compareAndSet(int expect, int update)
//        如果期望的值达到了,那么就更新,否则,就不更新,CAS 是 CPU 的并发原型
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());

        System.out.println(atomicInteger.compareAndSet(2021, 2020));
        System.out.println(atomicInteger.get());

        System.out.println(atomicInteger.compareAndSet(2020, 6666));
        System.out.println(atomicInteger.get());

    }
}

输出结果

在这里插入图片描述

常见示例:自旋锁实现

CAS是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步
当第一个线程进入myLock时while条件为false,可以加锁,当第二个线程进入时while条件为true,进入循环,直到myUnLock执行结束,第二个线程跳出while循环,加锁成功。
自旋锁是指对一个内容无限循环,当达成条件的时候对其加锁,底层使用的是CAS。

自定义自旋锁

public class SpinLock {
    AtomicReference<Thread> atomicReference = new AtomicReference<>();
    public void myLock(){
        Thread thread = Thread.currentThread();
        while (!atomicReference.compareAndSet(null,thread)){

        }
        System.out.println(Thread.currentThread().getName() + "==> myLock");
    }
    public void myUnLock(){
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread,null);
        System.out.println(Thread.currentThread().getName() + "==> myUnLock");
    }
}

自旋锁测试

public class SpinLockTest {
    public static void main(String[] args) throws InterruptedException {
        SpinLock lock = new SpinLock();

        new Thread(()->{
            lock.myLock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.myUnLock();
            }
        },"T1").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            lock.myLock();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.myUnLock();
            }
        },"T2").start();

    }
}

输出结果

在这里插入图片描述
线程T1占用时,线程T2会一直在循环中无法出来。只有当线程T1解锁,线程T2才会跳出循环,最终才会触发T2解锁。

常用示例:利用CAS解决ABA问题

CAS是java利用unsafe类通过对计算机底层的调用来进行数据的操作。底层实现为自旋锁。
CAS对数据修改时,可能会出现对数据修改两次,修改后值与之前相同的情况,因此会认定为未修改,此类问题被称为ABA问题,为了解决此类问题可以使用乐观锁,对每次记录新增一个记录,每次修改记录+1。

public class ABADemo {
    public static void main(String[] args) {
    
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1,1);
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int stamp = atomicStampedReference.getStamp();   //获得版本号
            System.out.println("a1 =>"+atomicStampedReference.getStamp());

            System.out.println(atomicStampedReference.compareAndSet(1, 2, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));

            System.out.println("a2 =>"+atomicStampedReference.getStamp());
            System.out.println(atomicStampedReference.compareAndSet(2, 1, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));

            System.out.println("a3 =>"+atomicStampedReference.getStamp());
        },"a").start();

        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();   //获得版本号
            System.out.println("b1 =>"+stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(atomicStampedReference.compareAndSet(1, 6, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
            System.out.println("b2 =>"+atomicStampedReference.getStamp());
        },"b").start();

    }
}

结果分析

在这里插入图片描述

避免指令重排

指令重排指的是,一段代码写出来的顺序可能为1=>2=>3=>4=>5,但是经过编译器以后的顺序并不一定严格按照这种顺序,可能在不影响结果的情况下改变为1=>3=>2=>4=>5。这种情况在单线程的情况下不会有任何问题,但是在多线程的情况下则会出现问题。而volatile关键词可以增加一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障。

常用示例:DCL懒汉式(双重检测锁模式)

//懒汉式单例模式
public class LazyMan {
    private LazyMan(){
    
    }
    private volatile static LazyMan lazyMan;

    //双重检测锁模式的懒汉式单例 DCL懒汉式
    public static LazyMan getInstance(){
        if (lazyMan == null){
            synchronized (LazyMan.class){
                if (lazyMan == null){
                    lazyMan = new LazyMan();    //不是一个原子性操作
                }
            }
        }
        return lazyMan;
    }
}

此时当第一次创建对象时会进行加锁,但是由于指令重排可能会造成以下情况,所以需要对对象加上volatile关键词防止指令重排

/**
* 1、分配内存空间
* 2、执行构造方法,初始化对象
* 3、把这个对象指向这个空间
*
* 此时程序执行顺序可能为
* 1->2->3
* 1->3->2
* 若为1->3->2则可能发生以下情境
*
* A线程执行完1->3时,B线程执行,此时对象有指向的内存空间,但是并未初始化
* 当B进行判断时 lazyMan == null 为 false,则会直接返回未初始化的对象。
* 因此在声明对象时必须加上 volatile 关键词来防止指令重排
*/

总结

Synchronized和Volatile的比较
1)Synchronized保证内存可见性和操作的原子性
2)Volatile只能保证内存可见性
3)Volatile不需要加锁,比Synchronized更轻量级,并不会阻塞线程(volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。)
4)volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化(如编译器重排序的优化).
5)volatile是变量修饰符,仅能用于变量,而synchronized是一个方法或块的修饰符。
volatile本质是在告诉JVM当前变量在寄存器中的值是不确定的,使用前,需要先从主存中读取,因此可以实现可见性。而对n=n+1,n++等操作时,volatile关键字将失效,不能起到像synchronized一样的线程同步(原子性)的效果。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值