分工指的是如何高效地拆解任务并分配给线程,而同步指的是线程之间如何协作,互斥
则是保证同一时刻只允许一个线程访问共享资源。
JDK并发包中的fork/Join框架就是分工,CountDownLatch是同步,而可重入锁是一种互斥手段。
并发编程不是Java特有的语言特性,它是一个通用且早已成熟的领域,学习的时候需要站在较高的层面且有体系的思考,就会容易很多。
按并发理论,并发工具类、并发设计模式、案例分析五个部分进行记录。
并发编程是一个综合学科,相关的概念和技术比较零散,不能盲人摸象,只看局部,不管全局。需要从一个一个知识点跳出来。
并发编程的核心问题是分工、同步和互斥。
分工很重要也很复杂,Java SDK并发包里的Executor, Fork/Join,Future本质上都是分工方法,另外还总结了一些设计模式,如:生产-消费者模式,Worker Thread模式等都是用来指导分工的。
学习这部分内容需要学会跟现实类比,如生产-消费者模式就类比饭店厨师和服务员。厨师是生产者,服务员是消费者。
分工后就是执行了,在任务执行过程中是互相有依赖的,一个任务结束后,依赖它的后续任务就可以开工了。在并发领域,主要指的就是线程之间的协作,即一个线程完成任务,如何通知后续任务的线程工作。
JDK中的CountDownLatch,CyclicBarriar, Phaser, Exchanger都是用来解决协作分工问题。
工作中需要的需要线程协作问题,都可以描述为:当某个条件不满足时,线程需要等待;当某个条件满足时,需要需要被唤醒执行。
在Java并发领域,解决协作的核心技术是管程,它是一种解决并发问题的通用模型,它除了能解决协作问题,还能解决互斥问题。可以说管程是解决并发问题的万能钥匙。(类比信号量也是解决并发问题的万能钥匙,既能用信号量作为互斥锁,也也可以用作条件变量。信号量 vs 管程?)
分工和同步主要强调的是性能,并发编程还有一个关注点是正确性,专业术语就是线程安全。
根源是当多个线程同时访问一个共享变量时,结果是不确定的。 而导致不确定的主要源头是可见性问题、有序性问题和原子性问题,为了解决这三个问题,Java 语言引入了内存模型,内存模型提供了一系列的规则,利用这些规则,我们可以避免可见性问题、有序性问题,但是还不足以完全解决线程安全问题。解决线程安全问题的核心方案还是互斥。
互斥即同一时刻,只允许一个线程访问共享变量。
实现互斥的核心技术是锁,如synchroized关键字,并发包中的lock。 锁可以解决线程安全问题,但带来了性能损耗,如何保持两者平衡,即不同场景使用不同的优化方案,Java SDK 里提供的 ReadWriteLock、StampedLock就可以优化读多写少场景下锁的性能。还可以使用无锁的数据结构,例如 Java SDK 里提供的原子类都是基于无锁技术实现的。
除此之外,还有一些方案原理是不共享变量和或变量只读,如ThreadLocal, final。 还有一种Copy-on-write模式。
除了锁性能问题,还有需要预防死锁(需要了解CPU和缓冲知识,要理解原子性需要学习OS的知识)。
学习并发切勿一上来就看Java 并发包,东西太多,不成体系,很容易遗忘,使用的时候也不知道是不是最优方案。原因就是没有形成知识体系。
Java并发包是并发大师Doug Lea设计并实现的,他一定不是随意设计,背后是他对并发问题的深刻认识。
另:经典并发编程相关书籍推荐:
《Java并发编程实战》
《Java并发编程的艺术》
《图解Java多线程设计模式》
《操作系统:精髓与设计原理》