当前位置: 首页 > news >正文

怎能建设个人网站网站建设网站推广服务公司

怎能建设个人网站,网站建设网站推广服务公司,网站怎么添加模块,现在做电脑做系统网站容易赚钱吗如何减少上下文切换 减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。 无锁并发编程#xff1a;多线程竞争锁时#xff0c;会引起上下文切换#xff0c;所以多线程处理数据时#xff0c;可以用一些办法来避免使用锁。如将数据的 ID 按照 Hash 算法…如何减少上下文切换 减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。 无锁并发编程多线程竞争锁时会引起上下文切换所以多线程处理数据时可以用一些办法来避免使用锁。如将数据的 ID 按照 Hash 算法取模分段不同的线程处理不同段的数据。CAS 算法Java 的 Atomic 包使用 CAS 算法来更新数据而不需要加锁。使用最少线程避免创建不需要的线程比如任务很少但是创建了很多线程来处理这样会造成大量线程都处于等待状态。协程在单线程里实现多任务的调度并在单线程里维持多个任务间的切换 volatile 是如何来保证可见性的呢 让我们在 X86 处理器下通过工具获取 JIT 编译器生成的汇编指令来查看对 volatile 进行写操作时CPU 会做什么事情。 Java代码如下 instance new Singleton(); // instance 是 volatile 变量转变成汇编代码如下 0x01a3de1d: movb $0×0,0×1104800(%esi); 0x01a3de24: lock addl $0×0,(%esp);有 volatile 变量修饰的共享变量进行写操作的时候会多出第二行汇编代码 Lock 前缀的指令在多核处理器下会引发两件事 1将当前处理器缓存行的数据写回到系统内存。2这个写回内存的操作会使在其他 CPU 里缓存了该内存地址的数据无效。 为了提高处理速度处理器不直接和内存进行通信而是先将系统内存的数据读到内部缓存L1, L2或其他后再进行操作但操作完之后不知道何时会写到内存如果对声明了Volatile变量进行写操作JVM就会向处理器发送一条Lock前缀的指令将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存如果其他处理器缓存的值还是旧的再执行计算操作就会有问题。所以在多处理器下为了保证各个处理器的缓存是一致的就会实现缓存一致性协议每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了当处理器发现自己缓存行对应的内存地址被修改就会将当前处理器的缓存行设置成无效状态当处理器要对这个数据进行修改操作的时候会强制重新从系统内存里把数据读到处理器缓存里。 下面来具体讲解 Volatile 的两条实现原则。 1Lock 前缀指令会引起处理器缓存回写到内存。Lock 前缀指令导致在执行指令期间声言处理器的 LOCK# 信号。在多处理器环境中LOCK# 信号确保在声言该信号期间处理器可以独占任何共享内存。但是在最近的处理器中LOCK#信号一般不锁总线而是锁缓存毕竟锁总线开销的比较大。在锁操作时总是在总线上声言LOCK#信号。但在 P6 和目前的处理器中如果访问的内存区域已经缓存在处理器内部则不会声言LOCK#信号。相反它会锁定这块内存区域的缓存并回写到内存并使用缓存一致性机制来确保修改的原子性此操作被称为“缓存锁定”缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。2一个处理器的缓存回写到内存会导致其他处理器的缓存无效。IA-32 处理器和 Intel 64 处理器使用 MESI修改、独占、共享、无效控制协议去维护内部缓存和其他处理器缓存的一致性。在多核处理器系统中进行操作的时候IA-32 和 Intel 64 处理器能嗅到其他的处理器访问系统内存和它们的内部缓存。处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。例如在 Pentium 和 P6 family 处理器中如果通过嗅探一个处理器来监测其他处理器打算写内存地址而这个地址当前处于共享状态那么正在嗅探的处理器将使它的缓存行无效在下次访问相同内存地址时强制执行缓存行填充。 synchronized 是如何实现同步的 先来看下利用 synchronized 实现同步的基础Java 中的每一个对象都可以作为锁。具体表现为以下 3 种形式。 对于普通同步方法锁是当前实例对象进入同步代码前要获得当前实例的锁对于静态同步方法锁是当前类的Class对象进去同步代码前要获得当前类对象的锁对于同步方法块锁是synchonized()括号里配置的对象。这需要指定加锁的对象进入同步代码前要获得指定对象的锁。 当一个线程试图访问同步代码块时它首先必须得到锁退出或抛出异常时必须释放锁。那么锁到底存在哪里呢锁里面会存储什么信息呢 从 JVM 规范中可以看到 Synchonized 在 JVM 里的实现原理JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步但两者的实现细节不一样。代码块同步是使用 monitorenter 和 monitorexit 指令实现的而方法同步是使用另外一种方式实现的细节在 JVM 规范里并没有详细说明。但是方法的同步同样可以使用这两个指令来实现。 monitorenter 指令是在编译后插入到同步代码块的开始位置而 monitorexit 是插入到方法结束处和异常处JVM 要保证每个 monitorenter 必须有对应的 monitorexit 与之配对。任何对象都有一个 monitor 与之关联当一个 monitor 被持有后它将处于锁定状态。线程执行到 monitorenter 指令时将会尝试获取对象所对应的 monitor 的所有权即尝试获得对象的锁。 synchronized 用的锁是存在 Java 对象头里的。如果对象是数组类型则虚拟机用 3 个字宽Word存储对象头如果对象是非数组类型则用 2 字宽存储对象头。在 32 位虚拟机中1 字宽等于 4 字节即 32bit如表2-2所示。 Java 对象头里的 Mark Word 里默认存储对象的HashCode、分代年龄和锁标记位。32 位 JVM 的 Mark Word 的默认存储结构 在运行期间Mark Word 里存储的数据会随着锁标志位的变化而变化。Mark Work 可能变化为存储以下 4 种数据如表2-4所示。 在 64 位虚拟机下Mark Word 是 64bit 大小的 其存储结构如图2-5所示。 处理器如何实现原子操作 32 位 IA-32 处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存当中读取或者写入一个字节是原子的意思是当一个处理器读取一个字节时其他处理器不能访问这个字节的内存地址。奔腾6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的但是复杂的内存操作处理器不能自动保证其原子性比如跨总线宽度跨多个缓存行跨页表的访问。但是处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。 1使用总线锁保证原子性 第一个机制是通过总线锁保证原子性。处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个 LOCK# 信号当一个处理器在总线上输出此信号时其他处理器的请求将被阻塞住那么该处理器可以独占共享内存。 2使用缓存锁保证原子性 第二个机制是通过缓存锁定保证原子性。在同一时刻我们只需保证对某个内存地址的操作是原子性即可但总线锁定把 CPU 和内存之间的通信锁住了这使得锁定期间其他处理器不能操作其他内存地址的数据所以总线锁定的开销比较大目前处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。 频繁使用的内存会缓存在处理器的 L1L2 和 L3 高速缓存里那么原子操作就可以直接在处理器内部缓存中进行并不需要声明总线锁在奔腾6和最近的处理器中可以使用“缓存锁定”的方式来实现复杂的原子性。所谓“缓存锁定”是指内存区域如果被缓存在处理器的缓存行中并且在Lock操作期间被锁定那么当它执行锁操作回写内存时处理器不在总线上声言LOCK信号而是修改内部的内存地址并允许它的缓存一致性机制来保证操作的原子性因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据当其他处理器回写已被锁定的缓存行的数据时会使缓存行无效。 但是有两种情况下处理器不会使用缓存锁定。 第一种情况是当操作的数据不能被缓存在处理器内部或操作的数据跨多个缓存行(cache line)则处理器会调用总线锁定。第二种情况是有些处理器不支持缓存锁定。对于Inter486和奔腾处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。 JAVA 如何实现原子操作 在java中可以通过锁和循环 CAS的方式来实现原子操作。 1使用循环 CAS 实现原子操作 JVM 中的 CAS 操作正是利用了处理器提供的 CMPXCHG 指令实现的。自旋 CAS 实现的基本思路就是循环进行 CAS 操作直到成功为止以下代码实现了一个基于 CAS 线程安全的计数器方法safeCount和一个非线程安全的计数器count。 import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger;public class CASTest {private AtomicInteger atomicI new AtomicInteger(0);private int i 0;public static void main(String[] args) throws InterruptedException {final CASTest cas new CASTest();ListThread ts new ArrayList(600);long start System.currentTimeMillis();for (int j 0; j 100; j) {Thread t new Thread(() - {for (int i 0; i 10000; i) {cas.count();cas.safeCount();}});ts.add(t);}for (Thread t : ts) {t.start();}// 等待所有线程执行完成for (Thread t : ts) {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}}TimeUnit.SECONDS.sleep(1);System.out.println(cas.i);System.out.println(cas.atomicI.get());System.out.println(System.currentTimeMillis() - start);}/** 使用CAS实现线程安全计数器 */private void safeCount() {for (;;) {int i atomicI.get();boolean suc atomicI.compareAndSet(i, i);if (suc) {break;}}}/** 非线程安全计数器 */private void count() {i;} }2CAS 实现原子操作的三大问题 ABA问题如果一个值原来是 A变成了 B又变成了 A那么使用 CAS 进行检查时会发现它的值没有发生变化。解决方案JDK 的 Atomic 包里提供了一个类 AtomicStampedReference 来解决ABA问题循环时间长开销大自旋 CAS 如果长时间不成功会给 CPU 带来非常大的执行开销只能保证一个共享变量的原子操作我们可以使用循环 CAS 的方式来保证原子操作但是对多个共享变量操作时循环 CAS 就无法保证操作的原子性这个时候就可以用锁。 3使用锁机制实现原子操作 锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM 内部实现了很多种锁机制有偏向锁、轻量级锁和互斥锁。有意思的是除了偏向锁JVM 实现锁的方式都用了循环 CAS即当一个线程想进入同步块的时候使用循环 CAS 的方式来获取锁当它退出同步块的时候使用循环 CAS 释放锁。 主内存和本地内存结构 Java 线程之间的通信由 Java 内存模型JMM控制JMM 决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度看JMM 定义了线程和主内存之间的抽象关系线程之间的共享变量存储在主内存main memory中每个线程都有一个私有的本地内存local memory本地内存中存储了该线程以读/写共享变量的副本。本地内存是 JMM 的一个抽象概念并不真实存在。本地内存它涵盖了缓存写缓冲区寄存器以及其他的硬件和编译器优化之后的一个数据存放位置。 从上图来看线程A与线程B之间如要通信的话必须要经历下面2个步骤 首先线程A把本地内存A中更新过的共享变量刷新到主内存中去。然后线程B到主内存中去读取线程A之前已更新过的共享变量。 下面通过示意图来说明这两个步骤 从源代码到指令序列的重排序 在执行程序时为了提高性能编译器和处理器常常会对指令做重排序。重排序分 3 种类型 编译器优化的重排序。编译器在不改变单线程程序语义的前提下可以重新安排语句的执行顺序。指令级并行的重排序。现代处理器采用了指令级并行技术Instruction-Level Parallelism ILP来将多条指令重叠执行。如果不存在数据依赖性处理器可以改变语句对应机器指令的执行顺序。内存系统的重排序。由于处理器使用缓存和读/写缓冲区这使得加载和存储操作看上去可能是在乱序执行。 从 Java 源代码到最终实际执行的指令序列会分别经历下面三种重排序 上述的1属于编译器重排序2和3属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。对于编译器JMM 的编译器重排序规则会禁止特定类型的编译器重排序不是所有的编译器重排序都要禁止。对于处理器重排序JMM 的处理器重排序规则会要求 java 编译器在生成指令序列时插入特定类型的内存屏障memory barriersintel称之为memory fence指令通过内存屏障指令来禁止特定类型的处理器重排序不是所有的处理器重排序都要禁止。 JMM 属于语言级的内存模型它确保在不同的编译器和不同的处理器平台之上通过禁止特定类型的编译器重排序和处理器重排序为程序员提供一致的内存可见性保证。 happens-before 的定义 从 JDK 5 开始Java使用新的 JSR-133 内存模型JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中如果一个操作执行的结果需要对另一个操作可见那么这两个操作之间必须存在happens-before关系。这里提到的两个操作既可以是在一个线程之内也可以是在不同线程之间。 JSR-133 对happens-before关系的定义如下 1如果一个操作 happens-before 另一个操作那么第一个操作的执行结果将对第二个操作可见而且第一个操作的执行顺序排在第二个操作之前。2两个操作之间存在 happens-before 关系并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果与按 happens-before 关系来执行的结果一致那么这种重排序并不非法也就是说JMM允许这种重排序。 与程序员密切相关的happens-before规则如下 程序顺序规则一个线程中的每个操作happens-before 于该线程中的任意后续操作。存在依赖关系的代码监视器锁规则对一个锁的解锁happens-before 于随后对这个锁的加锁。及时刷新主存原子化一组指令volatile变量规则对一个volatile域的写happens-before 于任意后续对这个volatile域的读。及时刷新主存禁止与前面的指令重排传递性如果 A happens-before B且 B happens-before C那么 A happens-before C。线程启动规则Thread 对象的 start() 方法先行发生于此线程的每一个动作线程中断规则对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生线程终结规则线程中所有的操作都先行发生于线程的终止检测我们可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值手段检测到线程已经终止执行对象终结规则一个对象的初始化完成先行发生于他的 finalize() 方法的开始 一个 happens-before 规则对应于一个或多个编译器和处理器重排序规则。 上面的 1是 JMM 对程序员的承诺。 从程序员的角度来说可以这样理解 happens-before 关系如果A happens-before B那么Java内存模型将向程序员保证——A操作的结果将对B可见且A的执行顺序排在B之前。注意这只是Java内存模型向程序员做出的保证 上面的 2是 JMM 对编译器和处理器重排序的约束原则。 正如前面所言JMM 其实是在遵循一个基本原则只要不改变程序的执行结果指的是单线程程序和正确同步的多线程程序编译器和处理器怎么优化都行。JMM 这么做的原因是程序员对于这两个操作是否真的被重排序并不关心程序员关心的是程序执行时的语义不能被改变即执行结果不能被改变。因此happens-before 关系本质上和 as-if-serial 语义是一回事。 as-if-serial语义 as-if-serial语义的意思是不管怎样重排序(编译器和处理器为了提高并行度), (单线程)程序的执行结果不能被改变。编译器、runtime、和处理器都必须遵守as-if-serial语义。 为了遵守as-if-serial语义编译器和处理器不会对存在数据依赖关系的操作做重排序因为这种重排序会改变执行结果。但是如果操作之间不存在数据依赖关系这些操作就可能被编译器和处理器重排序。 as-if-serial 语义保证单线程内程序的执行结果不被改变happens-before 关系保证正确同步的多线程程序的执行结果不被改变。as-if-serial 语义给编写单线程程序的程序员创造了一个幻境单线程序是按程序的顺序来执行的。happens-before 关系给编写正确同步的多线程程序的程序员创造了一个幻境正确同步的多线程程序是按 happens-before 指定的顺序来执行的。 as-if-serial 语义和 happens-before 这么做的目的都是为了在不改变程序执行结果的前提下尽可能地提高程序执行的并行度。 程序顺序规则 double pi 3.14; // A double r 1.0; // B double area pi * r * r; // C 根据 happens-before 的程序顺序规则上面计算圆面积的示例代码存在 3 个 happens-before 关系 1A happens-before B。 2B happens-before C。 3A happens-before C。 这里的第3个happens-before关系是根据happens-before的传递性推导出来的。 这里 A happens- before B但实际执行时B却可以排在A之前执行。如果A happens-before BJMM并不要求A一定要在B之前执行。JMM仅仅要求前一个操作执行的结果对后一个操作可见且前一个操作按顺序排在第二个操作之前。这里操作A的执行结果不需要对操作B可见而且重排序操作A和操作B后的执行结果与操作A和操作B按happens- before顺序执行的结果一致。在这种情况下JMM会认为这种重排序并不非法not illegalJMM允许这种重排序。 顺序一致性内存模型 特性 一个线程中的所有操作必须按照程序的执行顺序来执行所有的线程都只能看到一个单一的操作执行顺序不管是否正确同步每个操作都必须原子执行且立刻对所有线程可见。 在概念上顺序一致性模型有一个单一的全局内存这个内存通过一个左右摆动的开关可以连接到任意一个线程同时每一个线程必须按照程序的顺序来执行内存的读/写操作。上图中可以看出 在任意时刻最多只有一个线程可以连接到内存。因此在多线程并发执行时图中的开关装置能把所有的内存读/写操作串行化即在顺序一致性模型中所有操作之间具有全序关系。 假设线程A和线程B使用监视器锁来正确同步A线程的3个操作执行后释放监视器锁随后B线程获取同一个监视器锁。那么程序在顺序一致性模型中的执行效果如下所示 假设线程A和线程B没有做同步那么这个未同步的程序在顺序一致性模型中的另一种可能的效果如下所示 未同步程序在顺序一致性模型中虽然整体执行顺序是无序的但是所有线程都只能看到一个一致的整体执行顺序。以上图为例线程A和B看到的执行顺序都是A1 - B1 - A2 - B2 - A3 - B3。之所以能得到这个保证是因为顺序一致性内存模型中的每个操作必须立即对任意线程可见。 但是在 JMM 中就没有这个保证。未同步程序在 JMM 中不但整体的执行顺序是无序的而且所有线程看到的操作执行顺序也可能不一致。比如在当前线程把写过的数据缓存在本地内存中在没有刷新到主内存之前这个写操作仅对当前线程可见从其他线程的角度来观察会认为这个写操作根本不被当前线程执行。只有当前线程把本地内存中写过的数据刷新到主内存之后这个写操作才能对其他线程可见。这种情况就会出现多种运行结果。 JMM 在具体实现上的基本方针为在不改变正确同步程序执行结果的前提下尽可能为编译器和处理器的优化打开方便大门。 顺序一致性模型中所有操作完全按程序的顺序串行执行。而在 JMM 中临界区内的代码可以重排序但 JMM 不允许临界区的代码“逸出”到临界区之外那样会破坏监视器锁的语义。JMM 会在进入临界区和退出临界区的关键时间点做一些特殊处理使得线程在这两个时间点具有顺序一致性模型中相同的内存视图。虽然线程 A 在临界区内做了重排序但由于监视锁互斥执行的特性这里线程 B 无法“观察”到线程 A 在临界区内的重排序。这种重排序既提高了执行效率又没有改变程序的执行结果。 long 和 double 类型的操作 在一些 32 位的处理器上如果要求对 64 位数据的写操作具有原子性那么会有非常大的同步开销。为了照顾这种处理器Java 语言规范中鼓励但不强求 JVM 对 64 位 long 型和 double 类型的变量写操作具有原子性。当 JVM 在这种处理器上运行时可能会把一个 64 位的 long/double 变量的写操作拆成两个 32 位写操作来执行。这两个 32 位的写操作可能会被分配到不同的总线事务中执行此时对这个 64 位变量的写操作不具备原子性。 当单个内存操作不具有原子性时可能会产生意想不到的后果。 如上图假设处理器 A 写一个long类型的变量同时处理器 B 要读这个long类型的变量。处理器 A 中 64 位的写操作被拆分成两个 32 位的写操作且这两个 32 位的写操作被分配到不同的事务中执行。同时处理器 B 中 64 位的读操作被分配到单个读事务中执行。当处理器 A 和 B 按照上面的时序来执行时处理器 B 将看到仅仅被处理器 A “写了一半” 的无效值。 注意在 JSR-133 的旧内存模型中一个 64 位的 long/double 变量的读/写操作可以被拆分成两个 32 位的读/写操作来执行。从 JSR-133 内存模型开始即JDK1.5仅仅允许把一个 64 位 long/double 变量的写操作拆分成两个 32 位的写操作来执行任意的读操作在 JSR-133 中都必须具有原子性。 volatile 写 - 读的内存语义 volatile写的内存语义当写一个 volatile 变量时JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。volatile读的内存语义当读一个 volatile 变量时JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。 下面对 volatile 写和 volatile 读的内存语义做个总结 线程A写一个volatile变量实质上是线程A向接下来将要读这个volatile变量的某个线程发出了其对共享变量所做修改的消息。线程B读一个volatile变量实质上是线程B接受了之前某个线程发出的在写这个volatile变量之前对共享变量所做的修改的消息。线程A写一个volatile变量随后线程B读这个volatile变量这个过程实质上是线程A通过主内存向线程B发送消息。 为了实现 volatile 的内存语义编译器在生成字节码时会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说发现一个最优布置来最小化插入屏障的总数几乎不可能。为此JMM 采取保守策略。下面是基于保守策略的JMM内存屏障插入策略。 在每个volatile写操作前面插入一个StoreStore屏障在每个Volatile写操作后面插进入一个StoreLoad屏障在每个volatile读操作的后面插入一个LoadLoad屏障在每个volatile读操作的后面插入LoadStore屏障 上述内存屏障插入策略非常保守但它可以保证在任意处理器平台任意程序中都能得到正确的 volatile 内存语义。 锁的释放和获取的内存语义 当线程释放锁时JMM 会把该线程对应的本地内存中的共享变量刷新到主内存中。当线程获取锁时JMM 会把该线程对应的本地内存置为无效。从而使得被监视器包含的临界区代码必须从主内存中读取共享变量。 如下图 对比锁释放 - 获取的内存语义与volatile写-读的内存语义可以看出锁释放与 volatile 写有相同的内存语义锁获取与 volatile 读有相同的内存语义。 下面对锁释放和锁获取的内存语义做个总结 线程A释放一个锁实质上是线程A向接下来将要获取这个锁的某个线程发出了线程A 对共享变量所做修改的消息。线程B获取一个锁实质上是线程B接收了之前某个线程发出的在释放这个锁之前对共 享变量所做修改的消息。线程A释放锁随后线程B获取这个锁这个过程实质上是线程A通过主内存向线程B发送消息。 ReentrantLock class ReentrantLockExample {int a 0;ReentrantLock lock new ReentrantLock();public void writer() {lock.lock(); // 获取锁try {a;} finally {lock.unlock(); // 释放锁}}public void reader() {lock.lock(); // 获取锁try {int i a;...} finally {lock.unlock(); // 释放锁}} }在ReentrantLock中调用lock()方法获取锁调用unlock()方法释放锁。 ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer(AQS)。AQS使用一个整型的volatile变量state来维护同步状态这个volatile变量是ReentrantLock内存语义实现的关键。 ReentrantLock分为公平锁和非公平锁我们首先分析公平锁。 使用公平锁时加锁方法lock()调用轨迹如下 1ReentrantLocklock() 2FairLocklock() 3AbstractQueuedSynchronizeracquire(int arg) 4ReentrantLocktryAcquire(int acquires)。 第四步真正开始加锁下面是该方法的源码 /** * Fair version of tryAcquire. Dont grant access unless * recursive call or no waiters or is first. */ protected final boolean tryAcquire(int acquires) {final Thread current Thread.currentThread();int c getState(); // 获取锁的开始首先读取 volatile 变量 stateif (c 0) {if (!hasQueuedPredecessors() compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}} else if (current getExclusiveOwnerThread()) {int nextc c acquires;if (nextc 0)throw new Error(Maximum lock count exceeded);setState(nextc);return true;}return false; }从上面源码可以看出加锁方法首先读volatile变量state。 在使用公平锁时解锁方法unlock()调用轨迹如下 1ReentrantLockunlock() 2AbstractQueuedSynchronizerrelease(int arg) 3SynctryRelease(int releases)。 第三步真正开始释放锁下面是该方法的源码 protected final boolean tryRelease(int releases) {int c getState() - releases;if (Thread.currentThread() ! getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free false;if (c 0) {free true;setExclusiveOwnerThread(null);}setState(c); // 释放锁的最后写 volatile 变量 statereturn free; }从上面源码可以看出在释放锁的最后写 volatile 变量 state。 公平锁在释放锁的最后写volatile变量state在获取锁时首先读这个volatile变量。根据volatile的happens-before规则释放锁的线程在写volatile变量之前可见的共享变量在获取锁的线程读取同一个volatile变量后立即变得对获取锁的线程可见。 现在我们来分析非公平锁的内存语义的实现。非公平锁的释放和公平锁完全一样所以这里仅仅分析非公平锁的获取。使用非公平锁时加锁方法lock()调用轨迹如下 1ReentrantLocklock() 2NonFairSynclock() 3AbstractQueuedSynchronizercompareAndSetState(int expect,int update)。 第三步真正开始加锁下面是该方法源码 /** * 该方法以原子操作的方式更新state变量JDK中对该方法的说明如果当前状态值等于预期值 * 则以原子方式将同步状态设置为给定的更新值。此操作具有volatile读和写的内存语义。 */ protected final boolean compareAndSetState(int expect, int update) {// See below for intrinsics setup to support thisreturn unsafe.compareAndSwapInt(this, stateOffset, expect, update); }该方法以原子操作的方式更新state变量本文把 Java 的 compareAndSet() 方法调用简称为 CAS。JDK文档对该方法的说明如下如果当前状态值等于预期值则以原子方式将同步状态设置为给定的更新值。此操作具有volatile读和写的内存语义。 现在对公平锁和非公平锁的内存语义做个总结 公平锁和非公平锁释放时最后都要写一个volatile变量state公平锁获取时首先会去读volatile变量非公平锁获取时首先会用 CAS 更新volatile变量这个操作同时具有volatile读和写的内存语义。 从本文对 ReentrantLock 的分析可以看出锁释放-获取的内存语义的实现至少有下面两种方式 1利用 volatile 变量的写-读所具有的内存语义2利用 CAS 所附带的 volatile 读和 volatile 写的内存语义 conurrent包的实现 由于Java的CAS同时具有volatile读和volatile写的内存语义因此Java线程之间的通信现在有了下面4种方式 1A 线程写 volatile 变量随后 B 线程读这个 volatile 变量。 2A 线程写 volatile 变量随后 B 线程用 CAS 更新这个 volatile 变量。 3A 线程用 CAS 更新一个volatile变量随后 B 线程用 CAS 更新这个volatile变量。 4A 线程用 CAS 更新一个 volatile 变量随后 B 线程读这个 volatile 变量。 原子方式对内存执行读 - 改 - 写操作这是在多处理器中实现同步的关键。同时volatile变量的读/写和CAS可以实现线程之间的通信。把这些整合在一起就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源码会发现一个通用化的实现模式 首先声明共享变量为 volatile 然后使用 CAS 的原子条件更新来实现线程之间的同步 同时配以 volatile 的读/写和 CAS 所具有的 volatile 读和写的内存语义来实现线程之间的通信。 AQS、非阻塞数据结构和原子变量类这些 concurrent 包中的基础类都是使用这种模式来实现的而concurrent包中的高层类又是依赖于这些基础类来实现的。 final 域的内存语义 final 域的重排规则 对于 final 域编译器和处理器都要遵守两个重排序规则 1在构造函数内对一个 final 域的写入与随后把这个被构造对象的引用赋值给一个引用变量这两个操作之间不能重排序。2初次读一个包含 final 域的对象的引用与随后初次读这个 final 域这两个操作之间不能重排序。 写 final 域重排序规则 写 final 域的重排序规则禁止把 final 域的写重排序到构造函数之外这个规则的实现主要包含了两个方面 1JMM 禁止编译器把 final 域的写重排序到构造函数之外2编译器会在 final 域写之后构造函数 return 之前插入一个 storestore 屏障。这个屏障可以禁止处理器把 final 域的写重排序到构造函数之外。 写 final 域的重排序规则可以确保在对象引用为任意线程可见之前对象的 final 域已经被正确初始化过了而普通域就不具有这个保障。 读 final 域重排序规则 读 final 域重排序规则为在一个线程中初次读对象引用和初次读该对象包含的 final 域JMM 会禁止这两个操作的重排序。注意这个规则仅仅是针对处理器处理器会在读 final 域操作的前面插入一个 LoadLoad 屏障。实际上读对象的引用和读该对象的 final 域存在间接依赖性一般处理器不会重排序这两个操作。但是有一些处理器会重排序因此这条禁止重排序规则就是针对这些处理器而设定的。 读 final 域的重排序规则可以确保在读一个对象的 final 域之前一定会先读这个包含这个 final 域的对象的引用。 final 域为引用类型 上面我们看到的 final 域是基础数据类型如果 final 域是引用数据类型将会有什么效果请看下面示例代码。 public class FinalRefrenceExample { final int[] intArray; // final 是引用类型static FinalReferencExample obj;public FinalReferenceExample() { // 构造函数 intArray new int[1]; // 1 intArray[0] 1; // 2 }public static void writeOne() { // 写线程 A 执行 obj new FinalReferenceExample(); // 3}public static void writerTwo() { // 写线程 B 执行 obj.intArray[0] 2; // 4 }public static void reader() { // 读线程 C 执行 if (obj ! null) { // 5 int templ obj.intArray[0]; // 6 }} }针对引用数据类型写 final 域针的重排序规则对编译器和处理器增加了如下约束在构造函数内对一个 final 修饰的对象的成员域的写入与随后在构造函数之外把这个被构造的对象的引用赋给一个引用变量这两个操作是不能被重排序的。 对上面的示例程序假设首先线程 A 执行 writerOne() 方法执行完后线程 B 执行 writerTwo() 方法执行完后线程 C 执行 reader() 方法。下图是一种可能的线程执行时序。 在上图中1是对 final 域的写入2 是对这个 final 域引用的对象的成员域的写入3 是把被构造的对象的引用赋值给某个引用变量。这里除了前面提到的 1 不能和 3 重排序外2 和 3 也不能重排序。 JMM 可以确保读线程 C 至少能看到写线程 A 在构造函数中对 final 引用对象的成员域的写入。即 C 至少能看到数组下标 0 的值为 1。而写线程 B 对数组元素的写入读线程 C 可能看得到也可能看不到。JMM 不保证线程 B 的写入对读线程 C 可见因为写线程 B 和读线程 C 之间存在数据竞争此时的执行结果不可预知。 如果想要确保读线程 C 看到写线程 B 对数组元素的写入写线程 B 和读线程 C 之间需要使用同步原语lock 或 volatile来确保内存可见性。 为什么 final 引用不能从构造函数中“溢出” 前面提到写 final 域的重排序规则可以确保在引用变量为任意线程可见之前该引用变量指向的对象的 final 域已经在构造函数中被正确初始化了。其实要得到这个效果还需要一个保证在构造函数内部不能让这个被构造对象的引用为其他线程所见也就是对象引用不能在构造函数中“逸出”。为了说明这个问题让我们来看下面的示例代码 public class FinalReferenceEscapeExample { final int i;static FinalReferenceEscapeExample obj; public FinalReferenceEscapeExample() { i 1; // 1 写 final 域 obj this; // 2 this 引用在此“逸出”}public static void writer() {new FinalReferenceEscapeExample(); }public static void reader() {if (obj ! null) { // 3 int temp obj.i; // 4}} } 假设一个线程 A 执行 writer() 方法另一个线程 B 执行 reader() 方法。这里的操作 2 使得对象还未完成构造前就为线程 B 可见。即使这里的操作 2 是构造函数的最后一步且在程序中操作 2 排在操作 1 后面执行 read() 方法的线程仍然可能无法看到 final 域被初始化后的值因为这里的操作 1 和 操作 2 之间可能被重排序。实际的执行时序可能如图3-32所示。 从图 3-32 可以看出在构造函数返回前被构造对象的引用不能为其他线程所见因为此时的 final 域可能还没有被初始化。在构造函数返回后任意线程都将保证能看到 final 域正确初始化之后的值。 JSR-133 为什么要增强 final 语义 在旧的 Java 内存模型中一个最严重的缺陷就是线程可能看到 final 域的值会改变。比如一个线程当前看到一个整型 final 域的值为 0还未初始化之前的默认值过一段时间之后这个线程再去读这个 final 域的值时却发现值变为 1被某个线程初始化之后的值。最常见的例子就是在旧的 Java 内存模型中String 的值可能会发生改变。 为了修补这个漏洞JSR-133 专家组增强了 final 的语义。通过为 final 域增加写和读重排序规则可以为 Java 程序员提供初始化安全保证只要对象是正确构造的被构造对象的引用在构造函数中没有“逸出”那么不需要使用同步指 lock 和 volatile 的使用就可以保证任意线程都能看到这个 final 域在构造函数中被初始化后的值。 双重检查锁定的由来 public class UnsafeLazyInitialization {private static Instance instance; public static Instance getInstance() {if (instance null) { // 1A 线程执行instance new Instance(); // 2: B 线程执行}return instance;} }在UnsafeLazyInitialization中假设 A 线程执行代码 1 的同时B 线程执行代码 2。此时线程 A 可能会看到instance引用的对象还没有完成初始化出现这种情况的原因见后文的“问题的根源”。 对于UnsafeLazyInitialization我们可以对getInstance()方法做同步处理来实现线程安全的延迟初始化。示例代码如下 由于对getInstance()做了同步处理synchronized将导致性能开销。如果getInstance()被多个线程频繁的调用 将会导致程序执行性能的下降。反之如果getInstance()不会被多个线程频繁的调用那么这个延迟初始化方案将能提供令人满意的性能。 在早期的 JVM 中synchronized甚至是无竞争的synchronized存在这巨大的性能开销。因此人们想出了一个“聪明”的技 巧双重检查锁定double-checked locking。人们想通过双重检查锁定来降低同步的开销。下面是使用双重检查锁定来实现延迟初始化的示例代码 如上面代码所示如果第一次检查instance不为null那么就不需要执行下面的加锁和初始化操作。因此可以大幅降低synchronized带来的性能开销。上面代码表面上看起来似乎两全其美 在多个线程试图在同一时间创建对象时会通过加锁来保证只有一个线程能创建对象。在对象创建好之后执行getInstance()将不需要获取锁直接返回已创建好的对象。 双重检查锁定看起来似乎很完美但这是一个错误的优化在线程执行到第4行代码读取到instance不为null时instance引用的对象有可能还没有完成初始化。 问题的根源 前面的双重检查锁定示例代码的第7行instance new Singleton();创建一个对象。这一行代码可以分解为如下的 3 行伪代码 上面三行伪代码中的 2 和 3 之间可能会被重排序。2 和 3 之间重排序之后的执行时序如下 根据Java语言规范所有线程在执行java程序时必须要遵守 intra-thread semantics。intra-thread semantics 保证重排序不会改变单线程内的程序执行结果。换句话来说intra-thread semantics 允许那些在单线程内不会改变单线程程序执行结果的重排序。上面三行伪代码的 2 和 3 之间虽然被重排序了但这个重排序并不会违反 intra-thread semantics。这个重排序在没有改变单线程程序的执行结果的前提下可以提高程序的执行性能。 为了更好的理解intra-thread semantics请看下面的示意图假设一个线程A在构造对象后立即访问这个对象 如上图所示只要保证 2 排在 4 的前面即使 2 和 3 之间重排序了也不会违反 intra-thread semantics。 下面再让我们看看多线程并发执行的时候的情况。请看下面的示意图 由于单线程内要遵守 intra-thread semantics从而能保证 A 线程的程序执行结果不会被改变。但是当线程 A 和 B 按上图的时序执行时B 线程将看到一个还没有被初始化的对象。 回到本文的主题DoubleCheckedLocking示例代码的第 7 行instance new Singleton();如果发生重排序另一个并发执行的线程 B 就有可能在第 4 行判断 instance 不为 null。线程 B 接下来将访问 instance 所引用的对象但此时这个对象可能还没有被 A 线程初始化下面是这个场景的具体执行时序 这里 A2 和 A3 虽然重排序了但 Java 内存模型的 intra-thread semantics 将确保 A2 一定会排在 A4 前面执行。因此线程 A 的 intra-thread semantics 没有改变。但 A2 和 A3 的重排序将导致线程 B 在 B1 处判断出instance不为空线程 B 接下来将访问instance引用的对象。此时线程 B 将会访问到一个还未初始化的对象。 在知晓了问题发生的根源之后我们可以想出两个办法来实现线程安全的延迟初始化 不允许 2 和 3 重排序 允许 2 和 3 重排序但不允许其他线程“看到”这个重排序。 后文介绍的两个解决方案分别对应于上面这两点。 基于volatile的双重检查锁定的解决方案 对于前面的基于双重检查锁定来实现延迟初始化的方案指DoubleCheckedLocking示例代码我们只需要做一点小的修改把instance声明为volatile型就可以实现线程安全的延迟初始化。请看下面的示例代码 当声明对象的引用为volatile后“问题的根源”的三行伪代码中的 2 和 3 之间的重排序在多线程环境中将会被禁止。上面示例代码将按如下的时序执行 基于类初始化的解决方案 JVM在类的初始化阶段即在Class被加载后且被线程使用之前会执行类的初始化。在执行类的初始化期间JVM 会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。 基于这个特性可以实现另一种线程安全的延迟初始化方案这个方案被称之为Initialization On Demand Holder idiom public class Instance {private Instance() {}private static class InstanceHolder {public static Instance instance new Instance(); }public static Instance getInstance() {return InstanceHolder.instance; // 这里将导致InstanceHoLder类被初始化 } }假设两个线程并发执行getInstance()下面是执行的示意图 这个方案的实质是允许“问题的根源”的三行伪代码中的 2 和 3 重排序但不允许非构造线程这里指线程B“看到”这个重排序。 初始化一个类包括执行这个类的静态初始化和初始化在这个类中声明的静态字段。根据Java语言规范在首次发生下列任意一种情况时一个类或接口类型 T 将被立即初始化 1T 是一个类而且一个 T 类型的实例被创建2T 是一个类且 T 中声明的一个静态方法被调用3T 中声明的一个静态字段被赋值4T 中声明的一个静态字段被使用而且这个字段不是一个常量字段5T 是一个顶级类top level class见java语言规范的§7.6而且一个断言语句嵌套在 T 内部被执行。 在 InstanceFactory 示例代码中首次执行 getInstance() 的线程将导致 InstanceHolder 类被初始化符合情况4。 由于Java语言是多线程的多个线程可能在同一时间尝试去初始化同一个类或接口比如这里多个线程可能在同一时刻调用getInstance()来初始化InstanceHolder类。因此在Java中初始化一个类或者接口时需要做细致的同步处理。 Java语言规范规定对于每一个类或接口C都有一个唯一的初始化锁LC与之对应。从C到LC的映射由JVM的具体实现去自由实现。 JVM在类初始化期间会获取这个初始化锁并且每个线程至少获取一次锁来确保这个类已经被初始化过了。 通过对比基于volatile的双重检查锁定的方案和基于类初始化的方案我们会发现基于类初始化的方案的实现代码更简洁。 但基于volatile的双重检查锁定的方案有一个额外的优势除了可以对静态字段实现延迟初始化外还可以对实例字段实现延迟初始化。 延迟初始化降低了初始化类或创建实例的开销但增加了访问被延迟初始化的字段的开销。在大多数时候正常的初始化要优于延迟初始化。 如果确实需要对实例字段使用线程安全的延迟初始化请使用上面介绍的基于 volatile 的延迟初始化的方案如果确实需要对静态字段使用线程安全的延迟初始化请使用上面介绍的基于类初始化的方案。
http://www.pierceye.com/news/409214/

相关文章:

  • 正规品牌网站设计推荐如何上传自己的做的网站
  • 企业网站优化甲薇g71679做同等效果下拉词制作手机网站哪家好
  • 物流运输做网站的素材多用户商城系统价格
  • 营销型网站建设流程电脑怎么建网站
  • 郑州市汉狮做网站360免费建站
  • 安阳哪里有学做网站的学校做个公众号需要多少钱
  • 建站seo是什么成都做营销型网站
  • 网站建设哪个wordpress分类title
  • 建手机网站多少钱挂机软件定制
  • 网站建设 提案 框架河南一般建一个网站需要多少钱
  • 福建省建设人才市场网站深圳营销型网站建设优化
  • 晋城购物网站开发设计宣传网站有哪些
  • 在哪人网站要以接it项目做企业为什么要分析环境
  • 达令的网站建设wordpress上传视频
  • 织梦免费网站模块下载地址南充楼盘网
  • 深圳极速网站建设服务器做网站 然后百度推广
  • 西充县住房和城乡建设局网站深圳建设局网站打不开
  • 深圳常平网站建设制作公司网站开发qq群
  • 校园网站建设的感受论文专业微信网站建设公司首选
  • 国外免费logo设计网站免费网课平台
  • 高端网站设计定制公司页面跳转自动更新
  • 项目建设资金来源网站网站开发技术可以做什么工作
  • 可做易企秀的网站网页建站网站
  • 南京网站建设价格大型网站开发协调
  • 园林景观设计公司点评的网站和论坛大型网站搜索怎么做的
  • 河南省建设教育培训中心网站广告机器设备的价格表
  • 郑州做网站哪家最好中国能源建设集团有限公司是什么级别
  • 品牌设计公司排行榜前十名seo外包服务公司
  • 潍坊网站建设 58wordpress 酒店预订
  • 个人网站主机选择电商公司官网