并发编程知识点学习(2021年3月29日打卡)

Hello各位牛友们。今天博主就开新坑了,通过学习来弥补自己薄弱的并发知识这一块。博主现在主要通过看书的方式来学习,后续的话会结合面试和刷题,来巩固自己所学的知识。看的书籍目前来说主要是《JAVA并发编程的艺术》。这本书本身不是很厚,240页左右吧(相对于500多页的《深入理解JAVA虚拟机》来说确实如此),但博主本身以前很少精读这种技术类书籍,所以估计要花上的时间也不会少于一个月。不多说,进入正题

2021年3月27日打卡

第一章 并发编程的挑战

并不是启动更多的线程就能让程序运行的更快,会面临非常多的挑战,比如上下文切换和死锁。以及硬件软件资源限制

1.1 上下文切换

单核处理器也可以通过CPU给每个线程分配CPU时间片的方式来实现多线程执行代码。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。
CPU通过时间片分配算法来循环执行任务,执行一个时间片后会切换到下一个任务。切换前会保存上一个任务的状态,以便下一次切换回这个任务的时候可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

1.1.1 多线程一定快吗

答案是不一定,因为线程存在创建和上下文切换的开销

1.1.2 测试上下文切换次数和时长

  • 使用Lmbench3可以测量上下文切换的时长
  • 使用vmstat可以测量上下文切换的次数。(CountSwitch,CS)

1.1.3 如何减少上下文切换

  • 无锁并发编程。多线程竞争锁时,会引起上下文切换,可以使用一些办法避免使用锁,如将数据ID按照HASH算法分段取模,不同线程处理不同段的数据。
  • CAS算法。JAVA的Atomic包使用CAS算法来更新数据,而不需要加锁。
  • 使用最少线程。避免创建不需要的线程。创建很多线程但任务很少,会造成大量线程处于等待状态。
  • 协程: 在单线程里实现多任务的调度,并在单线程里维持多个人物间的切换。

1.1.4 减少上下文切换实战

第一步:使用jstack工具dump线程信息(今天刚学)。
第二步:统计所有线程分别处在什么状态
第三步:打开dump文件查看处于WAITING(onobjectmonitor)的线程在做什么。
第四步:减少相应程序工作线程数。
第五步:重启相应程序,再dump线程信息查看线程状态。

1.2 死锁

避免死锁的常见方法:
- 避免一个线程同时获取多个锁
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
- 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
- 对于数据库锁,加锁和解锁必须在同一个数据库连接里,否则会出现解锁失败的情况。

1.3 资源限制的挑战

(1)什么是资源限制
资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源
硬件资源限制有带宽的上传/下载速度硬盘读写速度和CPU处理速度。软件资源限制有数据库的连接数和socket连接数等。
(2)资源限制引发的问题。
并发编程中,代码执行速度加快的原则是将代码中串行执行的部分变成并发执行,但如果某段串行的代码并发执行,因为受限于资源,仍在串行执行,这时候程序不仅不会加快,反而会加慢,因为增加了上下文切换和资源调度时间
(3)如何解决资源限制的问题
对于硬件资源限制,可以考虑使用集群并行执行程序。比如用ODPS、Hadoop或自己搭建服务器集群,让不同的机器处理不同的数据。可以通过“数据ID%机器数”,计算得到机器编号,由对应编号机器处理对应数据。
对于软件资源限制,可以考虑使用资源池将资源复用。比如用连接池将数据库和Socket链接复用,或者在调用对方webservice接口获取数据时,只建立一个连接
(4)在资源限制情况下进行并发编程
如何在资源限制的情况下让程序执行的更快?--->根据不同的资源限制调整程序的并发度

1.4 本章小结

介绍了并发编程可能遇到的挑战,并给出了一些解决建议。多使用JDK并发包提供的并发容器和工具类来解决并发问题,这些类都已经通过了充分的测试和优化,均可以解决本章提到的几个挑战。

2021年3月28日 打卡

第二章 JAVA并发机制的底层实现原理

  JAVA代码在编译后变成JAVA字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,JAVA中所使用的并发机制依赖于JVM的实现和CPU的指令。本章将深入探索JAVA并发机制的底层实现原理。

2.1 volatile的应用

  多线程并发编程中synchronized和volatile都扮演重要角色,volatile是轻量级的synchronized,在多处理器开发中保证了共享变量的“可见性”。
  可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。让我们先从volatile的定义开始

1.volatile的定义与原理实现
  JAVA语言规范第3版堆volatile的定义如下:

  JAVA编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。
  如果一个字段被声明为volatile,JAVA线程内存模型确保所有线程看到这个变量的值是一致的。

  了解volatile实现原理之前,先来看看与实现原理相关的CPU术语和说明。

  • 内存屏障(memory barriers):一组处理器指令,用于实现对内存操作的顺序限制
  • 缓存行(原文写的是缓冲行,感觉可能有点翻译上的问题。)(cache line):缓存中可以分配的最小存储单位。处理器填写缓存线时会加载整个缓存线,需要使用多个主内存读周期。
  • 原子操作(atomic operations):不可中断的一个或一系列操作。
  • 缓存行填充(cache line fill):当处理器识别到从内存中读取操作数是可缓存的,处理器读取整个缓存行到适当的缓存(L1,L2,L3的或所有)
  • 缓存命中(cache hit):如果进行高速缓存行填充操作的内存位置仍然是下次处理器访问的地址时,处理器从缓存中读取操作数,而不是从内存读取
  • 写命中(write hit):当处理器将操作数写回到一个内存缓存的区域时,它会首先检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是写回到内存,这个操作被称为写命中。
  • 写缺失(write misses the cache):一个有效的缓存行被写入到不存在的内存区域。

    2021年3月29日打卡

    volatile如何保证可见性?
    JAVA代码最终被转变为汇编代码时,会多出第二行汇编代码,并带有lock指令。
    lock前缀指令在多核处理器下会引发两件事情:
    1)将当前处理器缓存行的数据写回系统内存。
    2)这个写回内存的操作会使其他CPU里缓存了该内存地址的数据无效。
    处理器不直接和CPU通信,而是将内存中的数据读到内部缓存后再操作。多处理器下,为了保证各个处理器的缓存是一致的,会实现缓存一致性协议。每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期。如果缓存行对应内存地址被修改,就会将当前处理器的缓存行设置为无效状态。处理器对这个数据进行修改操作的时候,会重新从系统内寸中把数据读到处理器缓存里。
    具体讲解一下volatile的两条实现原则:
    1)lock前缀指令让处理器缓存写回内存。
    lock前缀指令会在执行指令期间,声言处理器的LOCK#信号
    2)一个处理器的缓存回写到内存会导致其他处理器的缓存无效。
全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务