Java并发编程实践学习笔记(三)——共享对象之发布和异常

news2024/11/15 13:24:20

目录

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() {
            @Override
            public 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 block
            e.printStackTrace();
        }
        name = "ThisEscape初始化完成";
    }

    @Override
    public 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;
    }

    @Override
    void overrideMe() {
        System.out.println(x);
    }

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

        在子类初始化时,会先调用父类Base的构造函数,而父类的构造函数中调用了可重写的方法,实际上调用的是子类中重载的方法,然而此时子类尚未完成初始化,造成的结果就是尚未完成初始化的父类逸出到子类中。

        运行结果:

        这里,当Base构造函数调用时overrideMe,Child尚未完成初始化final int x,并且该方法获取错误的值,这几乎肯定会导致错误。

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

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

相关文章

奖品收到了

质量一流&#xff01;物流速度快&#xff01;下次继续努力&#xff01; 开心

抽象、封装、继承、多态

抽象 抽象是从众多的事物中抽取出共同的、本质性的特征&#xff0c;而舍弃其非本质的特征的过程。 下面是抽象到封装的过程 如果不考虑实际的情况下 人的定义&#xff1a;有嘴、有脚、有眼睛、有耳朵、会吃饭、会跑。 动物的定义&#xff1a;有嘴、有脚、有眼睛、有耳朵、会…

2023年网红经济研究报告

第一章 行业概况 随着社交媒体和移动互联网的普及&#xff0c;网红经济迅速崛起。网红经济&#xff0c;是指以网络为平台&#xff0c;通过网红的影响力、热度和传播力&#xff0c;形成的一种以流量变现为主的商业模式。网红经济涉及到的行业非常广泛&#xff0c;如美妆、时尚、…

ai聊天机器人chatgpt收费版

AI聊天机器人的功能通常包括以下几个方面&#xff1a; 自然语言理解。该功能可以识别并理解用户输入的自然语言&#xff0c;如文本、语音等&#xff0c;以便进行后续的处理和回复。 对话管理。该功能可以管理对话的上下文和流程&#xff0c;并根据用户的输入和意图来产…

Linux系统查看CPU信息命令cat /proc/cpuinfo详细说明

Linux操作系统服务器如何查看CPU处理器信息&#xff1f;使用命令cat /proc/cpuinfo可以查看CPU详细信息&#xff0c;包括CPU核数、逻辑CPU、物理CPU个数、CPU是否启用超线程等&#xff0c;阿里云服务器网分享Linux服务器查看CPU信息命令&#xff1a; 目录 Linux服务器查看CPU…

JavaWeb12-三大组件之监听器-Listener

1. 官方文档 文档&#xff1a;java_ee_api_中英文对照版.chm 2. Listener 监听器介绍 Listener 监听器它是 JavaWeb 的三大组件之一。JavaWeb 的三大组件分别是&#xff1a;Servlet 程序、Listener 监听器、Filter 过滤器Listener 是 JavaEE 的规范&#xff0c;就是接口监听…

(附源码)计算机毕业设计Java动物在线领养网站

项目运行 &#x1f345;包售后&#xff0c;包调试&#xff0c;包讲解&#x1f345; &#x1f345;获取方式1:文章末尾获取联系&#x1f345; &#x1f345;获取方式2:点我进入&#xff0c;文章末尾获取联系&#x1f345; &#x1f345;包售后&#xff0c;包调试&#xff0c;包讲…

WX小程序 - 2

条件渲染&#xff1a; wx:if "{{ newlist.length 0 }}" wx:else 跳路由&#xff1a;绑定点击事件&#xff0c;执行跳转页面 bindtap data-id"{{ item.id }}" 添加id wx.navigateTo 跳路由并传参&#xff0c; 下一个路由 onLoad生命周期可以获得参数…

每周一算法:前缀和

前缀和 前缀和可以理解为数列的前 n 项的和。它通过预处理的方式&#xff0c;能够快速查询序列中从第L个数到到第R个数的和。 算法思想 其基本思想是在原序列的基础上预处理一个前缀和数组 s [ ] s[] s[]&#xff0c;其中 s [ i ] s[i] s[i]表示序列前 i i i个数的和。通过前…

Exception in thread “main“ java.lang.UnsupportedClassVersionError

java MainDemo执行main方法报错 Error: A JNI error has occurred, please check your installation and try again Exception in thread "main" java.lang.UnsupportedClassVersionError: MainDemo has been compiled by a more recent version of the Java Runtime…

多台电脑共享鼠标键盘软件

背景 最近接手了2个不同base的项目&#xff0c;由于2个base的不同代码加密管理&#xff0c;必须要用两台电脑进行分别开发。于是&#xff0c;我不大的办公桌上要摆上2个键盘和2个鼠标&#xff0c;一下子就显得桌面特别杂乱&#xff0c;办公心情都不舒畅了。 我跟朋友吐槽了这件…

用LeangooScrum敏捷工具做缺陷管理和迭代规划和迭代执行

上一篇我们介绍了如何管理产品路线图&#xff08;用Leangoo领歌Scrum敏捷开发工具管理产品路线图&#xff1f;_哆啦B梦_的博客-CSDN博客)和敏捷需求管理&#xff08;使用敏捷开发工具做敏捷需求管理流程_哆啦B梦_的博客-CSDN博客&#xff09; 这一篇我们介绍下如何用Scrum敏捷…

纷享销客携手百捷、锐之旗走进湖南竞网,探索互联网营销创新

近日&#xff0c;《互联网营销创新增长路径-高管面对面闭门会》在长沙成功举办&#xff0c;活动邀请武汉百捷集团股份有限公司、河南锐之旗信息技术有限公司&#xff0c;两家互联网营销服务头部企业的高层走进湖南竞网数字科技集团有限公司&#xff08;以下简称“竞网”&#x…

[CSDN] 512创作纪念日,大处着眼,小处着手,乐观进取

大家好&#xff0c;我是一名程序员&#xff0c;也是一名CSDN博客作者&#xff0c;今天是我成为CSDN博客作者的512天纪念日&#xff0c;我想借此机会和大家分享一下我的创作历程。 初心与动力 从事IT行业多年&#xff0c;我深深感受到这个行业变化的速度非常之快&#xff0c;需…

责任链模式——使编程更有灵活性

● 责任链模式介绍 责任链模式&#xff08;Iterator Pattern)&#xff0c;是行为型设计模式之一。什么是“链”&#xff1f;我们将多个节点首位相连构成的模型称为链&#xff0c;比如生活中常见的锁链&#xff0c;就是由一个个圆角长方形的铁环串起来的结构。对于链式结构&…

浅谈绿色创新型校园的节约能耗与能耗管理的应用

摘要&#xff1a;保护地球资源和环境的可持续发展理论成为我国的基本国策。建筑节能上升到较高地位。仅有能量的“守恒”是不够的&#xff0c;更要研究用*小代价和*小能耗来满足人们的而合理需求&#xff0c;实现建筑合理用能。文章主要针对学校能源管理的问题进行研究&#xf…

带你简单了解Chatgpt背后的秘密:大语言模型所需要条件(数据算法算力)以及其当前阶段的缺点局限性

带你简单了解Chatgpt背后的秘密&#xff1a;大语言模型所需要条件&#xff08;数据算法算力&#xff09;以及其当前阶段的缺点局限性 1.什么是语言模型&#xff1f; 大家或多或少都听过 ChatGPT 是一个 LLMs&#xff0c;那 LLMs 是什么&#xff1f;LLMs 全称是 Large Language…

TomcatServletHTTP

1、Web概述 1.1 Web相关概念 Web是全球广域网&#xff0c;也称为万维网(www)&#xff0c;能够通过浏览器访问的网站。 在我们日常的生活中&#xff0c;经常会使用浏览器去访问百度、京东、传智官网等这些网站&#xff0c;这些网站统称为Web网站。 如下就是通过浏览器访问传智…

基于AT89C51单片机的电子计数器设计与仿真

点击链接获取Keil源码与Project Backups仿真图&#xff1a; https://download.csdn.net/download/qq_64505944/87770826 源码获取 主要内容&#xff1a; 设计一个电子计时器&#xff0c;数码管初始显示值为“00”&#xff0c;每隔1s电子秒表加1&#xff1b;秒计数到60时清0&a…

【网络】Socket编程-UDP篇

文章目录 预备知识源IP地址和目的IP地址源MAC地址和目的MAC地址源端口号和目的端口号"端口号port" 和 "进程ID"认识TCP/UDP协议网络字节序 Socket编程sockaddr结构API接口 简单的UDP网络程序服务器server服务端创建套接字:socket函数**socket的底层原理** …