funnycoding / blog

A Github Issue Blog
22 stars 0 forks source link

《Java并发编程实战》1. 简介 #16

Open funnycoding opened 4 years ago

funnycoding commented 4 years ago

读之前先问自己:

为什么有并发(多线程的作用):

  1. 将复杂的异步代码简化为一组同步的多线程代码

2.利用多核CPU,提高程序效率

并发的难题

典型的并发应用场景:

解决同步的手段:

## 1.1 并发简史 操作系统出现之前,整个计算机从头到尾只能执行一个程序,这个程序访问计算机中的所有资源。 操作系统出现之后,计算机能同时运行多个程序,并且不同程序都在单独的进程中运行:操作系统为各个独立执行的进程分配各种资源,包括内存,文件句柄以及安全证书等。 并且各个进程之间可以进行一些粗粒度的通信机制来交换数据,包括套接字、信号处理器、共享内存、信号量以及文件等。 **操作系统出现的原因**(为什么要有操作系统): - **资源利用率**:在某些情况下,程序必须等待某个外部操作执行完成。例如输入输出的 IO 操作等,而在等待时程序无法进行其他工作。因此如果在等待的同时运行另一个程序,则提高了资源的利用率。 - **公平性**:不同的用户和程序对于计算机上的资源有着相同的使用权。 一种高效的运行方式是通过粗粒度的**时间分片**(Time Slicing)使这些用户和程序能共享计算机资源,而不是由一个程序从头到尾运行,再启动下一个程序。 - **便利性**:通常来说,计算多个任务时应编写多个程序,每个程序执行一个任务并在必要时通信将数据汇总。这比只编写一个程序来计算所有任务更容易实现。 做事高效的人总能在串行性与异步性之间找到合理的平衡,程序也是如此。 **这三个促进进程出现的因素,同样也促进线程出现。**【为了更细粒度的掌控资源,就有了线程】 线程允许在**同一个进程**中同时存在**多个程序控制流**。**线程** **共享** **进程** 范围**内**的**资源**,例如**内存**句柄和**文件**句柄,但在每个线程中都有各自的**程序计数器(Program Counter)**、**栈以及局部变量**等。线程还提供了一种直观的分解模式来充分利用多处理器系统中的硬件并行性,而在**同一个程序**中的**多个线程**也可以被**同时调度**到**多个 CPU 上运行**。 **【这里就要配上 Java 的 JMM 内存模型了,这张图很明确的说明了进程中哪些是线程之间共享的资源,哪些是线程内线程自己使用的资源 ↓ ** **JMM 内存模型** 来自**《深入理解 Java虚拟机 第三版》** 】 https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200324141018.png 线程也被称为轻量级的进程,同时在**现代操作系统**上,大多数都**以线程为基本调度单位**,而不是进程。如果没有明确的**协同机制**,那么线程将**独立执行**。**同一个进程中的所有线程共享进程的内存地址空间**,因此这些线程都能访问相同的变量并在**同一个堆**上分配对象,这就需要实现一种比在进程空间内共享数据粒度更细的**数据共享**机制。 如果没有明确的**同步机制**来协同对共享数据的访问,那么当一个线程正在使用某个变量时,另一个线程可能同时访问这个变量,这将造成不可预测的结果。【也就是另一个线程访问到的其实是错误的变量,这个变量正在被更改 这种并发产生的问题称为 **竞态条件**】 ## 1.2 线程的优势 在使用得当的前提下,线程可以有效降低开发和维护成本,同时提升复杂应用程序的性能。 **线程能够将大部分的异步工作流转换成串行工作流**,因此能更好地模拟人类的工作方式和交互方式。 线程: **异步 → 串行** 【这个感觉挺核心的】 【**2020年03月19日23:36:53 下午去看的是 OnJava8 的编程章节,看了一会实在受不了了,太晦涩了,还是继续看 Java 编程实战吧。**】 在图形界面中,线程提高用户界面的响应灵敏度,线程还可以降低代码复杂度,使代码更加容易编写、阅读、和维护。 服务器程序中,线程简化 **JVM** 的实现,**垃圾收集器**通常在**一个或多个专门的线程中运行**。许多重要的 Java 应用程序都在一定程度上用到了线程。 ### 1.2.1发挥多核处理器的强大能力 因为基本调度单位是线程,如果程序中只有一个线程,则最多只能在一个处理器上运行。 现在的CPU核心数量越来越多,单线程意味着浪费CPU计算能力。 使用多线程可以提高单处理器系统上的吞吐率,因为当程序发生阻塞的时候,可以切换到别的线程继续运行。 ### 1.2.2 建模的简单性 对于软件来说:如果在程序中只包含一种类型的任务,那么比包含多种不同类型的任务的程序要更容易编写,错误更少,也更容易测试。 **使用多线程可以将复杂的异步工作流分解为一组简单并且同步的工作流**,每个工作流在单独的线程运行,并在特定的同步位置进行交互。 Servlet 与 RMI 框架使用了多线程技术。 ### 1.2.3异步任务的简化处理 为了避免I/O阻塞,单线程服务器应用程序必须使用非阻塞IO(NonBlocing I/O) 这种I/O很复杂,并且容易出错。 如果每个请求都有自己的处理线程,就可以将非阻塞I/O简化为同步I/O。**(相当于简化了IO的复杂性,但是也引入了多线程的复杂性)** ### 1.3.1 安全性问题 ```java @NotThreadSafe public class UnsafeSequence { private int value; /**返回一个唯一的数值*/ public int getNext() { return value++; } } ``` https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200320095558.png 这里可能导致2条线程同时操作变量 value。因为 value++ 并非原子操作,它由3部分组成: 1. 读取 value的值 2. 操作 value的值 + 1 3. 写入 value 的值 当2条线程同时读取了value = 9 这个情况下的value 的时候,就会造成问题,它们得到了相同的值,并都将这个值 + 1.结果就是在不同的线程的调用中返回了相同的数值。 这里书里定义了3个自动以注解: - **@NotThreadSafe** : 线程不安全的类 - **@ThreadSafe** : 线程安全的类 - **@Immutable** : 不可变的 在上面的 UnsafeSequence 中说明的是一种常见的并发安全问题:竞态条件(Race Condition) Java 中提供了各种**同步机制**的协同访问来解决这个问题。 ### 1.3.2 活跃性问题 开发并发代码时,**安全性是不可破坏的**,**安全性**对于**多线程**和**单线程程序**同样**重要**。 此外,线程还会导致单线程中不会出现的问题,比如:**活跃性**。 **安全性**:永远不发生糟糕的事情。 **活跃性**:某件正确的事情最终一定会发生。 当某个操作无法继续的时候,就会发生活跃性的问题。在**串行程序**中,**死循环**就是一个活跃性问题,循环之后的代码无法被执行。 在多线程中的其他活跃性问题:线程A 等待线程B 释放资源,而线程B 永远不释放该资源,那么A就会一直等待下去。 **第十章介绍各种形式的活跃性问题。以及如何避免这些问题,包括 死锁,饥饿,活锁。** **导致活跃性问题的错误总是难以分析的**,因为它们依赖于不同线程的事件发生时序,因此在开发或测试中并不总是能够复现。 ### 1.3.3 性能问题 性能问题包括很多方面:服务响应时间过长、响应不灵敏、吞吐率过低、资源消耗过高、可伸缩性差等。 多线程带来的性能问题是线程之间上下文频繁切换带来的开销。 线程的**同步机制**会**抑制**某些编译器优化,**使缓存区中的数据无效**,**增加共享内存总线的同步流量**。【也就是增加了程序运行的资源开销】 这些因素都将带来**额外开销**,**十一章**讲怎么**分析**和**减少**这些开销。 ### 1.4 线程无处不在 - **Timer** - **Servlet** - **RMI 远程方法调用** - **Swing 和 AWT Java 图形界面工具** **使用多线程,就必须熟悉并发性和线程安全性。** ### ### 总结: 第一章简介的介绍了线程的出现背景,线程的优缺点,线程带来的问题,使用线程的场景。