Java进阶知识点:不可变对象与并发
一、String的不可变特性
熟悉Java的朋友都知道,Java中的String有一个很特别的特性,就是你会发现无论你调用String的什么方法,均无法修改this对象的状态。当确实需要修改String的值时,String方法的实现是构造一个新的String返回给你。如下:
public static void main(String[] args) {
String origin = "Test";
String target = origin.replace("T", "t"); //replace不会修改this对象(即origin对象)的任何状态
System.out.println(origin); //输出"Test"
System.out.println(target); //输出"test"
}
这与C++ STL中的string有很大不同,刚从C++转Java的同学可能经常会忘记使用replace函数的返回值,以为调用了replace之后,this对象就已经是替换后的字符串了。
二、不可变对象
2.1 什么是不可变对象
其实不光是String对象,Java中的很多对象都符合上述不可改变状态的特性。简而言之,当一个对象构造完成后,其状态就不再变化,我们称这样的对象为不可变对象(Immutable Object),这些对象关联的类为不可变类(Immutable Class)。
比如Java中的Integer、Double、Long等所有原生类型的包装器类型,也都是不可变的。
那么明明可以直接修改this对象,为何Java中还要大费周章地去构造一个全新的对象返回呢?那这就要从不可变对象的好处说起了。
2.2 不可变对象的优点
2.2.1 对并发友好
提到多线程并发,最让人苦恼的莫过于线程间共享资源的访问冲突,古往今来,多少Bug因此而生。即便是最有经验的程序员,面对多线程编程时,也往往需瞻前顾后,反复思量后,才能逐渐对自己编写的代码产生信心。如果多线程错误可以跟编译错误一样,能够被自动发现该有多好。
目前大多数语言中,面对多线程冲突问题,都是采用序列化访问共享资源的方案。Java也不例外,Java语言中的synchronize关键字,Lock锁对象等机制,都是为实施此类方案准备的。此类方案最大的弊端在于:能不能保证多线程间没有冲突,完全取决于程序员对共享资源加锁解锁的时机对不对。如果程序员加锁的时机有丝毫差错,Java是不负责检测的,可能你的单元测试、集成测试、预发布测试也发现不了,程序上线后也看上去一切正常,但是等到某一个重要的时刻,它会以一个突如其来的线上Bug的形式通知你,是不是欲哭无泪。
然而,解决多线程冲突问题还有一个方向,就是从多线程冲突的根因 —— 共享资源上入手。
如果完全没有共享资源,多线程冲突问题就天然不存在了,比如Java中的ThreadLocal机制就是利用了这一点理念。
但是大多数时候,线程间是需要使用共享资源互通信息的。此时,如果该共享资源诞生之后就完全不再变更(犹如一个常量),多线程间共同并发读取该共享资源是不会产生线程冲突的,因为所有线程无论何时读取该共享资源,总是能获取到一致的、完整的资源状态,这样也能规避多线程冲突。不可变对象就是这样一种诞生之后就完全不再变更的对象,该类对象可以天生支持无忧无虑地在多线程间共享。
如果线程间对共享资源的访问不仅局限于读,还想改变共享资源的状态呢,这种时候不可变对象又能否从容应对呢?答案是肯定的。原理很简单,某个线程想要修改共享资源A的状态时,不要去直接修改A本身的状态,而是先在本线程中构造一个新状态的共享资源B,待B构造完整后,再用B去直接替换A,由于对引用赋值操作是原子性的,所以也不会造成线程冲突问题。不可变对象所提供的方法,不会改变自身的状态,最多构造一个新状态的新对象的返回,这也与上述思路完全契合。但是需要注意可见性问题,如果你想要A替换B后,其他所有线程实时感知到此变化,需要使用volatile关键字保证可见性。
如下:
public class Test {
private volatile String shared = "shared"; //使用volatile关键字保证共享资源的可见性 public void test() {
new Thread(() -> {
String newValue = shared.replace("s", "S"); //在本线程中先构建一个新String
shared = newValue; //用新String替换共享资源,引用的赋值是原子性的
}).start();
}
}
值得注意的是,线程安全需同时考虑原子性和可见性问题,所以网上常说的不可变对象是线程安全的,其实是不严谨的。
所以,不可变对象的好处在于,只要对象符合不可变原则,该对象在线程间传递是不会产生冲突的。这就将以前的到处可能是坑的多线程编程解耦为安全的两步,首先使用不可变对象,然后在线程间传递不可变对象。这能显著减少人脑需要考虑的情况分支,让编程更加轻松和可控。
其实,所有的函数式编程语言Lisp、Haskell、Erlang等,都从语法层面保证你只能使用不可变对象,所以所有函数编程语言是天生对并发友好的,这也是在一些高并发场景中,函数式编程语言更受青睐的原因。
2.2.2 易于在进程内缓存
当一个对象被频繁访问,而生成该对象的开销较大时,经常需要进行进程内缓存,即将频繁访问的对象存入一个缓存集合中(比如Map),当需要使用该对象时,优先从缓存中提取。
使用进程内缓存就不得不面对缓存污染问题,当缓存的对象被提取使用时,如果上层业务代码修改了该缓存对象的状态,那么当再次从缓存中提取该对象时,该对象的状态已经不再是最开始加入缓存时的状态了,即已经被污染了。缓存污染会导致很多问题,比如业务数据被意外篡改、业务数据间的互相干扰等。
通常为了保证缓存不被污染,当我们从缓存中提取对象时,会返回原始缓存对象的一个深拷贝,这样无论上层业务代码对提取到的对象如何修改,均不会对缓存本身造成影响。
但是深拷贝毕竟有额外的性能开销,此时如果缓存的是不可变对象,就皆大欢喜了。因为你可以放心大胆的把缓存对象的引用返回给上层代码使用,因为无论上层代码怎样操作,它也无法修改一个不可变对象的状态,这也就天然规避了缓存污染问题,同时也可将深拷贝带来的性能开销延迟到真正需要修改对象时才发生。
2.2.3 更好的可维护性
当我们在代码中看到一个不可变对象时,心情是轻松的,因为这类对象很单纯,不会在哪个隐藏的逻辑分支中偷偷改变自身的状态,对代码的测试、调试和阅读理解都有好处。
2.3 不可变对象的局限
既然不可变对象这么好用,那它是不是万能的呢,不可变对象有没有什么缺点呢?使用不可变对象主要有如下问题。
2.3.1 编程思维的转变
如果所有对象都被设计为不可变的,等价于使用函数式编程思维,编程思维上的变化并非所有程序员都能很好的适应,如果适应不了,强行推广只会适得其反。况且Java本身也并不是纯粹的函数式编程语言。
2.3.2 性能上的额外开销
由于不可变对象需要复制一份状态用于修改后返回新的的对象,如果设计和使用不当的话,可能因此形成性能瓶颈点。
但是不必过于担心性能问题,一方面内存拷贝速度极快,另外也并非所有额外的性能开销都是不可容忍的,代码性能测试时,你可能会发现很多各式各样的性能瓶颈点,大部分可能都是你意想不到的,所以过早考虑性能而放弃编码安全是不可取的。就好比汇编效率最高,但是也不会因此所有代码都直接汇编编程,遇到真正的性能瓶颈时,有针对性的做汇编层面的调优才是上策。
2.4 建议
在自己能力范围内,尽量优先考虑使用不可变对象的设计。性能问题可以不必过于担心,如果引发了性能瓶颈,再有针对性地做出调整。
三、总结
1、当一个对象构造完成后,其状态就不再变化,这样的对象即为不可变对象。不可变对象的所有方法,均不会改变this对象的状态,最多构造一个新状态的对象返回给你。
2、不可变对象对并发编程友好、易于在进程内缓存、且拥有更好的可维护性,建议在自己能力范围内,尽量优先考虑使用不可变对象的设计。
Java进阶知识点:不可变对象与并发的更多相关文章
- Java进阶知识点4:不可变对象与并发 - 从String说起
一.String的不可变特性 熟悉Java的朋友都知道,Java中的String有一个很特别的特性,就是你会发现无论你调用String的什么方法,均无法修改this对象的状态.当确实需要修改Strin ...
- 深入理解Java中的不可变对象
深入理解Java中的不可变对象 不可变对象想必大部分朋友都不陌生,大家在平时写代码的过程中100%会使用到不可变对象,比如最常见的String对象.包装器对象等,那么到底为何Java语言要这么设计,真 ...
- Java进阶知识点: 枚举值
Java进阶知识点1:白捡的扩展性 - 枚举值也是对象 一.背景 枚举经常被大家用来储存一组有限个数的候选常量.比如下面定义了一组常见数据库类型: public enum DatabaseType ...
- JavaScript进阶知识点——函数和对象详解
JavaScript进阶知识点--函数和对象详解 我们在上期内容中学习了JavaScript的基本知识点,今天让我们更加深入地了解JavaScript JavaScript函数 JavaScript函 ...
- 为什么Java字符串是不可变对象?
转自 http://developer.51cto.com/art/201503/468905.htm 本文主要来介绍一下Java中的不可变对象,以及Java中String类的不可变性,那么为什么Ja ...
- Java进阶知识点:并发容器背后的设计理念
一.背景 容器是Java编程中使用频率很高的组件,但Java默认提供的基本容器(ArrayList,HashMap等)均不是线程安全的.当容器和多线程并发编程相遇时,程序员又该何去何从呢? 通常有两种 ...
- Java进阶知识点6:并发容器背后的设计理念 - 锁分段、写时复制和弱一致性
一.背景 容器是Java编程中使用频率很高的组件,但Java默认提供的基本容器(ArrayList,HashMap等)均不是线程安全的.当容器和多线程并发编程相遇时,程序员又该何去何从呢? 通常有两种 ...
- Java进阶知识点:服务端高并发的基石 - NIO与Reactor AIO与Proactor
一.背景 要提升服务器的并发处理能力,通常有两大方向的思路. 1.系统架构层面.比如负载均衡.多级缓存.单元化部署等等. 2.单节点优化层面.比如修复代码级别的性能Bug.JVM参数调优.IO优化等等 ...
- Java进阶知识点5:服务端高并发的基石 - NIO与Reactor模式以及AIO与Proactor模式
一.背景 要提升服务器的并发处理能力,通常有两大方向的思路. 1.系统架构层面.比如负载均衡.多级缓存.单元化部署等等. 2.单节点优化层面.比如修复代码级别的性能Bug.JVM参数调优.IO优化等等 ...
随机推荐
- 简单几行代码使用百度地图API接口分页获取信息
首发于: 万能助手扩展开发:使用百度地图API接口分页获取信息_电脑计算机编程入门教程自学 http://jianma123.com/viewthread.aardio?threadid=426 使用 ...
- Centos7验证Kickstart文件是否完整方法
1.1 功能简介 CentOS 7 包含 ksvalidator 命令行程序,可使用该程序进行确认Kickstart文件.这个工具是 pykickstart 软件包的一部分.要安装pykicks ...
- tornado用户指引(四)------tornado协程使用和原理(三)
版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/happyAnger6/article/details/51291221几种常用的协程方式: 1.回调 ...
- tcp总结与简单实现
一.TCP简介 1. TCP介绍 1)TCP协议,传输控制协议(Transmission Control Protocol,缩写为 TCP)是一种面向连接的.可靠的.基于字节流的传输层通信协议 2)t ...
- 使用百度编辑器--ueditor,后台接收提交编辑的内容,HTML不见了, 赋值不了,赋值之后,html暴露出来了??
1.提交编辑好的内容, 后台post 接收发现 html 不见了,这个时候也许就是转义的问题, 既可以试试 $content = htmlspecialchars(stripslashes(input ...
- Delphi采用接口实现DLL调用
Delphi使用模块化开发,可以采用DLL或者BPL,两者的区别是BPL只能被同版本的Delphi使用,DLL可以被不同版本和不同开发工具的开发的软件调用. 因此我们的软件大多使用Delphi作为界面 ...
- python之变量的命名规则
变量的命名规则: 1.变量名由数字.字母和下划线组成名 2.变量名不能以数字开头 3.禁止使用python中的关键字 4.不能使用中文和拼音 5.变量名要区分大小写 6.变量名要有意义 7.推荐写法: ...
- python--模块之collection
collection模块: 在内置数据类型(dict.list.set.tuple)的基础上,collections模块还提供了几个额外的数据类型:Counter.deque.defaultdict. ...
- 20145234黄斐《Java程序设计》第六周学习总结
教材学习内容总结 第十章 输入/输出 文件的读写 网络上传数据的基础 父类 InputStream与OutputStream 流(Stream)是对「输入输出」的抽象,注意「输入输出」是相对程序而言的 ...
- 北京Uber优步司机奖励政策(1月12日)
滴快车单单2.5倍,注册地址:http://www.udache.com/ 如何注册Uber司机(全国版最新最详细注册流程)/月入2万/不用抢单:http://www.cnblogs.com/mfry ...