--极客时间学习笔记
一是跳出来,看全景
二是钻进去,看本质
并发编程可以抽象成三个核心问题:分工、同步和互斥。
1.分工
在并发编程领域,你就是项目经理,线程就是项目组成员。任务分解和分工对于项目成败非常关键,不过在并发领域,分工直接决定了并发程序的性能。Java SDK并发包里的Excutor、Fork/Join、Future本质上都是一种分工方法。此外,并发变成领域还总结了一些设计模式,基本上都是和分工方法相关的,例如生产者-消费者、Thread-Per-Message、Worker Thread模式等。
学习这部分内容,最佳的方式就是和现实世界做对比。例如生产者-消费者模式,可以类比为餐馆里的大厨和服务员,大厨就是生产者,负责做菜,做完放到出菜口,而服务员就是消费者,把做好的菜给你端过来。不过,我们会发现,出菜口有时候一下子出了好几个菜,服务员可以把这一批菜同时端给你。其实这就是生产者-消费者模式的一个优点,生产者一个一个地生产数据,而消费者可以批处理,这样就提高了性能。
2.同步
分工之后就是任务的执行,在项目执行过程中,任务之间是有依赖的。一个任务结束后,依赖他的后续任务就可以开工了,后续工作怎么指导可以开工呢?这个就靠够统协作了。在并发编程领域里的同步,主要指的就是线程间的协作,本质上和现实生活中的协作没区别,不过是一个线程执行完了一个任务,如何通知执行后续任务的线程开工而已。
协作一般都是和分工相关的。Java SDK并发包里的Execuor、Fork/Join、Future本质上都是分工方法,但同时也能解决协作问题。例如,用Future可以发起一个异步调用,当主线程get()方法取结果时,主线程就会等待,当异步执行的结果返回时,get()方法就自动返回了,主线程和一步线程之间的协作,Future工具类已经帮我们解决了。除此之外,Java SDK里提供的CountDownLatch、CyclicBarrier、Phaser、Exchanger也都是用来解决线程协作问题的。
工作中遇到的的线程协作问题,基本上都可以描述为:当某个条件不满足时,线程需要等待,当某个条件满足时,现车更需要被唤醒执行。
例如在生产者-消费者模型里,当队列满时,生产者线程等待,当队列不满时,生产者线程需要被唤醒执行;当队列空时,消费者线程等待,当队列不空时,消费者线程需要被唤醒执行。
在Java并发编程领域,解决协作问题的核心技术是管程,Monitor(管程/监视器),上述所有线程协作技术底层都是利用管程解决的。管程是一种解决并发问题的通用模型,除了能解决线程协作问题,还能解决将要介绍的互斥问题。可以说,管程是解决并发问题的万能钥匙。
因此这部分内容学习的关键是理解管程模型,其次是了解Java SDK并发包提供的几个线程协作的工具类的应用场景。用好他们妥妥地提高你的工作效率。
3.互斥
分工、同步主要强调的性能,但并发程序里还有一部分是关于正确性的,用专业术语叫“线程安全”。而导致线程安全问题的主要源头是可见性问题、有序性问题和原子性问题。为了解决这三个问题,Java语言引入了内存模型,内存模型提供了一系列的规则,利用这些规则我们可以避免可见性问题、有序性问题,但是还不足以完全解决线程安全问题。解决线程安全的核心方案还是互斥。所谓互斥,指的是同一时刻,只允许一个线程访问共享变量。
实现互斥的核心技术是锁,Java中的synchronized、SDK里的各种Lock都能解决互斥问题。但是,锁会带来性能问题。因此,锁如何提高性能呢?可以分场景优化,Java SDK里提供的ReadWriteLock、StampedLock就可以优化读多写少场景下锁的性能。还可以提供无锁的数据结构,例如Java SDK里提供的原子类都是基于无锁技术实现的。
除此之外,还有一些其他的方案,原理是不共享变量或者变量只允许读,这方面Java提供了ThreadLocal和final关键字,还有一种Copy-on-write的模式。
使用锁的时候,除了要注意性能问题,还需要注意死锁问题。
学习这部分内容需要对CPU、内存、缓存、操作系统有一定的了解。如可见性问题需要理解CPU和缓存;原子性问题需要理解操作系统的知识;很多无锁算法的实现往往也需要理解CPU缓存。
下面看全景图:
钻进去,看本质:
工程上的解决方案,一定要有理论做基础。
人贵有志,学贵有恒!