- 并发编程之痛
- 来源:MacTalk
一个程序员开始学习编程,熟悉了某个编程语言的语法和库,就可以写程序了,比如操作文件、处理数据库中的数据等等,把这些程序放到框架中,还能给互联网上的用户提供服务。那这里面会不会涉及到并发编程呢?如果你没有在代码中显式使用线程 API,那可能你的程序就是串行的。但是它为什么可以提供并发服务呢,也就是说,很多人可以同时访问你的程序功能?这是因为你是用的框架 —— Nginx、OpenResty、Tomcat 或数据库连接池等 —— 提供了并发服务。比如 Nginx 中的 worker 调度服务。
你当然可以使用现有的线程并发框架,但是,总有一天你会自己去编写多线程并发程序,这是任何一个程序员无法避开的挑战,它就像你苦练十年准备跨马提刀闯江湖时必须要打败的一个对手。事实上,并发编程并不像在花园陪妹子散步那么轻松。
在串行程序中,代码的执行路径是可预测的,遇到问题。你总可以顺藤摸瓜找到逻辑错误或某种 bug,比如内存泄露、缓存区溢出、文件和网络 IO 错误、数组越界等等。在并发算法和多线程编程中,你需要同时考虑有多个执行流程的情况,以及如何对这些流程进行协调以完成指定的计算任务。并发编程中的很多 bug 是由于线程并发执行中的不确定性和异步性导致的。在不同的机器环境,线程会按照某种次序执行而导致错误无法被发现,等上线了才知道,已经是生产事故了。
如何在多核和云平台上写出健壮、安全、高性能的并发程序呢?
其实很简单,只要你能从两个方面突破一下就可以了。一个是“跳出来,看全景”,另一个是“钻进去,看本质”。
跳出来,看全景
我们先说“跳出来”。你应该也知道,学习最忌讳的就是“盲人摸象”,只看到局部,而没有看到全局。所以,你需要从一个个单一的知识和技术中“跳出来”,高屋建瓴地看并发编程。当然,这首要之事就是你建立起一张全景图。
不过,并发编程相关的知识和技术还真是错综复杂,时至今日也还没有一张普遍认可的全景图,也许这正是很多人在并发编程方面难以突破的原因吧。好在经过多年摸爬滚打,我自己已经“勾勒”出了一张全景图,不一定科学,但是在某种程度上我想它还是可以指导你学好并发编程的。
在我看来,并发编程领域可以抽象成三个核心问题:分工、同步和互斥。
1、分工
所谓分工,类似于现实中一个组织完成一个项目,项目经理要拆分任务,安排合适的成员去完成。
在并发编程领域,你就是项目经理,线程就是项目组成员。任务分解和分工对于项目成败非常关键,不过在并发领域里,分工更重要,它直接决定了并发程序的性能。在现实世界里,分工是很复杂的,著名数学家华罗庚曾用“烧水泡茶”的例子通俗地讲解了统筹方法(一种安排工作进程的数学方法),“烧水泡茶”这么简单的事情都这么多说道,更何况是并发编程里的工程问题呢。
既然分工很重要又很复杂,那一定有前辈努力尝试解决过,并且也一定有成果。的确,在并发编程领域这方面的成果还是很丰硕的。Java SDK 并发包里的Executor、Fork/Join、Future 本质上都是一种分工方法。除此之外,并发编程领域还总结了一些设计模式,基本上都是和分工方法相关的,例如生产者-消费者、Thread-Per-Message、Worker Thread 模式等都是用来指导你如何分工的。
学习这部分内容,最佳的方式就是和现实世界做对比。例如生产者-消费者模式,可以类比一下餐馆里的大厨和服务员,大厨就是生产者,负责做菜,做完放到出菜口,而服务员就是消费者,把做好的菜给你端过来。不过,我们经常会发现,出菜口有时候一下子出了好几个菜,服务员是可以把这一批菜同时端给你的。其实这就是生产者-消费者模式的一个优点,生产者一个一个地生产数据,而消费者可以批处理,这样就提高了性能。
2、同步
分好工之后,就是具体执行了。在项目执行过程中,任务之间是有依赖的,一个任务结束后,依赖它的后续任务就可以开工了,后续工作怎么知道可以开工了呢?这个就是靠沟通协作了,这是一项很重要的工作。
在并发编程领域里的同步,主要指的就是线程间的协作,本质上和现实生活中的协作没区别,不过是一个线程执行完了一个任务,如何通知执行后续任务的线程开工而已。
协作一般是和分工相关的。Java SDK 并发包里的 Executor、Fork/Join、Future 本质上都是分工方法,但同时也能解决线程协作的问题。例如,用 Future 可以发起一个异步调用,当主线程通过 get() 方法取结果时,主线程就会等待,当异步执行的结果返回时,get() 方法就自动返回了。主线程和异步线程之间的协作,Future 工具类已经帮我们解决了。除此之外,Java SDK 里提供的 CountDownLatch、CyclicBarrier、Phaser、Exchanger 也都是用来解决线程协作问题的。
不过还有很多场景,是需要你自己来处理线程之间的协作的。
工作中遇到的线程协作问题,基本上都可以描述为这样的一个问题:当某个条件不满足时,线程需要等待,当某个条件满足时,线程需要被唤醒执行。例如,在生产者-消费者模型里,也有类似的描述,“当队列满时,生产者线程等待,当队列不满时,生产者线程需要被唤醒执行;当队列空时,消费者线程等待,当队列不空时,消费者线程需要被唤醒执行。”
在Java并发编程领域,解决协作问题的核心技术是管程,上面提到的所有线程协作技术底层都是利用管程解决的。管程是一种解决并发问题的通用模型,除了能解决线程协作问题,还能解决下面我们将要介绍的互斥问题。可以这么说,管程是解决并发问题的万能钥匙。
所以说,这部分内容的学习,关键是理解管程模型,学好它就可以解决所有问题。其次是了解Java SDK并发包提供的几个线程协作的工具类的应用场景,用好它们可以妥妥地提高你的工作效率。
3、互斥
分工、同步主要强调的是性能,但并发程序里还有一部分是关于正确性的,用专业术语叫“线程安全”。并发程序里,当多个线程同时访问同一个共享变量的时候,结果是不确定的。不确定,则意味着可能正确,也可能错误,事先是不知道的。而导致不确定的主要源头是可见性问题、有序性问题和原子性问题,为了解决这三个问题,Java 语言引入了内存模型,内存模型提供了一些列的规则,利用这些规则,我们可以避免可见性问题、有序性问题,但是还不足以完全解决线程安全问题。解决线程安全问题的核心方案还是互斥。
所谓互斥,指的是同一时刻,只允许一个线程访问共享变量。
实现互斥的核心技术就是锁,Java语言里 synchronized、SDK里的各种 Lock 都能解决互斥问题。虽说锁解决了安全性问题,但同时也带来了性能问题,那如何保证安全性的同时又尽量提高性能呢?可以分场景优化,Java SDK 里提供的ReadWriteLock、StampedLock 就可以优化读多写少场景下锁的性能。还可以使用无锁的数据结构,例如 Java SDK 里提供的原子类都是基于无锁技术实现的。
除此之外,还有一些其他的方案,原理是不共享变量或者变量只允许读。这方面,Java 提供了 Thread Local 和final 关键字,还有一种 Copy-on-write 的模式。
使用锁除了要注意性能问题外,还需要注意死锁问题。
这部分内容比较复杂,往往还是跨领域的,例如要理解可见性,就需要了解一些 CPU 和缓存的知识;要理解原子性,就需要理解一些操作系统的知识;很多无锁算法的实现往往也需要理解 CPU 缓存。这部分内容的学习,需要博览群书,在大脑里建立起 CPU、内存、I/O 执行的模拟器。这样遇到问题就能得心应手了。
跳出来,看全景,可以让你的知识成体系,所学知识也融汇贯通起来,由点成线,由线及面,画出自己的知识全景图。
并发编程全景图之思维导图
钻进去,看本质
但是光跳出来还不够,还需要下一步,就是在某个问题上钻进去,深入理解,找到本质。
就拿我个人来说,我已经烦透了去讲述或被讲述一堆概念和结论,而不分析这些概念和结论是怎么来的,以及它们是用来解决什么问题的。在大学里,这样的教材很流行,直接导致了芸芸学子成绩很高,但解决问题的能力很差。其实,知其然知其所以然,才算真的学明白了。
我属于理论派,我认为工程上的解决方案,一定要有理论做基础。所以在学习并发编程的过程中,我都会探索它背后的理论是什么。比如,当看到Java SDK里面的条件变量Condition的时候,我会下意识地问,“它是从哪儿来的?是Java的特有概念,还是一个通用的编程概念?”当我知道他来自管程的时候,我又会问,“管程被提出的背景和解决的问题是什么?”这样一路探索下来,我发现Java语言里的并发技术基本都是有理论基础的,并且这些理论在其他编程语言里也有类似的实现。所以我认为,技术的本质是背后的理论模型。
总结
本文部分内容来自极客时间新上线的专栏《Java 并发编程实战》,作者王宝令老师是京东资深架构师,写过十五年程序,先后在用友、惠普等公司工作过,主导研发了支持高并发处理能力的 API 网关、高性能数据库连接池和海量数据归档平台等。
他的专栏将从理论开始,以实战和工具为主,为大家讲述并发编程的故事。你将获得:
1、全面了解并发编程核心原理
2、深入掌握 12 个 Java 并发工具类
3、搞懂 9 种并发编程模式
4、四大经典并发编程实战案例
优惠期 68 元,欢迎加入学习。没有需要的,就当读篇文章涨点知识吧。
名人
-
- 恋爱教科书 | 别人约会时都在聊些什么?
- ÒÅÔ¸Çåµ¥×îºóÒ»Ìõ£ºÃ¿Öܸú²»Í¬µÄ¼Ò»ïÈ¥Ô¼»á
- 胡辛束
-
- 屈臣氏O2O活动打响新年营销第一枪,破解零售行业增长密码
- Çü³¼ÊÏÁªºÏ¶¶ÒôÉÌÒµ»¯£¬½èÖúPOI¡°ÍæÁ˴δóµÄ¡±¡£
- 阑夕
-
- “抖音治好了那个90后妈妈的抑郁”
- 音乐/ Sam-a brief reply II 图/Pexels “世 间 所 有 美 好 ,都 因 相 信 而 存 在” “一天的公主,十个月的皇后,一个月的太后。” 很多人都拿这段话来形
- 末那大叔
-
- 小八 ||从奥斯卡影后看,欧美人真的比亚洲人老得快?
- ±£³ÖÃÀÀö£¬ÊÇÒ»³¡ÖÕÉíµÄ³Ö¾ÃÕ½¡£
- 蓝小姐和黄小姐
-
- 大运河文化带5︱美爆了!颐和园的西堤春色美景!!
- ½ñÌìÎÒÃǸø´ó¼Ò·îÏ×ÉÏÁ˼¸Ê®ÕÅÎ÷µÌÃÀͼ£¬Ç§Íò±ð´í¹ý¡£ÄÇÎ÷µÌÉϵ½µ×ÓÐʲôÃÀ¾°ÄØ£¿Ò»ÆðÀ´¿´°É£¡Ìرð·îÏ׺áÖôóͼŶ£¡
- 徐徐道来话北京
-
- 一粒尘埃背后,其实藏着整个宇宙
- ¸æËßÄã7¸ö¹ØÓÚ³¾°£µÄÒþÃØÊÂʵ
- 罗辑思维
-
- 告别职业联赛,还有欠税这个理由...
- ¸»µÂºÍÑÓ±ßÖÝÌåÓý¾Ö·Ö±ðÕ¼70%ºÍ30%µÄ¹É·Ý£¬Ñӱ߸»µÂÊÇûÓг¹µ×Ö°Òµ»¯µÄ¾ãÀÖ²¿¡£
- 黄健翔谈
-
- 大运河文化带1︱大运河源头北京的“母亲泉”在哪儿?
- ˵µ½°×¸¡Èª£¬¾Í²»Äܲ»Ìáµ½Ôª´úÊÀ½ç¼¶µÄË®Àûר¼Ò£¬¹ùÊؾ´£¬ÄǹùÊؾ´µ±ÄêÊÇÔõôѡÔñ°×¸¡Èª×÷Ϊ¾©º¼´óÔ˺ӵÄˮԴµÄÄØ£¿
- 徐徐道来话北京
-
- 九个不打不骂管教孩子的方法
- ¡°¿ÆѧµÄ¡±³Í·£º¢×Ó
- 李开复
-
- 糖吉诃德 | 几乎每个月都有时装周???
- ²»ÊÇ˵ºÃÒ»Äê¾ÍÁ½´ÎÂ𣿣¡
- 吉良先生
-
- 可能你的出国发财梦要醒醒了!
- Ìá¸ß×Ô¼ºµÄÄÜÁ¦ºÍ¾ºÕùÁ¦£¬Ç®£¬×ÔÈ»¾Í»á¹ýÀ´ÕÒÄã¡£
- 庞门正道
-
- 前任眼里的你是怎样的
- ÄãÊǸöºÃÈË
- 胡辛束
-
- “我永远屈服于温柔”
- 没有一个女孩子,不会被温柔打动。 对于男生来说,温柔不是一上来就暧昧: 也不是明明不熟,还要嘘寒问暖: 更不是天花乱坠地夸,给你点赞: 温柔,是一种独立存在的天赋。 不用高,不用帅,不用
- 末那大叔