面向对象思想写好并发程序

  • 前言
  • 一、封装共享变量
  • 二、识别共享变量间的约束条件
  • 三、制定并发访问策略
  • 总结

前言

在工作中很多时候在设计之初都是直接按照单线程的思路来写程序的,而忽略了本应该重视的并发问题;等上线后的某天,突然发现诡异的 Bug,再历经千辛万苦终于定位到问题所在,却发现对于如何解决已经没有了思路。

面向对象思想与并发编程本来是没关系的,它们分属两个不同的领域,但是在 Java 语言里,这两个领域被融合在一起了,融合的效果还是不错的:在 Java 语言里,面向对象思想能够让并发编程变得更简单。

用面向对象思想写好并发程序:封装共享变量、识别共享变量间的约束条件、制定并发访问策略。

一、封装共享变量

并发程序关注的一个核心问题,不过是解决多线程同时访问共享变量的问题。在解决原子性问题中,类比过球场门票的管理,现实世界里门票管理的一个核心问题是:所有观众只能通过规定的入口进入,否则检票就形同虚设。在编程世界这个问题也很重要,编程领域里面对于共享变量的访问路径就类似于球场的入口,必须严格控制。好在有了面向对象思想,对共享变量的访问路径可以轻松把控。

面向对象思想里面有一个很重要的特性是封装,封装的通俗解释就是将属性和实现细节封装在对象内部,外界对象只能通过目标对象提供的公共方法来间接访问这些内部属性,这和门票管理模型匹配度相当的高,球场里的座位就是对象属性,球场入口就是对象的公共方法。把共享变量作为对象的属性,那对于共享变量的访问路径就是对象的公共方法,所有入口都要安排检票程序就相当于前面提到的并发访问策略。

利用面向对象思想写并发程序的思路:将共享变量作为对象属性封装在内部,对所有公共方法制定并发访问策略。就拿很多统计程序都要用到计数器来说,下面的计数器程序共享变量只有一个,就是 value,我们把它作为 Counter 类的属性,并且将两个公共方法 get() 和 addOne() 声明为同步方法,这样 Counter 类就成为一个线程安全的类了。

public class Counter {private long value;synchronized long get(){return value;}synchronized long addOne(){return ++value;}
}

当然实际工作中,很多的场景都不会像计数器这么简单,经常要面临的情况往往是有很多的共享变量,例如,信用卡账户有卡号、姓名、身份证、信用额度、已出账单、未出账单等很多共享变量。这么多的共享变量,如果每一个都考虑它的并发安全问题,那就累死了。但其实仔细观察会发现,很多共享变量的值是不会变的,例如信用卡账户的卡号、姓名、身份证。对于这些不会发生变化的共享变量,建议用 final 关键字来修饰。这样既能避免并发问题,也能很明了地表明设计意图:考虑过这些共享变量的并发安全问题了。

二、识别共享变量间的约束条件

识别共享变量间的约束条件非常重要。因为这些约束条件,决定了并发访问策略。例如,库存管理里面有个合理库存的概念,库存量不能太高,也不能太低,它有一个上限和一个下限。关于这些约束条件,可以用下面的程序来模拟一下。在类 SafeWM 中,声明了两个成员变量 upper 和 lower,分别代表库存上限和库存下限,这两个变量用了 AtomicLong 这个原子类,原子类是线程安全的,所以这两个成员变量的 set 方法就不需要同步了。

public class SafeWM {// 库存上限private final AtomicLong upper =new AtomicLong(0);// 库存下限private final AtomicLong lower =new AtomicLong(0);// 设置库存上限void setUpper(long v){upper.set(v);}// 设置库存下限void setLower(long v){lower.set(v);}// 省略其他业务代码
}

虽说上面的代码是没有问题的,但是忽视了一个约束条件,就是库存下限要小于库存上限,在 setUpper() 和 setLower() 中增加了参数校验,这乍看上去好像是对的,但其实存在并发问题,问题在于存在竞态条件。看到代码里出现 if 语句的时候,就应该立刻意识到可能存在竞态条件。

假设库存的下限和上限分别是 (2,10),线程 A 调用 setUpper(5) 将上限设置为 5,线程 B 调用 setLower(7) 将下限设置为 7,如果线程 A 和线程 B 完全同时执行,线程 A 能够通过参数校验,因为这个时候,下限还没有被线程 B 设置,还是 2,而 5>2;线程 B 也能够通过参数校验,因为这个时候,上限还没有被线程 A 设置,还是 10,而 7<10。当线程 A 和线程 B 都通过参数校验后,就把库存的下限和上限设置成 (7, 5) 了,显然此时的结果是不符合库存下限要小于库存上限这个约束条件的。

public class SafeWM {// 库存上限private final AtomicLong upper =new AtomicLong(0);// 库存下限private final AtomicLong lower =new AtomicLong(0);// 设置库存上限void setUpper(long v){// 检查参数合法性if (v < lower.get()) {throw new IllegalArgumentException();}upper.set(v);}// 设置库存下限void setLower(long v){// 检查参数合法性if (v > upper.get()) {throw new IllegalArgumentException();}lower.set(v);}// 省略其他业务代码
}

在没有识别出库存下限要小于库存上限这个约束条件之前,制定的并发访问策略是利用原子类,但是这个策略,完全不能保证库存下限要小于库存上限这个约束条件。所以在设计阶段,一定要识别出所有共享变量之间的约束条件,如果约束条件识别不足,很可能导致制定的并发访问策略南辕北辙

共享变量之间的约束条件,反映在代码里,基本上都会有 if 语句,所以,一定要特别注意竞态条件。

三、制定并发访问策略

制定并发访问策略,是一个非常复杂的事情。从方案上来看是以下“三件事”:

  1. 避免共享:避免共享的技术主要是利于线程本地存储以及为每个任务分配独立的线程。
  2. 不变模式:这个在 Java 领域应用的很少,但在其他领域却有着广泛的应用,例如 Actor 模式、CSP 模式以及函数式编程的基础都是不变模式。
  3. 管程及其他同步工具:Java 领域万能的解决方案是管程,但是对于很多特定场景,使用 Java 并发包提供的读写锁、并发容器等同步工具会更好。

Java 并发工具类以及他们的应用场景,并发编程的设计模式,这些都是和制定并发访问策略有关的。

宏观原则有助于写出“健壮”的并发程序,主要有以下三条:

  1. 优先使用成熟的工具类:Java SDK 并发包里提供了丰富的工具类,基本上能满足日常的需要,熟悉它们,用好它们,而不是自己再“发明轮子”,毕竟并发工具类不是随随便便就能发明成功的。
  2. 迫不得已时才使用低级的同步原语:低级的同步原语主要指的是 synchronized、Lock、Semaphore 等,这些虽然感觉简单,但实际上并没那么简单,一定要小心使用。
  3. 避免过早优化:安全第一,并发程序首先要保证安全,出现性能瓶颈后再优化。在设计期和开发期,很多人经常会情不自禁地预估性能的瓶颈,并对此实施优化,但残酷的现实却是:性能瓶颈不是你想预估就能预估的。

总结

利用面向对象思想编写并发程序,一个关键点就是利用面向对象里的封装特性。而对共享变量进行封装,要避免“逸出”,所谓“逸出”简单讲就是共享变量逃逸到对象的外面,Java 解决可见性和有序性问题中,构造函数里的 this“逸出”。这些都是必须要避免的。

面向对象思想写好并发程序相关推荐

  1. 你知道如何用面向对象思想写好并发编程吗?

    在工作中,我发现很多人在设计之初都是直接按照单线程的思路来写程序的,而忽略了本应该重视的并发问题:等上线后的某天,突然发现诡异的 Bug,再历经千辛万苦终于定位到问题所在,却发现对于如何解决已经没有了 ...

  2. 信不信?以面向对象的思想是可以写好高并发程序的!

    来自:冰河技术 前言 面向对象思想与并发编程有关系吗?本来二者是没有什么鸟关系的!它们是分属两个不同的领域,但是,Java却将二者融合在一起了!而且融合的效果不错:我们利用Java的面向对象的思想能够 ...

  3. 面向对象的编程思想写单片机程序——(3)学习笔记 之 程序分层、数据产生流程

    系列文章目录 面向对象的编程思想写单片机程序--(1)学习笔记 之 程序设计 面向对象的编程思想写单片机程序--(2)学习笔记 之 怎么抽象出结构体 面向对象的编程思想写单片机程序--(3)学习笔记 ...

  4. 用面向对象思想编写方法写出atm机取款流程

    在java学习中,面向对象思想是学习这门语言的核心,通过定义各种类和方法并调用 他们来写出一个项目,这样在修改项目时可以通过修改里面的方法而不是直接修改整 个代码,达到了方便简单的目的,下面就用一个a ...

  5. 学习面向对象思想,开始考虑通过封装、 继承、多态把程序的耦合度降低

    学习面向对象思想,开始考虑通过封装. 继承.多态把程序的耦合度降低 转载于:https://www.cnblogs.com/Andys/archive/2013/04/05/3001175.html

  6. 消除switch/case语句,不破坏代码的封闭性,使程序结构更符合面向对象思想(二)

    在 "消除switch/case语句,不破坏代码的封闭性,使程序结构更符合面向对象思想(一)"中,我们曾讨论过维护一个消息管理器来记录不同消息和它对应的消息处理类. 但是,这种实现 ...

  7. 简单的面向对象思想,写一个传奇人物的属性

    简单的面向对象思想,写一个传奇人物的属性 package com.hz.game;import java.util.Random;/*** //hat,weapon,necklace,ring,clo ...

  8. Java面向对象思想以及原理以及内存图解

    文章目录 什么是面向对象 面向对象和面向过程区别 创建一个对象用什么运算符? 面向对象实现伪代码 面向对象三大特征 类和对象的关系. 基础案例 代码实现 实例化 创建car对象时car引用的内存图 对 ...

  9. 穿越Java - 基础篇 第三章 面向对象介绍 | 第1节 面向对象思想

    面向对象思想 理解: 面向对象更多的是一种思想的体现. 面向过程更多体现的是什么事情都要亲力亲为. 而面向对象跟多体现的是如何指挥别人做事情,只要明确我要干什么即可. 通过将相关或类似的功能 封装在不 ...

最新文章

  1. UVa 167(八皇后)、POJ2258 The Settlers of Catan——记两个简单回溯搜索
  2. django captcha 验证码插件
  3. k8s基础学习-存储卷
  4. hdu 5616 Jam's balance(dp 正反01背包)
  5. GitHub in vs2010、vs2013
  6. 本周四直播预告(内含福利)丨 Oracle RAC集群安装部署
  7. SharePoint Secure Store Service(SSSS)的使用(一)
  8. HDU 1210 Eddy's 洗牌问题(foj1062) || FOJ1050 Number lengths水
  9. apache 禁用rc4_如何在Apache中禁用过时的TLS和SSL版本
  10. 安卓和ios的ui设计区别_简析Android系统与ios系统界面设计区别
  11. 《华为工作法》8 自我提升的华为人
  12. springboot下,JedisPool getResource导致大量线程WAITING,服务假死
  13. 阿里云打造离线下载服务器
  14. mysql的填充因子_SQL Server表索引:调整填充因子
  15. 十部委联合发布《关于促进互联网金融健康发展的指导意见》
  16. 考研从机械到计算机难吗,考研机械真的不行吗?
  17. archivelog模式和flashback db以及guarantee restore point之间的相互制约关系!
  18. RaspberryPi树莓派连接Wifi
  19. 全新的Uber App设计
  20. Go分享好的github插件和项目

热门文章

  1. 微慕插件二开wordpress微信小程序源码下载 星尘UI v1.3.1
  2. 郝姆斯公司库存管理系统的分析与设计
  3. 透视HTTPS建造固若金汤的城堡
  4. 知网查论文时怎么筛选核心期刊?
  5. spring框架之面试
  6. Pycharm(Python)下imshow函数显示问题的解决方法
  7. 简单直接了解沉浸式状态栏
  8. hdu1072翻译火星文(未解决)
  9. 什么是CPS什么是CPT
  10. linux 时区获取时间,【linux】linux修改系统时间和linux查看时区、修改时区的方法...