如何安全发布对象

发布对象与逸出

发布对象:

  • 使一个对象能够被当前范围之外的代码所使用,例如通过方法返回对象的引用,或者通过公有的静态变量发布对象

对象逸出:

  • 一种错误的发布,当一个对象还没有构造完成时,就使它被其他线程所见

不正确的发布可变对象导致的两种错误:

  1. 发布线程意外的所有线程都可以看到被发布对象的过期的值
  2. 线程看到的被发布对象的引用是最新的,然而被发布对象的状态却是过期的

不安全的发布示例:

package org.zero.concurrency.demo.example.publish;

import lombok.extern.slf4j.Slf4j;

import java.util.Arrays;

/**
 * @program: concurrency-demo
 * @description: 不安全的对象发布示例
 * @author: 01
 * @create: 2018-10-16 16:21
 **/
@Slf4j
public class UnsafePublish {

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

    /**
     * 过public访问级别发布了类的域,在类的外部,任何线程都可以访问这个域
     * 这样发布的对象是不安全的,因为我们无法得知其他线程是否会修改这个域导致该类里数据的错误
     *
     * @return String[]
     */
    public String[] getStates() {
        return states;
    }

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

        unsafePublish.getStates()[0] = "d";
        // 输出 [d, b, c]
        log.info("{}", Arrays.toString(unsafePublish.getStates()));
    }
}

在这个例子中,我们通过new对象得到了对象实例。获得这个对象后,我们可以调用getStates()方法得到私有属性的引用,这样就可以在其他任何线程中,修改该属性的值。那么这就会导致我们在其他线程中,获取该属性的值时是不确定的,因为并不能得知该属性的值是否已被其他线程所修改过,所以这就是不安全的对象发布。

对象逸出示例:

package org.zero.concurrency.demo.example.publish;

import lombok.extern.slf4j.Slf4j;
import org.zero.concurrency.demo.annotations.NoRecommend;
import org.zero.concurrency.demo.annotations.NotThreadSafe;

/**
 * @program: concurrency-demo
 * @description: 对象逸出示例,在对象构造完成之前,不可以将其发布
 * @author: 01
 * @create: 2018-10-16 16:36
 **/
@Slf4j
@NotThreadSafe
@NoRecommend
public class Escape {

    private int thisCanBeEscape = 0;

    public Escape() {
        new InnerClass();
    }

    private class InnerClass {
        public InnerClass() {
            log.info("{}", Escape.this.thisCanBeEscape);
        }
    }

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

在以上这个例子中,内部类的构造器里包含了对封装实例的隐含引用,这样在对象没有被正确构造完成之前就会被发布,由此会导致不安全的因素在里面。其中一个就是导致this引用在构造期间逸出的错误,它是在构造函数构造过程中启动了一个线程,无论是显式启动还是隐式启动,都会造成this引用的逸出。新线程总会在所属对象构造完毕之前就已经看到它了,所以如果要在构造函数中创建线程,那么不要启动它,而是应该采用一个专有的start,或是其他初始化的方式统一启动线程。这里其实我们可以使用工厂方法和私有构造函数来完成对象创建和监听器的注册等等来避免不正确的发布。


如何安全发布对象

上一小节中,我们简述了什么是发布对象,以及给出了不安全发布对象的示例和对象逸出的示例和说明。所以本小节我们将看看如何安全的发布对象,想要安全的发布对象主要有四种方法:

  1. 在静态初始化函数中初始化一个对象的引用
  2. 将对象的引用保存到volatile类型域或者AtomicReference对象中
  3. 将对象的引用保存到某个正确构造对象的final类型域中
  4. 将对象的引用保存到一个由锁保护的域中

以上所提到的几种方法都可以应用到单例模式中,所以本文将以单例模式为例,介绍如何安全发布对象,以及单例实现的一些问题。

众所周知,单例模式是最常用的设计模式了。Spring容器中所管理的类的实例默认也是单例的,虽然单例看似简单,但也是有不少需要注意的地方,特别是在多线程环境下。基础的单例模式实现方式就不赘述了,我们来看看为什么采用了双重同步锁的懒汉式单例还是线程不安全的。示例代码如下:

/**
 * 双重同步锁懒汉式单例-线程不安全
 * 实例在第一次使用的时候创建
 *
 * @author 01
 */
public class SingletonExample4 {

    /**
     * 单例对象
     */
    private static SingletonExample4 instance = null;

    /**
     * 私有构造函数
     */
    private SingletonExample4() {
    }

    /**
     * 静态工厂方法-获取实例
     *
     * @return instance
     */
    public static SingletonExample4 getInstance() {
        // 双重检查机制
        if (instance == null) {
            // 同步锁
            synchronized (SingletonExample4.class) {
                if (instance == null) {
                    instance = new SingletonExample4();
                }
            }
        }
        return instance;
    }
}

以上代码中在执行 instance = new SingletonExample4(); 语句的时候,底层实际进行了以下三步操作:

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

在代码逻辑上,看似不会出现线程不安全的问题。但是在JVM里,这几步可能会被乱序执行,即便是乱序执行,在单线程下也不会有什么问题,但是在多线程下就不一样了。经过JVM和CPU的优化,指令可能会重排成下面的顺序:

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

假设按照这个指令顺序执行的话,那么当线程A执行完1和3时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行 if (instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。如下图所示:

那么要如何避免这一情况呢?我们需要给instance对象增加一个volatile关键字进行修饰,这样就不会出现指令重排的情况了。若对volatile不是很清楚的话,可以参考我另一篇文章中对volatile关键字的介绍:

修改后线程安全的懒汉式单例代码如下:

public class SingletonExample5 {
    /**
     * 单例对象,使用 volatile 关键字禁止指令重排
     */
    private volatile static SingletonExample5 instance = null;

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

    /**
     * 静态工厂方法-获取实例
     *
     * @return instance
     */
    public static SingletonExample5 getInstance() {
        // 双重检查机制
        if (instance == null) {
            // 同步锁
            synchronized (SingletonExample5.class) {
                if (instance == null) {
                    instance = new SingletonExample5();
                }
            }
        }
        return instance;
    }
}

经过volatile的修饰,当线程A执行instance = new Singleton的时候,JVM执行顺序是什么样?始终保证是下面的顺序:

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

如此在线程B看来,instance对象的引用要么指向null,要么指向一个初始化完毕的Instance,而不会出现某个中间态,保证了安全。



实现单例模式的方式有很多种,除了以上所提到的,我们还可以使用静态内部类来实现单例,这样更简单,不需要判空也不需要加 volatile 关键字去防止指令重排的问题。示例代码如下:

/**
 * 使用静态内部类实现的单例模式-线程安全
 * 实例在第一次使用的时候创建
 *
 * @author 01
 */
public class SingletonExample8 {
    /**
     * 私有构造函数
     */
    private SingletonExample8() {
    }

    /**
     * 静态工厂方法-获取实例
     *
     * @return instance
     */
    public static SingletonExample8 getInstance() {
        return LazyHolder.INSTANCE;
    }

    /**
     * 用静态内部类创建单例对象
     */
    private static class LazyHolder {
        private static final SingletonExample8 INSTANCE = new SingletonExample8();
    }
}

这里有几个需要注意的点:

  1. 从外部无法访问静态内部类LazyHolder,只有当调用Singleton.getInstance方法的时候,才能得到单例对象INSTANCE。
  2. INSTANCE对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,使得静态内部类LazyHolder被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。


以上所提到的单例实现方式并不能算是完全安全的,这里的安全不仅指线程安全还有发布对象的安全。因为以上例子所实现的单例模式,我们都可以通过反射机制去获取私有构造器更改其访问级别从而实例化多个不同的对象,虽然一般不会这么干,但也难免会有这种情况。那么如何防止利用反射构建对象呢?这时我们就需要使用到内部枚举类了,因为JVM可以阻止反射获取枚举类的私有构造方法。示例代码如下:

/**
 * 使用枚举类实现的单例模式-线程最为安全
 * 实例在第一次使用的时候创建
 *
 * @author 01
 */
public class SingletonExample7 {
    /**
     * 私有构造函数
     */
    private SingletonExample7() {
    }

    /**
     * 静态工厂方法-获取实例
     *
     * @return instance
     */
    public static SingletonExample7 getInstance() {
        return Singleton.INSTANCE.getInstance();
    }

    /**
     * 由枚举类创建单例对象
     */
    @Getter
    private enum Singleton {
        INSTANCE;

        /**
         * 单例对象
         */
        private SingletonExample7 instance;

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

使用枚举实现的单例模式,是最为推荐的一种写法,因为这种实现方式不但可以防止利用反射强行构建单例对象,而且可以保证线程安全,并且可以在枚举类对象被反序列化的时候,保证反序列的返回结果是同一对象。这里之所以使用内部枚举类的原因是为了让这个单例对象可以懒加载,相当于是结合了静态内部类的实现思想。若不使用内部枚举类的话,单例对象就会在枚举类被加载的时候被构建。



单例模式实现总结:

原文地址:http://blog.51cto.com/zero01/2300908

时间: 2024-11-06 03:51:50

如何安全发布对象的相关文章

Java 并发编程(二)对象的不变性和安全的发布对象

一.不变性 满足同步需求的另一种方法是使用不可变对象(Immutable Object).到目前为止,我们介绍了许多与原子性和可见性相关的问题,例如得到失效数据,丢失更新操作或光查到某个对象处于不一致的状态等等,都与多线程视图同时访问同一个可变的状态相关.如果对象的状态不会改变,那么这些问题与复杂性也就自然消失了. 如果某个对象在被创建后其状态就不能被修改,那么这个对象就被成为不可变对象.线程安全型是不可变对象的固有属性之一,他们的不变性条件是由构造函数创建的,只要他们的状态不改变,那么这些不变

如何安全的发布对象

对象的发布:使对象能够在当前作用域之外的代码中使用 如何安全的发布对象? 1:可变对象比如通过加锁的方式发布 2:不可变对象可以随意的被发布 可变对象必须通过安全的方式发布,这意味着发布和使用该对象的线程都必须使用同步. 要安全的发布对象,对象的状态和对象的引用必须对其他的线程可见,常见的做法有: 1:在静态初始化函数中初始化一个对象引用 2:将对象的引用保存到volatile类型域中 3:将对象的引用保存到正确构造对象的final域中 4:将对象的引用保存到一个由锁保护的域中 5:使用线程安全

【并发编程】安全发布对象

原文:慕课网高并发实战(五)- 安全发布对象 发布对象 发布对象:使一个对象能够被当前范围之外的代码所使用 对象溢出:一种错误的发布,当一个对象还没有构造完成时,就使它被其他线程所见 不正确的发布可变对象导致的两种错误: 1.发布线程意外的所有线程都可以看到被发布对象的过期的值 2.线程看到的被发布对象的引用是最新的,然而被发布对象的状态却是过期的 下面使用代码对不安全的发布和对象溢出进行说明: 不安全的发布示例 import com.gwf.concurrency.annoations.Not

jedis实现redis的消息队列、发布对象消息、字节数组与字符串相互转换

redis支持发布/订阅的消息队列机制,jedis提供了java访问redis的客户端,本文将描述如何用jedis实现简单的消息队列,并传输对象. redis支持发布.订阅的功能,基本的命令有publish.subscribe等.在jedis中,有对应的java方法,并且只能发布字符串消息.为了传输对象,需要将对象进行序列化,并封装成字符串进行处理.将对象序列化后,只能成为字节流,如何封装成字符串是一个难点,具体可参考下面的代码. 实现三个类,一个对应publish.一个对应subscribe.

SpringBoot中RedisTemplate订阅发布对象

解说 RedisMessageListenerContainer Redis订阅发布的监听容器,你的消息发布.订阅配置都必须在这里面实现 addMessageListener(MessageListenerAdapter,PatternTopic) 新增订阅频道及订阅者,订阅者必须有相关方法处理收到的消息. setTopicSerializer(RedisSerializer) 对频道内容进行序列化解析 MessageListenerAdapter 监听适配器 MessageListenerAd

并发环境下,不安全发布对象示例代码

package com.mm.concurrent; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TestConcurrent { public static 

Java 并发编程(二)对象的发布逸出和线程封闭

对象的发布与逸出 "发布(Publish)"一个对象是指使对象能够在当前作用域之外的代码中使用.可以通过 公有静态变量,非私有方法,构造方法内隐含引用 三种方式. 如果对象构造完成之前就发布该对象,就会破坏线程安全性.当某个不应该发布的对象被发布时,这种情况就被称为逸出(Escape). 下面我们首先来看看一个对象是如何逸出的. 发布对象最简单的方法便是将对象的引用保存到一个共有的静态变量中,以便任何类和线程都能看见对象,如下面代码. public static Set<Stri

Java线程安全性中的对象发布和逸出

发布(Publish)和逸出(Escape)这两个概念倒是第一次听说,不过它在实际当中却十分常见,这和Java并发编程的线程安全性就很大的关系. 什么是发布?简单来说就是提供一个对象的引用给作用域之外的代码.比如return一个对象,或者作为参数传递到其他类的方法中. 什么是逸出?如果一个类还没有构造结束就已经提供给了外部代码一个对象引用即发布了该对象,此时叫做对象逸出,对象的逸出会破坏线程的安全性. 概念我们知道了,可我们要关注什么地方呢?我们要关注的时候就是逸出问题,在不该发布该对象的地方就

发布线程安全的对象

一.总 发布对象:使对象能在当前作用域之外的代码中使用.既将对象的引用传递到其他类的变量和方法. (1)变量的静态初始化 (2)声明为volatile变量 或 AtomicReferance对象 (3)声明为final变量 (4)将变量保存在线程安全的容器中(既保存在一个由锁保护的域中) 二.分 1.成员变量的初始化: (1)直接初始化 (2)构造函数初始化 2.在Java内存模型中,final域能确保初始化过程的安全性,从而可以不受限制的访问不可变对象,并在共享这些对象时无需同步. 在可变对象