谈谈JAVA中的安全发布

谈谈JAVA中的安全发布

昨天看到一篇文章阐述技术类资料的"等级",看完之后很有共鸣。再加上最近在工作中越发觉得线程安全性的重要性和难以捉摸,又掏出了《Java并发编程实战》研读一番,这本书应该是属于为“JAVA 多线程作注解”的一本书,那我就为书中关于对象安全发布的内容作一些注解,作为今年的第一篇博文。

我读的是中文版,确实感觉书中有些地方的描述晦涩难懂,也没有去拿英文原文来对照,就按中文版描述,再配上一些示例代码记录我的一些理解吧。

1. 安全发布的定义

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

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

import java.util.HashSet;
import java.util.Set;/*** @author psj* @date 2019/03/10*/
public class PublishExample {public static Set<Secret> knowSecrets;public void initialize() {knowSecrets = new HashSet<>();}
}

public修饰引用knowSecrets,导致 在其他类中也能访问到这个HashSet对象,比如向HashSet添加元素或者删除元素。因此,也就发布了这个对象。

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

另外,值得注意的是:添加到HashSet集合中的Secret对象也被发布了。

2. 不安全的发布

因为对象一般是在构造函数里面初始化的(不讨论反射),当 new 一个对象时,会为这个对象的属性赋值,当前时刻对象各个属性拥有的 称为对象的状态。

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")改变了对象的状态。

创建对象的目的是使用它,而要用它,就要把它发布出去。同时,也引出了一个重要问题,我们是在哪些地方用到这个对象呢?比如:只在一个线程里面访问这个对象,还是有可能多个线程并发访问该对象?

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

2.1 this引用逸出

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

当我第一次看到"this引用逸出"时,是懵逼的。后来在理解了“发生在先”原则、“初始化过程安全性”、"volatile关键字的作用"之后才慢慢理解了。这些东西后面再说。

外部类ThisEscape和它的内部类EventListener

public class ThisEscape {private int intState;//外部类的属性,当构造一个外部类对象时,这些属性值就是外部类状态的一部分private String stringState;public ThisEscape(EventSource source) {source.registerListener(new EventListener(){@Overridepublic 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;}

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

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

看下面示例:我故意让EventSource对象持有EventListener对象的引用,也意味着:隐式地持有ThisEscape对象的引用了,这就是this引用逸出。

public  class EventSource {ThisEscape.EventListener listener;//EventSource对象 持有外部类ThisEscape的 内部类EventListener 的引用public ThisEscape.EventListener getListener() {return listener;}public void registerListener(ThisEscape.EventListener listener) {this.listener = listener;}
}
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 回收 thisEscapeconsistentHold(listener);//但是在其他代码中长期持有listener引用}
}

额外提一下:内部类对象隐式持有外部类对象,可能会发生内存泄漏问题。

2.2 不安全的延迟初始化

Happens Before 发生在先关系

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

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

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

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

Java内存模型(JMM)定义了Happens-Before关系,用来判断程序执行顺序的问题。这个概念还是太抽象,下面会用具体的示例说明。在我写代码的过程中,发现有四个规则对判断多线程下程序执行顺序非常有帮助:

  • 程序顺序规则:

    如果程序中操作A在操作B之前(即:写的代码语句的顺序),那么在单个线程执行中A操作将在B操作之前执行。

  • 监视器规则:

    这个规则是关于锁的,定义是:在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前。咋一看,没啥用。我这里扩展一下,如下图:

    715283-20190310145547267-1107129980.png

    在线程A内部的所有操作都按照它们在源程序中的先后顺序来排序,在线程B内部的操作也是如此。(这就是程序顺序规则)

    由于A释放了锁,而B获得了锁,因此A中所有在释放锁之前的操作 位于 B中请求锁之后的所有操作之前。这句话:它的意思就是:在线程A解锁M之前的所有操作,对于线程B加锁M之后的所有操作都是可见的。这样,在线程B中就能看到:线程A对 变量x 、变量y的所写入的值了。

    再扩展一下:为了在线程之间传递数据,我们经常用到BlockingQueue,一个线程调用put方法添加元素,另一个线程调用take方法获取元素,这些操作都满足发生在先关系。线程B不仅仅是拿到了一个元素,而且还能看到线程A修改的一些对象的状态(这就是可见性

    715283-20190310145604106-212125976.png

    总结一下:

    同步操作,比如锁的释放和获取、volatile变量的读写,不仅满足发生在先关系(偏序),而且还满足全序关系。总之:要想保证执行操作B的线程看到操作A的结果(不管操作A、操作B 是否在同一个线程中执行),操作A、操作B 之间必须满足发生在先关系

  • volatile变量规则:对volatile变量的写入操作必须在该变量的读取操作之前执行。这条规则帮助理解:为什么在声明类的实例变量时用了volatile修饰,作者的意图是什么?

  • 传递性:如果操作A在操作B之前执行,操作B在操作C之前执行,那么操作A必须在操作C之前执行。在你看到一大段代码,这个线程里面调用了synchronized修饰的方法、那个线程又向阻塞队列put了一个元素、另一个线程又读取了一个volatile修饰的变量…从这些发生在先规则里面 使用 传递性 就能大致推断整个代码的执行流程了。

扯了这么多,看一个不安全发布的示例。

public class UnsafeLazyInitialization {private static Resource resource;public static Resource getResource() {if (resource == null) {resource = new Resource();//不安全的发布}return resource}
}

这段代码没有应用到前面提到的任何一个发生在先规则,代码在执行过程中发生的指令重排导致了不安全的发布。

在创建对象、发布对象时,隐藏了很多操作的。new Resource对象时需要给Resource对象的各个属性赋值,赋值完了之后,在堆中对象的地址要赋值给 静态变量resource。在整个过程中就有可能存在指令重排,看图:

715283-20190310145618465-1176325411.png

类似地,双重检查加锁也会导致不安全的发布。

3. 安全的发布

public class EagerInitialization {private static Resource resource = new Resource();public static Resource getResource() {return resource;}
}

在声明静态变量时同时初始化,由JVM来保证初始化过程的安全性。static修饰说明是类变量,因而符合单例模式。

3.1 初始化安全性

初始化安全性是一种保证:正确构造的对象在没有同步的情况下也能安全地在多个线程之间共享,而不管它是如何被发布的。换句话说:对于被正确构造的对象,所有线程都能看到由构造函数为对象各个final域设置的正确值。

再换句话说:对于含有final域的对象,初始化安全性可以防止对象的初始引用被重排序到构造过程之前。这句话已经点破了关键了。看上一幅图,线程A在赋值到半路,太累了,休息了一下,抽了一根烟。然后继续开始了它的赋值,这些赋值操作,就是对象的构造过程。而在赋值的中间,存在着一个指令重排---将尚未构造完成的对象的堆地址写入到初始引用中去了,而如果这个时候恰好有其他线程拿着这个初始引用去访问对象(比如访问该对象的某个属性),但这个对象还未初始化完成啊,就会导致bug。

哈哈哈哈……是不是还是看不懂、很抽象?这就是 。经书级别的经,难念的经。咱用代码来说明一下:

public class Resource {private int x;//没有用final修饰private String y;//没有用final修饰public Resource(int x, String y) {this.x = x;this.y = y;}
}

而如果,这两个属性都用final修饰的话,那么就满足初始化安全的保证,就没有指令重排了。

715283-20190310145631930-1053492186.png

这就是final关键字所起的作用。

另外,你是不是注意到,如果用final修饰实例变量时,IDEA会提示你尚未给final修饰的实例变量赋初始值?哈哈……

总结一下:

构造函数对final域的所有写入操作,以及对通过这些域可以到达的任何变量的写入操作,都将被“冻结”,并且任何获得该对象引用的线程都至少能确保看到被冻结的值。对于通过final域可到达的初始变量的写入操作,将不会与构造过程后的操作一起被重排序。

所以:如果Resouce是一个不可变对象,那么UnsafeLazyInitialization就是安全的了。

//不可变
public class Resource {private final int x;private final String y;public Resource(){x=10;y="hello"}public Resource(int x, String y) {this.x = x;this.y = y;}
}//UnsafeLazyInitialization 不仅是安全的发布,而且在多线程访问中也是线程安全的。
//因为Resource的属性x、y 都是不可变的。
public class UnsafeLazyInitialization {private static Resource resource;public static Resource getResource() {if (resource == null) {resource = new Resource();//安全的发布!}return resource;}
}

关于初始化安全性,只能保证 final 域修饰的属性在构造过程完成时的可见性。如果,构造的对象存在非final域修饰的属性,或者在构造完成后,在程序中其他地方能够修改属性的值,那么必须采用同步来保证可见性(必须采用同步保证线程安全),示例如下:

import java.util.HashMap;
import java.util.Map;
/*** @author psj* @date 2019/03/10*/
public class UnSafeStates {/*** UnSafeStates 唯一的一个属性是由final修饰的,初始化安全性还是存在的* 即:其他线程能看到一个正确且 **构造完成** 的UnSafeStates对象*/private final Map<String,String> states;public UnSafeStates() {states = new HashMap<>();states.put("hello", "he");states.put("world", "wo");}public String getAbbreviation(String s) {return states.get(s);}/*** 这个方法能够修改 states 属性的值, UnSafeStates 不再是一个线程安全的类了* 如果多线程并发调用 setAbbreviation 方法, 就存在线程安全性问题. HashMap的循环引用了解一下?哈哈……* @param key* @param value*/public void setAbbreviation(String key, String value) {states.put(key, value);}
}

3.2 volatile 修饰的属性的安全发布问题

这个和final关键字中讨论的初始化安全性类似。只不过,volatile修饰的属性是满足发生在先关系的。

套用volatile变量规则:在volatile变量的写入操作必须在对该变量的读取操作之前执行,那volatile也能避免前面提到的指令重排了。因为,初始化到一半,然后好累,要休息一下,说明初始化过程尚未完成,也即:变量的写入操作尚未彻底完成。那根据volatile变量规则:对该变量的访问也不能开始。这样就保证了安全发布。这也是为什么DCL双重检查锁中定义的static变量 用volatile修饰就能安全发布的原因。

4. 总结

在写代码过程中,有时不太刻意地去关注安全发布,在声明一个类的属性时,有时就顺手给实例变量用一个final修饰。抑或是在考虑多线程访问到一个状态变量时,给它用个volatile修饰,并没有真正地去思考总结final到底起作用在哪里了?

所以总结起来就是:final关键字在初始化过程中防止了指令重排,保证了初始化完成后对象的安全发布。volatile则是通过JMM定义的发生在先关系,保证了变量的内存可见性。

最近在看ES源码过程中,看别人写的代码,就好奇,哎,为什么这里这个属性要用个final呢?为什么那个属性加了volatile修饰呢?其实只有明白背后原理,才能更好地去理解别人的代码吧。

当然,上面写的全是自己的理解,有可能出错,因为我并没有将源代码编译成字节码、甚至是从机器指令角度去分析 上面示例的执行流程,因为我看不懂那些汇编指令,哈哈哈哈哈哈……

5. 参考资料

《Java并发编程实战》第3章、第16章

这篇文章前前后后加起来居然写了6个小时,没时间打球了…^:(^ ^:(^

原文:https://www.cnblogs.com/hapjin/p/10505337.html

转载于:https://www.cnblogs.com/hapjin/p/10505337.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/463226.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微机原理8086CPU

8086CPU有14个寄存器&#xff1a;AX BX CX DX SI DI SP BP IP CS SS DS ES PSW寄存器16位&#xff0c;存放2个字节ABCDX为通用寄存器2^16-1最大数据存放8086上一代CPU中的寄存器都是8位 四个寄存器可以分为两个独立的8位寄存器使用分为AH、AL 高八位低八位数据最大存放16位822…

软件工程 - 设计模式学习之工厂方法模式Factory Method

在现实生活中我们可以看见,乐事在卖薯片,可比克也在卖.我敢肯定的说它们各自都有自己的工厂来生产薯片,而且生产出来的薯片味道还是不一样的.这就是我们这里所谓的工厂方法模式. 我们来看看这个模式的UML图: 这个模式中我们可以看到:产品和工厂2个基类,然后它们下面又有些子孙类…

一个简单的Makefile模板

FLAGS -Wall -g INCLUDE-I$(ACE_ROOT)/include ACE_LIB-L$(ACE_ROOT)/libN_LIB-lACEDD-DACE_HAS_EVENT_POLL Server:Server.o TaskWork.o  g -o Server Server.o TaskWork.o $(ACE_LIB) $(N_LIB) $(DD)Server.o:Server.cpp ClassDefined.h  g $(FLAGS) $(INCLUDE) $(DD) -…

new 与 malloc区别

new和malloc的区别 属性 new/delete是C关键字&#xff0c;需要编译器支持。malloc/free是库函数&#xff0c;需要头文件支持c。 参数 使用new操作符申请内存分配时无须指定内存块的大小&#xff0c;编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。 …

树莓派3B+学习笔记:4、查看GPIO

GPIO&#xff08;General Purpose I/O Ports&#xff09;意思为通用输入/输出端口。可以在终端重直接查看GPIO的定义。查看方式1&#xff1a;gpio readall 查看方式2&#xff1a; pinout 可以看出&#xff0c;这条命令执行后提示要查看更多信息访问pinout.xyz 转载于:https://w…

汇编语言基础知识点

汇编指令和机器指令一一对应&#xff0c;有编译器翻译识别&#xff0c;计算机无法直接识别一个存储单元等于1字节 1字节等于8位1KB1024Byte(字节)1Byte8bit&#xff08;位&#xff09;CPU中&#xff0c;指令和数据都是二进制形式存在磁盘的数据&#xff0c;CPU无法直接使用&…

java开发环境搭建--写给java新手

最近我遇到很多java开发的新人&#xff0c;包括很自认为不是新人的人根本就不会自己搭建一个完整的开发环境&#xff0c;因此想写此文&#xff0c;希对他们有所帮助&#xff0c;解除他们的一些开发困惑&#xff0c;更好的了解java. 一、windows下开发环境的搭建 1、从http://ja…

Silverlight的自定义tooltip提示工具条

这种应用场景其实很多&#xff0c;比如游戏中装备/魔法的选择菜单&#xff0c;这里借用了"深蓝色右手"的一张图 再比如聊天室中的文本颜色设置 虽然sl的ToolTipService.ToolTip属性可以设置任何对象&#xff0c;比如下面这样 代码 1 <Rectangle Fill"Red&qu…

c++ 线程间通信方式

一&#xff1a;两个进程间的两个线程通信&#xff0c;相当于进程间通信 二&#xff1a;一个进程中的两个线程间通信 通信方式&#xff1a; 1.互斥锁 mutex; lock_guard (在构造函数里加锁&#xff0c;在析构函数里解锁&#xff09; unique_lock 自动加锁、解锁 2.读写锁 shar…

Linux dmidecode备忘

dmidecode之前笔者在文章Linux下查看主板的相关信息中已经介绍了该命令查看主板信息的用法&#xff0c;这里进一步介绍 DMI&#xff0c;即DesktopManagement Interface。也有被称为SMBIOS&#xff0c;即System Management BIOS。DMI表的意义在于让我们在不探测硬件实体的情况下…

[Silverlight]使用PagedCollectionView配合复选框实现动态筛选的解决方案

在之前的文章中提到&#xff0c;PagedCollection提供了筛选&#xff08;Filter&#xff09;功能。 实际项目中我们往往有这样的需求 即通过复选框动态的筛选DataGird的相关项&#xff0c;比如上面截图所示例的筛选Gender列特定项。有的朋友可能已经想到了&#xff1a;直接操作O…

linux与汇编

Linux操作系统是用C语言编写的&#xff0c;汇编只在必要的时候才被人们想到&#xff0c;但它却是减少代码尺寸和优化代码性能的一种非常重要的手段&#xff0c;特别是在与硬件 直接交互的时候&#xff0c;汇编可以说是最佳的选择。Linux提供了非常优秀的工具来支持汇编程序的开…

MyBatis 逆向工程(MyBatis 自动生成接口以及xml)的使用

刚学MyBatis逆向工程&#xff08;还以为要反汇编呢.....&#xff09; MyBatis逆向工程 个人理解就是链接数据库自动生成相关的增删改查相关的类 以及xml文件 &#xff08;其中有一些不足 应该就是多表链接的问题需要自己写吧&#xff09; MyBatis逆向工程 一般和主项目分开 比较…

IE6-IE9兼容性问题列表及解决办法_补充之五:在IE9下, disabled的文本框内容被选中后,其他控件无法获得焦点问题...

先看一段Htm代码&#xff0c;里面一个disabled的文本框&#xff0c;一个普通可写的文本框&#xff0c;还有一个按钮&#xff0c;非常简单&#xff0c;代码如下&#xff1a;<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.o…

group by 的实现原理

转载&#xff1a;https://cloud.tencent.com/developer/article/1513067 写过 Sql 的同学应该都知道 group by 是用来对数据进行分组的&#xff0c;一般与聚合函数一起使用&#xff0c;对分组后的数据进行聚合。虽然大家都在用&#xff0c;但是有些同学还是不太清楚 group by …

怎么让sublime text3可以运行c/c++

轻巧便捷的sublime text 3代码编辑功能非常强大&#xff0c;并且很漂亮啊有木有&#xff01;&#xff01;&#xff01;&#xff01;以前我会在安装了CodeBlocks这样的编译器的基础上&#xff0c;再安装一个NotePad&#xff0c;作为编辑器。因为编辑器类似于记事本&#xff0c;只…

LeetCode:Path Sum II

Given a binary tree and a sum, find all root-to-leaf paths where each paths sum equals the given sum. For example: Given the below binary tree and sum 22, 5/ \4 8/ / \11 13 4/ \ / \7 2 5 1return [[5,4,11,2],[5,8,4,5] ]解题思路:通过遍历树保…

设有n个正整数,将他们连接成一排,组成一个最大的多位整数

题目描述&#xff1a; 设有n个正整数&#xff0c;将他们连接成一排&#xff0c;组成一个最大的多位整数。 如:n3时&#xff0c;3个整数13,312,343,连成的最大整数为34331213。 如:n4时,4个整数7,13,4,246连接成的最大整数为7424613。输入描述: 有多组测试样例&#xff0c;每组测…

C++中使用try{}catch()的优/缺点

优点&#xff1a;提高了代码的健壮性&#xff0c;防止因为没有接收到异常导致崩溃。 缺点&#xff1a;增加了系统的开销。 增加系统开销的原因 &#xff1a; try catch会在已有的代码上面增加额外的cost, 导致性能的降低。 这个额外的cost不是说只有throw exception的时候才会…

还在公司

原本都买好6点半回程的票了,但是突然来了个重要的会议.不得不留到现在&#xff0c;杯具的是参加会议的希腊人英语惨不忍睹. 10点钟坐GF的姐夫的车子回去&#xff0c;到家要12点以后了吧.转载于:https://www.cnblogs.com/JeffChen/archive/2009/12/18/2600174.html