学习并发编程之前一定要学JVM
并发编程的目的:并发编程的目的是为了让程序运行得更快,但是,并不是启动更多的线程就能让程序最大限度地并发执行。在进行并发编程时,如果希望通过多线程执行任务让程序运行得更快。面临的问题:比如上下文切换的问题、死锁的问题,以及受限于硬件和软件的资源限制问题。
1.上下文切换问题
1.1 什么是上下文切换
CPU 通 过时间 片分配算法来循 环执 行任 务 ,当前任 务执 行一个 时间 片后会切 换 到下一个任务 。但是,在切 换 前会保存上一个任 务 的状 态 ,以便下次切 换 回 这 个任 务时 ,可以再加 载这个任务 的状 态 。所以任 务 从保存到再加 载 的 过 程就是一次上下文切 换 。
1.2 问题
多线程不一定快
因为线程有创建和上下文切换的开销,上下文切换的开销比直接用单线程大,因为在多线程中,需要保存和恢复更多的上下文信息。过多的上下文切换会降低系统的运行效率,因此需要尽可能减少上下文切换的次数。
1.3 如何减少上下文的切换
减少上下文切 换 的方法有无 锁 并 发编 程、 CAS 算法、使用最少 线 程和使用 协 程。
· 无 锁 并 发编 程。多 线 程 竞 争 锁时 ,会引起上下文切 换 ,所以多 线 程 处 理数据 时 ,可以用一
些 办 法来避免使用 锁 ,如将数据的 ID 按照 Hash 算法取模分段,不同的 线 程 处 理不同段的数据。
·CAS 算法。 Java 的 Atomic 包使用 CAS 算法来更新数据,而不需要加 锁 。
· 使用最少 线 程。避免 创 建不需要的 线 程,比如任 务 很少,但是 创 建了很多 线 程来 处 理, 这
样 会造成大量 线 程都 处 于等待状 态 。
· 协 程:在 单线 程里 实现 多任 务 的 调 度,并在 单线 程里 维 持多个任 务间 的切 换 。
2.死锁
2.1 什么叫做死锁
死锁是指两个或两个以上的进程(或线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
2.2 死锁产生的条件
(1)互斥条件:一个资源每次只能被一个进程使用。
(2)占有且等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)不可强行占有:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
(2)占有且等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)不可强行占有:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
2.3 如何解除死锁
· 避免一个 线 程同 时获 取多个 锁 。
· 避免一个 线 程在 锁 内同 时 占用多个 资 源,尽量保 证 每个 锁 只占用一个 资 源。
· 尝试 使用定 时锁 ,使用 lock.tryLock ( timeout )来替代使用内部 锁 机制。
· 对 于数据 库锁 ,加 锁 和解 锁 必 须 在一个数据 库连 接里,否 则 会出 现 解 锁 失 败 的情况。
3. 资源限制的挑战
3.1 什么是资源限制
资源限制是指在 进 行并 发编 程 时 ,程序的 执 行速度受限于 计 算机硬件 资 源或 软 件 资 源。 例如,服务 器的 带宽 只有 2Mb/s ,某个 资 源的下 载 速度是 1Mb/s 每秒,系 统 启 动 10 个 线 程下 载资
源,下 载 速度不会 变 成 10Mb/s ,所以在 进 行并 发编 程 时 ,要考 虑这 些 资 源的限制。硬件 资 源限
制有 带宽 的上 传 / 下 载 速度、硬 盘读 写速度和 CPU 的 处 理速度。 软 件 资 源限制有数据 库 的 连 接
数和 socket 连 接数等。
3.2 资源限制引发的问题
在并发编 程中,将代 码执 行速度加快的原 则 是将代 码 中串行 执 行的部分 变 成并 发执 行,
但是如果将某段串行的代 码 并 发执 行,因 为 受限于 资 源,仍然在串行 执 行, 这时 候程序不 仅 不
会加快 执 行,反而会更慢,因 为 增加了上下文切 换 和 资 源 调 度的 时间 。例如,之前看到一段程
序使用多 线 程在 办 公网并 发 地下 载 和 处 理数据 时 , 导 致 CPU 利用率达到 100% ,几个小 时 都不
能运行完成任 务 ,后来修改成 单线 程,一个小 时 就 执 行完成了。
3.3 如何解决资源限制问题
对于硬件 资 源限制,可以考 虑 使用集群并行 执 行程序。既然 单 机的 资 源有限制,那么就 让
程序在多机上运行。比如使用 ODPS 、 Hadoop 或者自己搭建服 务 器集群,不同的机器 处 理不同
的数据。可以通 过 “ 数据 ID% 机器数 ” , 计 算得到一个机器 编 号,然后由 对应编 号的机器 处 理 这
笔数据。
对于 软 件 资 源限制,可以考 虑 使用 资 源池将 资 源复用。比如使用 连 接池将数据 库 和 Socket
连 接复用,或者在 调 用 对 方 webservice 接口 获 取数据 时 ,只建立一个 连 接。
3.4 在资源限制情况下进行并发编程
方法就是,根据不同的 资 源限制 调 整程序的并发 度,比如下 载 文件程序依 赖 于两个 资 源 —— 带宽 和硬 盘读 写速度。有数据 库 操作时,涉及数据 库连 接数,如果 SQL 语 句 执 行非常快,而 线 程的数量比数据 库连 接数大很多, 则某些线 程会被阻塞,等待数据 库连 接。
拓展:CAS算法(非常重要,后面一定用到)
CAS(Compare And Swap)算法是一种用于实现并发控制的原子操作,主要用于多线程环境下的无锁算法和数据结构,以保证并发安全性。以下是对CAS算法的详细解释:
(1)CAS算法的基本概念
- 定义:CAS算法通过比较内存中的值和预期值,如果相等则将新值写入内存,否则不做任何操作。它是一种轻量级的同步机制,能够在不使用锁(如synchronized、Lock)的情况下,对共享数据进行线程安全的操作。
- 操作数:CAS算法涉及三个主要操作数——需要读写的内存位置V、需要进行比较的预期值A和新值B。
(2)CAS算法的工作原理
- 读取内存值:首先,CAS算法会读取要更新的内存位置V的当前值。
- 比较内存值和预期值:然后,将预期值A与读取到的内存值进行比较。
- 写入新值:如果两者相等,说明该位置的值在读取到之后没有被其他线程修改过,此时将新值B写入内存位置V。如果两者不相等,则说明该位置的值已经被其他线程修改,此时CAS操作失败,通常需要重新尝试。
(3)CAS算法的实现
- 硬件支持:CAS算法的原子性操作依赖于硬件的支持,通常是通过处理器提供的原子指令来实现。
- Java实现:在Java中,CAS算法的实现主要依赖于sun.misc.Unsafe类(在Java 9及以后版本中,该类的部分功能被封装在jdk.internal.misc.Unsafe中),它提供了一些底层操作方法来实现CAS算法。此外,Java 5之后,JDK提供了java.util.concurrent.atomic包,其中的原子类(如AtomicInteger、AtomicLong等)就是基于CAS算法实现的。
(4)CAS算法的优点
- 非阻塞性:CAS算法是一种非阻塞算法,一个线程失败或挂起并不会导致其他线程也失败或挂起,这有助于提高并发性能。
- 避免死锁:由于CAS算法不使用锁,因此可以避免传统锁机制中的死锁问题。
- 减少上下文切换:CAS算法减少了线程间的竞争,从而减少了上下文切换的开销。
(5)CAS算法的缺点
- ABA问题:在CAS算法中,如果共享数据的值在操作过程中被修改了两次,但是最终值又恢复为原始值,那么CAS算法无法检测出这种情况,可能导致数据异常。
- 循环时间长:在多线程竞争激烈的情况下,由于CAS算法是通过不断自旋来比较和更新共享数据的值,可能会导致线程长时间自旋,降低了性能。
- 只能保证一个共享变量的原子操作:如果操作多个共享变量,则需要使用其他同步机制来保证原子性。
(6)CAS算法的应用场景
CAS算法广泛应用于并发编程中,如实现无锁数据结构、实现线程安全的计数器等。在Java中,java.util.concurrent.atomic包下的原子类就是基于CAS算法实现的,它们提供了丰富的原子操作,方便开发者在多线程环境下进行并发编程。
综上所述,CAS算法是一种重要的并发控制机制,它通过比较和交换共享数据的值来保证线程安全,避免了传统锁机制中的一些问题,但同时也存在一些缺点和限制。在实际应用中,需要根据具体场景和需求来选择合适的并发控制机制。