并发编程2:如何进行对象共享?

目录

1、对象的可见性:Volatile 变量

2、发布和逸出

3、线程封闭:ThreadLocal

4、对象的不变性

5、安全发布

5.1 - 安全发布常用的模式

5.2 - 可变对象

5.3 - 安全地共享对象


        同步在用于实现原子性或者确定 “临界区(Critical Section)” 的同时,还有另一个重要的作用:内存可见性 (Memory Visibility)//一个线程对共享变量的修改操作对另一个线程可见

1、对象的可见性:Volatile 变量

        首先,贴一段代码:

public class NoVisibility {private static boolean ready;private static int     number;private static class ReaderThread extends Thread {public void run() {while (!ready) {Thread.yield();}System.out.println(number);}}public static void main(String[] args) {new ReaderThread().start();number = 42;ready  = true;}
}

        上边 NoVisibility 可能会持续循环下去,因为读线程可能永远都看不到 ready 的值。一种更奇怪的现象是,NoVisibility 可能会输出 0,因为读线程可能看到了后写入 ready 的值,但却没有看到先写入 number 的值,这种现象被称为 “重排序(Reordering)”//由于重排序的存在,使线程操作没有按照程序中定义的顺序来执行

       内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果,如下图所示。

        当线程 B 执行由锁保护的同步代码块时,可以看到线程 A 之前在同一个同步代码块中的所有操作结果。

        现在,我们可以进一步理解为什么在访问某个共享且可变的变量要求所有线程在同一个锁上同步,就是为了确保某个线程写入该变量的值对于其他线程来说都是可见的。否则,如果一个线程在未持有正确锁的情况下读取某个变量,那么读到的可能是一个失效值//同一个锁,保证可见性

        加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。

        Volatile 变量

        Java 语言提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程。//轻量级同步,但是并不能保证互斥(原子操作,即读写不分离)

        当把变量声明为 volatile 类型后,编译器与运行时都会注意到这个变量是共享的因此不会将该变量上的操作与其他内存操作一起重排序。volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取 volatile 类型的变量时总会返回最新写入的值。//禁止重排序和缓存

        volatile 变量通常用做某个操作完成、发生中断或者状态的标志//比如终结循环标志

        虽然 volatile 变量很方便,但也存在一些局限性,volatile 的语义不足以确保操作的原子性//多个线程对同一个 volatile 变量进行修改操作,仍然会出现数据安全问题

        加锁机制既可以确保可见性又可以确保原予性,而 volatile 变量只能确保可见性。

        当且仅当满足以下所有条件时,才应该使用 volatile 变量:

  • 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。//不同时更新
  • 该变量不会与其他状态变量一起纳入不变性条件中。//非不变性变量
  • 在访问变量时不需要加锁

2、发布和逸出

        “发布(Publish)”一个对象的思是指,使对象能够在当前作用域之外的代码中使用。例如,将一个指向该对象的引用保存到其他代码可以访问的地方,或者将引用传递到其他类的方法中。

        发布内部状态可能会破坏封装性,并使得程序难以维持不变性条件。例如,如果在对象构造完成之前就发布该对象,就会破坏线程安全性。当某个不应该发布的对象被发布时,这种情况就被称为逸出( Escape)

        发布对象的最简单方法是将对象的引用保存到一个公有的静态变量中,以便任何类和线程都能看见该对象。如下所示:

    //保存引用public static Set<Secret> knownSecrets;public void initialize() {//发布一个HashSet<Secret>对象fknownSecrets = new HashSet<Secret>();}

        当发布某个对象时,可能会间接地发布其他对象。如果将一个 Secret 对象添加到集合knownSecrets 中,那么同样会发布这个对象,因为任何代码都可以遍历这个集合,并获得对这个新 Secret 对象的引用。

        不要在构造过程中使 this 引用逸出//对象逸出会存在线程安全问题

//ThisEscape对象逸出
public class ThisEscape {public ThisEscape(EventSource source) {//不要再构造函数中注册一个监听器或者启动一个线程source.registerListener(new EventListener() {public void onEvent(Event e) {doSomething(e);}});}
}

        当 ThisEscape 发布 EventListener 时,也隐含地发布了 ThisEscape 实例本身,因为在这个内部类的实例中包含了对 ThisEscape 实例的隐含引用,如果 this 引用在构造过程中逸出,那么这种对象就被认为是不正确构造//内部类隐式包含了外部内的引用,对象可能还没创建完成就被使用

      在构造过程中使 this 引用逸出的一个常见错误是,在构造函数中启动一个线程。当对象在其构造函数中创建一个线程时,无论是显式创建(通过将它传给构造函数)还是隐式创建(由于 Thread 或 Runnable 是该对象的一个内部类) this 引用都会被新创建的线程共享。在对象尚未完全构造之前,新的线程就可以看见它。在构造函数中创建线程并没有错误,但最好不要立即启动它,而是通过一个 start 或 initialize 方法来启动。

        如果想在构造函数中注册一个事件监听器或启动线程,那么可以使用一个私有的构造函数和一个公共的工厂方法(Factory Method),从而避免不正确的构造过程,如下所示。

public class SafeListener {private final EventListener listener;//1-私有构造private SafeListener() {listener = new EventListener() {public void onEvent(Event e) {doSomething(e);}};}//2-公共方法:防止this逸出public static SafeListener newInstance(EventSource source) {SafeListener safe = new SafeListener();source.registerListener(safe.listener);return safe;}
}

3、线程封闭:ThreadLocal

        当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术被称为线程封闭 (ThreadConfinement),它是实现线程安全性的最简单方式之一。//即不进行数据共享

        栈封闭线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。局部变量的固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。//使用局部变量

        ThreadLocal 对象通常用于防止对可变的单实例变量 (Singleton)全局变量进行共享。例如,在单线程应用程序中可能会维持一个全局的数据库连接,并在程序启动时初始化这个连接对象,从而避免在调用每个方法时都要传递一个 Connection 对象。通过将 JDBC 的连接保存到 ThreadLocal 对象中,每个线程都会拥有属于自己的连接。//封装数据源是ThreadLocal的经典应用

4、对象的不变性

        满足同步需求的另一种方法是使用不可变对象 (Immutable Object)

        如果某个对象在被创建后其状态就不能被修改,那么这个对象就称为不可变对象。不可变对象一定是线程安全的

@Immutable
public final class ThreeStooges {//一旦构造完成就不能修改private final Set<String> stooges = new HashSet<String>();//构造函数public ThreeStooges() {stooges.add("Moe");stooges.add("Larry");stooges.add("Curly");}public boolean isStooge(String name) {return stooges.contains(name);}
}

        在不可变对象的内部仍可以使用可变对象来管理它们的状态,如上边代码 ThreeStooges 所示。尽管保存姓名的 Set 对象是可变的,但在 Set 对象构造完成后无法对其进行修改。//不可变对象在构造完成后就不能进行修改

5、安全发布

5.1 - 安全发布常用的模式

        可变对象必须通过安全的方式来发布,这通常意味着在发布和使用该对象的线程时都必须使用同步。

        要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全地发布:

  • 在静态初始化函数中初始化一个对象引用。//使用static关键字
  • 将对象的引用保存到 volatile 类型的成员变量或者 AtomicReferance 对象中。//保证可见性
  • 将对象的引用保存到某个正确构造对象的 final 类型域中//依赖注入用的就是 final 域
  • 将对象的引用保存到一个由锁保护的域中。//使用同步容器等

        如果线程 A 将对象 X 放入一个线程安全的容器,随后线程 B 读取这个对象,那么可以确保 B 看到 A 设置的 X 状态,即便在这段读/写 X 的应用程序代码中没有包含显式的同步。在 Java 线程安全库中的容器类提供了以下的安全发布保证://使用安全容器进行发布

  • 通过将一个键或者值放入 Hashtable、synchronizedMap 或者 ConcurrentMap 中,可以安全地将它发布给任何从这些容器中访问它的线程。
  • 通过将某个元素放入 Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList 或 synchronizedSet 中,可以将该元素安全地发布到任何从这些容器中访问该元素的线程。
  • 通过将某个元素放人 BlockingQueue 或者 ConcurrentLinkedQueue 中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程。

        通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态的初始化器

public static Holder holder = new Holder(42);

        静态初始化器由 JVM 在类的初始化阶段执行。由于在 JVM 内部存在着同步机制,因此通过这种方式初始化的任何对象都可以被安全地发布。

5.2 - 可变对象

        如果对象在构造后可以修改,那么安全发布只能确保“发布当时”状态的可见性。对于可变对象,不仅在发布对象时需要使用同步,而且在每次访向对象时同样需要使用同步来确保后续修改操作的可见性。要安全地共享可变对象,这些对象就必须被安全地发布,该对象必须是线程安全的或者由某个锁保护起来。

        对象的发布需求取决于它的可变性:

  • 不可变对象可以通过任意机制来发布。
  • 事实不可变对象必须通过安全方式来发布。//事实不可变:对象发布后不再被修改
  • 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来。

5.3 - 安全地共享对象

        当获得对象的一个引用时,你需要知道在这个引用上可以执行哪些操作。在使用它之前是否需要获得一个锁?是否可以修改它的状态,或者只能读取它?许多并发错误都是于没有理解共享对象的这些“既定规则”而导致的。当发布一个对象时,必须明确地说明对象的访问方式//当对象发布时,就需要考虑它的同步问题

        在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:

        线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。//对于成员变量的定义需要格外小心

        只读共享。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象

        线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。//同步容器的做法

        保护对象被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。//并发编程常使用的方式

        总结:如果需要安全的共享对象,那么需要安全的发布对象。安全的发布对象,需要保证对象的完整性以及可见性。

        //勉励:没有什么知识是一次性学会的,学习知识的过程就是对思想一次次的打磨过程

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

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

相关文章

启动RocketMQ报错

说明&#xff1a;启动RocketMQ消费者时&#xff0c;报以下错误&#xff1a;java.lang.IllegalStateException&#xff1a;Failed to start RocketMQ push consumer. 解决&#xff1a;看下所有的监听器类&#xff0c;检查是不是有相同的消费者组名&#xff0c;注释掉其中一个即可…

踩坑 视觉SLAM 十四讲第二版 ch13 编译及运行问题

一、安装Geset 库 sudo apt-get install libgtest-dev cd /usr/src/gtest sudo mkdir build cd build sudo cmake .. //一定要以sudo的方式运行&#xff0c;否则没有写入权限 sudo make //这个也一样要以sudo的方式 sudo cp libgtest*.a /usr/local/lib //将生成…

使用 ESP32 Arduino 和机器学习实现WIFI室内定位

在这个 Arduino 机器学习项目中,我们将使用附近的 WiFi 接入点来定位我们所在的位置。为了使该项目正常运行,您需要一块配备 WiFi 的板,例如 ESP8266、ESP32 或 MKR WiFI 1010。 什么是室内定位? 我们都习惯了 GPS 定位,我们的设备将使用卫星来跟踪我们在地球上的位置。GP…

如何使用win10专业版系统自带远程桌面公司内网电脑,从而实现居家办公?

使用win10专业版自带远程桌面公司内网电脑 文章目录 使用win10专业版自带远程桌面公司内网电脑 在现代社会中&#xff0c;各类电子硬件已经遍布我们身边&#xff0c;除了应用在个人娱乐场景的消费类电子产品外&#xff0c;各项工作也离不开电脑的帮助&#xff0c;特别是涉及到数…

SAM 大模型Colab快速上手【Segment Anything Model】

Google Colab 是一个基于云的 Jupyter 笔记本环境&#xff0c;允许您通过浏览器编写、运行和共享 Python 代码。 它就像 Google 文档&#xff0c;但用于代码。 通过免费版本的 Google Colab&#xff0c;你可以获得带有约 16GPU VRAM 的 Nvidia Tesla T4 GPU&#xff0c;这对于…

c语言每日一练(3)

前言&#xff1a;每日一练系列&#xff0c;每一期都包含5道选择题&#xff0c;2道编程题&#xff0c;博主会尽可能详细地进行讲解&#xff0c;令初学者也能听的清晰。每日一练系列会持续更新&#xff0c;暑假时三天之内必有一更&#xff0c;到了开学之后&#xff0c;将看学业情…

模块化原理:source-map

1. webpack打包基本配置 1.安装webpack与webpack-cli npm i webpack webpack-cli 2.配置 "build":"webpack" 3. 新建webpack.config.js const path require(path); module.exports {// mode: "development",// 默认production&#xff08;什么…

数据库总结

第一章绪论 一、数据库系统概述 1. 数据库的4个基本概念 1.数据&#xff1a;数据库中存储的基本对象&#xff0c;描述事物的符号记录。 2.数据库&#xff1a;长期储存在计算机内、有组织的、可共享的大量数据的集合。较小的冗余度、较高的数据独立性、易扩展性 3.数据库管…

Mybatis异常Invalid bound statement (not found)原因之Mapper文件配置不匹配

模拟登录操作 $.post("/admin/login", {aname, pwd }, rt > {if (rt.code 200) {location.href "manager/index.html";return;}alert(rt.msg)});网页提示服务器代码错误 POST http://localhost:8888/admin/login 500后端显示无法找到Mapper中对应的…

代理模式及常见的3种代理类型对比

代理模式及常见的3种代理类型对比 代理模式代理模式分类静态代理JDK动态代理CGLIBFastclass机制 三种代理方式之间对比常见问题 代理模式 代理模式是一种设计模式&#xff0c;提供了对目标对象额外的访问方式&#xff0c;即通过代理对象访问目标对象&#xff0c;这样可以在不修…

YOLOv5源码中的参数超详细解析(2)— 配置文件yolov5s.yaml

前言&#xff1a;Hello大家好&#xff0c;我是小哥谈。YOLOv5配置了5种不同大小的网络模型&#xff0c;分别是YOLOv5n、YOLOv5s、YOLOv5m、YOLOv5l、YOLOv5x&#xff0c;其中YOLOv5n是网络深度和宽度最小但检测速度最快的模型&#xff0c;其他4种模型都是在YOLOv5n的基础上不断…

Java中的SimpleDateFormat方法分析

Java中的SimpleDateFormat方法分析 先进行专栏介绍SimpleDateFormat方法分析 常用方法构造方法格式化&#xff08;从Date到String&#xff09;举例分析 解析(从String到Date)举例分析 设置方法&#xff1a;注意 代码示例代码结果 综合案例效果 先进行专栏介绍 本专栏是自己学J…

新能源汽车交流充电桩控制主板的功能维度

新能源汽车交流充电桩控制主板的功能维度 交流充电桩主板是电动汽车充电站的关键组件&#xff0c;它负责控制充电过程&#xff0c;保护设备和电网免受电动汽车充电的冲击。它具有控制、保护、检测、报警和记录等功能&#xff0c;可以有效地控制充电过程&#xff0c;保证交流充电…

tomcat虚拟主机配置演示

一.新建用于显示的index.jsp文件&#xff0c;写入内容 二.修改tomcat/apache-tomcat-8.5.70/conf/server.xml配置文件 匹配到Host那部分&#xff0c;按上面格式在后面添加自己的域名和文件目录信息 主要是修改name和docBase 保存退出重启tomcat&#xff0c;确保tomcat运行…

开源数据库Mysql_DBA运维实战 (部署服务篇)

前言❀ 1.数据库能做什么 2.数据库的由来 数据库的系统结构❀ 1.数据库系统DBS 2.SQL语言(结构化查询语言) 3.数据访问技术 部署Mysql❀ 1.通过rpm安装部署Mysql 2.通过源码包安装部署Mysql 前言❀ 1.数据库能做什么 a.不论是淘宝&#xff0c;吃鸡&#xff0c;爱奇艺…

以肠道微生物群为新视角的研究和治疗癫痫

谷禾健康 在漫长的历史中&#xff0c;一种神秘而令人不安的疾病一直困扰着人类&#xff0c;那就是癫痫。 癫痫是最常见的神经系统疾病之一&#xff0c;影响着全世界近7000万人。它会导致突发性的、不可控制的、反复发作的痉挛和意识丧失。 突如其来的发病行为&#xff0c;不仅让…

uni——不规则tab切换(skew)

案例展示 案例代码 <!-- 切换栏 --> <view class"tabBoxs"><view class"tabBox"><block v-for"(item,index) in tabList" :key"index"><view class"tabItem":class"current item.id&…

什么是Milvus

原文出处&#xff1a;https://www.yii666.com/blog/393941.html 什么是Milvus Milvus 是一款云原生向量数据库&#xff0c;它具备高可用、高性能、易拓展的特点&#xff0c;用于海量向量数据的实时召回。 Milvus 基于 FAISS、Annoy、HNSW 等向量搜索库构建&#xff0c;核心是…

开发工具Eclipse的使用

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于Eclipse使用的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.Eclipse是什么 二.使用Eclipse的…

NB-IOT 和蜂窝通信(2/3/4/5G)的区别和特点是什么?

NB-IOT 和蜂窝通信(2/3/4/5G)的区别和特点是什么? 参考链接:https://www.sohu.com/a/221664826_472880 NB IOT是窄带物联网技术,主要解决的是低速率数据传输,可使用GSM900或DCS1800频段,在频段使用上比较灵活,可以和GSM,UMTS或LTE共存,具备优异的MCL(最小耦合损耗…