重庆点优建设网站公司,广州市建设交易中心,wordpress 宋体、,seo产品优化推广java并发编程的艺术 第一章–并发的挑战 1。上下文切换 上下文切换是由于多任务操作系统需要管理多个线程或进程的并发 第二章—java并发机制的底层实现原理 java代码编译成字节码#xff0c;然后被类加载器加载到jvm中#xff0c;jvm执行#xff0c;最终转换为汇编指令在cp…java并发编程的艺术 第一章–并发的挑战 1。上下文切换 上下文切换是由于多任务操作系统需要管理多个线程或进程的并发 第二章—java并发机制的底层实现原理 java代码编译成字节码然后被类加载器加载到jvm中jvm执行最终转换为汇编指令在cpu上执行java的并发机制依赖于jvm和cpu的指令。 1.volatile的应用 volatile加在共享变量上保证所有线程看到这个变量的值是一致的即valoatile变量相当于加了一个读写锁是通过原子操作实现的。 实现原理汇编代码给volatile变量加了lock前缀指令。 lock指令将当前cpu缓存的volatile变量写回系统内存使其他cpu缓存中的volatile变量数据无效。
MESI 协议是一种常见的缓存一致性协议用于处理多处理器系统中的缓存一致性问题。 1.ModifiedM 这个状态表示缓存行已被修改并且与主存中的数据不一致。这意味着这个缓存行中的数据是处理器专有的并且尚未写回主存。其他处理器无权访问此缓存行。 2.ExclusiveE私有的 这个状态表示缓存行是干净的与主存中的数据一致。这个缓存行只存在于一个处理器的缓存中其他处理器没有副本。当一个处理器读取数据时数据会从主存加载到这个处理器的缓存中并切换到 Exclusive 状态。 3.SharedS 这个状态表示缓存行是干净的与主存中的数据一致。多个处理器可以拥有这个缓存行的副本因此在多处理器系统中它可以存在于多个处理器的缓存中。如果一个处理器写入了这个缓存行它必须先将其转换为 Modified 状态然后进行修改。 4.InvalidI 这个状态表示缓存行是无效的无法使用。这可能是因为其他处理器修改了与之相关的数据或者处理器主动将其标记为无效。在 Invalid 状态下缓存行不能用于读或写操作。
MESI 协议的一般实现方式 1.状态标记 每个缓存行都有一个状态标记用于表示它的状态可以是 ModifiedM、ExclusiveE、SharedS、或 InvalidI之一。处理器在缓存中维护这些标记以跟踪每个缓存行的状态。 2.缓存操作 当处理器进行缓存读取或写入操作时MESI 协议规定了缓存状态的转换条件。例如如果一个处理器要读取一个缓存行但当前状态为 InvalidI则它必须从主存中加载数据将状态转换为 ExclusiveE或 SharedS并执行读取操作。 3.写回和写分配 当处理器对一个缓存行执行写入操作时MESI 协议规定了数据如何写回主存以保持一致性。如果一个缓存行处于 ModifiedM状态写回主存是必需的然后状态转换为 ExclusiveE。如果处于 ExclusiveE状态写入操作可以直接写入缓存不需要写回主存。 4.缓存间通信 当一个处理器修改一个缓存行并使其状态从 ExclusiveE转换为 ModifiedM时它必须通知其他可能拥有该缓存行的处理器。这通常涉及到发送消息或信号给其他处理器以通知它们相应缓存行的状态变化。 5.原子操作 在某些情况下 MESI 协议需要保证多个缓存操作是原子的以确保数据一致性。这可能涉及到使用原子操作来实现状态的转换和数据的读写以防止竞态条件。 MESI 协议的实现要求硬件支持并且通常由处理器内部的缓存控制逻辑来管理。硬件支持 MESI 协议可以确保缓存状态的正确管理和转换从而确保数据的一致性。不同的处理器架构和硬件实现可能会略有不同但遵循了 MESI 协议的一般原则。
1.缓存锁定Cache Locking缓存行锁后其他缓存中的share状态数据转换为invalid需要重新加载。
缓存锁定是一种方法通过它可以锁定缓存中的某些数据防止其被其他处理器修改。当一个处理器锁定缓存中的数据时其他处理器将不能修改或访问这些数据从而确保数据一致性。缓存锁定通常以粒度更细的方式进行例如锁定某个特定的缓存行而不是整个缓存。这种方法通常会引入更低的争用因为只有某些数据被锁定而其他数据可以继续并发访问。 2.总线锁定Bus Locking锁总线后其他cpu无法访问内存从而保证一致性但是很影响性能。 同时处理器也实现了嗅探技术来保证缓存一致性。 嗅探技术 当一个处理器中的缓存进行修改后会在总线上通知其他处理器将其他处理器的share状态改变为invalid。 硬件的缓存一致性是通过MESI状态转换和嗅探技术实现的。
2.synchronized的实现 synchronized保证一个方法或者代码块同时只有一个线程执行不会发生中断和抢占。 1Java中每个对象都可以作为锁对象头 具体实现 《1》普通同步方法锁是当前实例对象。 《2》静态同步方法锁是当前类的class文件。 《3》对于配置了synchonized对象的锁是配置对象。 当进程访问代码块时必须先得到锁退出或抛出异常时释放锁。 synchronized是通过jvm实现的jvm是通过monitorenter和monitorexit指令实现的。 monitorenter获得对象锁monitorexit释放对象锁。对象锁存在于对象头中。 对象头的内容对象锁信息 markword存储对象的hashcode和锁信息。 ClassMetadataAddress对象的元数据包括方法表构造方法成员变量。。 arrayLength数组的长度如果是数组的话,如果不是数组没有这个) 锁存在于MarkWord中 锁状态25bit4bit1bit是否是偏向锁2bit锁标志位对象的hashcode对象分代年龄001 各种锁的MarkWord 轻量级锁指向栈中锁记录的指针00重量级锁指向互斥量重量级锁的指针10GC标记11偏向锁线程IDepoch对象分代年龄101 2锁升级 在synchronized中有四种级别的锁无锁状态偏向锁状态轻量级锁状态重量级锁状态。 锁只能升级不能降级。 1偏向锁一个锁由一个对象多次获得为了代价最低引入了偏向锁 当一个线程获得了无锁状态的锁那么就会将自己线程ID通过CAS记录进对象头MarkWord中从无锁升级为偏向锁。 当线程再次获得了锁对象就检测一下锁对象的对象头的MardWCASDord中的偏向锁是否是自己的线程ID如果是直接用如果不是则用CAS修改为自己的线程ID然后使用。 如果CAS失败次数超过自旋阈值也就是存在大量竞争那么就将升级为轻量级锁。 2轻量级锁 jvm现在当前线程的栈中创建用于存储锁记录的空间并且将对象头中的MarkWord复制到锁记录中然后线程会尝试用CAS将对象头中的markword替换为指向锁的指针指向栈的指针如果成功当前线程获得锁如果失败表示其他进程在竞争锁当前线程便使用自旋来获得锁。 如果自旋很多次都没有获得锁那么就会膨胀成重量级锁。 3重量级锁 这是一个互斥锁一旦线程完成了synchronized就会释放锁其他进程竞争锁。 没有获得锁的进程会进入阻塞当有锁时候会被唤醒这会造成上下文切换造成较大的性能损失。
2。处理器实现原子操作 当存在跨总线宽度跨多个缓存行和跨页表时候处理器不能保证其原子性但是可以提供总线锁定和缓存锁定来保证原子性。 MESI协议 1总线锁定当一个cpu处理时其他cpu无法访问这个内存。 2缓存锁定使用缓存一致性MESI使用缓存行时候其他cpu缓存中的共享缓存行设置为invalied。
3。java实现原子操作 1使用循环CAS 缺点ABA问题占用cpu使用只能保证一个原子元素的原子操作。 2使用锁
第三章----java内存模型线程间通信顺序一致性 java是通过共享内存来实现进程间通信的。 同步是指程序中应用于控制不同进程间操作发生的相对顺序的操作。 对于实例域和静态域和数组是存储在主内存中的是共享变量需要从主内存中取出然后放在属于内存的本地内存中。 本地内存并非真实存在它是cpu缓存寄存器的抽象概念。 局部变量存在本地内存上。 进程间通信就是通过修改共享变量来实现的。线程修改本地内存然后刷入主内存。 1从源代码到指令代码的重排序 编译器和处理器为了提高效率会对指令作重排序。 源代码-》编译器优化重排序-》指令级并行重排序-》内存系统重排序-》最终指向的指令序列 这些重排序会导致并行执行发生冲突。 对于编译器级别的重排序JMM会禁止部分编译器的重排序。 对于处理器级别的重排序JMM会Java编译器在生成指令序列时候插入内存屏障通过内存屏障来实现对处理器重排序的禁止。 JMMjava内存模型是语言级别的内存模型它确保在不同编译器和处理器平台上通过禁止重排序来实现一致的内存可见性保证。 2内存屏障通过缓存刷入保证顺序一致性 有四种屏障只拿StoreLoad Barriers举例。 StoreLoad Barriers : Store 1 ;StoreLoad; Load 2; 确保先将数据保存到主内存然后加载出到本地内存中加载出的是新数据。 执行该屏障花费很大会将cpu缓存中所有的数据都刷入到内存中flush。 3happens-before关系一个偏序关系 happens-before关系代表前一个执行结果对于u后一个操作可见且第一个操作在第二个操作之前。 是一种为了对程序员方便的抽象概念。 4数据依赖性后一个操作依赖前一个操作 单线程的数据依赖性编译器和处理器会自然保证顺序。 对于数据依赖关系的操作编译器和处理器不会进行重排序。 不管怎样单线程的执行结果不会被改变这就是as-if-serial语义。 5当单线程happens-before不会影响结果的会重排序提高并行性。 6顺序一致性内存模型所有进程串行执行 在顺序一致性模型中操作是全序关系每个线程都全知整体。 但是JMM没有这个保证未同步程序整体是无须的并且线程也不知道彼此之间的执行顺序。 7JMM实现同步程序的顺序一致性的相似效果 通过给临界区加锁临界区内的代码可以重排序但是不能溢出到临界区外临界区外的代码也可以重排序。 这种JMM方法在不改变执行结果的前提下尽可能地位编译器和处理器的优化开方便之门。 8未同步没有加锁。。的执行特征 不会保证和顺序一致性内存模型相同的执行结果。 为了实现最小安全性JVM在分配对象时首先会对内存空间清零。 1.volatile的内存语义 使用volatile相当于给变量操作都加锁保证不会中断而且刷入主内存。 volatile能建立happens-before关系volatile读写不能重排序是编译器在生成字节码文件时在指令序列中插入内存屏障来阻止处理器重排序实现的。 屏障通过刷入主内存来阻止屏障上下进行重排序。 JMM对于volatile十分保守在每一个volatile写的后面或读的前面都会插入一个内存屏障。 所有类型的屏障都是通过组织屏障两边重排序实现的。
2.锁的内存语义 临界区共享资源可能被多个线程访问。需要同步控制保证一个时间只有一个线程访问。 锁的释放–获取建立的happens-before关系。 锁除了让临界区互斥执行外还可以让释放的锁的线程向获取锁的进程发送消息保证happens-before。 实例方法synchronized在调用的对象上加锁每个对象实例都能唯一使用多个对象可以同时执行。 静态方法在类的class上加锁只能有一个对象调用。 1)锁的释放和获取的内存语义 线程获取锁时线程的本地内存无效需要去主内存中获取。 线程释放锁时线程会将本地内存写入主内存。 2公平锁和非公平锁都是通过一个volatile变量实现的。
3.final域的内存语义 final不可变 final类不可继承 final方法不可重写 final值常量 1final的重排序规则 禁止将final域的写操作重排序到构造函数之外。 实现JMM禁止把final域的写重排序到构造函数之外。编译器会将构造函数返回之前插入内存屏障将缓存写入主内存。 2作用 一个对象初始化给其他线程执行成员变量的初始化可能重排序到返回对象之后从而导致其他线程检测到了这个引用对象但是对象的成员变量为0。 而final确保了在引用对象被所有线程可见之前final域已经在构造函数返回对象之前初始化完成了其他线程看到的都是初始化后的。
4.happens-before happens-before时指两个操作之间的执行顺序可以在一个线程也可以不在。 程序员不想要重排序为了容易理解。 而电脑想要重排序为了效率。 所以JMM改变结果的重排序被禁止而不改变结果的被允许。 例如如果一个锁只会被一个线程访问那么锁就被消除了。volatile变量也是。 happens-before的规则 1一个线程中结果串行。 2锁解锁后happens-before随后对锁加锁。 3volatile写happens-before读。 4传递性 5Thread.start()在这个线程的所有操作之前。 6Thread.join()happens-before 这个线程的所有操作。
5.线程安全的初始化 延迟初始化懒汉模式用的时候才初始化 但是面对多线程的访问延迟初始化可能会看到引用对象没有初始化的值。因为普通变量重排序到了构造函数return之后。 我们的解决办法是给初始化加synchronized。synchronized在进入时会加内存屏障出去时也会加内存屏障确保synchronized代码中的操作输入主内存中相当于成为了一个原子操作。同时保证只有一个线程能创建对象不会引起多个线程创建的问题。
synchronized static instance getInstance(){
if(instancenull)
instancenew instance;
return instance;
}但是开销很大。 双重检查锁定方法
public static Instance getInstance(){
if(instancenul){
synchronized(this.class){
if(instancenull)
instancenew instance;
}
}
return instance;
}如果第一次检查不为空就不会执行synchroized的代码从而减少开支。 但是这是有问题的第一次检查对象不为空但是对象还没有初始化完成这样返回这个对象会出问题的。synchronied有内存屏障保证了初始化的完整性而这个没办法保证 《》双重检查锁定延迟初始化问题的根源 创建一个对象其过程可以分解为 1分配空间 2初始化 3指向这个空间 在单线程中可以交换2和3即先指向再初始化不会影响结果。 所以双重检查延迟初始化的多线程执行过程如下 1-a线程分配instance空间 2-a线程设置instance指向分配的空间 3-b线程判断instance不为空访问instance 4-a线程初始化instance。 5-a线程访问instance。 在知晓了问题根源后我们的解决方案如下 1不允许指向insatnce空间和初始化instance重排序。 2允许重排序但是保证两个操作不可中断原子性。 《1》基于volatile的解决方案 volatile也会在进入和出去之前加内存屏障所以能保证所有线程看到的都是最新的。 而给instance加上volatile那么就会在初始化后才能读。 《2》基于类初始化的解决方案
第四章–java并发基础 1.并发与并行 并发是多cpu并行是时间块轮询。 2.进程状态 newrunnable,blocked(阻塞于锁,waiting等被通知唤醒,time_waiting超时等待,terminated 3.Daemon线程 只有当所有非守护线程结束后才自动结束。 Daemon的finally代码不会执行不能依靠finally来执行关闭或清理。 4.构造线程 一个新构造的线程对象是由其parent线程来进行空间分配的以及继承了parent的是否为Daemon优先级和加载资源的contextClassLoader和可继承的ThreadLocal同时还会分配一个唯一的Id来标识这个线程至此一个可以运行的线程对象就初始化好了在堆中等待着运行了。 使用start来启动进程。 5.中断interrupt是进程间通信的一种方式 中断就是一个标识位属性其他线程的interrupt会将其设置为true。 线程通过检查自身是否被中断来进行响应通过isInterrupute来判断是否被中断。 中断标识为true程序并不会终止。想要终止需要程序员主动检查标志位主动终止程序。