Java并发-推荐使用不可变对象的原因分析

本文探讨了Java中不可变对象的概念,如String类的不可变性及其原因,包括节省内存、多线程安全性及缓存优势。不可变对象在并发编程中提供了便利,减少了线程冲突和缓存污染的风险,同时也提高了代码的可维护性。然而,使用不可变对象也可能带来编程思维的转变和潜在的性能开销。文章建议在适当情况下优先考虑不可变对象设计。
摘要由CSDN通过智能技术生成

一、不可变对象的引出

在Java语法中,String即是不可变对象,一旦创建,假设你若想修改String对象值,只能重新创建String对象。
实现方式如下:1.将内部char类型数组用priovate以及final关键词修饰。2.将String类修饰为final
以下是String的JDK源码:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
   
    /** The value is used for character storage. */
    private final char value[];
    //.....(省略很多其他实现细节)
1.1String类不可变实现的途径:
  1. final修饰的String,代表了String的不可继承性,用final修饰char[]意味着引用变量value的不可更改性,但数组中单个元素的值是可以改变的。
    举例说明:
    value[3]=‘o’;//合法
    value={
         'h','e','l','l','o'};//不合法
    
  2. private修饰了char[]数组,并且不提供set方法,和final一起使得String实现了不可变性。
1.2保证String不可变的原因和目的:
  1. 只有当字符串是不可变的,字符串池才有可能实现,字符串池的实现可以在运行时节约很多heap空间。
  2. 如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接。
  3. 因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。节约了线程同步问题上额外的开销。
  4. 因为字符串是不可变的,所以在它创建的时候HashCode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
1.3引入不可变对象的疑虑:

在并发编程中,不可变对象特别有用。由于创建后不能被修改,所以不会出现由于线程干扰产生的错误或是内存一致性错误。
但是程序员们通常并不热衷于使用不可变对象,因为他们担心每次创建新对象的开销。实际上这种开销常常被过分高估,而且使用不可变对象所带来的一些效率提升也抵消了这种开销。例如:使用不可变对象降低了垃圾回收所产生的额外开销,也减少了用来确保使用可变对象不出现并发错误的一些额外代码。
另外一点,我们创建对象应秉持避免创建不必要的对象为原则,而不是尽量创建更少的对象为原则。


二、不可变对象

2.1 什么是不可变对象

其实不光是String对象,Java中的很多对象都符合上述不可改变状态的特性。简而言之,当一个对象构造完成后,其状态就不再变化,我们称这样的对象为不可变对象(Immutable Object),这些对象关联的类为不可变类(Immutable Class)。

比如Java中的Integer、Double、Long等所有原生类型的包装器类型,也都是不可变的。

那么明明可以直接修改this对象,为何Java中还要大费周章地去构造一个全新的对象返回呢?那这就要从不可变对象的好处说起了。

不可变对象需要满足的条件:

  1. 对象创建以后其状态就不能修改
  2. 对象的所有域都是final类型
  3. 对象是正确创建的(在对象创建期间,this引用没有逸出)
2.2 不可变对象的优点
2.2.1 对并发友好

提到多线程并发,最让人苦恼的莫过于线程间共享资源的访问冲突,古往今来,多少Bug因此而生。即便是最有经验的程序员,面对多线程编程时,也往往需瞻前顾后,反复思量后,才能逐渐对自己编写的代码产生信心。如果多线程错误可以跟编译错误一样,能够被自动发现该有多好。

目前大多数语言中,面对多线程冲突问题,都是采用序列化访问共享资源的方案。Java也不例外,Java语言中的synchronize关键字,Lock锁对象等机制,都是为实施此类方案准备的。此类方案最大的弊端在于:能不能保证多线程间没有冲突,完全取决于程序员对共享资源加锁解锁的时机对不对。如果程序员加锁的时机有丝毫差错,Java是不负责检测的,可能你的单元测试、集成测试、预发布测试也发现不了,程序上线后也看上去一切正常,但是等到某一个重要的时刻,它会以一个突如其来的线上Bug的形式通知你,是不是欲哭无泪。

然而,解决多线程冲突问题还有一个方向,就是从多线程冲突的根因 —— 共享资源上入手

如果完全没有共享资源,多线程冲突问题就天然不存在了,比如Java中的ThreadLocal机制就是利用了这一点理念。

但是大多数时候,线程间是需要使用共享资源互通信息的。此时,如果该共享资源诞生之后就完全不再变更(犹如一个常量),多线程间共同并发读取该共享资源是不会产生线程冲突的,因为所有线程无论何时读取该共享资源,总是能获取到一致的、完整的资源状态,这样也能规避多线程冲突。不可变对象就是这样一种诞生之后就完全不再变更的对象,该类对象可以天生支持无忧无虑地在多线程间共享。

如果线程间对共享资源的访问不仅局限于读,还想改变共享资源的状态呢,这种时候不可变对象又能否从容应对呢?答案是肯定的。原理很简单,某个线程想要修改共享资源A的状态时,不要去直接修改A本身的状态,而是先在本线程中构造一个新状态的共享资源B,待B构造完整后,再用B去直接替换A,由于对引用赋值操作是原子性的,所以也不会造成线程冲突问题。不可变对象所提供的方法,不会改变自身的状态,最多构造一个新状态的新对象的返回,这也与上述思路完全契合。但是需要注意可见性问题,如果你想要A替换B后,其他所有线程实时感知到此变化,需要使用volatile关键字保证可见性。
如下:

public 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值