资讯专栏INFORMATION COLUMN

Java并发,volatile+不可变容器对象能保证线程安全么?!

tyheist / 2486人阅读

摘要:同样,用类型的变量来保存这些值也不是线程安全的。仅保证可见性,无法保证线程安全性。并且返回的结果是对象,是局部变量,并未使对象逸出,所以这里也是线程安全的。

《Java并发编程实战》第3章原文 《Java并发编程实战》中3.4.2 示例:使用Volatile类型来发布不可变对象

在前面的UnsafeCachingFactorizer类中,我们尝试用两个AtomicReferences变量来保存最新的数值及其因数分解结果,但这种方式并非是线程安全的,因为我们无法以原子方式来同时读取或更新这两个相关的值。同样,用volatile类型的变量来保存这些值也不是线程安全的。然而,在某些情况下,不可变对象能提供一种弱形式的原子性。
因式分解Servlet将执行两个原子操作:更新缓存的结果,以及通过判断缓存中的数值是否等于请求的数值来决定是否直接读取缓存中的因数分解结果。每当需要对一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据,例如程序清单3-12中的OneValueCache。

程序清单 3-12 对数值及其因数分解结果进行缓存的不可变容器类
@Immutable  
class OneValueCache {  
   private final BigInteger lastNumber;  
   private final BigInteger[] lastFactors;  
 
   public OneValueCache(BigInteger i,  
                        BigInteger[] factors) {  
       lastNumber  = i;  
       lastFactors = Arrays.copyOf(factors, factors.length);  
   }  
 
   public BigInteger[] getFactors(BigInteger i) {  
       if (lastNumber == null || !lastNumber.equals(i))  
           return null;  
       else  
           return Arrays.copyOf(lastFactors, lastFactors.length);  
   }  
} 

对于在访问和更新多个相关变量时出现的竞争条件问题,可以通过将这些变量全部保存在一个不可变对象中来消除。如果是一个可变的对象,那么就必须使用锁来确保原子性。如果是一个不可变对象,那么当线程获得了对该对象的引用后,就不必担心另一个线程会修改对象的状态。如果要更新这些变量,那么可以创建一个新的容器对象,但其他使用原有对象的线程仍然会看到对象处于一致的状态。
程序清单3-13中的VolatileCachedFactorizer使用了OneValueCache来保存缓存的数值及其因数。当一个线程将volatile类型的cache设置为引用一个新的OneValueCache时,其他线程就会立即看到新缓存的数据。

程序清单 3-13 使用指向不可变容器对象的volatile类型引用以缓存最新的结果
@ThreadSafe  
public class VolatileCachedFactorizer implements Servlet {  
   private volatile OneValueCache cache =  
       new OneValueCache(null, null);  
 
   public void service(ServletRequest req, ServletResponse resp) {  
       BigInteger i = extractFromRequest(req);  
       BigInteger[] factors = cache.getFactors(i);  
       if (factors == null) {  
           factorfactors = factor(i);  
           cache = new OneValueCache(i, factors);  
       }  
       encodeIntoResponse(resp, factors);  
   }  
} 

与cache相关的操作不会相互干扰,因为OneValueCache是不可变的,并且在每条相应的代码路径中只会访问它一次。
通过使用包含多个状态变量的容器对象来维持不变性条件,并使用一个volatile类型的引用来确保可见性,使得VolatileCachedFactorizer在没有显式地使用锁的情况下仍然是线程安全的。

分析

程序清单3-13中存在『先检查后执行』(Check-Then-Act)的竞态条件。

OneValueCache类的不可变性仅保证了对象的原子性。

volatile仅保证可见性,无法保证线程安全性。

综上,对象的不可变性+volatile可见性,并不能解决竞态条件的并发问题,所以原文的这段结论是错误的。

更新

疑惑已经解决了。

结论:
cache对象在service()中只有一处写操作(创建新的cache对象),其余都是读操作,这里符合volatile的应用场景,确保cache对象对其他线程的可见性,不会出现并发读的问题。并且返回的结果是factors对象,factors是局部变量,并未使cache对象逸出,所以这里也是线程安全的。

该问题已经提交到提问区和知乎:
https://segmentfault.com/q/10...
https://www.zhihu.com/questio...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/66397.html

相关文章

  • 线程基础必要知识点!看了学习多线程事半功倍

    摘要:是需要我们去处理很多事情,为了防止多线程给我们带来的安全和性能的问题下面就来简单总结一下我们需要哪些知识点来解决多线程遇到的问题。 前言 不小心就鸽了几天没有更新了,这个星期回家咯。在学校的日子要努力一点才行! 只有光头才能变强 回顾前面: 多线程三分钟就可以入个门了! Thread源码剖析 本文章的知识主要参考《Java并发编程实战》这本书的前4章,这本书的前4章都是讲解并发的基...

    YPHP 评论0 收藏0
  • Java并发编程实战」之对象的共享

    摘要:当某个不应该发布的对象被发布时,这种情况被称为逸出。线程安全共享线程安全的对象在其内部实现同步,因此多线程可以通过对象的公有接口来进行访问而不需要进一步的同步。 前言   本系列博客是对《Java并发编程实战》的一点总结,本篇主要讲解以下几个内容,内容会比较枯燥。可能大家看标题不能能直观的感受出到底什么意思,这就是专业术语,哈哈,解释下,术语(terminology)是在特定学科领域用...

    phodal 评论0 收藏0
  • 【J2SE】java并发编程实战 读书笔记( 一、二、三章)

    摘要:发布的对象内部状态可能会破坏封装性,使程序难以维持不变性条件。不变性线程安全性是不可变对象的固有属性之一。可变对象必须通过安全方式来发布,并且必须是线程安全的或者有某个锁保护起来。 线程的优缺点 线程是系统调度的基本单位。线程如果使用得当,可以有效地降低程序的开发和维护等成本,同时提升复杂应用程序的性能。多线程程序可以通过提高处理器资源的利用率来提升系统的吞吐率。与此同时,在线程的使用...

    QLQ 评论0 收藏0
  • 学习Java线程的一些总结

    摘要:多线程环境下的一些问题安全性问题在没有正确同步的情况下,多线程环境下程序可能得出错误的结果。一些相关概念竞争条件多线程的环境下,程序执行的结果取决于线程交替执行的方式。而线程的交替操作顺序是不可预测的,如此程序执行的结果也是不可预测的。 入口 Java多线程的应用复杂性之如jvm有限的几个内存方面的操作和规范,就像无数纷繁复杂的应用逻辑建立在有限的指令集上。 如何写出线程安全的程序,有...

    coolpail 评论0 收藏0
  • Akka系列(四):Akka中的共享内存模型

    摘要:共享内存相信对并发有所了解的同学都应该知道在推出后,对内存管理有了更高标准的规范了,这使我们开发并发程序也有更好的标准了,不会有一些模糊的定义导致的无法确定的错误。 通过前几篇的学习,相信大家对Akka应该有所了解了,都说解决并发哪家强,JVM上面找Akka,那么Akka到底在解决并发问题上帮我们做了什么呢? 共享内存 众所周知,在处理并发问题上面,最核心的一部分就是如何处理共享内存,...

    baukh789 评论0 收藏0

发表评论

0条评论

tyheist

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<