java安全编码指南之:可见性和原子性

简介

java类中会定义很多变量,有类变量也有实例变量,这些变量在访问的过程中,会遇到一些可见性和原子性的问题。这里我们来详细了解一下怎么避免这些问题。

不可变对象的可见性

不可变对象就是初始化之后不能够被修改的对象,那么是不是类中引入了不可变对象,所有对不可变对象的修改都立马对所有线程可见呢?

实际上,不可变对象只能保证在多线程环境中,对象使用的安全性,并不能够保证对象的可见性。

先来讨论一下可变性,我们考虑下面的一个例子:

public final class ImmutableObject {private final int age;public ImmutableObject(int age){this.age=age;}
}

我们定义了一个ImmutableObject对象,class是final的,并且里面的唯一字段也是final的。所以这个ImmutableObject初始化之后就不能够改变。

然后我们定义一个类来get和set这个ImmutableObject:

public class ObjectWithNothing {private ImmutableObject refObject;public ImmutableObject getImmutableObject(){return refObject;}public void setImmutableObject(int age){this.refObject=new ImmutableObject(age);}
}

上面的例子中,我们定义了一个对不可变对象的引用refObject,然后定义了get和set方法。

注意,虽然ImmutableObject这个类本身是不可变的,但是我们对该对象的引用refObject是可变的。这就意味着我们可以调用多次setImmutableObject方法。

再来讨论一下可见性。

上面的例子中,在多线程环境中,是不是每次setImmutableObject都会导致getImmutableObject返回一个新的值呢?

答案是否定的。

当把源码编译之后,在编译器中生成的指令的顺序跟源码的顺序并不是完全一致的。处理器可能采用乱序或者并行的方式来执行指令(在JVM中只要程序的最终执行结果和在严格串行环境中执行结果一致,这种重排序是允许的)。并且处理器还有本地缓存,当将结果存储在本地缓存中,其他线程是无法看到结果的。除此之外缓存提交到主内存的顺序也肯能会变化。

怎么解决呢?

最简单的解决可见性的办法就是加上volatile关键字,volatile关键字可以使用java内存模型的happens-before规则,从而保证volatile的变量修改对所有线程可见。

public class ObjectWithVolatile {private volatile ImmutableObject refObject;public ImmutableObject getImmutableObject(){return refObject;}public void setImmutableObject(int age){this.refObject=new ImmutableObject(age);}
}

另外,使用锁机制,也可以达到同样的效果:

public class ObjectWithSync {private  ImmutableObject refObject;public synchronized ImmutableObject getImmutableObject(){return refObject;}public synchronized void setImmutableObject(int age){this.refObject=new ImmutableObject(age);}
}

最后,我们还可以使用原子类来达到同样的效果:

public class ObjectWithAtomic {private final AtomicReference<ImmutableObject> refObject= new AtomicReference<>();public ImmutableObject getImmutableObject(){return refObject.get();}public void setImmutableObject(int age){refObject.set(new ImmutableObject(age));}
}

保证共享变量的复合操作的原子性

如果是共享对象,那么我们就需要考虑在多线程环境中的原子性。如果是对共享变量的复合操作,比如:++, -- *=, /=, %=, +=, -=, <<=, >>=, >>>=, ^= 等,看起来是一个语句,但实际上是多个语句的集合。

我们需要考虑多线程下面的安全性。

考虑下面的例子:

public class CompoundOper1 {private int i=0;public int increase(){i++;return i;}
}

例子中我们对int i进行累加操作。但是++实际上是由三个操作组成的:

  1. 从内存中读取i的值,并写入CPU寄存器中。
  2. CPU寄存器中将i值+1
  3. 将值写回内存中的i中。

如果在单线程环境中,是没有问题的,但是在多线程环境中,因为不是原子操作,就可能会发生问题。

解决办法有很多种,第一种就是使用synchronized关键字

    public synchronized int increaseSync(){i++;return i;}

第二种就是使用lock:

    private final ReentrantLock reentrantLock=new ReentrantLock();public int increaseWithLock(){try{reentrantLock.lock();i++;return i;}finally {reentrantLock.unlock();}}

第三种就是使用Atomic原子类:

    private AtomicInteger atomicInteger=new AtomicInteger(0);public int increaseWithAtomic(){return atomicInteger.incrementAndGet();}

保证多个Atomic原子类操作的原子性

如果一个方法使用了多个原子类的操作,虽然单个原子操作是原子性的,但是组合起来就不一定了。

我们看一个例子:

public class CompoundAtomic {private AtomicInteger atomicInteger1=new AtomicInteger(0);private AtomicInteger atomicInteger2=new AtomicInteger(0);public void update(){atomicInteger1.set(20);atomicInteger2.set(10);}public int get() {return atomicInteger1.get()+atomicInteger2.get();}
}

上面的例子中,我们定义了两个AtomicInteger,并且分别在update和get操作中对两个AtomicInteger进行操作。

虽然AtomicInteger是原子性的,但是两个不同的AtomicInteger合并起来就不是了。在多线程操作的过程中可能会遇到问题。

同样的,我们可以使用同步机制或者锁来保证数据的一致性。

保证方法调用链的原子性

如果我们要创建一个对象的实例,而这个对象的实例是通过链式调用来创建的。那么我们需要保证链式调用的原子性。

考虑下面的一个例子:

public class ChainedMethod {private int age=0;private String name="";private String adress="";public ChainedMethod setAdress(String adress) {this.adress = adress;return this;}public ChainedMethod setAge(int age) {this.age = age;return this;}public ChainedMethod setName(String name) {this.name = name;return this;}
}

很简单的一个对象,我们定义了三个属性,每次set都会返回对this的引用。

我们看下在多线程环境下面怎么调用:

        ChainedMethod chainedMethod= new ChainedMethod();Thread t1 = new Thread(() -> chainedMethod.setAge(1).setAdress("www.flydean.com1").setName("name1"));t1.start();Thread t2 = new Thread(() -> chainedMethod.setAge(2).setAdress("www.flydean.com2").setName("name2"));t2.start();

因为在多线程环境下,上面的set方法可能会出现混乱的情况。

怎么解决呢?我们可以先创建一个本地的副本,这个副本因为是本地访问的,所以是线程安全的,最后将副本拷贝给新创建的实例对象。

主要的代码是下面样子的:

public class ChainedMethodWithBuilder {private int age=0;private String name="";private String adress="";public ChainedMethodWithBuilder(Builder builder){this.adress=builder.adress;this.age=builder.age;this.name=builder.name;}public static class Builder{private int age=0;private String name="";private String adress="";public static Builder newInstance(){return new Builder();}private Builder() {}public Builder setName(String name) {this.name = name;return this;}public Builder setAge(int age) {this.age = age;return this;}public Builder setAdress(String adress) {this.adress = adress;return this;}public ChainedMethodWithBuilder build(){return new ChainedMethodWithBuilder(this);}}

我们看下怎么调用:

      final ChainedMethodWithBuilder[] builder = new ChainedMethodWithBuilder[1];Thread t1 = new Thread(() -> {builder[0] =ChainedMethodWithBuilder.Builder.newInstance().setAge(1).setAdress("www.flydean.com1").setName("name1").build();});t1.start();Thread t2 = new Thread(() ->{builder[0] =ChainedMethodWithBuilder.Builder.newInstance().setAge(1).setAdress("www.flydean.com1").setName("name1").build();});t2.start();

因为lambda表达式中使用的变量必须是final或者final等效的,所以我们需要构建一个final的数组。

读写64bits的值

在java中,64bits的long和double是被当成两个32bits来对待的。

所以一个64bits的操作被分成了两个32bits的操作。从而导致了原子性问题。

考虑下面的代码:

public class LongUsage {private long i =0;public void setLong(long i){this.i=i;}public void printLong(){System.out.println("i="+i);}
}

因为long的读写是分成两部分进行的,如果在多线程的环境中多次调用setLong和printLong的方法,就有可能会出现问题。

解决办法本简单,将long或者double变量定义为volatile即可。

private volatile long i = 0;

 

原文链接
本文为阿里云原创内容,未经允许不得转载。

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

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

相关文章

Gartner:云安全的未来——中国的安全访问服务边缘架构

作者 | Gartner高级研究总监 Evan Zeng 编辑 | 宋 慧 头图 | 付费下载于东方IC Gartner最新的“安全领域新兴技术及趋势影响雷达”&#xff08;Emerging Technologies and Trends Impact Radar: Security&#xff09;显示&#xff0c;安全服务及接入边缘技术具有极高重要性&am…

springboot word excel ppt 图片aspose 转换PDF 在线预览

文章目录1. 引入依赖2. 注册工具类3. 文件转换工具类4. 文件预览工具类5. 文件处理实现类6. 文件处理入口7. 配置类8. 配置文件9. 依赖目录文件列表10. 图片转换工具类11. 测试连接12. 效果图12. 执行shell命令工具类补充1. 引入依赖 下载 Aspose 的依赖 Jar 包可以通过一下仓…

架构制图:工具与方法论

简介&#xff1a; 软件工程也是工程&#xff0c;因此传统工程制图的一些基本理论&#xff0c;在软件行业同样适用。但另一方面&#xff0c;软件与实体制造业之间还是有着本质区别&#xff0c;所以在制图方面的需求和方式也大相径庭&#xff0c;无法直接套用。作为软件行业的从业…

Gartner:云安全的未来,是安全访问服务边缘架构

作者 | Gartner高级研究总监 Evan Zeng编辑 | 宋 慧头图 | 付费下载于东方ICGartner最新的“安全领域新兴技术及趋势影响雷达”&#xff08;Emerging Technologies and Trends Impact Radar: Security&#xff09;显示&#xff0c;安全服务及接入边缘技术具有极高重要性&#x…

奥哲孟凡俊:低代码平台对当代企业智能管理的支撑

简介&#xff1a; 导读&#xff1a;在数字化时代&#xff0c;业务拓展快&#xff0c;迭变快将成为常态和主流&#xff0c;企业数字化转型除了企业内部协同提效之外&#xff0c;基于多项底层技术框架的低代码平台更是智能协作不可或缺的底层支持&#xff0c;奥哲高级副总裁兼奥哲…

yarn 不是内部或外部命令,也不是可运行的程序(亲测可用)

这个时候报 yarn 不是内部或外部命令 相信你的npm已经安装好了 方法一&#xff08;网上大多数是这个&#xff09;&#xff1a; npm install -g yarn方法二&#xff08;我的是这个&#xff09;&#xff1a;配置环境变量&#xff08;你的yarn地址直接复制上去就好了&#xff09…

基于 Flink + Hive 构建流批一体准实时数仓

简介&#xff1a; 想要搭建流式链路就必须得抛弃现有的 Hive 数仓吗&#xff1f;并不是&#xff0c;借助 Flink 可以实现已有的 Hive 离线数仓准实时化。本文整理自 Apache Flink Committer、阿里巴巴技术专家李劲松的分享&#xff0c;文章将分析当前离线数仓实时化的难点&…

腾讯云~kafka伪集群搭建

文章目录一、zookeeper伪集群搭建1. 下载安装包2. 解压安装包3. 创建目录4. 修改配置文件5. 修改dataDir&#xff0c;clientPort两个配置项5. 在data目录下创建myid文件6. 复制多个zookeeper7. 修改内存大小8. 启动zookeper9. 查看zookeeper运行状态二、kafka 伪集群搭建2.1. 下…

脚本征集大赛开启啦!100%有奖!

对一个程序爱好者来说&#xff0c;最酷的事情莫非就是用你喜欢的语言一步步实现你的idea&#xff0c; 现在用 CSDN 浏览器助手插件&#xff0c; 不仅能提升浏览器效率&#xff0c;还能在上面开发黑科技脚本&#xff0c;帮助你实现各种 idea~

蚂蚁御术:我的前端十年成长之路

我是御术&#xff0c;10年北邮毕业参加工作至今&#xff0c;刚好10年。一直觉得自己特别幸运&#xff0c;一路走来遇到了那么多好人好事&#xff0c;有机会一同做了点事情&#xff0c;由衷感激家人朋友们的支持和帮助。 自以为每一个人的发展轨迹都是不可复制的&#xff0c;过…

vue将json字符串转换为数组_json字符串、json对象、数组 三者之间的转换

var Obj JOSN.parse(“cscac”); //将JSON字符串转换成JSON对象 var “cscac” JSON.stringify(Obj) //将JSON对象转换成JSON字符串 将json对象转换成json数组 let obj JSON.parse(ress);const userinfo[];for (let i in obj ){userinfo.push(obj [i])}console.log(userinf…

看动画学算法之:排序-基数排序

简介 之前的文章我们讲了count排序&#xff0c;但是count排序有个限制&#xff0c;因为count数组是有限的&#xff0c;如果数组中的元素范围过大&#xff0c;使用count排序是不现实的&#xff0c;其时间复杂度会膨胀。 而解决大范围的元素排序的办法就是基数排序。 基数排序…

mPaaS-RPC 拦截器各种场景下的使用指南

简介&#xff1a; RPC拦截器机制在preHandle、postHandle、exceptionHandle以及H5等场景中的应用 mPaaS 移动网关服务&#xff08;Mobile Gateway Service&#xff0c;简称 MGS&#xff09;作为 mPaas 最重要的组件之一&#xff0c;连接了移动客户端与服务端&#xff0c;简化了…

npm 代理替换

npm install --registryhttps://registry.npmmirror.com# 启动服务 npm run dev发布 # 构建测试环境 npm run build:stage# 构建生产环境 npm run build:prod

数据之光 · 安全未来 | 第四届中国数据安全治理高峰论坛圆满召开!

摘要&#xff1a;5月13日&#xff0c;2021DSG峰会在京盛大开幕&#xff0c;共推数据安全治理实践落地&#xff0c;共商数据安全发展未来&#xff01; 一、五年四届&#xff0c;峰会再启 2021年5月13日&#xff0c;“第四届中国数据安全治理高峰论坛”在北京香格里拉饭店圆满召…

Sentinel-Go 集成 Nacos 实现外部动态数据源

简介&#xff1a; 2020年&#xff0c;Sentinel 推出 Go 原生版本Sentinel-Golang&#xff0c;在云原生领域继续突破。本文将从实际出发 结合案例说明 在Sentinel-Golang中如何集成Nacos&#xff0c;使其做为外部动态数据源&#xff0c;将流控规则存储在nacos中&#xff0c;并且…

前端如何快速上手 Web 3D 游戏的开发

简介&#xff1a; 本文以「余额宝3D跑酷游戏」为例&#xff0c;介绍了前端如何快速上手 Web 3D 游戏的开发。 作者 | RichLab楺楺 诚空 本文以「余额宝3D跑酷游戏」为例&#xff0c;介绍了前端如何快速上手 Web 3D 游戏的开发。跑酷游戏是余额宝七周年的主玩法&#xff0c;用户…

vue+element ui后台返回数据为数字,前台转化成对应的中文显示在表格中

关于根据后台数字显示相应的文字&#xff0c;文档中没有详细说明&#xff0c;所以写这篇博客记录一下。 这里主要介绍三种方法&#xff0c;可以自己的需要选择哦 先放个效果图吧&#xff08;先说明一下0表示是&#xff0c;1代表否&#xff09; 方法一&#xff1a;调用method…

浪潮云发布全新“1231”业务战略,打造“一朵分布式云”

编辑 | 宋 慧 出品 | CSDN云计算 头图 | 浪潮云业务战略发布会现场图 在政务云必提浪潮云的十年之后&#xff0c;2021年5月14日&#xff0c;浪潮云发布了全新业务战略“1231”&#xff0c;浪潮“分布式云”行动计划首次亮相。 数采、数算、数用一体化 浪潮云“1231”战略正式发…

一文快速入门分库分表(必修课)

之前有不少刚入坑 Java 的粉丝留言&#xff0c;想系统的学习一下分库分表相关技术&#xff0c;可我一直没下定决心搞&#xff0c;眼下赶上公司项目在使用 sharding-jdbc 对现有 MySQL 架构做分库分表的改造&#xff0c;所以借此机会出一系分库分表落地实践的文章&#xff0c;也…