第5章 并发编程
并发:同时处理多个任务,即不必等待一个任务完成就能开始处理其他任务。并发解决的是阻塞问题,即一个任务必须要等待非其可控的外部条件满足后才能继续执行,最常见的例子是I/O,一个任务必须要等待输入才能执行(即被阻塞),类似的场景称为I/O密集型问题。
并行:同时在多处执行多个任务。并行解决的是所谓的计算密集型问题,即通过把任务分成多个部分,并在多个处理器上执行,从而提升程序运行的速度。
两者的关键都是【同时处理多个任务】,而并行则额外包括了多处理器分布式处理的概念。更重要的是,两者处理不同类型的问题。
并发是一系列聚焦于如何减少等待并提升性能的技术。
多线程并不是在多任务操作系统中fork出来的额外的进程,而是在执行程序所维护的单个进程内部创建多任务。
Java并发四定律:
- 不要使用并发。切忌自己手动写并发,尽量使用知名的库。
- 一切都不可信,一切都很重要。要保持质疑的态度面对并发,因为在非并发程序里不会有的问题,在并发里可能会有。
- 能运行并不代表没有问题。
- 终究要理解并发。不能因为你并未手动开启一个线程,就认为你可以避免编写线程代码。要熟悉使用的框架,它内含线程代码。
并行流:
代码里使用了.parallel()
- 流的并行化将输入的数据拆分成多个片段,这样就可以针对这些独立的数据片段应用各种算法。
- 数组的切分非常轻量、均匀,并且可以完全掌握切分的大小。
- 链表则完全没有这些属性,对其切分意味着会将其拆分成第一个元素 和 其他剩余的部分,这没有什么实际用处。
- 无状态生成器的表现很像数组,以上对range的使用就是无状态的。
- 迭代式生成器的表现很像链表,iterate()就是一个迭代式生成器
盲目地应用内建parallel操作有时反而能让程序变得特别慢。
单纯对集合调用stream或者parallelStream是没有问题的,然而有时使用并行后再进行collect会导致数据和非并行的不一致。
将parallel方法和limit方法一起配合使用,可以告诉程序预先选取一组值,以作为流输出。
peek()方法:它会从流中拉取出一个值来进行想要的操作,但并不会影响在流中传递下去的元素。
创建和运行任务:
Java5新增了一些线程池。
ExecutorService exec = Executors.newSignleThreadExcutor();
exec.shutdown();
exec.shutdown会告诉ExecutorService完成所有已提交的任务,但不再接收任何新任务。
多个任务同时修改同一个变量会导致所谓的竞态条件。
使用Callable类代替Runnable类,优势是方法有返回值。
线程池执行实现Callable类的线程方法是exec.invokeAll
只有当所有任务都完成时,invokeAll才会返回由Future组成的List,每个Future都对应一个任务。
得到Future的返回结果是需要阻塞的,并不是一个有效的解决办法。可以用CompletableFuture代替
CompletableFuture<Object> cf = CompletableFuture.completedFuture(new Object());
Object obj = cf.get();
可以使用thenApply将任务串联起来,一个执行完执行下一个。
死锁:
谁都动不了,这称为死锁。
哲学家用餐问题。
出现死锁需要同时满足的条件:
- 互斥。这些任务使用的至少一项资源必须不是共享的。
- 至少一个任务必须持有一项资源,并且等待正被另一个任务持有的资源。
- 不能从一个任务中抢走一项资源。
- 会发生循环等待,其中一个任务等待另一个任务持有的资源,另一个任务又在等待另一个任务持有的资源。
最简单方法是永远不要共享资源。
构造器并不是线程安全的:
将构造器设为同步是没有实际意义,因为这样做会阻塞正在构造的对象。在对象的所有构造器完成工作之前,其他线程通常无法使用该对象。
单例模式。
并行流方案最合适解决无脑并行类型问题,即那种很容易将数据拆分成无差别、易处理的片段来处理的问题。
CompletableFuture处理的工作片段最好是各不相同的,这样效果最好。CompletableFuture看起来更像面向任务的,而不是面向数据。
并发的缺点:
- 线程等待资源时会导致系统变慢;
- 管理线程需要额外的CPU开销;
- 不合理的设计决策会导致不必要的复杂性;
- 会带来饥饿、竞争、死锁、活锁等病态现象;
- 平台不一致。