当前位置: 首页 > news >正文

并发编程实战--对象的共享

在之前章节我们知道对于并发程序可以确保原子性的方式来进行同步操作。但是同步是否成功还有另一个更为重要的方面:内存可见性。

我们不仅希望防止某个线程使用对象状态而另一个线程在同时修改状态,而且希望确保当一个线程修改了对象的状态其他线程能够看到发生的状态变化。如果没有同步,那么这种情况就无法实现

可见性

可见性是一个重要属性,对于java内存模型中当不同的线程进行对象的修改时候其他线程并不会立马看见修改后的结果,这是由于java的内存模型机制,每个线程修改后是将值存放到线程的私有内存而不是立马存放到公共内存中,因此对于其他线程来说是不可见的。所以当其他线程读取变量的时候很大可能读取到旧值也就是失效值。因此必须采用同步机制来保证读取到的值是最新值

不仅如此,还可能会出现“重排序”的现象。只要在某个线程中出现重排序的情况,那么就无法保证代码的执行是按照顺序进行执行的

在没有同步的情况下,编译器,处理器等都可能对操作的执行顺序金意想不到的调整。在缺乏足够同步的多线程程序中,要相对内存操作的执行顺序进行判断,几乎无法得到正确的结论

有一种简单的方法就能够避免这一系列复杂的问题--采用同步机制

更加深入的理解同步

在之前的文章中我们说到保证原子性的操作就是使用原子类,synchronized关键字等以锁的方式来保证同一时刻只能有一个线程执行整个操作过程。其实这些锁不仅仅能保证原子性而且还能保证可见性,对于内置锁可以确保某个线程以一种可预测的方式来查看另一个线程的执行结果。当线程A执行某个同步代码块时,线程B随后进入由同一个锁保护的同步代码块,在这种情况可以保证在锁被释放之前A看到的变量值在B获得锁后同样可以由B看到,也就是保证了可见性,如果没有加内置锁则无法保证上述实现

所以,这就是为什么访问某个共享可变变量的时候要求所有的线程都使用同一个锁上同步,就是为了保证某个线程写入变量之后对于后续线程是可见的,否则,如果一个线程在未持有正确锁的情况下读取某个变量,那么读到的可能就是一个失效值。

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

volatile:

volatile变量是一个轻量级同步关键字,对于volatile修饰的变量线程对其进行的任何操作都能达到可见性,操作后的数据并没有放到寄存器中而是直接赋值给了共享变量因此达到了可见性的效果,同时对于被volatile修饰的变量来说可以禁止指令重排序

volatile相比于synchronized来说更加轻量级对于变量的修改所造成的性能损耗更少,但是volatile修饰的变量一般只用作校验场景,也就是操作复杂度低的场景。原因是volatile无法保证原子性,也就是说无法保证同一时间段只有一个线程去处理数据

所以当且仅当满足以下所有条件的时候使用volatile变量:

1.对变量的写入操作不依赖当前变量的值,或者能本证只有一个线程去更新变量值

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

3.在访问变量时不需要加锁

逸出:

对于一些对象在不该发布(使得一个对象可以在作用域之外的代码中使用)的时候发布出去就是逸出

一个简单的例子,下属代码:

class Test{private String[] states=new String[]{ "xx","xx"...}public String[] getStates(){return states;}}

上述变量states就属于不安全发布可以理解为逸出。任何调用者都能够修改数组内的值,无论其他线程是否会对已发布的引用执行任何操作都不重要了,因为误用该引用的风险是始终存在的也就是不安全发布。还有一种不安全的发布方式--构造函数发布,对于一些对象来说

class Test{private Integer num;public  Test(){
num=1;
new advance(this);
}}

上述方式就是在构造函数中直接通过this发布本身对象,这样会造成不安全发布因为指令重排序会调换两个代码的位置,即使是写在最后一行的也可能会导致发不出半成品对象

不要在构造过程中使用this引用逸出

所以对于那些不安全的发布或者程序员无意发布的对象称它为逸出,一旦逸出之后就需要使用同步策略来保证线程安全

可能有些人会觉得采用同步方式的代价比较高,有没有一些代价较低的方式依然能达到线程安全的效果呢。答案是有的,下面介绍一些不是采用同步的方式来达到线程安全的方法。

线程封闭:

出现线程安全问题的原因就是多线程中同一时间段会有多个线程访问同样的资源,那么如果仅在单线程内访问数据就不需要同步。这种技术被称为线程封闭,他是实现线程安全的最简单方式之一。

线程封闭技术常用的地方就在JDBC中,JDBC的Connection对象并不是线程安全的对象,但是最终却能正确的使用,原因在于Connection本身在连接池中存放(连接池是同步处理过的),每个线程来到的时候取出一个Connection进行操作处理,处理完毕后归还给连接池。在这整个过程当中实际上都是只有一个线程去操作Connection的这就实现了线程封闭从而无需同步就能完成线程安全

还有一个线程封闭的例子则是ThreadLocal类,这个类能使每个线程与某个值关联起来。ThreadLocal为每个线程都存放着互相独立的副本,因此采用get方法或者set方法操作的时候各个值都是互相隔离的。原理是ThreadLocal本身维护着一个map,key是thread,value则对应一个threadlocal的map。这样每个线程就实现了隔离机制

总的来说线程封闭技术无法通过一些现有的实现类来实现,更多的则是程序员自己的代码设计结合不同的场景来完成线程封闭的技术。

栈封闭:

除了线程封闭之外,还有一种栈封闭技术也能实现线程安全。在栈封闭中只能通过局部变量才能访问对象,这样实现线程安全。局部变量固有的属性就是在封闭的执行线程中,也就是说每个线程都存储着自己的局部变量,因此可以实现线程安全。

不可变对象:

满足线程安全的另一种方式则是不可变对象。不可变对象天然就是线程安全的对象。不可变对象不仅能避免原子性问题,而且如果被final修饰的对象具有可见性的。

当满足以下条件时,对象才是不可变的:

  1. 类声明为 final:防止被继承,避免子类修改其行为。
  2. 所有字段声明为 private 和 final:确保字段不可直接访问,且初始化后不可重新赋值。
  3. 不提供修改字段的方法:不定义 setter 方法,或其他修改内部状态的方法。
  4. 构造器初始化所有字段:确保对象创建时所有状态已完全初始化。
  5. 处理可变对象引用:如果包含对可变对象的引用,确保客户端无法获取或修改该引用。

因此保证线程安全的方法到目前为止有同步,线程封闭,栈封闭,以及不可变对象

相关文章:

  • 基于机器学习的策略开发和Backtrader回测
  • JAVA SE — 循环与分支和输入输出
  • VS Code + Maven 创建项目
  • JDK8中的 Stream流式编程用法优化(工具类在文章最后)
  • 【记录】PPT|PPT打开开发工具并支持Quicker VBA运行
  • C++初阶-list的使用1
  • Ubuntu 通过指令远程命令行配置WiFi连接
  • GuzzleHttp和DomCrawler的具体用途?
  • 【自用-python】生成准心居中exe程序,防止云电脑操作时候鼠标偏移
  • 谷歌开源医疗领域多模态生成式AI模型:medgemma-4b-it
  • 关于常见日志的几种级别和格式
  • mapbox V3 新特性,实现三维等高线炫酷效果
  • 工业物联网中隐私保护入侵检测的联邦学习增强型区块链框架
  • MyBatis-Plus的自带分页方法生成的SQL失败:The error occurred while setting parameters
  • 522UART是什么
  • 【项目】抽奖系统bug历程(持续更新)
  • Git分支的强制回滚
  • Python Click库:轻松构建优雅的命令行工具
  • 技术篇-2.1.C\C++应用场景及开发工具安装
  • Java使用Collections集合工具类
  • 南宁有什么做网站的好公司/泉州百度开户
  • 网站开发后台 amp/网络营销学什么
  • 外贸网站建设价格/人工智能培训机构排名
  • 电子商务系统 网站建设/湘潭网站建设
  • 学校网站下载/老域名
  • 网站首页html代码/网络推广企划