1. 并发编程—概念

2022-08-30 14:21:59 浏览数 (2)

资料获取:kdocs.cn/l/coFeTd0J6teu

1. 并发编程

并发编程最早的领域就是操作系统的实现,其中涉及到的知识点有很多,比如操作系统,尤其是在互斥中,涉及到CPU和缓存,需要在大脑里建立起CPU,内存,I/O执行的模拟器等等

在学习一门技术知识的时候,我们需要先学习技术背后的理论和模型,在大脑里形成一定的知识体系架构。而理论的应用更宽广,一项优秀的理论往往能够运用于多种语言,只有理论知识

宽广了,在遇到问题时,才能快速的定位问题并解决。

2. 什么是并发?

先来看一下几个概念:串行,并发,并行

  • 串行:无关任务数量,仅仅与资源和顺序有关,一个任务访问一个资源,一直持有资源到用完即会归还,如果多个任务来访问,则按顺序执行,且必须等前一个任务用完归还后,才能执行
  • 并发:至少两个任务访问同一个资源,与顺序无关,进行资源抢夺,而抢到资源的任务并非一直持有到用完,而是听从cpu的时间调度,随时在释放(此时,资源可能被另外一个线程所持有),持有中执行完任务
  • 并行:多个CPU真正意义上的同时执行不同的任务,进程和进程之间互不关联

3. 并发编程核心

Java里synchronized、wait()/notify()相关的知识很琐碎,看懂难,会用更难。但实际上synchronized、wait()、notify()不过是操作系统领域里管程模型的一种实现而已,Java SDK并发包里的条件变量Condition也是管程里的概念

Java经过这些年的发展,Java SDK并发包提供了非常丰富的功能,对于初学者来说可谓是眼花缭乱,好多人觉得无从下手。但是,Java SDK并发包乃是并发大师Doug Lea出品,堪称经典,它内部一定是有章可循的。那它的章法在哪里呢?

其实并发编程可以总结为三个核心问题:分工、同步、互斥。Java SDK并发包很大部分内容都是按照这三个维度组织的,而且,这三个核心问题是跨语言的,其余的内容则是并发容器和原子类,属于辅助问题,如下:

  • 分工:高效得拆解任务并分配给线程。例如:Fork/Join框架就是一种分工模式
  • 同步:线程之间如何高效合作。例如:CountDownLatch就是一种典型的同步方式
  • 互斥:保证同一时刻只允许一个线程访问共享资源。例如:可重入锁就是一种互斥手段

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执行的模拟器。这样遇到问题就能得心应手了。

FAQ

  1. 什么是管程模型?

管程作为一种解决并发问题(同步,互斥)的模型,是继信号量模型之后的一项重大创新,它与信号量在逻辑上是等价的(可以用管程实现信号量,也可以用信号量实现管程),但是相比之下管程更易用。而且,很多编程语言都支持管程程,搞懂管程,对学习其他很多语言的并发编程有很大帮助。下一章,我们详聊管程和信号量!

2. 什么是信号量?

总结

理论知识的学习不仅仅只是概念学习,肤浅了不是,要去看知识背后的本质,万物所有知识的出现都是有原因的,所以出现是必然的,而这个因便是我们要找的本质问题,知其然又知其所以然,才算是真正的学明白了

0 人点赞