volatile和synchronized详解

一、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;
    }
  • 15
    点赞
  • 68
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值