Java并发(5)--线程安全发布对象:懒汉模式、饿汉模式

文章目录

    • 零:方法逃逸与线程逃逸
    • 一. 发布与逸出概念
    • 二. 安全发布对象的四种方式
      • 1. 懒汉模式
      • 2. 饿汉模式
      • 3. 枚举模式

零:方法逃逸与线程逃逸

逃逸分析的基本行为就是分析对象动态作用域:
当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中去,称为方法逃逸。甚至可能被外部线程访问到,比如赋值给类变量或可以在其他线程中访问到的实例变量,称为线程逃逸。

如果能证明一个对象不会逃移到方法外或者线程之外,也就是别的方法或线程无法通过任何途径访问到这个对象,则可能为这个变量进行一些高效的优化:

(1)栈上分配

Java虚拟机中,对象在堆上分配这个众所周知。虚拟机的垃圾收集系统可以回收堆中不再使用的对象,但回收动作无论是筛选可回收对象还是回收和整理内存都要耗费时间。如果确定一个对象不会逃逸出方法之外,那么让这个对象在栈上分配将会是一个不错的主意,对象所占用的内存空间就可以随着栈帧出栈而销毁,这样垃圾收集系统的压力将会小很多

(2)同步消除

线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定不会有颈枕,对这个变量实施的同步措施也就可以消除掉

(3)标量替换

标量是指一个数据已经无法再分解成更小的数据来表示了,Java中的基本数据类型即引用类型都不能进一步分解,因此,它们可以称为标量。相对的,一个数据如果还可以继续分解,那么就称为聚合量,Java中的对象就是最典型的聚合量。如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。将对象拆分后,除了可以让对象的成员在栈上分配和读写外,还可以为后续进一步的优化手段创建条件。

一. 发布与逸出概念

发布(publish)对象意味着其作用域之外的代码可以访问操作此对象。例如将对象的引用保存到其他代码可以访问的地方,或者在非私有的方法中返回对象的引用,或者将对象的引用传递给其他类的方法。
为了保证对象的线程安全性,很多时候我们要避免发布对象,但是有时候我们又需要使用同步来安全的发布某些对象。
逸出即为发布了本不该发布的对象。

  1. 非私有的方法内返回一个私有变量的引用会导致私有变量的逸出
@NotThreadSafe
public class UnsafePublish {

    private String[] states = {"a", "b", "c"};

    public String[] getStates() {
        return states;
    }

    public static void main(String[] args) {
        UnsafePublish unsafePublish = new UnsafePublish();
        log.info("{}", Arrays.toString(unsafePublish.getStates()));

        unsafePublish.getStates()[0] = "d";
        log.info("{}", Arrays.toString(unsafePublish.getStates()));
    }
}

上面的UnsafePublish就是线程不安全的类,因为它的属性states 在UnsafePublish对象发布后,可能会被修改,也就是共享数据不安全。

  1. 在对象未完成构造之前不能将其发布
public class Escape {

    private int thisCanBeEscape = 0;

    public Escape () {
        new InnerClass();
    }

    private class InnerClass {
	
        public InnerClass() {
        	// 引用了为构造完成的 Escape 对象
            log.info("{}", Escape.this.thisCanBeEscape);
        }
    }

    public static void main(String[] args) {
        new Escape();
    }
}

二. 安全发布对象的四种方式

在spring中,由spring管理的类都是单例模式的
Java并发(5)--线程安全发布对象:懒汉模式、饿汉模式_第1张图片

1. 懒汉模式

  1. 单例实例在第一次使用时进行创建(线程不安全)
package com.hust.concurrency.service.publish;

import com.hust.concurrency.annoations.NotThreadSafe;

/**
 * 懒汉模式
 * 单例实例在第一次使用时进行创建
 */
@NotThreadSafe
public class SingletonExample1 {

    // 私有构造函数,不允许外部直接调用
    private SingletonExample1() {

    }

    // 单例对象,每次返回的对象是同一个
    private static SingletonExample1 instance = null;

    // 静态的工厂方法
    public static SingletonExample1 getInstance() {
        //这是是线程不安全的,如果多个线程同时判断出instance为null,那么每个线程都会new个对象出来
        if (instance == null) {
            instance = new SingletonExample1();
        }
        return instance;
    }
}

  1. 懒汉模式改进(不推荐,synchronized 会引起性能开销):

 // 静态的工厂方法 用synchronized 修饰静态方法,修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
    public static synchronized SingletonExample3 getInstance() {
        if (instance == null) {
            instance = new SingletonExample3();
        }
        return instance;
    }
}

  1. 懒汉模式进一步改进:

懒汉模式 -》 双重同步锁单例模式


    // 静态的工厂方法
    public static SingletonExample4 getInstance() {
        if (instance == null) { // 双重检测机制        // B
            synchronized (SingletonExample4.class) { // 同步锁
                if (instance == null) {
                    instance = new SingletonExample4(); // A - 3
                }
            }
        }
        return instance;
    }
}

上述代码一个经典的单例的双重检测的代码,这段代码在单线程环境下并没有什么问题,但如果在多线程环境下就可以出现线程安全问题。原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。因为instance = new SingletonExample4(),可以分为以下3步完成伪代码.

memory = allocate(); //1.分配对象内存空间
instance(memory);    //2.初始化对象
instance = memory;   //3.设置instance指向刚分配的内存地址,此时instance!=null

由于步骤1和步骤2间可能会重排序,如下:

memory = allocate(); //1.分配对象内存空间 
instance = memory; //3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
instance(memory); //2.初始化对象

由于步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。那么该如何解决呢,很简单,我们使用volatile禁止instance变量被执行指令重排优化即可。

  1. 最终版
    volatile + 双重检测机制 -> 禁止指令重排

/**
 * 懒汉模式 -》 双重同步锁单例模式
 * 单例实例在第一次使用时进行创建
 */
@ThreadSafe
public class SingletonExample5 {

    // 私有构造函数
    private SingletonExample5() {

    }

    // 1、memory = allocate() 分配对象的内存空间
    // 2、ctorInstance() 初始化对象
    // 3、instance = memory 设置instance指向刚分配的内存

    // 单例对象 volatile + 双重检测机制 -> 禁止指令重排
    private volatile static SingletonExample5 instance = null;

    // 静态的工厂方法
    public static SingletonExample5 getInstance() {
        if (instance == null) { // 双重检测机制        // B
            synchronized (SingletonExample5.class) { // 同步锁
                if (instance == null) {
                    instance = new SingletonExample5(); // A - 3
                }
            }
        }
        return instance;
    }
}


2. 饿汉模式

单例实例在类装载时进行创建(线程安全),缺点在于如果构造函数中处理动作很多的话,加载类就会变慢,存在性能问题。

package com.hust.concurrency.service.publish;

import com.hust.concurrency.annoations.ThreadSafe;

/**
 * 饿汉模式
 * 单例实例在类装载时进行创建
 */
@ThreadSafe
public class SingletonExample2 {

    // 私有构造函数
    private SingletonExample2() {

    }

    // 单例对象,在类装载的时候就进行创建了 instance对象,没到运行期
    private static SingletonExample2 instance = new SingletonExample2();

    // 静态的工厂方法
    public static SingletonExample2 getInstance() {
        return instance;
    }
}


3. 枚举模式


/**
 * 枚举模式:最安全
 */
@ThreadSafe
@Recommend
public class SingletonExample7 {

    // 私有构造函数
    private SingletonExample7() {

    }

    public static SingletonExample7 getInstance() {
        return Singleton.INSTANCE.getInstance();
    }

    private enum Singleton {
        INSTANCE;

        private SingletonExample7 singleton;

        // JVM保证这个方法绝对只调用一次
        Singleton() {
            singleton = new SingletonExample7();
        }

        public SingletonExample7 getInstance() {
            return singleton;
        }
    }
}


参考:
全面理解Java内存模型(JMM)及volatile关键字

你可能感兴趣的:(并发)