前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java-安全发布

Java-安全发布

作者头像
Fisherman渔夫
发布2019-07-31 15:48:46
7070
发布2019-07-31 15:48:46
举报
文章被收录于专栏:渔夫渔夫

一、安全发布的定义

 发布是一个动词,是去发布对象。而对象,通俗的理解是:JAVA里面通过 new 关键字 创建一个对象。

 发布一个对象的意思是:使对象在当前作用域之外的代码中使用。比如下面knowSecrets指向的HashSet类型的对象,由static修饰,是一个类变量。当前作用域为PublishExample类。

Demo-1:

import java.util.HashSet;
import java.util.Set;

public class PublishExample {
    public static Set<Secret> knowSecrets;
    
    public void initialize() {
        knowSecrets = new HashSet<>();
    }

 public修饰引用knowSecrets,导致在其他类中也能访问到这个HashSet对象,比如向HashSet添加元素或者删除元素。因此,也就发布了这个对象。Demo-2代码段相当于使用了被发布的

Demo-2:

public class UsingSecret {
    public static void main(String[] args) {
        PublishExample.knowSecrets.add(new Secret());
        PublishExample.knowSecrets.remove(new Secret());
    }
}

 另一方面,值得注意的是:被发布对象中所存的引用类对象也会被发布,比如Demo-2代码段中添加的到HashSet集合中的Secret对象也被发布了。


二、 不安全的发布

 因为对象一般是在构造函数里面初始化的(不讨论反射),当构造一个对象时,会为这个对象的属性赋值,当前时刻对象各个属性拥有的值称为对象的状态。给出一个常见的单线程代码例子:Demo-3

Demo-3:

public class Secret {
    private String password;
    private int length;
    public Secret(){}

    public Secret(String password, int length) {
        this.password = password;
        this.length = length;
    }

    public static void main(String[] args) {
        //"current state" 5 组成了secObjCurrentState对象的当前状态
        Secret secObjCurrentState = new Secret("current state", 5);

        //改变 secObjCurrentState 对象的状态
        secObjCurrentState.setPassword("state changed");
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

 Secret对象有两个状态(属性):password和length,利用构造方法进行状态的初始化。

 利用secObjCurrentState.setPassword("state changed")进行password状态的修改。

创建对象的目的是使用它,而要用它,就要把它发布出去。同时,也引出了一个重要问题,我们是在哪些地方用到这个对象呢?比如:只在一个线程里面访问这个对象,还是有可能多个线程并发访问该对象?然而在Demo-3代码段中显然没有考虑多线程的安全性问题。

 对象被发布后,是无法知道其他线程对已发布的对象执行何种操作的,这也是导致线程安全问题的原因。

2.1 this引用逸出

 先看一个不安全发布的示例----this引用逸出。参考《Java并发编程实战》第3章程序清单3-7。

this逸出主要指我们仍未构造好对象(某些状态并未初始化,或并未按照构造函数指定初始化状态),却将对象发布出去了。在其中可能显式地用到了this关键字,也有可能隐式地发布了this。下面利用Dome-4来说明这个问题。

Demo-4:

public class ThisEscape {
    private int intState;//外部类的属性,当构造一个外部类对象时,这些属性值就是外部类状态的一部分
    private String stringState;

    public ThisEscape(EventSource source) {
        source.registerListener(new EventListener(){
            @Override
            public void onEvent(Event e) {
                doSomething(e);
            }
        });
//执行到这里时,new 的EventListener就已经把ThisEscape对象隐式发布了,而ThisEscape对象尚未初始化完成
        
        intState=10;//ThisEscape对象继续初始化....
        stringState = "hello";//ThisEscape对象继续初始化....
        
        //执行到这里时, ThisEscape对象才算初始化完成...
    }

    /**
     * EventListener 是 ThisEscape的 非静态 内部类
     */
    public abstract class EventListener {
        public abstract void onEvent(Event e);
    }

    private void doSomething(Event e) {}

    public int getIntState() {
        return intState;
    }

    public void setIntState(int intState) {
        this.intState = intState;
    }

    public String getStringState() {
        return stringState;
    }

    public void setStringState(String stringState) {
        this.stringState = stringState;
    }

 EventListener是ThisEscape类的内部类。

 **创建内部类必须得要有外部类创建的实例。**现在要创建一个ThisEscape对象,于是执行ThisEscape的构造方法,构造方法里面有 new EventListener对象,于是EventListener对象就隐式地持有外部类ThisEscape对象的引用。

 如果能在其他地方访问到EventListner对象,就意味着"隐式"地发布了ThisEscape对象,而此时ThisEscape对象可能还尚未初始化完成,因此ThisEscape对象就是一个尚未构造完成的对象,这就导致只能看到ThisEscape对象的部分状态!

 看下面示例(Demo-5):其中让EventSource对象持有EventListener对象的引用,也意味着:隐式地持有ThisEscape对象的引用了,这就是this引用逸出。

Demo-5:

public  class EventSource {
    ThisEscape.EventListener listener;//EventSource对象 持有外部类ThisEscape的 内部类EventListener 的引用
    public ThisEscape.EventListener getListener() {
        return listener;
    }
    public void registerListener(ThisEscape.EventListener listener) {
        this.listener = listener;
    }
}

 内部类对象隐式持有外部类对象,可能会发生内存泄漏问题。持有内部类的实例,其对应的外部类对象不一定能够安全地释放相关资源。用Demo6代码段来进行说明。

Demo-6:

public class ThisEscapeTest {
    public static void main(String[] args) {
        EventSource eventSource = new EventSource();
        ThisEscape thisEscape = new ThisEscape(eventSource);
        ThisEscape.EventListener listener = eventSource.getListener();//this引用逸出
        thisEscape.setStringState("change thisEscape state...");
        
        //--------演示一下内存泄漏---------//
        thisEscape = null;//希望触发 GC 回收 thisEscape
        consistentHold(listener);//但是在其他代码中长期持有listener引用
    }
}

2.2 不安全的延迟初始化

 Happens Before 发生在先关系

 深刻理解这个关系,对判断代码中是否存在线程安全性问题很有帮助。扯一下发生在先关系的来龙去脉。

 为了加速代码的执行,底层硬件有寄存器、CPU本地缓存、CPU也有多个核支持多个线程并发执行、还有所谓的指令重排…那如何保证代码的正确运行?因此Java语言规范要求JVM:

 JVM在线程中维护一种类似于串行的语义:只要程序的最终执行结果与在严格串行环境中执行的结果相同,那么寄存器、本地缓存、指令重排都是允许的,从而既保证了计算性能又保证了程序运行的正确性。

 在多线程环境中,为了维护这种串行语义,比如说:操作A发生了,执行操作B的线程如何看到操作A的结果?

 Java内存模型(JMM)定义了Happens-Before关系,用来判断程序执行顺序的问题。这个概念还是太抽象,下面会用具体的示例说明。

 有四个规则对判断多线程下程序执行顺序非常有帮助

  1. 程序顺序规则: 如果程序中操作A在操作B之前(即:写的代码语句的顺序),那么在单个线程执行中A操作将在B操作之前执行。
  2. 监视器规则(确保了某个线程对于某个锁的独占): 这个规则是关于锁的,定义是:在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前(如果把监视器大致看作线程,那么这个用线程的知识来把握就比较容易理解了)。  在线程A内部的所有操作都按照它们在源程序中的先后顺序来排序,在线程B内部的操作也是如此。(这就是程序顺序规则)  由于A释放了锁,而B获得了锁,因此A中所有在释放锁之前的操作位于B中请求锁之后的所有操作之前。这句话:它的意思就是:在线程A解锁M之前的所有操作,对于线程B加锁M之后的所有操作都是可见的。这样,在线程B中就能看到:线程A对 变量x 、变量y的所写入的值了。 可见性:为了在线程之间传递数据,我们经常用到BlockingQueue(阻塞队列),一个线程调用set/put方法添加元素,另一个线程调用get/take方法获取元素,而这些操作都必须满足发生在先关系。线程B不仅仅是拿到了一个元素,而且还能看到线程A修改的一些对象的状态。
  1. volatile变量规则:对volatile变量的写入操作必须在该变量的读取操作之前执行。
  2. 操作顺序的传递性:如果操作A在操作B之前执行,操作B在操作C之前执行,那么操作A必须在操作C之前执行。在你看到一大段代码,这个线程里面调用了synchronized修饰的方法、那个线程又向阻塞队列put了一个元素、另一个线程又读取了一个volatile修饰的变量…从这些发生在先规则里面 使用 传递性 就能大致推断整个代码的执行流程了。

3.安全发布

安全发布由4种常见模式,接下来单独介绍各自线程安全的原因(模式1和模式3)。

注意事项:安全发布并不能保证在对象发布后的相关操作是线程安全的。

3.1安全发布的常用模式
  1. 在静态初始化函数中初始化一个对象的引用;
  2. 将对象的引用保存到volatile类型的域或者AtomicReference对象中;
  3. 将对象的引用保存到某个正确构造对象的final类型域中(避免this逸出);
  4. 将对象的引用保存到一个由锁保护的域中。
3.2在静态初始化函数中初始化一个对象的引用

通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态的初始化器。使用静态的工厂方法返回此实例

Demo-7

public class SecurityPublish {
    public static void main(String[] args) {
        Holder holder=Holder.holder;//静态的方法属于类,无需调用构造器
    }
}

class Holder{
    private int n;

    private Holder(int n){//私有化构造函数的目的是为了单例模式下的线程安全性
        this.n =n;
    }
    public static Holder holder = new Holder(42);
}

 线程安全的实现途径:为了确保对象引用的安全性,如果不适用锁机制,还有另外一个方法就是使用静态初始化器(单例模式)。这是由于JVM的一个特性:静态初始化器由JVM在类的初始化阶段执行,JVM依靠其自身的同步机制,可以使初始化的任何对象都可以被安全地发布。私有化构造函数的目的是为了单例模式下的线程安全性,如果用public修饰构造方法,那么确保不了一个尚未完全创建的对象拥有完整性,如下面的代码:

Demo-8

public class SecurityPublish {
    public static void main(String[] args) {
        Holder holder=new Holder(42);
    }
}

class Holder{
    private int n;

    public Holder(int n){//私有化构造函数的目的是为了单例模式下的线程安全性
        this.n =n;
    }
}
3.2 volatile 修饰的属性的安全发布问题

 在volatile变量的写入操作必须在对该变量的读取操作之前执行,变量的写入操作尚未彻底完成。那根据volatile变量规则:对该变量的访问也不能开始,这样就保证了安全发布。

3.3 将对象的引用保存到某个正确构造对象的final类型域中(避免this逸出)

 对于含有final域的对象,JVM必须保证对对象的初始引用在构造函数之后执行,不能乱序执行(out of order),也就是可以保证一旦你得到了引用,final域的值都是完成了初始化的工作。所以其能够进行对象的正确发布。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019年07月27日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、安全发布的定义
  • 二、 不安全的发布
    • 2.1 this引用逸出
      • 2.2 不安全的延迟初始化
        • 3.1安全发布的常用模式
        • 3.2在静态初始化函数中初始化一个对象的引用
        • 3.2 volatile 修饰的属性的安全发布问题
        • 3.3 将对象的引用保存到某个正确构造对象的final类型域中(避免this逸出)
    • 3.安全发布
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档