在工作中,我发现很多人在设计之初都是直接按照单线程的思路来写程序的,而忽略了本应该重视的并发问题;等上线后的某天,突然发现诡异的 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 中,声明了两个成员变量 upperlower,分别代表库存上限和库存下限,这两个变量用了 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);}// 省略其他业务代码
}

虽说上面的代码是没有问题的,但是忽视了一个约束条件,就是库存下限要小于库存上限,这个约束条件能够直接加到上面的 set 方法上吗?我们先直接加一下看看效果(如下面代码所示)。我们在 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. 迫不得已时才使用低级的同步原语:低级的同步原语主要指的是 synchronizedLockSemaphore 等,这些虽然感觉简单,但实际上并没那么简单,一定要小心使用。
  3. 避免过早优化:安全第一,并发程序首先要保证安全,出现性能瓶颈后再优化。在设计期和开发期,很多人经常会情不自禁地预估性能的瓶颈,并对此实施优化,但残酷的现实却是:性能瓶颈不是你想预估就能预估的。

总结

写在最后

很多人感叹“学习无用”,实际上之所以产生无用论,是因为自己想要的与自己所学的匹配不上,这也就意味着自己学得远远不够。无论是学习还是工作,都应该有主动性,所以如果拥有大厂梦,那么就要自己努力去实现它。

以上学习资料均免费放送,最后祝愿各位身体健康,顺利拿到心仪的offer!

由于文章的篇幅有限,所以这次的蚂蚁金服和京东面试题答案整理在了PDF文档里

资料获取方式:点赞+评论我的文章,关注我,然后戳这里即可免费领取

CuqNXO-1623614570590)]

[外链图片转存中…(img-dlpWA0LK-1623614570592)]

[外链图片转存中…(img-mswpUISq-1623614570593)]

你知道如何用面向对象思想写好并发编程吗?相关推荐

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

    面向对象思想写好并发程序 前言 一.封装共享变量 二.识别共享变量间的约束条件 三.制定并发访问策略 总结 前言 在工作中很多时候在设计之初都是直接按照单线程的思路来写程序的,而忽略了本应该重视的并发 ...

  2. 树结构之树和二叉树的概念以及如何用面向对象思想进行结构定义01

    树和二叉树的概念及结构定义 前言 一.树的基本概念及代码层面如何定义 1.树的概念 2.代码层面树如何定义 二.二叉树的基本概念及代码层面如何定义 1.二叉树的概念 2.代码层面二叉树如何定义 3.二 ...

  3. 前端小白用面向对象思想实现元素拖拽

    上篇文章分享了如何用面向对象思想编写选项卡,在文章最后留了一个拖拽的例子,希望大家可以试着写一下,现在我就谈谈我在这过程中遇到的一些问题和解决方法.(本文主要是想和js初学者分享经验,代码中的更改th ...

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

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

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

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

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

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

  7. ASP.Net面向对象思想

    转摘自:http://www.7kai.net/article.aspx?id=189 首先,我们还是来谈一下面向对象的编程思想吧.我想现在的主流编程思想无非两种:结构与面向对象.以前,在ASP中我们 ...

  8. 第一节:从面向对象思想(oo)开发、接口、抽象类以及二者比较

    一. 面向对象思想 1. 面向过程(OP)和面向对象(OO)的区别: (1):面向过程就是排着用最简单的代码一步一步写下去,没有封装,当业务复杂的时候,改动就很麻烦了 (2):面向对象将复杂的业务分离 ...

  9. linux 设备驱动总结,linux设备驱动归纳总结(三):3面向对象思想和lseek

    linux设备驱动归纳总结(三):3.设备驱动面向对象思想和lseek的实现 一.结构体struct file和struct inode 在之前写的函数,全部是定义了一些零散的全局变量.有没有办法整合 ...

最新文章

  1. 【多标签文本分类】Semantic-Unit-Based Dilated Convolution for Multi-Label Text Classification
  2. 百度线下赛道报名通知!
  3. ICML2021 | Self-Tuning: 如何减少对标记数据的需求?
  4. 中国未来的可能性思考- 系统化思维-公司培训
  5. 升级win10遇到的一些问题
  6. 541.反转字符串||
  7. oracle新建定时任务,Oracle创建定时任务
  8. EPS FB信令流程
  9. python计算所得税费用_Python实现的个人所得税计算器示例
  10. sklearn之make_blobs:产生数据集
  11. CentOS-6.2安装Nvidia显卡驱动
  12. ios9遇到 App Transport Security has blocked a cleartext HTTP(http://) resource load 错误
  13. Latex 参考文献格式
  14. 游戏造物者,7天创造完美世界
  15. 使用vh,vw配合媒体查询做pc端的界面自适应
  16. 学校wifi覆盖解决方案
  17. alexa技能个数_如何使用Alexa蓝图创建自己的Alexa技能
  18. 仿照jQuery进行一些简单的框架封装(欢迎指教~)
  19. MySQL8.0 存储过程和函数
  20. AI头发笔刷_Photoshop拥有强大的AI工具 可能会让您质疑什么是真实的

热门文章

  1. C++ new/new operator、operator new、placement new初识
  2. Python服务器开发三:Socket
  3. Eclipse for android 中设置java和xml代码提示功能(转)
  4. apache arm 交叉编译_MacOS 下交叉编译的折腾笔记
  5. native层 安卓_安卓逆向——拼xx协议java层分析
  6. 地铁闸门会夹伤人吗_家长们注意啦!又有孩子被地铁闸机夹翻
  7. 463. 岛屿的周长
  8. C语言判断系统是32位还是64位
  9. scala中何时使用下划线_在Scala中使用下划线
  10. db,dbms,dba_DBMS中的数据库管理员(DBA)