volatile和synchronized详解

本文详细介绍了Java内存模型(JMM)、volatile关键字和synchronized关键字的原理与应用。volatile保证了变量的可见性和有序性,但不保证原子性,适合于简单读写操作。synchronized则提供了更高层次的线程同步,保证了原子性、可见性和有序性,适用于需要锁住整个代码块或方法的情况。文章还通过实例展示了它们在多线程环境下的区别和使用场景。
摘要由CSDN通过智能技术生成

一、JMM简介

1.什么是JMM?

​ JMM 是Java内存模型( Java Memory Model),简称JMM。它本身只是一个抽象的概念,并不真实存在,它描述的是一种规则或规范。通过这组规范,定义了程序中对各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

​ 计算机在执行程序时,每条指令都是在CPU中执行的。而执行指令的过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程,跟CPU执行指令的速度比起来要慢的多(硬盘 < 内存 <缓存cache < CPU)。因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。也就是当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时,就可以直接从它的高速缓存中读取数据或向其写入数据了。当运算结束之后,再将高速缓存中的数据刷新到主存当中。

2.JMM的三大特性:

1.原子性

一个或多个操作,要么全部执行(执行的过程是不会被任何因素打断的),要么全部不执行。

2.可见性

只要有一个线程对共享变量的值做了修改,其他线程都将马上收到通知,立即获得最新值。

3.有序性

​ 有序性可以总结为:在本线程内观察,所有的操作都是有序的;而在一个线程内观察另一个线程,所有操作都是无序的。前半句指 as-if-serial 语义:线程内似表现为串行,后半句是指:“指令重排序现象”和“工作内存与主内存同步延迟现象”。处理器为了提高程序的运行效率,提高并行效率,可能会对代码进行优化。编译器认为,重排序后的代码执行效率更优。这样一来,代码的执行顺序就未必是编写代码时候的顺序了,在多线程的情况下就可能会出错。

​ 在代码顺序结构中,我们可以直观的指定代码的执行顺序, 即从上到下按序执行。但编译器和CPU处理器会根据自己的决策,对代码的执行顺序进行重新排序,优化指令的执行顺序,提升程序的性能和执行速度,使语句执行顺序发生改变,出现重排序,但最终结果看起来没什么变化(在单线程情况下)。

有序性问题 指的是在多线程的环境下,由于执行语句重排序后,重排序的这一部分没有一起执行完,就切换到了其它线程,导致计算结果与预期不符的问题。这就是编译器的编译优化给并发编程带来的程序有序性问题。

Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行进入。

3.关于同步的规定:

1.线程解锁前,必须把共享变量的值刷新回主内存。

2.线程加锁前,必须将主内存的最新值读取到自己的工作内存。

3.加锁解锁是同一把锁。

4.解释说明

​ 在JVM中,栈负责运行(主要是方法),堆负责存储(比如new的对象)。由于JVM运行程序的实体是线程,而每个线程在创建时,JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域。而JAVA内存模型中规定,所有变量都存储在主内存中,主内存是共享内存区域,所有线程都可以访问。

​ 但线程对变量的操作(读取赋值等)必须在自己的工作内存中进行。首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后,再将变量写回到主内存。由于不能直接操作主内存中的变量,各个线程的工作内存中存储着主内存中的变量副本,因此,不同的线程之间无法直接访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。

二、volatile关键字

1.volatile简介:

volatile 是 JVM 提供的轻量级的同步机制。volatile 关键字可以保证并发编程三大特征(原子性、可见性、有序性)中的可见性和有序性,不能保证原子性

2.三大特性

1>.保证可见性:

加了volatile关键字修饰的变量,只要有一个线程将主内存中的变量值做了修改,其他线程都将马上收到通知,立即获得最新值。当写线程写一个volatile变量时,JMM会把该线程对应的本地工作内存中的共享变量值刷新到主内存。当读线程读一个volatile变量时,JMM会把该线程对应的本地工作内存置为无效,线程将到主内存中重新读取共享变量。

volatile语义实现原理:

先来看两个与CPU相关的专业术语:

  • 内存屏障(memory barriers):一组处理器指令,用于实现对内存操作的顺序限制。
  • 缓存行(cache line):CPU高速缓存中可以分配的最小存储单位。处理器填写缓存行时会加载整个缓存行。

volatile可见性的实现是借助了CPU的lock指令,lock指令在多核处理器下,可以将当前处理器的缓存行的数据写回到系统内存,同时使其他CPU里缓存了该内存地址的数据置为无效。通过在写volatile的机器指令前加上lock前缀,使写volatile具有以下两个原则:

  1. 写volatile时处理器会将缓存写回到主内存。
  2. 一个处理器的缓存写回到内存,会导致其他处理器的缓存失效。

代码验证:

例如:

int number = 0;此时number变量是没有可见性的。

volatile int number = 0;前面添加了volatile关键字之后,可以解决可见性问题。

没加volatile关键字之前:

/**
 * 普通类:
 * 为了验证volatile的可见性
 */
public class Test1 {
    int number = 0;

    public void add(){
        this.number = 10;
    }


    public static void main(String[] args) {
        Test1 test1 = new Test1();

        //创建第一个线程
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"开始执行时,number = "+test1.number);

            try{ Thread.sleep(3000);}catch (Exception e){e.printStackTrace();}
            test1.add();//暂停3秒后,修改number的值。
            System.out.println(Thread.currentThread().getName()+"执行add()方法之后,number = "+test1.number);

        },"Thread_One").start();


        //第二个是main线程
        while (test1.number == 0){
            //如果第二个main线程 可以监测到number值的改变,就会跳出当前循环,执行后续程序。
        }

        System.out.println(Thread.currentThread().getName()+"程序结束!");

    }
}

运行结果:(程序卡死在 循环while (test1.number == 0)里,跳不出来)
在这里插入图片描述

加上volatile关键字之后:

/**
 * 变量上加了volatile关键字:
 * 为了验证volatile的可见性。
 */
public class Test2 {
    volatile int number = 0;

    public void add(){
        this.number = 10;
    }


    public static void main(String[] args) {
        Test2 test2 = new Test2();

        //创建第一个线程
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"开始执行时,number = "+test2.number);

            try{ Thread.sleep(3000);}catch (Exception e){e.printStackTrace();}
            test2.add();//暂停3秒后,修改number的值。
            System.out.println(Thread.currentThread().getName()+"执行add()方法之后,number = "+test2.number);

        },"Thread_One").start();


        //第二个是main线程
        while (test2.number == 0){
            //由于变量number上加了volatile关键字,
            // 使得第二个main线程可以监测到number值的改变,从而跳出了循环。
        }

        System.out.println(Thread.currentThread().getName()+"程序运行结束!");

    }
}

运行结果:( 循环while (test1.number == 0)可以正常结束)
在这里插入图片描述

2>.保证有序性(禁止指令重排序)

简单说明:

计算机在执行程序时,为了提高计算性能,编译器和处理器常常会对指令进行重排序,一般分为如下3种:

源代码 ——> 编译器优化的重排 ——> 指令并行的重排 ——>内存系统的重排 ——> 最终执行的指令
在这里插入图片描述

解释说明:

·单线程环境下,可以确保程序最终执行结果和代码顺序执行结果的一致性(单线程环境下不用关注指令重排,因为是否重排都不会出错)。处理器在进行重排序时,必须要考虑指令之间的数据依赖性。

·多线程环境中,线程交替执行。由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果也就无法预测。而用volatile关键字修饰的变量,可以禁止指令重排序,从而避免多线程环境下,程序出现乱序执行的现象。

有序性的实现原理:

volatile有序性的保证就是通过禁止指令重排序来实现的。指令重排序包括编译器和处理器重排序,JMM会分别限制这两种指令重排序。

volatile通过加内存屏障来实现禁止指令重排序。JMM为volatile加内存屏障有以下4种情况:

  • 在每个volatile写操作的前面插入一个StoreStore屏障,防止写volatile与后面的写操作重排序。

  • 在每个volatile写操作的后面插入一个StoreLoad屏障,防止写volatile与后面的读操作重排序。

  • 在每个volatile读操作的后面插入一个LoadLoad屏障,防止读volatile与后面的读操作重排序。

  • 在每个volatile读操作的后面插入一个LoadStore屏障,防止读volatile与后面的写操作重排序。

volatile写是在前面和后面分别插入内存屏障,而volatile 读操作是在后面插入两个内存屏障。

内存屏障解释说明
StoreStore屏障禁止上面的普通写和下面的volatile 写重排序。
StoreLoad屏障防止上面的volatile '写与下面可能存在的volatile 读/写重排序。
LoadLoad屏障禁止下面所有的普通读操作和上面的volatile读重排序。
LoadStore屏障禁止下面所有的普通写操作和上面的volatile读重排序。

3>.不保证原子性:

​ 原子性指的是,当某个线程正在执行某件事情的过程中,是不允许被外来线程打断的。也就是说,原子性的特点是要么不执行,一旦执行就必须全部执行完毕。而volatile是不能保证原子性的,即执行过程中是可以被其他线程打断甚至是加塞的。

​ 所以,volatile变量的原子性与synchronized的原子性是不同的。synchronized的原子性是指,只要声明为synchronized的方法或代码块,在执行上就是原子操作的。而volatile是不修饰方法或代码块的,它只用来修饰变量,对于单个volatile变量的读和写操作都具有原子性,但类似于volatile++这种复合操作不具有原子性。所以volatile的原子性是受限制的。并且在多线程环境中,volatile并不能保证原子性。

代码验证:

加上volatile关键字 ,不能保证原子性:

/**
 * 变量上加了volatile关键字:
 * 为了验证volatile的 不保证原子性。
 */
public class Test1 {

    volatile int number = 0;

    public void add(){
        number++;
    }


    public static void main(String[] args) {
        Test1 test1 = new Test1();


        //创建10个线程
        for (int i = 0;i < 10;i++){
            new Thread(() -> {
                //每个线程执行1001次+1操作
                for (int j = 0;j<100;j++){
                    test1.add();
                }
            },"Thread_"+(i+1)).start();
        }

        //如果正在运行的线程数>2个(除了main线程和GC线程以外,还有其他线程正在运行)
        while(Thread.activeCount() >2){
            Thread.yield();//礼让其他线程,暂不执行后续程序
        }

        System.out.println("执行 1000次 +1操作后,number = "+test1.number);

    }
}

运行结果:(部分数据可能会丢失)
在这里插入图片描述

解决方式:

方式一:方法上加 synchronized 关键字。

方式二:利用AtomicInteger类实现原子性。代码如下:

import java.util.concurrent.atomic.AtomicInteger;

/**
 * 变量上加了volatile关键字 ,
 * 但 不能保证原子性 的 解决方式。
 */
public class Test2 {

    volatile int number = 0;

    //解决方式一:方法上加 synchronized 关键字
    public void add(){
        number++;
    }

    //解决方式二:如下
    AtomicInteger atomicInteger = new AtomicInteger();
    public void addMyAtomic(){
        //每调用一次此方法,加个一。
        atomicInteger.getAndIncrement();
    }



    public static void main(String[] args) {
        Test2 test2 = new Test2();


        //创建10个线程
        for (int i = 0;i < 10;i++){
            new Thread(() -> {
                //每个线程执行1001次+1操作
                for (int j = 0;j<100;j++){
                    test2.add();//调用不能保证原子性的方法
                    test2.addMyAtomic();//调用可以保证原子性的方法。
                }
            },"Thread_"+(i+1)).start();
        }

        //如果正在运行的线程数>2个(除了main线程和GC线程以外,还有其他线程正在运行)
        while(Thread.activeCount() >2){
            Thread.yield();//礼让其他线程,暂不执行后续程序
        }

        System.out.println("执行 1000次 +1操作后,number = "+test2.number);
        System.out.println("执行 1000次 +1操作后,atomicInteger = "+test2.atomicInteger);

    }
}

运行结果对比:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ybGTypMn-1649781292558)(图片\volatile关键字\不保证原子性2.png)]

三、synchronized关键字

1.synchronized简介:

​ synchronized是Java多线程中经常使用的一个关键字。synchronized可以保证原子性、可见性、有序性。它包括两种用法:synchronized 方法和 synchronized 代码块。它可以用来给对象、方法或代码块进行加锁。当它锁定一个方法或者一个代码块时,同一时刻最多只有一个线程可以执行这段代码,其他线程想在此时调用该方法只能排队等候。当它锁定一个对象时,同一时刻最多只有一个线程可以对这个类进行操作,没有获得锁的线程,在该类所有对象上的任何操作都不能进行。

锁的类型:

  • synchronized 是悲观锁的实现,因为 synchronized 修饰的代码,每次执行时都会进行加锁操作,同时只允许一个线程进行操作,所以它是悲观锁的实现。

  • synchronized 是非公平锁,并且是不可设置的。这是因为非公平锁的吞吐量大于公平锁,并且是主流操作系统线程调度的基本选择,所以这也是 synchronized 使用非公平锁原因。

  • 同时,synchronized是一个典型的可重入锁,可重入锁最大的作用是避免死锁。

2.实际应用:

例1:

public synchronized void synMethod(){
	//方法体
}

​这时,线程获得的是成员锁,即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候,当前线程(就是在synchronized方法内部的线程)执行完该方法后,别的线程才能进入。

例2:

public Object synMethod(Object a1){
    synchronized(a1){
		//一次只能有一个线程进入
  }
}

​对某一代码块使用 synchronized后跟括号,括号里是变量。如上所示,此时,一次只有一个线程进入该代码块,此时,线程获得的是成员锁。

例3:

public classMyThread implements Runnable{
    public static void main(Stringargs[]){
        Thread t1=newThread(mt,"t1");
        Thread t2=newThread(mt,"t2");
        t1.start();
        t2.start();
}   
public void run(){
    synchronized(this){
      System.out.println(Thread.currentThread().getName());
   }
}

​如果synchronized后面括号里是一个对象,此时,线程获得的是对象锁。如果线程进入,则得到当前对象锁,那么其他没有获得锁的线程,在该类所有对象上的任何操作都不能进行。

例4:

classArrayWithLockOrder{
 public ArrayWithLockOrder(int[]a){
    	synchronized(ArrayWithLockOrder.class){
          //代码逻辑
      }
 } 
}

​如果synchronized后面括号里是类,此时线程获得的是对象锁。如果其他线程进入,则线程在该类中所有操作不能进行,包括静态变量和静态方法。实际上,对于含有静态方法和静态变量的代码块的同步,我们通常选用例4来加锁。

3.实现原理

​ 为了解决线程安全问题,Java提供了同步机制、互斥锁机制,这个机制保证了在同一时刻只有一个线程能访问共享资源。这个机制的保障来源于监视锁Monitor。每个Object对象中都内置了一个monitor对象,monitor对象存在于每个Java对象的对象头中(存储的是指针)。monitor相当于一个许可证,线程拿到许可证即可以进行操作,没有拿到则需要阻塞等待。任何对象都有一个monitor与之相关联,当monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,会尝试获取对象所对应的monitor的所有权,即尝试获取对象的锁。

·ObjectMonitor中的关键属性:

_owner:指向持有ObjectMonitor对象的线程。

_WaitSet:存放处于wait状态的线程队列。

_EntryList:存放处于等待锁block状态的线程队列。

_recursions:锁的重入次数。

_count:用来记录该线程获取锁的次数。

解释说明:

·当多个线程同时访问,这些线程会被放进_EntryList队列,此时线程处于blocked状态。

·当一个线程获得了对象的monitor后,就可以进入running状态执行方法,此时,ObjectMonitor对象的 _ Owner指向当前线程,_count加1表示当前对象锁被一个线程获取。

·如果running状态的线程调用wait()方法时,当前线程释放monitor对象,进入waiting状态,ObjectMonitor对象的 _ owner变为null,_ count减1,同时线程进入 _ WaitSet队列。直到有线程调用notify()方法唤醒该线程,则该线程进入 _ EntryList队列,竞争到锁再进入 _Owner区。

·如果当前线程执行完毕,那么也释放monitor对象,ObjectMonitor对象的_ owner变为null,_count减1

·同步方法和同步代码块的实现原理:

同步代码块:
monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。执行monitorenter指令进入同步代码块,执行monitorexit指令退出同步代码块。但实际应用中,往往是有2个monitorexit指令,第一个是正常退出时执行的,第二个是发生异常时执行的。

同步方法:
synchronized方法的字节码中,并没有看到monitorenter和monitorexit指令。synchronized方法会被翻译成普通方法的调用和返回指令,如:invokevirtual、areturn指令。在JVM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中,会将该方法的access_flags字段中的synchronized标志为1,表示该方法是同步方法。并将调用该方法的对象或该方法所属的Class,在JVM的内部对象表示Klass做为锁对象。

4.三大特性:

synchronized保证原子性:
1.通过monitorenter和monitorexit指令,可以保证被synchronized修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。
2.即使在执行过程中,由于某种原因,比如CPU时间片用完,线程1放弃了CPU,但是它并没有进行解锁。而由于synchronized的锁是可重入的,这就保证下一个时间片还是只能被他自己获取到,还是会继续执行代码。直到所有代码执行完为止。

synchronized保证可见性:
对于一个被synchronized修饰的变量,在其解锁之前,必须先把此变量同步回主存当中。

synchronized保证有序性:
​ 尽管synchronized无法禁止指令重排和处理器优化,但是可以通过单线程机制来保证有序性。由于synchronized修饰的代码,在同一时刻只能被同一线程访问,从根本上避免了多线程的情况。而单线程环境下,在本线程内观察到的所有操作都是天然有序的,所以synchronized可以通过单线程的方式来保证程序的有序性。

5.锁升级过程:

synchronized锁优化:

​ 在Java的早期版本中,synchronized属于重量级锁,效率低下,因为操作系统实现线程之间的切换时,需要从用户态转换到核心态,这个状态之间的转换需要较长的时间,时间成本相对较高。在jdk1.6之后,Java官方从JVM层面对synchronized进行优化。为了减少获得锁和释放锁所带来的性能消耗,引入了偏向锁和轻量级锁。其中,synchronized锁升级是锁优化的一种具体实现方式。

synchronized锁升级:

synchronized锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁

无锁 → 偏向锁:
​ 当锁对象第一次被线程获取的时候,虚拟机会将锁对象的对象头中的锁标志位设置为01,并将偏向锁标志设置为1。线程通过CAS的方式将自己的ID值放置到对象头中(因为在这个过程中有可能会有其他线程来竞争锁,所以要通过CAS的方式,一旦有竞争就会升级为轻量级锁)。如果成功,线程就成功的获得到了该对象的偏向锁。这样每次再进入该锁对象的时候,就不用进行任何的同步操作,直接比较当前锁对象的对象头是不是该线程的ID,如果是就可以直接进入。

偏向锁 → 轻量级锁:
​ 偏向锁是一种无竞争锁,一旦出现了竞争,大多数情况下就会升级为轻量级锁。现在我们假设有线程1持有偏向锁,线程2来竞争偏向锁,会经历以下几个过程:

  1. 首先线程2会先检查偏向锁标记,如果是1,说明当前是偏向锁,那么JVM会找到线程1,查看线程1是否还存活着。

  2. 如果线程1已经执行完毕,即线程1已经不存在了(线程1自己不会主动去释放偏向锁),那么先将偏向锁置为0,对象头设置为无锁的状态,用CAS的方式尝试将线程2的ID放入到对象头中,不进行锁升级,还是偏向锁。

  3. 如果线程1还活着,就先暂停线程1,将锁标志位变成00(轻量级锁),将偏向锁变成了轻量级锁,然后继续执行线程1,此时线程2采用CAS的方式尝试获取锁。

轻量级锁 → 重量级锁:
​ 一旦竞争加剧(如自旋次数或自旋线程数超过阈值),轻量级锁就会膨胀为重量级锁,锁的状态变成10。此时,对象头中存储的就是指向重量级锁的栈帧的指针。而其他等待锁的线程要进入阻塞状态,等重量级锁被释放后,再被唤醒,然后去竞争锁。

​ 重型锁可以认为是,直接对应操作系统底层中的互斥量,通过使用互斥量来进行锁的竞争。由于直接使用底层操作系统的调度,会消耗大量性能,所以称之为重型锁

优点缺点使用场景
偏向锁加锁和解锁不需要CAS操作;没有额外的性能消耗;和执行非同步方法相比,仅存在纳秒级的差距。如果线程间存在锁竞争,会带来额外的锁撤销的消耗。适用于基本没有线程竞争锁的同步场景。
轻量级锁竞争的线程不会阻塞,使用自旋,提高了程序的响应速度。如果线程一直不能获取锁,长时间的自旋,会造成CPU的消耗。适用于少量线程竞争锁对象,且线程持有锁的时间不长,同步块执行速度较快,追求响应速度的场景。
重量级锁线程竞争不使用CPU自旋,不会因为CPU空转而消耗CPU资源。线程阻塞,响应时间较长,在多线程下,频繁的获取锁、释放锁,会带来巨大的性能消耗。很多线程竞争锁,且锁持有的时间较长,追求吞吐量的场景。

四、volatile和synchronized的区别:

应用范围:
volatile关键字是对变量进行上锁,锁住的是单个变量,而synchronized还能对方法以及代码块进行上锁。

是否保证原子性:
在多线程环境下,volatile可以保证可见性和有序性,不能保证原子性,而synchronized在保证可见性和有序性的基础上,还可以保证原子性。

volatile变量的原子性与synchronized的原子性是不同的。synchronized的原子性是指,只要声明为synchronized的方法或代码块,在执行上就是原子操作,synchronized能保证被锁住的整个代码块的原子性。而volatile是不修饰方法或代码块的,它只用来修饰变量,对于单个volatile变量的读和写操作都具有原子性,但类似于volatile++这种复合操作不具有原子性。所以volatile的原子性是受限制的。所以,在多线程环境中,volatile并不能保证原子性。

使用场景:
volatile主要用于解决共享变量的数据可见性问题,而synchronized主要用于保证访问数据的同步性(同时也能保证可见性)。

保证有序性的方式:
volatile的有序性是通过禁止指令重排序来实现的。synchronized无法禁止指令重排,但是可以通过单线程机制来保证有序性。由于synchronized修饰的代码,在同一时刻只能被一个线程访问,从根本上避免了多线程的情况。而单线程环境下,在本线程内观察到的所有操作都是天然有序的,所以synchronized可以通过单线程的方式来保证程序的有序性。

性能方面:
volatile是线程同步的轻量级实现,性能高于synchronized。多线程访问volatile修饰的变量时不会发生阻塞(主要是因为volatile采用CAS加锁),而访问synchronized修饰的资源时会发生阻塞。

五、实际应用

·多线程下的单例模式:

关于单例模式的详细介绍:单例模式(Singleton)

这里只是简单的介绍一下:

/**
 * 单例模式 singleton :
 * 实现此类只能创建一个唯一的实例对象。
 */
public class Singleton {

    /**
     * 1.私有的构造方法,
     * 可以确保外部类无法通过构造方法随便new出新的实例对象
     */
    private Singleton(){}


    /**
     * 2.将唯一的一个 实例对象instance,作为属性。
     *
     * 静态的static,可以保证此实例对象是唯一存在的,
     * 且外部类不需要通过创建此Singleton类,就可以直接获得instance对象.
     *
     * 私有的private,可保证其他外部类,不能随便使用这个唯一的实例对象,保证数据的安全。
     *
     * 添加 volatile :防止在给属性赋值的时候,JVM指定重排序(禁止指令重排序)。
     */
    private static volatile Singleton instance ;



    /**
     * 3.向外部类 提供 可以获得这个唯一实例对象的方法。
     *
     * 之所以是 静态的static,也是为了让外部类不需要创建对象,就可以直接使用这个方法.
     *
     * 为临界资源添加锁 synchronized,
     * 既 解决了 多线程的安全问题,又提高了工作效率。
     */
    public static Singleton getInstance(){
        if (instance == null){
            /**     锁定的资源 为当前Singleton类     */
            synchronized(Singleton.class){
                if (instance == null){
                    System.out.println("创建了一个对象!");
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
通过以上引用的内容,我们可以得出以下结论: volatile关键字和synchronized关键字都是用来保证线程之间操作的有序性和可见性。 然而,volatile关键字不能保证操作的原子性,只能保证被修饰变量的可见性。它的主要作用是禁止指令重排序,即保证变量的写操作对其他线程的读操作可见。 而synchronized关键字可以保证被修饰的变量在解锁之前会被同步回主存,从而保证了变量的可见性。 此外,synchronized关键字还可以保证同一时刻只有一个线程能够对持有同一个对象锁的同步块进行操作,从而实现了线程的串行执行。 综上所述,volatilesynchronized关键字在保证线程之间操作的有序性和可见性方面具有不同的作用。使用时需要根据具体的需求选择合适的关键字。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [volatilesynchronized 详解](https://blog.csdn.net/ywlmsm1224811/article/details/103166419)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [volatilesynchronized详解](https://blog.csdn.net/LYQ20010417/article/details/124138846)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值