![](https://img-blog.csdnimg.cn/20201014180756916.png?x-oss-process=image/resize,m_fixed,h_224,w_224)
并发编程
文章平均质量分 92
并发编程
进京务工小卢
不爱敲代码 就爱小彬彬的小卢同学
展开
-
00.并发编程目录
1原创 2021-02-06 15:41:45 · 74 阅读 · 0 评论 -
01.并发编程核心问题:分工 同步 互斥
目录1. 分工2. 同步/协作3. 互斥并发编程可以总结为3个核心问题:分工 同步 互斥分工:指的是如何高效的拆解任务并分配给线程 同步:指的是线程之间如何协作 互斥:则是保证同一时刻只允许一个线程访问共享资源JavaSDK并发包很大部分都是按照这三个维度组织的。例如Fork/Join框架就是一种分工模式,CountDownLatch就是一种典型的同步方式,而可重入锁则是一种互斥手段。1. 分工所谓分工,类似于现实中一个组织完成一个项目,项目经理要拆分任务,安排合原创 2021-01-15 00:08:03 · 332 阅读 · 0 评论 -
02.并发编程BUG的源头:可见性、原子性、有序性问题
这些年,我们的CPU、内存、I/O设备都在不断的迭代,但是在快速发展的过程中,有一个核心矛盾一直存在,就是这三者的速度差异。CPU和内存的速度差异可以形象的描述为:CPU是天上一天,内存是地上一年(假设CPU执行一条普通指令需要一天,那么CPU读写内存得等待一年的时间)。内存和I/O设备的速度差异就更大了,内存是天上一天,I/O设备是地上十年。程序里大部分语句都要访问内存,有些还要访问I/O,根据木桶理论(一只水桶能装多少水取决于它最短的那块木板),程序整体的性能取决于最慢的操作——读写I/O设备,也就原创 2021-01-15 00:31:18 · 150 阅读 · 2 评论 -
03.JAVA内存模型:解决可见性和有序性问题
JAVA内存模型简介Volatile关键字简介Happens-Before规则简介原创 2021-01-15 22:04:44 · 92 阅读 · 0 评论 -
04.互斥锁Synchronized:解决原子性问题
原子性问题该如何解决简易锁模型改进后的锁模型Java 提供的锁技术:Synchronized锁和受保护资源的关系总结思考原创 2021-01-16 00:40:45 · 327 阅读 · 0 评论 -
05.如何处理线程死锁
在上一篇文章中,我们用 Account.class 作为互斥锁,来解决银行间的转账问题,虽然这个方案不存在并发问题,但是所有的账户的转账都是串行的,例如账户 A 转账户 B、账户 C 转账户 D 这两个转账操作现实世界里是可以并行的,但是在这个方案里却被串行化了,这样的话,性能太差。那下面我们就尝试着把性能提升一下。1. 从现实世界寻找答案现实世界里,账户转账操作是支持并发的,而且绝对是真正的并发。我们先试想一下古代,账户的存在形式就是一个账本,而且每个账户都有一个账本,统一放在文件架上,在做转原创 2021-01-26 12:35:05 · 237 阅读 · 0 评论 -
06.管程以及相关的一些概念
管程是一种可以很方便解决并发问题的核心技术,Java 语言在 1.5 之前,提供的唯一的并发原语就是管程,而且 1.5 之后提供的 SDK并发包,也是以管程技术为基础的。可以说,管程就是一把解决并发问题的万能钥匙。1. 什么是管程不知道你是否曾思考过这个问题:为什么 Java 在 1.5 之前仅仅提供了 synchronized 关键字及 wait()、notify()、notifyAll() 这三个看似从天而降的方法?在刚接触 Java 的时候,我以为它会提供信号量这种编程原语,因为操作系统.原创 2021-01-26 23:39:20 · 1349 阅读 · 0 评论 -
07.Java线程的生命周期
在Java领域,实现并发程序的主要手段就是多线程。线程是操作系统里的一个概念,虽然各种不同的开发语言如Java、C#等都对其进行了封装,但是万变不离操作系统。Java语言里的线程本质上就是操作系统的线程,它们是一一对应的。在操作系统层面,线程也有“生老病死”,专业的说法叫有生命周期。对于有生命周期的事物,要学好它,思路非常简单,只要能搞懂生命周期中各个节点的状态转换机制就可以了。虽然不同的开发语言对于操作系统线程进行了不同的封装,但是对于线程的生命周期这部分,基本上是雷同的。所以,我们可以先来了解一原创 2021-01-27 00:01:45 · 157 阅读 · 0 评论 -
08.线程池
虽然在Java语言中创建线程看上去就像创建一个对象一样简单,只需要new Thread()就可以了,但实际上创建线程远不是创建一个对象那么简单。创建对象,仅仅是在JVM的堆里分配一块内存而已;而创建一个线程,却需要调用操作系统内核的API,然后操作系统要为线程分配一系列的资源,这个成本就很高了,所以线程是一个重量级的对象,应该避免频繁创建和销毁。那如何避免呢?应对方案估计你已经知道了,那就是线程池。线程池的需求是如此普遍,所以Java SDK并发包自然也少不了它。但是很多人在初次接触并发包里线程池相原创 2021-01-28 23:41:31 · 79 阅读 · 0 评论 -
09.Lock和Condition
Java SDK并发包内容很丰富,包罗万象,但是我觉得最核心的还是其对管程的实现。因为理论上利用管程,你几乎可以实现并发包里所有的工具类。在前面《08 | 管程:并发编程的万能钥匙》中我们提到过在并发编程领域,有两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作。这两大问题,管程都是能够解决的。Java SDK并发包通过Lock和Condition两个接口来实现管程,其中Lock用于解决互斥问题,Condition用于解决同步问题。今天我们重点介绍Lo原创 2021-01-27 20:36:24 · 191 阅读 · 0 评论 -
10.Semaphore信号量
Semaphore,现在普遍翻译为“信号量”,以前也曾被翻译成“信号灯”,因为类似现实生活里的红绿灯,车辆能不能通行,要看是不是绿灯。同样,在编程世界里,线程能不能执行,也要看信号量是不是允许。信号量是由大名鼎鼎的计算机科学家迪杰斯特拉(Dijkstra)于1965年提出,在这之后的15年,信号量一直都是并发编程领域的终结者,直到1980年管程被提出来,我们才有了第二选择。目前几乎所有支持并发编程的语言都支持信号量机制,所以学好信号量还是很有必要的。下面我们首先介绍信号量模型,之后介绍如何使用信号量原创 2021-01-27 20:47:41 · 306 阅读 · 0 评论 -
11.读多写少场景的ReadWriteLock和更优秀的StampedLock
前面我们介绍了管程和信号量这两个同步原语在Java语言中的实现,理论上用这两个同步原语中任何一个都可以解决所有的并发问题。那Java SDK并发包里为什么还有很多其他的工具类呢?原因很简单:分场景优化性能,提升易用性。今天我们就介绍一种非常普遍的并发场景:读多写少场景。实际工作中,为了优化性能,我们经常会使用缓存,例如缓存元数据、缓存基础数据等,这就是一种典型的读多写少应用场景。缓存之所以能提升性能,一个重要的条件就是缓存的数据一定是读多写少的,例如元数据和基础数据基本上不会发生变化(写少),但是使用它原创 2021-01-27 20:47:27 · 607 阅读 · 0 评论 -
12.AQS源码分析
目录1)AQS 概要1.AQS 原理2.AQS 资源共享方式2)AQS 源码分析2.1 AQS 类结构2.1.1 属性2.1.2 方法2.1.3 内部类2.2 同步队列2.2.1 队列结构2.2.2 节点类2.2.3 入队操作2.3 获取锁2.4 释放锁2.5 回顾整个历程3)总结1)AQS 概要1.AQS 原理AbstractQueuedSynchronizer 类如其名,抽象的队列式的同步器.AQS 定义了一套多线程访问共原创 2021-02-22 23:50:21 · 98 阅读 · 0 评论 -
13.CAS无锁
目录1)CAS 介绍2)CAS 解决原子性问题3)CAS 原理4)CAS 问题4.1 ABA 问题4.2 自旋时间过长4.3 只能保证一个共享变量的原子操作5)Java 中的原子操作类5.1 基本数据类型的原子操作类5.2 数组类型的原子操作类5.3 引用类型的原子操作类5.4 字段类型的原子操作类5.5 原子化的累加器6)总结并发编程,为了保证数据的安全,需要满足三个特性:原子性、可见性、有序性。Java 中可以通过锁和 CAS 的方式来实原创 2021-02-23 09:21:54 · 257 阅读 · 1 评论 -
(三十一)两阶段终止模式:如何优雅地终止线程?
前面两篇文章我们讲述的内容,从纯技术的角度看,都是启动多线程去执行一个异步任务。既启动,那又该如何终止呢?今天咱们就从技术的角度聊聊如何优雅地终止线程,正所谓有始有终。在《09 | Java线程(上):Java线程的生命周期》中,我曾讲过:线程执行完或者出现异常就会进入终止状态。这样看,终止一个线程看上去很简单啊!一个线程执行完自己的任务,自己进入终止状态,这的确很简单。不过我们今天谈到的“优雅地终止线程”,不是自己终止自己,而是在一个线程T1中,终止线程T2;这里所谓的“优雅”,指的是给T2一个机会料原创 2021-02-06 00:58:45 · 267 阅读 · 0 评论 -
(三十)Worker Thread模式:如何避免重复创建线程?
在上一篇文章中,我们介绍了一种最简单的分工模式——Thread-Per-Message模式,对应到现实世界,其实就是委托代办。这种分工模式如果用Java Thread实现,频繁地创建、销毁线程非常影响性能,同时无限制地创建线程还可能导致OOM,所以在Java领域使用场景就受限了。要想有效避免线程的频繁创建、销毁以及OOM问题,就不得不提今天我们要细聊的,也是Java领域使用最多的Worker Thread模式。Worker Thread模式及其实现Worker Thread模式可以类比现实世界里原创 2021-02-06 00:19:26 · 515 阅读 · 0 评论 -
(三十二)生产者-消费者模式:用流水线思想提高效率
前面我们在《34 | Worker Thread模式:如何避免重复创建线程?》中讲到,Worker Thread模式类比的是工厂里车间工人的工作模式。但其实在现实世界,工厂里还有一种流水线的工作模式,类比到编程领域,就是生产者-消费者模式。生产者-消费者模式在编程领域的应用也非常广泛,前面我们曾经提到,Java线程池本质上就是用生产者-消费者模式实现的,所以每当使用线程池的时候,其实就是在应用生产者-消费者模式。当然,除了在线程池中的应用,为了提升性能,并发编程领域很多地方也都用到了生产者-消费者模原创 2021-02-06 09:57:10 · 474 阅读 · 0 评论 -
(二十九)Thread-Per-Message模式
我们曾经把并发编程领域的问题总结为三个核心问题:分工、同步和互斥。其中,同步和互斥相关问题更多地源自微观,而分工问题则是源自宏观。我们解决问题,往往都是从宏观入手,在编程领域,软件的设计过程也是先从概要设计开始,而后才进行详细设计。同样,解决并发编程问题,首要问题也是解决宏观的分工问题。并发编程领域里,解决分工问题也有一系列的设计模式,比较常用的主要有Thread-Per-Message模式、Worker Thread模式、生产者-消费者模式等等。今天我们重点介绍Thread-Per-Message模式原创 2021-02-06 00:18:20 · 262 阅读 · 0 评论 -
(二十八)Balking模式:再谈线程安全的单例模式
上一篇文章中,我们提到可以用“多线程版本的if”来理解Guarded Suspension模式,不同于单线程中的if,这个“多线程版本的if”是需要等待的,而且还很执着,必须要等到条件为真。但很显然这个世界,不是所有场景都需要这么执着,有时候我们还需要快速放弃。需要快速放弃的一个最常见的例子是各种编辑器提供的自动保存功能。自动保存功能的实现逻辑一般都是隔一定时间自动执行存盘操作,存盘操作的前提是文件做过修改,如果文件没有执行过修改操作,就需要快速放弃存盘操作。下面的示例代码将自动保存功能代码化了,很显然原创 2021-02-06 00:10:12 · 187 阅读 · 0 评论 -
(二十七)Guarded Suspension模式:等待唤醒机制的规范实现
前不久,同事小灰工作中遇到一个问题,他开发了一个Web项目:Web版的文件浏览器,通过它用户可以在浏览器里查看服务器上的目录和文件。这个项目依赖运维部门提供的文件浏览服务,而这个文件浏览服务只支持消息队列(MQ)方式接入。消息队列在互联网大厂中用的非常多,主要用作流量削峰和系统解耦。在这种接入方式中,发送消息和消费结果这两个操作之间是异步的,你可以参考下面的示意图来理解。消息队列(MQ)示意图在小灰的这个Web项目中,用户通过浏览器发过来一个请求,会被转换成一个异步消息发送给MQ,等MQ返回结果原创 2021-02-05 23:54:09 · 161 阅读 · 0 评论 -
(二十六)ThreadLocal
民国年间某山东省主席参加某大学校庆演讲,在篮球场看到十来个人穿着裤衩抢一个球,观之实在不雅,于是怒斥学校的总务处长贪污,并且发话:“多买几个球,一人发一个,省得你争我抢!”小时候听到这个段子只是觉得好玩,今天再来看,却别有一番滋味。为什么呢?因为其间蕴藏着解决并发问题的一个重要方法:避免共享。我们曾经一遍一遍又一遍地重复,多个线程同时读写同一共享变量存在并发问题。前面两篇文章我们突破的是写,没有写操作自然没有并发问题了。其实还可以突破共享变量,没有共享变量也不会有并发问题,正所谓是没有共享,就没有伤害。原创 2021-02-04 14:01:30 · 473 阅读 · 0 评论 -
(二十五)Copy-on-Write模式
在上一篇文章中我们讲到Java里String这个类在实现replace()方法的时候,并没有更改原字符串里面value[]数组的内容,而是创建了一个新字符串,这种方法在解决不可变对象的修改问题时经常用到。如果你深入地思考这个方法,你会发现它本质上是一种Copy-on-Write方法。所谓Copy-on-Write,经常被缩写为COW或者CoW,顾名思义就是写时复制。不可变对象的写操作往往都是使用Copy-on-Write方法解决的,当然Copy-on-Write的应用领域并不局限于Immutabilit原创 2021-02-04 12:14:53 · 521 阅读 · 0 评论 -
(二十四) Immutability模式:如何利用不变性解决并发问题?
我们曾经说过,“多个线程同时读写同一共享变量存在并发问题”,这里的必要条件之一是读写,如果只有读,而没有写,是没有并发问题的。解决并发问题,其实最简单的办法就是让共享变量只有读操作,而没有写操作。这个办法如此重要,以至于被上升到了一种解决并发问题的设计模式:不变性(Immutability)模式。所谓不变性,简单来讲,就是对象一旦被创建之后,状态就不再发生变化。换句话说,就是变量一旦被赋值,就不允许修改了(没有写操作);没有修改操作,也就是保持了不变性。快速实现具备不可变性的类实现一个具备不可变原创 2021-02-03 18:57:25 · 300 阅读 · 0 评论 -
(二十三)Fork|Join:单机版的MapReduce
前面几篇文章我们介绍了线程池、Future、CompletableFuture和CompletionService,仔细观察你会发现这些工具类都是在帮助我们站在任务的视角来解决并发问题,而不是让我们纠缠在线程之间如何协作的细节上(比如线程之间如何实现等待、通知等)。对于简单的并行任务,你可以通过“线程池+Future”的方案来解决;如果任务之间有聚合关系,无论是AND聚合还是OR聚合,都可以通过CompletableFuture来解决;而批量的并行任务,则可以通过CompletionService来解决。原创 2021-02-03 12:18:12 · 410 阅读 · 0 评论 -
(二十二)CompletionService:如何批量执行异步任务?
在《23 | Future:如何用多线程实现最优的“烧水泡茶”程序?》的最后,我给你留了道思考题,如何优化一个询价应用的核心代码?如果采用“ThreadPoolExecutor+Future”的方案,你的优化结果很可能是下面示例代码这样:用三个线程异步执行询价,通过三次调用Future的get()方法获取询价结果,之后将询价结果保存在数据库中。// 创建线程池ExecutorService executor = Executors.newFixedThreadPool(3);// 异步向电商S原创 2021-02-03 12:13:19 · 454 阅读 · 0 评论 -
(二十一)CompletableFuture实现异步编程
前面我们不止一次提到,用多线程优化性能,其实不过就是将串行操作变成并行操作。如果仔细观察,你还会发现在串行转换成并行的过程中,一定会涉及到异步化,例如下面的示例代码,现在是串行的,为了提升性能,我们得把它们并行化,那具体实施起来该怎么做呢?//以下两个方法都是耗时操作doBizA();doBizB();还是挺简单的,就像下面代码中这样,创建两个子线程去执行就可以了。你会发现下面的并行方案,主线程无需等待doBizA()和doBizB()的执行结果,也就是说doBizA()和doBizB()两原创 2021-02-02 15:19:03 · 257 阅读 · 0 评论 -
(二十)Feature实现烧水泡茶
在上一篇文章《22 | Executor与线程池:如何创建正确的线程池?》中,我们详细介绍了如何创建正确的线程池,那创建完线程池,我们该如何使用呢?在上一篇文章中,我们仅仅介绍了ThreadPoolExecutor的void execute(Runnable command)方法,利用这个方法虽然可以提交任务,但是却没有办法获取任务的执行结果(execute()方法没有返回值)。而很多场景下,我们又都是需要获取任务的执行结果的。那ThreadPoolExecutor是否提供了相关功能呢?必须的,这么重要..原创 2021-02-02 15:01:53 · 431 阅读 · 0 评论 -
(十九)CAS和Atomic
前面我们多次提到一个累加器的例子,示例代码如下。在这个例子中,add10K()这个方法不是线程安全的,问题就出在变量count的可见性和count+=1的原子性上。可见性问题可以用volatile来解决,而原子性问题我们前面一直都是采用的互斥锁方案。public class Test { long count = 0; void add10K() { int idx = 0; while(idx++ < 10000) { count += 1; }原创 2021-01-28 23:32:26 · 236 阅读 · 0 评论 -
(十八)并发容器
Java并发包有很大一部分内容都是关于并发容器的,因此学习和搞懂这部分的内容很有必要。Java 1.5之前提供的同步容器虽然也能保证线程安全,但是性能很差,而Java 1.5版本之后提供的并发容器在性能方面则做了很多优化,并且容器的类型也更加丰富了。下面我们就对比二者来学习这部分的内容。同步容器及其注意事项Java中的容器主要可以分为四个大类,分别是List、Map、Set和Queue,但并不是所有的Java容器都是线程安全的。例如,我们常用的ArrayList、HashMap就不是线程安全的。原创 2021-01-28 23:27:51 · 99 阅读 · 0 评论 -
(十七)CountDownLatch和CyclicBarrier
前几天老板突然匆匆忙忙过来,说对账系统最近越来越慢了,能不能快速优化一下。我了解了对账系统的业务后,发现还是挺简单的,用户通过在线商城下单,会生成电子订单,保存在订单库;之后物流会生成派送单给用户发货,派送单保存在派送单库。为了防止漏派送或者重复派送,对账系统每天还会校验是否存在异常订单。对账系统的处理逻辑很简单,你可以参考下面的对账系统流程图。目前对账系统的处理逻辑是首先查询订单,然后查询派送单,之后对比订单和派送单,将差异写入差异库。对账系统流程图对账系统的代码抽象之后,也很简单,核心代原创 2021-01-27 20:56:28 · 385 阅读 · 0 评论 -
(十六)StampedLock
在上一篇文章中,我们介绍了读写锁,学习完之后你应该已经知道“读写锁允许多个线程同时读共享变量,适用于读多写少的场景”。那在读多写少的场景中,还有没有更快的技术方案呢?还真有,Java在1.8这个版本里,提供了一种叫StampedLock的锁,它的性能就比读写锁还要好。下面我们就来介绍一下StampedLock的使用方法、内部工作原理以及在使用过程中需要注意的事项。StampedLock支持的三种锁模式我们先来看看在使用上StampedLock和上一篇文章讲的ReadWriteLock有哪些区别。原创 2021-01-27 20:51:40 · 285 阅读 · 0 评论 -
(十二)面向对象思想进行并发编程
面向对象思想与并发编程有关系吗?本来是没关系的,它们分属两个不同的领域,但是在 Java 语言里,这两个领域被无情地融合在一起了,好在融合的效果还是不错的:在 Java 语言里,面向对象思想能够让并发编程变得更简单。 那如何才能用面向对象思想写好并发程序呢?我觉得你可以从封装共享变量、识别共享变量间的约束条件和制定并发访问策略这三个方面下手。 1. 封装共享变量 并发程序,我们关注的一个核心问题,不过是解决多线程同时访问共享变量的问题。在《03 | 互斥锁(上):解决原子性问题》中,我原创 2021-01-27 20:30:06 · 128 阅读 · 0 评论 -
(十一)为什么局部变量是线程安全的
在工作中,我发现很多同学在设计之初都是直接按照单线程的思路来写程序的,而忽略了本应该重视的并发问题;等上线后的某天,突然发现诡异的Bug,再历经千辛万苦终于定位到问题所在,却发现对于如何解决已经没有了思路。关于这个问题,我觉得咱们今天很有必要好好聊聊“如何用面向对象思想写好并发程序”这个话题。面向对象思想与并发编程有关系吗?本来是没关系的,它们分属两个不同的领域,但是在Java语言里,这两个领域被无情地融合在一起了,好在融合的效果还是不错的:在Java语言里,面向对象思想能够让并发编程变得更简单。原创 2021-01-27 00:08:18 · 189 阅读 · 0 评论