Java并发编程实践学习笔记(三)——共享对象之发布和异常
目录
1 公共静态变量逸出
2 非私有方法逸出私有变量
3 this引用逸出
4 构造函数中的可覆盖方法调用逸出
发布(publishing)一个对象的意思是:使对象能够在当前作用域之外的代码中使用。例如,将一个指向该对象的引用保存到其他代码可以访问的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递到其他类的方法中。
发布内部状态可能会破坏封装性,并使程序难以维持不变性条件。例如,如果在对象构造完成之前就发布该对象,就会破坏线程安全性。 当某个不应该发布的对象被发布时,这种情况就成为逸出(escape)。
简而言之,发布就是把对象暴露给他人使用,这就是为什么会需要用到封装;逸出就是把不应该发布的对象发布了,比如对象还没完成实例化,就被外界使用了。
1 公共静态变量逸出
发布对象的最常见方式就是将对象的引用保存到一个公有的静态变量中,任何类和线程都能看见该对象。如下代码所示,initialize方法实例化一个新的HashSet实例,并通过将它存储到knownSecrets引用,从而发布这个实例:
// 3-5 发布一个对象
public static Set<Secret> knownSecrets;
public void initialize() {knownSecrets = new HashSet<Secret>();
}
当发布某个对象时,可能会间接地发布其他对象。如果将一个Secret对象添加到集合knownSecrets中,那么同样会发布这个对象,因为任何代码都可以遍历这个集合,并获得对这个新Secret对象的引用。
2 非私有方法逸出私有变量
从非私有方法中返回一个引用,也能发布返回的对象。下面的代码发布了包含洲名缩写的数组,而这个数组本应是私有的:
// 3-6 使内部可变状态逸出(不要这样做)
public class UnsafeStates {private String[] states = new String[] { "AK", "AL", "LW" };public String[] getStates() {return states;}public static void main(String[] args) {UnsafeStates us = new UnsafeStates();System.out.println(Arrays.toString(us.getStates()));us.getStates()[0] = "NY";System.out.println(Arrays.toString(us.getStates()));}
}
这样发布states会出现问题,因为任何调用者都能修改这个数组的内容。通过访问对象中的共有方法获取私有变量的值,然后更改内部数据,则导致变量逸出作用域。数组states已经逸出了它所在的作用域,这个本该私有的数据,事实上已经变成共有了。
发布一个对象时,该对象的非私有域中引用的所有对象同样会被发布。更一般的,一个已发布的对象中,那些非私有的引用链及方法调用链中的可获得对象也都会被发布。
3 this引用逸出
最后一种发布对象或其内部状态的机制就是发布一个内部的类实例。当ThisEscape发布内部类EvnetLister时,也隐含地发布了ThisEscape实例本身,因为在这个内部类的实例中也包含了对ThisEscape实例的隐含引用。
1、EventListener接口
public interface EventListener {void onEvent(Object obj);
}
2、EventSource
public class EventSource<T> {private final List<T> eventListeners;public EventSource() {eventListeners = new ArrayList<>();}public synchronized void registerListener(T eventListener) {this.eventListeners.add(eventListener);this.notifyAll();}public synchronized List<T> retrieveListeners() throws InterruptedException {List<T> dest = null;if (eventListeners.size() <= 0) {this.wait();}dest = new ArrayList<>(eventListeners.size());dest.addAll(eventListeners);return dest;}}
3、ThisEscape
public class ThisEscape {public final int id;public final String name;public ThisEscape(EventSource source) {id = 100;// ThisEscape尝试在构造函数中注册一个事件监听器source.registerListener(new EventListener() {@Overridepublic void onEvent(Object obj) {System.out.println("id: " + ThisEscape.this.id);System.out.println("name: " + ThisEscape.this.name);}});try {// 调用sleep模拟其他耗时的初始化操作Thread.sleep(1000);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}name = "ThisEscape初始化完成";}@Overridepublic String toString() {return "ThisEscape{" +"id=" + id +", name='" + name + '\'' +'}';}
}
ThisEscape演示了一种重要的逸出特例,this引用在构造时逸出。发布的内部EventListener实例是一个封装的ThisEscape中的实例。但是这个对象只有通过构造器函数返回后,才处于可预言的、稳定的状态,所以从构造器函数内部发布的对象,只是一个未完成构造的对象。
内部类、匿名内部类都可以访问外部类的对象的域,因为内部类构造的时候,会把外部类的对象this隐式的作为一个参数传递给内部类的构造方法,这个工作是编译器做的,它会给内部类所有的构造方法添加这个参数,所以这个例子的匿名内部类在构造ThisEscape时就把ThisEscape创建的对象隐式的传给匿名内部类了。这样 source就持有ThisEscape的内部类EvenListener,而Evenlistener可能会带出ThisEscape中的保护数据引用,如果此时ThisEscape还未初始化完成,Evenlistener可能会访问到ThisEscape中未完成初始化的数据,因为this引用提前被EventListener实例对象拿到,这就是this引用的逸出。
public class ThisEscapeTest {public static void main(String[] args) throws InterruptedException {EventSource<EventListener> source = new EventSource<>();new Thread(() -> {try {List<EventListener> listeners = source.retrieveListeners();for(EventListener listener : listeners) {listener.onEvent(new Object());}} catch (InterruptedException e) {throw new RuntimeException(e);}}).start();ThisEscape escape = new ThisEscape(source);System.out.println("ThisEscape 构造完成结果:"+escape);}
}
运行结果:
这个测试案例中,另一个线程在ThisEscape还未完成初始化时,就访问ThisEscape的内部数据了。
总结这个案例,造成this逸出,一个是在构造函数中创建内部类(EventListener) ,另一个是在构造函数中就把这个内部类给发布了出去(source.registerListener)。那么,对应的解决方法就是:如果要在构造函数中创建内部类,那么就不能在构造函数中将其发布了,应该在构造函数外发布,即等构造函数执行完毕,初始化工作已全部完成,再发布内部类。如果需要在构造函数中注册一个事件监听器或者启动线程,可以使用一个私有的构造函数和一个公共的工厂方法(Factory Method),从而避免不正确的构造过程。
public class SafeListener {private final EventListener listener;private SafeListener() {listener = new EventListener() {public void onEvent(Event e) {doSomething(e);}};}public static SafeListener newInstance(EventSource source) {SafeListener safe = new SafeListener();source.registerListener(safe.listener);return safe;}void doSomething(Event e) {}}
如上示例代码所示,注册监听在构造之后执行,保证onEvent()方法在SafeListener的构造之后才能被调用,对象正确初始化后再调用this引用指向的对象的方法修改属性就不是逸出,而是发布。
在构造函数过程中使this引用逸出的一个常见错误是,在构造器中启动一个线程。当对象在其构造函数中创建一个线程时,无论是显式创建还是隐式创建,this引用都会被新创建的线程共享。在对象尚未完全构造之前,新的线程就可以看见它。在构造函数中创建线程并没有错误,但最好不要立即启动它,而是通过一个start或initialize方法来启动。
4 构造函数中的可覆盖方法调用逸出
在构造函数中调用一个可覆盖的实例方法时(既不是private,也不是final的),同样会导致this引用在构造期间逸出。
Base类:
public abstract class Base {Base() {System.out.println("Base构造函数");// 在构造函数中调用可重写的方法overrideMe();}// 一个可重写的方法abstract void overrideMe();
}
子类:
public class Child extends Base{final int x;Child(int x) {System.out.println("Child构造函数");this.x = x;}@Overridevoid overrideMe() {System.out.println(x);}public static void main(String[] args) {new Child(42);}
}
在子类初始化时,会先调用父类Base的构造函数,而父类的构造函数中调用了可重写的方法,实际上调用的是子类中重载的方法,然而此时子类尚未完成初始化,造成的结果就是尚未完成初始化的父类逸出到子类中。
运行结果:
这里,当Base构造函数调用时overrideMe,Child尚未完成初始化final int x,并且该方法获取错误的值,这几乎肯定会导致错误。
相关文章:

Java并发编程实践学习笔记(三)——共享对象之发布和异常
目录 1 公共静态变量逸出 2 非私有方法逸出私有变量 3 this引用逸出 4 构造函数中的可覆盖方法调用逸出 发布(publishing)一个对象的意思是:使对象能够在当前作用域之外的代码中使用。例如,将一个指向该对象的引用保存到其他代…...

Python学习之Image模块图片滤镜效果操作示例
前言 滤镜效果是图像处理中常用的一种技术,可以用来增强图像的视觉效果,实现不同的效果,比如增强对比度、饱和度、色彩等。滤镜效果可以帮助用户快速地调整图像的特性,从而使图像更加适合用户的需求。 Image模块对于图像处理的…...

Grafana 系列-统一展示-5-AWS Cloudwatch 仪表板
系列文章 Grafana 系列文章 👍️强烈推荐 强烈推荐使用 GitHub 上的 monitoringartist/grafana-aws-cloudwatch-dashboards 仪表板。该 repo 有一系列 AWS 资源的仪表板,包括但不限于: EC2EBSAPI GWAutoscalingBillingEKSLambdaLogsRDSS3…...

MySQL---控制流函数、窗口函数(序号函数、开窗聚合函数、分布函数、前后函数、头尾函数、其他函数)
1. 控制流函数 格式 解释 案例 IF(expr,v1,v2) 如果表达式 expr 成立,返回结果 v1;否则,返回结果 v2。 SELECT IF(1 > 0,正确,错误) ->正确 IFNULL(v1,v2) 如果 v1 的值不为 NULL,则返回 v1ÿ…...

一心报国的西工大网安人走出新手村
大二下学期5月5日晚上,西工大长安校区教学西楼,作为一名网安专业本科生,从大一便立志学好网安知识,报效祖国,却苦于没有优秀学习资源,就把这事儿拖到了大二,最近上了一门专业课,如同…...

如何安装oracle的sample schema
首先从如下的地址选择合适的版本进行下载 https://github.com/oracle-samples/db-sample-schemas/releases 如果是rac环境,最好是将这个数据库停掉,然后只启动一个instance,然后再开始安装 [Tue May 09 20:26:34][377951][oraclenshqae01adm…...

ChatGPT :国内免费可用 ChatGPT +Midjourney绘图
前言 ChatGPT(全名:Chat Generative Pre-trained Transformer),美国OpenAI 研发的聊天机器人程序 ,于2022年11月30日发布 。ChatGPT是人工智能技术驱动的自然语言处理工具,它能够通过理解和学习人类的语言来…...

女孩子转数据分析难吗?难在哪里?
对于数据分析,很多人乍一听会觉得没啥技术难度,是个适合女孩子的专业。我们面对很多零基础小白也是用通俗的语言来形容这个专业:一般是通过Excel或者power BI工具对数据进行分析,制作成可视化的报表给领导层,为公司业务…...

基于常用设计模式的业务框架
前言 做开发也有好几年时间了,最近总结和梳理自己在工作中遇到的一些问题,工作中最容易写出BUG的需求就是改造需求了。一个成熟的业务系统是需要经过无数次迭代而成的,也意味着经过很多开发人员之手,最后到你这里,大部…...

ubuntu重启ssh服务
一、开启ssh服务首先需要安装打开ssh服务的库: sudo apt-get install openssh-server 二、检查当前的ssh开启情况: ps -e |grep ssh 三、如果有sshd,则ssh-server已经启动;若仅有agent,则尚未启动; 开启ssh…...

【19】SCI易中期刊推荐——计算机 | 人工智能领域(中科院2区)
💖💖>>>加勒比海带,QQ2479200884<<<💖💖 🍀🍀>>>【YOLO魔法搭配&论文投稿咨询】<<<🍀🍀 ✨✨>>>学习交流 | 温澜潮生 | 合作共赢 | 共同进步<<<✨✨ 📚📚>>>人工智能 | 计算机视觉…...

Vue.js条件、循环语句
文章目录 条件语句v-ifv-elsev-else-ifv-show 循环语句v-for 指令v-for 迭代对象valuevalue ,keyvalue ,key,index v-for 迭代整数 条件语句 v-if 在元素 和 template 中使用 v-if 指令 <div id"app"><p v-if"seen">现在你看到我…...

Go语言学习查缺补漏ing Day4
Go语言学习查缺补漏ing Day4 一、掌握iota的使用 请看下面这段代码: package mainimport "fmt"const (a iota_bc "ReganYue"dd1e iotaf iota )func main() {fmt.Println(a, b, c, d, d1, e, f) }思考一下输出结果会是什么? …...

说服审稿人,只需牢记这 8 大返修套路!
本文作者:雁门飞雪 如果说科研是一场修炼,那么学术界就是江湖,投稿就是作者与审稿人或编辑之间的高手博弈。 在这一轮轮的对决中,有时靠的是实力,有时靠的是技巧,然而只有实力和技巧双加持的作者才能长久立…...

Java 责任链模式详解
责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,它用于将请求的发送者和接收者解耦,使得多个对象都有机会处理这个请求。在责任链模式中,有一个请求处理链条,每个处理请求的对象都是一个…...

使用MASA全家桶从零开始搭建IoT平台(三)管理设备的连接状态
文章目录 前言分析方案1:遗嘱消息演示遗嘱消息的使用实施流程 方案2:使用WebHook开启WebHook演示Webhook编写代码 前言 获取一个设备的在线和离线状态,是一个很关键的功能。我们对设备下发的控制指令,设备处于在线状态才能及时给我们反馈。这里的在线和…...

我的新书上架了!
talk is cheap,show you my book! 新书《从0开始学ARM》终于在各大平台上架了!! 一、关于本书 1. 本书主要内容 ARM体系架构是目前市面上的主流处理器体系架构,在手机芯片和嵌入式芯片领域,ARM体系架构…...

语言与专业的奇迹:如何利用ChatGPT优化跨国贸易
贸易公司,在进行跨国贸易时,往往需要面对不同国家的甲方或者乙方,在与之沟通的过程中,语言和专业是必须要过的一关,顺畅的交流,往往会带来更好的收益。 今天以“茶”为例,给大家介绍一“知否AI…...

云服务器安装宝塔Linux面板命令脚本大全
阿里云服务器安装宝塔Linux面板,操作系统不同安装命令脚本也不同,支持CentOS、Alibaba Cloud Linux、Ubuntu/Deepin等Linux系统,阿里云服务器网分享阿里云服务器安装宝塔Linux面板命令脚本大全: 云服务器安装宝塔Linux面板命令 …...

zed2i相机中imu内参的标定及外参标定
zed2i中imu内参的标定 参考: https://blog.csdn.net/weixin_42681311/article/details/126109617 https://blog.csdn.net/weixin_43135184/article/details/123444090 值得注意,imu内参的标定其实不是那么重要,大致上给一个值应该影响不大…...

Java中的JUnit是什么?如何使用JUnit进行单元测试
JUnit是Java中最流行的单元测试框架之一。它可以帮助开发人员在代码编写过程中检测出错误和异常,从而提高代码的质量和可靠性。 什么是JUnit? JUnit是一个由Kent Beck和Erich Gamma创建的开源Java单元测试框架,它已经成为Java开发中最常用的…...

【seata的部署和集成】
seata的部署和集成 seata的部署和集成一、部署Seata的tc-server1.下载2.解压3.修改配置4.在nacos添加配置5.创建数据库表6.启动TC服务 二、微服务集成seata1.引入依赖2.修改配置文件 三、TC服务的高可用和异地容灾1.模拟异地容灾的TC集群2.将事务组映射配置到nacos3.微服务读取…...

uniapp学习日记之request自定义请求头
uniapp学习日记之request自定义请求头 在学习uniapp的过程中,由于笔者是从Vue项目转来学习uniapp,在使用uni.request时,发现在浏览器调试时,无法在请求头header中添加token字段,愤而弃之,便开始使用axios组…...

【Rust】速度入门---打印个螃蟹先
参考: 菜鸟教程 1 输出到命令行 这不得打印个螃蟹 // 代码来自官方入门教程 // ferris_say需要另外安装 use ferris_says::say; use std::io::{stdout, BufWriter};fn main() {let stdout: std::io::Stdout stdout();let msg: String String::from("Hello fellow Rusta…...

《Linux 内核设计与实现》12. 内存管理
文章目录 页区获得页获得填充为 0 的页释放页 kmalloc()gfp_mask 标志kfree()vmalloc() slab 层slab 层的设计slab 分配器的接口 在栈上的静态分配单页内核栈 高端内存的映射永久映射临时映射 每个 CPU 的分配新的每个 CPU 接口 页 struct page 结构表示系统中的物理页&#x…...

公司新来个卷王,让人崩溃...
最近内卷严重,各种跳槽裁员,相信很多小伙伴也在准备今年的面试计划。 在此展示一套学习笔记 / 面试手册,年后跳槽的朋友可以好好刷一刷,还是挺有必要的,它几乎涵盖了所有的软件测试技术栈,非常珍贵&#x…...

Docker 安全及日志管理
Docker 安全及日志管理 Docker 容器与虚拟机的区别隔离与共享性能与损耗 Docker 存在的安全问题Docker 自身漏洞Docker 源码问题Docker 架构缺陷与安全机制Docker 安全基线标准 容器相关的常用安全配置方法容器最小化Docker 远程 API 访问控制重启 Docker在宿主机的 firewalld …...

大厂面试必备 - MAC 地址 和 IP 地址分别有什么作用?
数据链路层 1、MAC 地址 和 IP 地址分别有什么作用? MAC 地址是数据链路层和物理层使用的地址,是写在网卡上的物理地址。MAC 地址用来定义网络设备的位置。IP 地址是网络层和以上各层使用的地址,是一种逻辑地址。IP 地址用来区别网络上的计…...

【sqlite】联查Join更新
系列文章 C#底层库–MySQLBuilder脚本构建类(select、insert、update、in、带条件的SQL自动生成) 本文链接:https://blog.csdn.net/youcheng_ge/article/details/129179216 C#底层库–MySQL数据库操作辅助类(推荐阅读࿰…...

asp.net+C#德育课程分数统计管理系统
本中小学德育管理系统主要学校内部提供服务,系统分为管理员,教师和学生3个大模块。 本研究课题重点主要包括了下面几大模块:用户登录,管理员信息管理学生信息管理,教师信息管理,班级成绩管理,学…...