对象的共享

作者:数字化发布时间:2024-07-24 15:53浏览量:10556
logo
织信企业级低代码开发平台
提供表单、流程、仪表盘、API等功能,非IT用户可通过设计表单来收集数据,设计流程来进行业务协作,使用仪表盘来进行数据分析与展示,IT用户可通过API集成第三方系统平台数据。
免费试用

本文介绍如何共享和发布对象,使它们能够安全地由多个线程同时访问。

两篇博文合起来就形成了构建线程安全类以及通过juc类库构建并发应用程序的重要基础。

1 可见性

通常,我们无法保证执行读操作的线程能看到其他线程写入的值,因为每个线程都由自己的缓存机制。为确保多个线程之间对内存写入操作的可见性,必须使用同步机制。

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; } }

看起来会输出42,但事实上很可能根本无法终止,因为读线程可能永远看不到ready的值;更奇怪的是可能输出0,因为读线程看到了写入ready的值,却没有看到之后写入number的值,这种现象称为“重排序”(Reordering)。

在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。

有种简单方法避免这些复杂的问题:只要有数据在多个线程之间共享,就该使用正确的同步。

1.1 失效数据

除非在每次访问变量时使用同步,否则很可能获得变量的一个失效值。失效值可能不会同时出现:一个线程可能获得一个变量的最新值,而获得另一个变量的失效值。

失效数据还可能导致一些令人困惑的故障,如:意料之外的异常、被破坏的数据结构、不精确的计算、无限循环等。

//非线程安全的可变整数类 @NotThreadSafe public class MutableInteger { private int value; public int get() { return value; } public void set(int value) { this.value = value; } }

此类非线程安全,因为get和set方法都是在没有同步的情况下访问value的失效值很容易出现:若某线程调用set,则另一个正在调用get的线程可能看到更新后的value值,也可能看不到。

//线程安全的可变整数类 @ThreadSafe public class SynchronizedInteger { @GuardedBy("this") private int value; public synchronized int get() { return value; } public synchronized void set(int value) { this.value = value; } }

通过对set,get进行同步,可使此类成为一个线程安全的类。仅对set同步不够,调用get的线程仍可能看见失效值。

1.2 非原子的64位操作

对于非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分解为两个32位的操作。所以,当读取该类变量的操作在不同的线程时,很可能会读取到某个值的高32位和另一个值的低32位,造成读取到是一个随机值。除非用关键字volatile来声明它们,或者用锁保护起来。

1.3 加锁和可见性

当某线程执行由锁保护的同步代码块时,可以看到其他线程之前在同一同步代码块中的所有操作结果。如果没有同步,将无法实现上述保证。

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

1.4 volatile变量

用于确保将变量的更新操作通知到其他线程,访问volatile变量时不会执行加锁操作,也就不会使执行线程阻塞,是一种比sychronized更轻量级的同步机制.

编译器与运行时都会注意到此变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序.

volatile变量不会被缓存在寄存器或其他处理器不可见的地方,因此在读取volatile变量时总会返回最新写入的值.

从内存可见性来看:写入volatile变量相当于退出同步代码块,读取则相当于进入同步代码块(并不建议过度依赖此特性,通常比使用锁的代码还复杂)

仅当能简化代码的实现及对同步策略的验证时,才该用.若在验证正确性时需要复杂判断可见性,就不要使用!正确使用方式包括:

确保它们自身状态的可见性

确保它们所引用对象的状态的可见性

标识一些重要的程序周期事件的发生(如初始化或关闭)

// 数绵羊 volatile boolean asleep; ... while(!asleep){ countSomeSheep(); }

代码分析

一种典型用法:检查某个状态标记判断是否退出循环.示例中,线程试图通过数绵羊方法进入休眠状态.为了使此示例能正确执行,asleep必须为volatile型.否则,当asleep被另一个线程修改时,执行判断的线程却发现不了.亦可使用加锁保证,但代码会很复杂.

虽然方便,但也存在局限性.常用做某个操作完成,发生中断或状态的标志,如上例的asleep标志…但语义不足以确保递增操作的原子性,除非确保只有一个线程对变量执行写操作(后文的原子变量常做一种"更好的volatile变量").

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

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

对变量的写入操作不依赖变量的当前值,或能确保只有单个线程更新变量的值

该变量不会与其他状态变量一起纳入不变性条件中

在访问变量时不需要加锁

2 发布与逸出

发布:使对象能够在当前作用域之外的代码中使用.

发布方式:

将一个指向该对象的引用保存到其他代码可以访问的地方(最简单的就是保存到公有的静态变量)

非私有方法中返回该引用

将引用传递到其他类的方法中

当某个不应该发布的对象被发布时,就被称为逸出.

//使内部的可变状态逸出(不要这样做!!!) class UnsafeStates { private String[] states = new String[]{ "AK", "AL" /*...*/ }; public String[] getStates() { return states; } }

代码分析

如此发布states有问题,因为任何调用者都能修改这个数组的内容.states已经逸出了它所在的作用域,因为这个本应是private的变量已经被发布了.

//this引用隐式地在构造函数中逸出 public class ThisEscape { public ThisEscape(EventSource source) { source.registerListener(new EventListener() { public void onEvent(Event e) { doSomething(e); } }); } }

代码分析

当ThisEscape发布EventListener时,也隐含发布了ThisEscape实例本身,因为内部类的实例包含了对外部类实例的隐含引用.

构造过程中,另一个常见错误是,在构造器启动一个线程.此时,无论是显式创建(传给构造器)或隐式(内部类),this引用都会被创建的线程共享.在对象尚未完全构造之前,新的线程就可以看见它.在构造器创建线程并无错误,但最好不要立即启动,而是通过start或initialize方法启动.在构造器调用一个可改写的实例方法时,也会导致this引用逸出.

想在构造器注册一个监听器或启动线程,可使用一个私有的构造器和一个公共的工厂方法.如下示例:

public class SafeListener { private final EventListener listener; private SafeListener() { listener = e -> doSomething(e); } public static SafeListener newInstance(EventSource source) { SafeListener safe = new SafeListener(); source.registerListener(safe.listener); return safe; } }

3 线程封闭

一种避免使用同步的方式就是不共享数据.

如果仅在单线程内访问数据,就不需要同步,这就被称为线程封闭.线程封闭是程序设计中的考虑因素,必须在程序中实现.Java也提供了一些机制帮助维护线程封闭性,比如局部变量和ThreadLocal类.

3.1 Ad-hoc线程封闭

维护线程封闭性的职责完全由程序实现来承担.

使用volatile变量是实现Ad-hoc线程封闭的一种方式,只要能保证只有单个线程对共享的volatile变量执行写操作,就可以安全地在这些变量上进行“读-改-写”操作,volatile变量的可见性又保证了其他线程能够看到最新的值。

Ad-hoc线程封闭是非常脆弱的,没有语言特性可使对象直接封闭到目标线程.因此在程序中尽量少使用.

在可能的情况下,使用其他更强的线程封闭技术.

##3.2 栈封闭

在栈封闭中,只能通过局部变量才能访问对象.

局部变量的固有属性之一就是封闭在执行线程中

它们位于执行线程的栈中,其他线程无法访问此栈.

即使使用了非线程安全的对象,该对象仍然是线程安全的.

3.3 ThreadLocal类

使用ThreadLocal是一种更规范的线程封闭方式,它能使线程中的某个值与保存值的对象关联起来。提供了get与set等访问接口方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值.

常用于防止对可变的单实例变量或全局变量进行共享.

如下示例,通过将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接:

//使用TheadLocal来维持线程封闭性 public class ConnectionDispenser { static String DB_URL = "jdbc:mysql://localhost/mydatabase"; private ThreadLocal connectionHolder = new ThreadLocal() { public Connection initialValue() { try { return DriverManager.getConnection(DB_URL); } catch (SQLException e) { throw new RuntimeException("Unable to acquire Connection, e"); } }; }; public Connection getConnection() { return connectionHolder.get(); } }

当某个频繁执行的操作需要一个临时对象,如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象,就可以使用该技术.

当某个线程初次调用get方法时,就会调用initialValue来获取初始值.可将ThreadLocal 看作包含了Map对象,保存了特定于该线程的值,但ThreadLocal的实现并非如此.这些特定于线程的值存在Thread对象中,当线程终止后,这些值会作为垃圾被回收.

ThreadLocal 变量类似于全局变量,它能降低代码的可重用性,并在类之间引入隐含的耦合性,使用时需要格外小心.

4 不变性

不可变对象:

满足以下条件:

对象创建以后其状态就不能修改

对象的所有域都是final类型(final类型域是不能被修改的)

对象是正确创建的(在对象的创建期间,this引用没有逸出)

在被创建后其状态就不能被修改,且必线程安全.

在JMM中,final域能确保初始化过程的安全性,从而可以无限制地访问不可变对象,并在共享这些对象时无须同步.

5 安全发布

任何线程都可在无额外同步情况下安全访问不可变对象,即使在发布时没有使用同步.

然而,若final域所指向为可变对象,访问这些可变对象的状态时仍需同步.

安全发布常用模式

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

为安全发布,对象的引用以及对象的状态必须同时对其他线程可见.

一个正确构造的对象可以通过以下方式来安全发布

在静态初始化函数里初始化一个对象引用

将对象的引用保存到volatile类型的域或者AtomicReference对象中

将对象的引用保存到某个正确构造对象的final类型域中

将对象的引用保存到一个由锁保护的域中

线程安全库中的容器类提供了以下的安全发布保证:

通过将一个键或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程

通过将某个对象放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或者synchronizedSet中,可以将该对象安全地发布到任何从这些容器中访问该对象的线程

通过将某个对象放入BlockingQueue或者ConcurrentLinkedQueue中,可以将该对象安全地发布到任何从这些队列中访问该对象的线程

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

public static Holder holder = new Holder(42);

由JVM在类的初始化阶段执行,且由于JVM内部存在着同步机制,因此这样初始化的任何对象都能被安全发布.

事实不可变对象:对象从技术上来看是可变的,但其状态在发布后不会再改变.

在没有额外的同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象.

对于可变对象,不仅在发布对象时需要同步,而且在每次对象访问时同样需要使用同步来确保后续修改操作的可见性.

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

不可变对象可以通过任意机制来发布。

事实不可变对象必须通过安全方式来发布。

可变对象必须通过安全方式来发布,而且必须是线程安全的或者用某个锁保护起来。

安全的共享对象

实用策略:

线程封闭 线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改

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

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

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

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们微信:Informat_5 处理,核实后本网站将在24小时内删除侵权内容。

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系邮箱:hopper@cornerstone365.cn 处理,核实后本网站将在24小时内删除。

最近更新

2026年低代码开发平台怎么选?5家主流厂商全方位对比
07-27 18:02
低代码平台如何选?需求梳理/功能适配/场景验证/安全合规/性能支持,少一条都不行
06-05 15:01
传统开发 vs 低代码:大型企业数字化建设成本对比分析
06-05 14:58
2026年5月分享:AI低代码是什么?企业如何用AI低代码构建核心业务系统?
05-29 09:52
微软按下vibe coding暂停键:AI写代码的狂欢,该醒醒了
05-27 16:44
企业数字化转型进入深水区:一位CIO亲述选型低代码平台的血泪史
05-25 16:44
探路中台、RPA、低代码引领企业级IT服务未来式
05-22 09:43
低代码AI实战指南:从"拖拽搭应用"到"对话即开发"的底层逻辑到底是什么?
05-21 15:00
2026企业级低代码平台TOP10实测:附选型评分表
05-20 14:12
为什么选择织信?
织信AI低代码开发底座,赋能企业快速构建复杂业务系统,驱动业务与IT高效创新
AI驱动开发
通过自然语言交互完成数据建模与逻辑编排,非技术人员也能快速上手,开发周期从数月压缩至数周。
高性能数据支持
提供上亿级数据承载能力与分布式集群部署,支持海量业务数据的高并发处理。
企业级场景覆盖
支持ERP、MES、CRM、SRM、WMS等核心系统搭建,无缝集成钉钉、企微、飞书及各类异构系统。
专业服务保障
支持私有化部署模式,全面保障数据安全。已累计服务制造、军工、金融等50000+企业客户。
B2C跨境电商知名品牌——朗驰实业
集设计、生产、销售于一体的综合性服装企业,专注女性快时尚B2C跨境电商,目前设有供应链中心、仓储中心、亚马逊运营中心、信息化中心、产品研发中心等20余个部门,引入织信低代码平台个性化定制一套研发、生产、销售全链路的数字化系统,打通服装从设计、生产到销售的各个环节。
全球500强车企巨头——吉利集团
作为一家全球知名的超大型企业,吉利需要大量的技术人员来满足各事业部门的日常数字化需求。在内部强调“降本增效”的大环境下,吉利通过采购“织信低代码平台”,开发周期平均缩短61%,人力投入减少47%,解决了开发需求常年堆积的难题。
医院后勤服务领军者——某管家
国内市场化运作、跨区域经营、集团化管理的大型专业医疗机构后勤服务供应商,全国80多座城市,每天为超过百万的病人和医护人员提供服务,通过织信低代码平台构建线上数字化的方式服务各医院的后勤保障和正常运行,主要为运送条线、保洁条线、秩序条线、工程条线、医废条线等解决工单调度、医辅材料运输、多端协同的效率难题。
中国兵器工业集团——银光化学
国家“一五”期间156个重点项目之一。属于国家高新技术企业,在信息化升级建设中,存在大量“小、散、碎”的信息化需求,需要投入大量人力资源进行开发,通过引入织信低代码平台,解决当下遇到的各类业务难题,提升整体的IT研发效率。
石油领域重点工程单位——川庆钻探
随着国企工规模的不断扩大和内部数字化转型的要求不断提升,公司着眼长远,决定借助织信低代码的各方面能力,从物资储备管理入手,并辐射经营、生产、工程、日常管理等多个板块,为后续内部信息化建设打好基座。
汽车零部件上市企业——川环科技
川环为了有效应对残酷的市场现实,高层一致决定加强公司内部管理,8大部门将全面进行数字化转型,耗时10月,成功上线8套系统,通过织信低代码平台对接现有用友U9ERP,实现各部门的业务线上化,并通过数据治理,实现整个企业从战略到经营管理的分析。
B2C跨境电商知名品牌——朗驰实业
集设计、生产、销售于一体的综合性服装企业,专注女性快时尚B2C跨境电商,目前设有供应链中心、仓储中心、亚马逊运营中心、信息化中心、产品研发中心等20余个部门,引入织信低代码平台个性化定制一套研发、生产、销售全链路的数字化系统,打通服装从设计、生产到销售的各个环节。
全球500强车企巨头——吉利集团
作为一家全球知名的超大型企业,吉利需要大量的技术人员来满足各事业部门的日常数字化需求。在内部强调“降本增效”的大环境下,吉利通过采购“织信低代码平台”,开发周期平均缩短61%,人力投入减少47%,解决了开发需求常年堆积的难题。

各行业用户的共同选择

国防军工
国防军工
央国企
央国企
生产制造
生产制造
生物医疗
生物医疗
科技服务
科技服务
金融证券
金融证券
科研院所
科研院所
物业地产
物业地产
织信适合谁?
如您有以下几种需求,欢迎 填写表单 联系我们
企业员工
《找工具开发功能》
公司老板
《找人定制系统》
软件集成商
《想快速交付项目》
  • 深圳市基石协作科技有限公司
  • 地址:深圳市南山区科发路8号金融基地1栋5F5
  • 手机:137-1379-6908
  • 电话:0755-86660062
  • 邮箱:sales@cornerstone365.cn
  • 微信公众号二维码

© copyright 2019-2026. 织信INFORMAT 深圳市基石协作科技有限公司 版权所有 | 粤ICP备15078182号

前往Gitee仓库
微信公众号二维码
咨询织信数字化顾问获取最新资料
客服咨询热线1
0755-86660062
客服咨询热线2
137-1379-6908
申请预约演示
立即与行业专家交流