这篇记录一下保证并发安全性的策略之——不变性。
(注意:是Immutable,不是Invariant!)

将一连串行为组织为一个原子操作以保证不变性条件,或者使用同步机制保证可见性,以防止读到失效数据或者对象变为不一致状态,这些问题都是因为共享了可变的数据。

如果我们能保证数据不可变,则这些复杂的问题就自然不用去考虑了。

不可变对象一定是线程安全的。

说简单也简单,不可变对象只有一种状态,且由构造器控制。

因此,判断不可变对象的状态变得特别简单。

当我们共享一个可变对象,其状态的改变行为都是难以预料的,尤其是作为参数传给了可覆盖的方法时,更糟糕的是这些client代码都可以保留该对象的引用,也就是说状态改变的时机也同样难以预料。

相对于可变对象的共享,不可变对象的共享则简单很多,而且几乎不用考虑弄一个快照。

于是我们现在有了一个新的问题:如何让状态不可变?

对于"不可变"这一说法无论是JLS还是什么地方都没有明确的定义,但不可变绝对不仅仅是加个final修饰那么简单,比如final修饰的field引用的是一个可变对象,而final保证的仅仅是引用的指向不会发生变化。

没错,不可变对象和不可变的对象引用是两码事

对于如何构建一个不可变对象,我们有三个条件(虽然说是"条件",但并不是那么硬性的,可以算是某种建议):

  1. 对象创建后保证状态不可变

  2. 对象的所有field都是final

  3. 创建期间没有逸出自身引用,保证对象的创建正确。

关于上面三条,这里举一个例子:

 public final class ThreeStooges {
private final Set<String> stooges = new HashSet<String>(); public ThreeStooges() {
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
} public boolean isStooge(String name) {
return stooges.contains(name);
} public String getStoogeNames() {
List<String> stooges = new Vector<String>();
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
return stooges.toString();
}
}

让我们检查一下是否满足三个条件:

  1. 对象创建后保证状态不可变,是否有变化? 我们首先是用private修饰了stooges,接着提供的两个公有方法中第一个方法是返回boolean而第二个方法getStoogeNames中我们重新创建了一个stooges且保证相同的逻辑而不是直接引用stooges field。

  2. 对象的所有field都是final,很明显,我们用了final进行描述以防止对象状态在对象生命周期内改变其引用。

  3. 创建期间没有逸出自身引用,在stooges声明时我们就指定了引用,并在构造函数中将其初始化,不会有外来方法可以引用到该状态并将其改编。

不得不说这个final修饰是关键。

通常我们对final关键字最直观的印象是,如果一个用final修饰的对象引用的指向是不会改变的(发现这话怎么说都很难表达清楚,但是你懂的),但即使引用了可变的实例,就判断状态而言,加了final就可以简化不少,分析基本不可变的对象总比分析完全可变的对象来得容易多了吧....

而final和synchronized关键字那样也有多个语义,就是——能确保初始化过程的安全性,从而可以自由共享,不需要进行同步处理(这个同步处理不包括可见性)。

下面是一段用final(更确切地说应该是不可变性)保证了操作原子性(以保证可变性条件)一段例子。

某个Servlet接收参数后将参数传入factor方法对其进行运算并将结果进行响应。

假设这个factor方法非常耗时,于是我们想出了一个方法暂时缓解这一状况,即下一次请求的参数和上一次请求的参数相同则响应缓存中的结果。

也就是说每一次请求时我们多了一个步骤,也就是需要判断请求的数字是否和缓存中的一样,如果不同则重新计算,而这一段并不是原子操作,并发出现时会出现破坏可变性条件的情况。

而为了应对这个问题,我们可以将这一部分用synchronized保证其原子性,但这里使用另一种方式,使用不可变对象:

public 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);
}
}

这个不可变对象是如何设计的?

首先我们保证了所有状态用final进行修饰并在唯一的构造器中进行初始化,注意构造器中对lastFactors进行初始化的那一段,我们用Arrays.copyOf保证了其正确构造,也就是防止逸出。

然后是唯一一个公有方法,这个方法要返回的正是我们计算好的factors,但我们不能直接返回factors,也是为了防止逸出,我们使用了Arrays.copyOf。

下面是使用缓存的Servlet,整个对象只有一个field就是cache,我们用volatile修饰以保证并发时的可见性,即线程A改变了引用时线程B可以立即看到新的缓存。

public class VolatileCachedFactorizer extends GenericServlet 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) {
factors = factor(i);
cache = new OneValueCache(i, factors);
}
encodeIntoResponse(resp, factors);
} void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
} BigInteger extractFromRequest(ServletRequest req) {
return new BigInteger("7");
} BigInteger[] factor(BigInteger i) {
return new BigInteger[]{i};
}
}

Java - 多线程中的不变性问题的更多相关文章

  1. java多线程中的三种特性

    java多线程中的三种特性 原子性(Atomicity) 原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行. 如果一个操作时原子性的,那么多线程并 ...

  2. java 多线程中的wait方法的详解

    java多线程中的实现方式存在两种: 方式一:使用继承方式 例如: PersonTest extends Thread{ String name; public PersonTest(String n ...

  3. java多线程中并发集合和同步集合有哪些?区别是什么?

    java多线程中并发集合和同步集合有哪些? hashmap 是非同步的,故在多线程中是线程不安全的,不过也可以使用 同步类来进行包装: 包装类Collections.synchronizedMap() ...

  4. java多线程中最佳的实践方案是什么?

    java多线程中最佳的实践方案是什么? 给你的线程起个有意义的名字.这样可以方便找bug或追踪.OrderProcessor, QuoteProcessor or TradeProcessor 这种名 ...

  5. Java多线程中的常用方法

    本文将带你讲诉Java多线程中的常用方法   Java多线程中的常用方法有如下几个 start,run,sleep,wait,notify,notifyAll,join,isAlive,current ...

  6. Java多线程中的竞争条件、锁以及同步的概念

    竞争条件 1.竞争条件: 在java多线程中,当两个或以上的线程对同一个数据进行操作的时候,可能会产生“竞争条件”的现象.这种现象产生的根本原因是因为多个线程在对同一个数据进行操作,此时对该数据的操作 ...

  7. Java多线程中的死锁

    Java多线程中的死锁 死锁产生的原因 线程死锁是指由两个以上的线程互相持有对方所需要的资源,导致线程处于等待状态,无法往前执行. 当线程进入对象的synchronized代码块时,便占有了资源,直到 ...

  8. Java多线程中易混淆的概念

    概述 最近在看<ThinKing In Java>,看到多线程章节时觉得有一些概念比较容易混淆有必要总结一下,虽然都不是新的东西,不过还是蛮重要,很基本的,在开发或阅读源码中经常会遇到,在 ...

  9. Java多线程中变量的可见性

    之所以写这篇博客, 是因为在csdn上看到一个帖子问的就是这个问题. 废话不多说, 我们先看看他的代码(为了减少代码量, 我将创建线程并启动的部分修改为使用方法引用). 1 2 3 4 5 6 7 8 ...

随机推荐

  1. “全栈2019”Java第六十三章:接口与抽象方法详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...

  2. Codeforces 464E. The Classic Problem

    题目大意 给定一张$n$个点, $m$条边的无向图,求$S$ 到$T$的最短路,其中边权都是$2^k$的形式$n,m,k<=10^5$,结果对$10^9+7$取模 题解 大佬好厉害 跑一边dij ...

  3. 关于logstash-out-mongodb插件说明

    从kafka获取数据,存到mongodb中.适合空间查询geo_point设置.配置文件如下: input {  kafka {    type => "test"    a ...

  4. java内存模型(jMM)(一)

    在说java的内存模型之前先简单的了解计算机的主存和缓存的相关概念. 多任务和高并发是衡量一台计算机处理器的重要指标.一般衡量一个服务器性能的高低好坏,使用每秒事务处理数(Transactions P ...

  5. 大数据list去重

    MaxList模块主要是对Java集合大数据去重的相关介绍. 背景: 最近在项目中遇到了List集合中的数据要去重,大概一个2500万的数据,开始存储在List中,需要跟一个2万的List去去重. 直 ...

  6. threejs的学习笔记

    很多时候,我们在开发3d效果的时候,容易搞不清楚x,y,z坐标分别指示哪个方向 在开发threejs的时候可以先把坐标系添加到场景中,起到一个启示作用. js // show axes in the ...

  7. Spark 错误日志中看到的一些问题

    2014-4-23 18:42:09 org.jivesoftware.spark.util.log.Log error 严重: Unable to contact shared group info ...

  8. HTML-制作图片的自动播放和手动切换

    思路:将想要播放的图片放入集合中,设置一个div,将图片依次从集合中定时取出放到div中展示:设置一个变量,通过变量与集合元素索引联系起来,点击改变时,获取当前图片的索引以切换图片 整体代码: < ...

  9. Echo团队Alpha冲刺随笔 - 第七天

    项目冲刺情况 进展 服务器部署完成.小程序改了几个BUG,WEB端大部分完成,后端主体功能大致完成. 问题 交接的时候出现很多新问题 心得 软工实践真棒!yeah!!! 今日会议内容 黄少勇 今日进展 ...

  10. 使用IntelliJ IDEA 前最好修改的配置

    目录 1.下载 2.破解 3.修改配置 下载 详见我的另外一篇博客:<软件开发资源下载>中的[IDE]->[IntelliJ IDEA] 破解 详见我的另外一篇博客:<最新版I ...