【Java并发基础】并发编程领域的三个问题:分工、协作和同步

前言

可以将Java并发编程抽象为三个核心问题:分工、协作和同步。
这三个问题的产生源自对性能的需求。最初时,为提高计算机的效率,当IO在等待时不让CPU空闲,于是就出现了分时操作系统也就出现了并发。后来,多核CPU出现,不同的任务可以同时独立运行,于是就出现了并行【分工】。有了分工后,效率得到了很大的提升,但是为了更合理的安排以及控制任务的进行,就需要让进程之间可以通信【协作】,让彼此知道进度的执行。分工进行提高了效率,但是却带来了多线程访问共享资源会冲突的问题。于是对共享资源的访问又需要串行化。所以,依据现实世界的做法设计了锁等机制来使得多线程【同步】访问共享资源。

分工(性能)

分工的主要工作是:如何高效拆解任务并分配给线程。   

Java SDK并发包中的ExecutorFork/JoinFuture本质上都是分工方法。
并发编程中的一些设计模型也是指导如何分工:生产者——消费者Thread-Per-MessageWork Thread等。

协作(性能)

在并发编程的协作指的是当一个线程执行完了,该如何通知后续任务的线程展开工作。

协作一般和分工相关。Java SDK中ExecutorFork/JoinFuture本质上是分工方法但是也解决线程之间的协作问题(如Future异步调用,get())。Java SDK里提供的CountDownLatchCyclicBarrierPhaserExchanger也是用于解决线程之间的协作问题。

深入了解下,线程间协作所使用的通信机制。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。
在共享内存的并发模式里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。

Java的并发采取的就是共享内存模型,Java线程之间的通信总是隐式地进行,所以只有理解了这种通信模型,当遇见关于内存可见性问题时才理解如何解决。

同步(正确性/线程安全)

当多个线程访问某个共享变量并且其中有一个线程执行写操作时,就必须要采用同步机制来协同这些线程对共享变量的访问。或者说是控制不同线程之间操作发生相对顺序的机制。 在共享内存并发模型里,同步是显式进行的,我们必须显式指定某个方法或者某段代码块需要在线程之间互斥执行。在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。

因为 可见性有序性原子性(后面会有文章介绍)问题,多个线程对一个共享变量进行操作会导致结果的不确定 。
为了解决这三个问题,Java语言引入了内存模型,内存模型提供了一系列的规则,利用这些规则我们可以避免可见性问题、有序性问题,但是还不能完全解决线程安全问题。

解决线程安全问题的核心方案还是同步机制

同步机制的核心技术就是锁,当然还有非锁方案,如volatile类型的变量和原子变量。 Java语言中synchronized、SDK中的各种Lock都可以解决同步问题,但是锁却会带来性能问题,于是我们就需要平衡。

主要方案有:分场景优化,优化读多写少场景:ReadWriteLockStampledLock以及无锁结构Java SDK中的原子类;其他方案,原理为不共享变量或者变量只允许读,Java中提供了Thread Local Final关键字和Copy-on-write 模式。

小结

在看极客时间专栏《Java并发编程实战》学习攻略时,感触还是比较深。平时学习知识都是“独立”的,没有一种“全局”观念,也很少联系其他一些理论来侧面验证学习的知识,导致学过后就很容易忘记。看了这篇专栏前言后,总结出:学习知识时,要跳出来看全景,钻进去看本质。要知道每一种技术背后都应该有理论支持,并且这个理论可能是跨领域的,所以,掌握技术背后的理论十分很重要!
针对Java并发编程应该要结合操作系统一起来学习,如后面将要介绍的可见性、有序性和原子性。理解可见性就需要了解CPU和缓存的知识;理解原子性就需要理解操作系统的知识;很多无锁算法也是和CPU缓存有关。要联系起CPU、内存、I/O之间的关系。

参考:
[1]极客时间专栏王宝令《Java并发编程实战》
[2]Brian Goetz.Tim Peierls. et al.Java并发编程实战[M].北京:机械工业出版社,2016

posted @ 2020-01-13 21:10  sakuraxx  阅读(751)  评论(0编辑  收藏  举报