《深刻理解Java虚拟机》读书笔记十二

第十二章  Java内存模型与线程html

一、硬件效率与一致性java

  • 因为计算机的存储设备与处理器的运算速度有几个数量级的差距,因此现代计算机系统都不得不加入一层读写速度尽量接近处理器运算速度的高速缓存(Cache)来做为内存与处理器之间的缓冲。
  • 每一个处理器都有本身的高速缓存,而它们又共享同一主内存(Main Memory),当多个处理器的运算任务都涉及同一块主内存区域时,将可能致使各自的缓存数据不一致,为了解决一致性的问题,须要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操做,这类协议有MSI、MESI(Illinois Protocol)、MOSI、Synapse、Firefly及Dragon Protocol等。

二、Java内存模型数据库

主内存和工做内存:编程

  • Java内存模型主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量(Variable)与Java编程中的变量略有区别,它包括实例变量/静态字段和构成数组对象的元素,不包括局部变量和方法参数(线程私有)。Java内存模型和Java内存域没有联系。
  • Java内存模型规定全部变量都存储在主存(Main Memory)中(虚拟机内存的一部分)。每条线程还有本身的工做内存(Working Memory),线程的工做内存保存了被线程使用到的变量的主内存副本拷贝,线程对变量的全部操做(读取/赋值等)都必须在工做内存中进行,而不能直接读写主内存中的变量。不一样线程之间也没法直接访问对方工做内存中的变量,线程间变量值的传递均须要经过主存来完成。

内存间交互操做:数组

  • 关于主内存与工做内存之间具体的交互协议,即一个变量如何从主内存拷贝到工做内存、如何从工做内存同步回主内存之类的实现细节,Java内存模型中定义了如下8种操做来完成。
  1. lock(锁定):做用于主内存的变量,它把一个变量标识为一条线程独占的状态。
  2. unlock(解锁):做用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才能够被其余线程锁定。
  3. read(读取):做用于主内存的变量,它把一个变量的值从主内存传输到线程的工做内存中,以便随后的load动做使用。
  4. load(载入):做用于工做内存的变量,它把read操做从主内存中获得的变量值放入工做内存的变量副本中。
  5. use(使用):做用于工做内存的变量,它把工做内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个须要使用到变量的值的字节码指令时将会执行这个操做。
  6. assign(赋值):做用于工做内存的变量,它把一个从执行引擎接收到的值赋给工做内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操做。
  7. store(存储):做用于工做内存的变量,它把工做内存中一个变量的值传送到主内存中,以便随后的write操做使用。
  8. write(写入):做用于主内存的变量,它把store操做从工做内存中获得的变量的值放入主内存的变量中。
  • 若是要把一个变量从主内存复制到工做内存,那就要顺序地执行read和load操做,若是要把变量从工做内存同步回主内存,就要顺序地执行store和write操做。注意,Java内存模型只要求上述两个操做必须按顺序执行,而没有保证是连续执行。也就是说,read与load之间、store与write之间是可插入其余指令的,如对主内存中的变量a、b进行访问时,一种可能出现顺序是read a、read b、load b、load a。除此以外,Java内存模型还规定了在执行上述8种基本操做时必须知足以下规则:
  1. 不容许read和load、store和write操做之一单独出现,即不容许一个变量从主内存读取了但工做内存不接受,或者从工做内存发起回写了但主内存不接受的状况出现。
  2. 不容许一个线程丢弃它的最近的assign操做,即变量在工做内存中改变了以后必须把该变化同步回主内存。
  3. 不容许一个线程无缘由地(没有发生过任何assign操做)把数据从线程的工做内存同步回主内存中。
  4. 一个新的变量只能在主内存中“诞生”,不容许在工做内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store操做以前,必须先执行过了assign和load操做。
  5. 一个变量在同一个时刻只容许一条线程对其进行lock操做,但lock操做能够被同一条线程重复执行屡次,屡次执行lock后,只有执行相同次数的unlock操做,变量才会被解锁。
  6. 若是对一个变量执行lock操做,那将会清空工做内存中此变量的值,在执行引擎使用这个变量前,须要从新执行load或assign操做初始化变量的值。
  7. 若是一个变量事先没有被lock操做锁定,那就不容许对它执行unlock操做,也不容许去unlock一个被其余线程锁定住的变量。
  8. 对一个变量执行unlock操做以前,必须先把此变量同步回主内存中(执行store、write操做)。

对于volatile型变量的特殊规则:缓存

  • 一个变量定义为volatile以后,它将具有两种特性。第一是保证此变量对全部线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其余线程来讲是能够当即得知的。第二是使用volatile变量经过内存屏障来禁止指令重排序优化。内存屏障是被插入两个 CPU 指令之间的一种指令,用来禁止处理器指令发生重排序(像屏障同样),从而保障有序性的。
  • 因为volatile变量只能保证可见性,在不符合如下两条规则的运算场景中:安全

    • 运算结果并不依赖变量的当前值,或者可以确保只有单一的线程修改变量的值。
    • 变量不须要与其余的状态变量共同参与不变约束。

    仍然要经过加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性。多线程

  • Java内存模型中对volatile变量定义的特殊规则。假定T表示一个线程,V和W分别表示两个volatile型变量,那么在进行read、load、use、assign、store和write操做时须要知足以下规则:
  1. 只有当线程T对变量V执行的前一个动做是load的时候,线程T才能对变量V执行use动做;而且,只有当线程T对变量V执行的后一个动做是use的时候,线程T才能对变量V执行load动做。线程T对变量V的use动做能够认为是和线程T对变量V的load、read动做相关联,必须连续一块儿出现(这条规则要求在工做内存中,每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其余线程对变量V所作的修改后的值)。
  2. 只有当线程T对变量V执行的前一个动做是assign的时候,线程T才能对变量V执行store动做;而且,只有当线程T对变量V执行的后一个动做是store的时候,线程T才能对变量V执行assign动做。线程T对变量V的assign动做能够认为是和线程T对变量V的store、write动做相关联,必须连续一块儿出现(这条规则要求在工做内存中,每次修改V后都必须马上同步回主内存中,用于保证其余线程能够看到本身对变量V所作的修改)。
  3. 假定动做A是线程T对变量V实施的use或assign动做,假定动做F是和动做A相关联的load或store动做,假定动做P是和动做F相应的对变量V的read或write动做;相似的,假定动做B是线程T对变量W实施的use或assign动做,假定动做G是和动做B相关联的load或store动做,假定动做Q是和动做G相应的对变量W的read或write动做。若是A先于B,那么P先于Q(这条规则要求volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同)。

对于long和double型变量的特殊规则:并发

  • Java内存模型要求lock、unlock、read、load、assign、use、store、write这8个操做都具备原子性,可是对于64位的数据类型(long和double),在模型中特别定义了一条相对宽松的规定:容许虚拟机将没有被volatile修饰的64位数据的读写操做划分为两次32位的操做来进行,即容许虚拟机实现选择能够不保证64位数据类型的load、store、read和write这4个操做的原子性,这点就是所谓的long和double的非原子性协定(Nonatomic Treatment ofdouble and long Variables)。

原子性、可见性与有序性:函数

  • Java内存模型是围绕着在并发过程当中如何处理原子性、可见性和有序性这3个特征来创建的,咱们逐个来看一下哪些操做实现了这3个特性。
  •  原子性(Atomicity):由Java内存模型来直接保证的原子性变量操做包括read、load、assign、use、store和write,咱们大体能够认为基本数据类型的访问读写是具有原子性的(例外就是long和double的非原子性协定)。
  • 可见性(Visibility):可见性是指当一个线程修改了共享变量的值,其余线程可以当即得知这个修改。
  • 有序性(Ordering):若是在本线程内观察,全部的操做都是有序的;若是在一个线程中观察另外一个线程,全部的操做都是无序的。

先发生原则:

  • 它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,下面是Java内存模型下一些“自然的”先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,能够在编码中直接使用。若是两个操做之间的关系不在此列,而且没法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机能够对它们随意地进行重排序。
  • 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操做先行发生于书写在后面的操做。准确地说,应该是控制流顺序而不是程序代码顺序,由于要考虑分支、循环等结构。
  • 管程锁定规则(Monitor Lock Rule):一个unlock操做先行发生于后面对同一个锁的lock操做。这里必须强调的是同一个锁,而“后面”是指时间上的前后顺序。
  • volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操做先行发生于后面对这个变量的读操做,这里的“后面”一样是指时间上的前后顺序。
  • 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每个动做。
  • 线程终止规则(Thread Termination Rule):线程中的全部操做都先行发生于对此线程的终止检测,咱们能够经过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
  • 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,能够经过Thread.interrupted()方法检测到是否有中断发生。
  • 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
  • 传递性(Transitivity):若是操做A先行发生于操做B,操做B先行发生于操做C,那就能够得出操做A先行发生于操做C的结论。

三、Java与线程

线程的实现:

  • 实现线程主要有3种方式:使用内核线程实现、使用用户线程实现和使用用户线程加轻量级进程混合实现。
  • 使用内核线程实现:
    • 内核线程(Kernel-Level Thread,KLT)就是直接由操做系统内核(Kernel,下称内核)支持的线程,这种线程由内核来完成线程切换,内核经过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。
    • 每一个内核线程能够视为内核的一个分身,这样操做系统就有能力同时处理多件事情,支持多线程的内核就叫作多线程内核(Multi-Threads Kernel)
    • 程序通常不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是咱们一般意义上所讲的线程,因为每一个轻量级进程都由一个内核线程支持,所以只有先支持内核线程,才能有轻量级进程。
    • 这种轻量级进程与内核线程之间1:1的关系称为一对一的线程模型。
    • 缺点:首先因为是基于内核线程实现的,因此各类线程操做,如建立、析构及同步,都须要进行系统调用。而系统调用的代价相对较高,须要在用户态(User Mode)和内核态(Kernel Mode)中来回切换。其次,每一个轻量级进程都须要有一个内核线程的支持,所以轻量级进程要消耗必定的内核资源(如内核线程的栈空间),所以一个系统支持轻量级进程的数量是有限的。
  • 使用用户线程实现:
    • 从广义上来说,一个线程只要不是内核线程,就能够认为是用户线程(User Thread,UT)而狭义上的用户线程指的是彻底创建在用户空间的线程库上,系统内核不能感知线程存在的实现。
    • 用户线程的创建、同步、销毁和调度彻底在用户态中完成,不须要内核的帮助。若是程序实现得当,这种线程不须要切换到内核态,所以操做能够是很是快速且低消耗的,也能够支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。这种进程与用户线程之间1:N的关系称为一对多的线程模型。
    • 使用用户线程的优点在于不须要系统内核支援,劣势也在于没有系统内核的支援,全部的线程操做都须要用户程序本身处理。使用用户线程实现的程序通常都比较复杂。
  • 使用用户线程加轻量级进程混合实现
    • 在这种混合实现下,既存在用户线程,也存在轻量级进程。用户线程仍是彻底创建在用户空间中,所以用户线程的建立、切换、析构等操做依然廉价,而且能够支持大规模的用户线程并发。
    • 而操做系统提供支持的轻量级进程则做为用户线程和内核线程之间的桥梁,这样可使用内核提供的线程调度功能及处理器映射,而且用户线程的系统调用要经过轻量级线程来完成,大大下降了整个进程被彻底阻塞的风险。
    • 在这种混合模式中,用户线程与轻量级进程的数量比是不定的,即为N:M的关系。许多UNIX系列的操做系统,如Solaris、HP-UX等都提供了N:M的线程模型实现。                                                                                         

Java线程调度:

  •  线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是协同式线程调度(Cooperative Threads-Scheduling)和抢占式线程调度(Preemptive Threads-Scheduling)。
  • 协同式调度:若是使用协同式调度的多线程系统,线程的执行时间由线程自己来控制,线程把本身的工做执行完了以后,要主动通知系统切换到另一个线程上。
  • 抢占式调度:若是使用抢占式调度的多线程系统,那么每一个线程将由系统来分配执行时间,线程的切换不禁线程自己来决定(在Java中,Thread.yield()可让出执行时间,可是要获取执行时间的话,线程自己是没有什么办法的)。在这种实现线程调度的方式下,线程的执行时间是系统可控的,也不会有一个线程致使整个进程阻塞的问题,Java使用的线程调度方式就是抢占式调度。

 状态转换:

Java语言定义了5种线程状态,在任意一个时间点,一个线程只能有且只有其中的一种状态,这5种状态分别以下。

  • 新建(New):建立后还没有启动的线程处于这种状态。
  • 运行(Runable):Runable包括了操做系统线程状态中的Running和Ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间。
  • 无限期等待(Waiting):处于这种状态的线程不会被分配CPU执行时间,它们要等待被其余线程显式地唤醒。如下方法会让线程陷入无限期的等待状态:
    • 没有设置Timeout参数的Object.wait()方法。
    • 没有设置Timeout参数的Thread.join()方法。
    • LockSupport.park()方法。
  • 限期等待(Timed Waiting):处于这种状态的线程也不会被分配CPU执行时间,不过无须等待被其余线程显式地唤醒,在必定时间以后它们会由系统自动唤醒。
    • Thread.sleep()方法。
    • 设置了Timeout参数的Object.wait()方法。
    • 设置了Timeout参数的Thread.join()方法。
    • LockSupport.parkNanos()方法。
    • LockSupport.parkUntil()方法。
  • 阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是:“阻塞状态”在等待着获取到一个排他锁,这个事件将在另一个线程放弃这个锁的时候            发生;而“等待状态”则是在等待一段时间,或者唤醒动做的发生。在程序等待进入同步区域的时候,线程将进入这种状态。
  • 结束(Terminated):已终止线程的线程状态,线程已经结束执行。

第十三章  线程安全与锁优化

一、线程安全

Java语言中的线程安全:

  • 按照线程安全的“安全程度”由强至弱来排序,咱们能够将Java语言中各类操做共享的数据分为如下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
  • 不可变:不可变(Immutable)的对象必定是线程安全的,不管是对象的方法实现仍是方法的调用者,都不须要再采起任何的线程安全保障措施,final关键字带来的可见性,只要一个不可变的对象被正确地构建出来(没有发生this引用逃逸的状况),那其外部的可见状态永远也不会改变,永远也不会看到它在多个线程之中处于不一致的状态。“不可变”带来的安全性是最简单和最纯粹的。
  • 绝对线程安全:一个类要达到“无论运行时环境如何,调用者都不须要任何额外的同步措施”,一般须要付出很大的,甚至有时候是不切实际的代价。在Java API中标注本身是线程安全的类,大多数都不是绝对的线程安全。
  • 相对线程安全:相对的线程安全就是咱们一般意义上所讲的线程安全,它须要保证对这个对象单独的操做是线程安全的,咱们在调用的时候不须要作额外的保障措施,可是对于一些特定顺序的连续调用,就可能须要在调用端使用额外的同步手段来保证调用的正确性。在Java语言中,大部分的线程安全类都属于这种类型,例如Vector、HashTable、Collections的synchronizedCollection()方法包装的集合等。
  • 线程兼容:线程兼容是指对象自己并非线程安全的,可是能够经过在调用端正确地使用同步手段来保证对象在并发环境中能够安全地使用,咱们日常说一个类不是线程安全的,绝大多数时候指的是这一种状况。Java API中大部分的类都是属于线程兼容的,如与前面的Vector和HashTable相对应的集合类ArrayList和HashMap等。
  • 线程对立:线程对立是指不管调用端是否采起了同步措施,都没法在多线程环境中并发使用的代码。因为Java语言天生就具有多线程特性,线程对立这种排斥多线程的代码是不多出现的,并且一般都是有害的,应当尽可能避免。一个线程对立的例子是Thread类的suspend()和resume()方法,若是有两个线程同时持有一个线程对象,一个尝试去中断线程,另外一个尝试去恢复线程,若是并发进行的话,不管调用时是否进行了同步,目标线程都是存在死锁风险的,若是suspend()中断的线程就是即将要执行resume()的那个线程,那就确定要产生死锁了。也正是因为这个缘由,suspend()和resume()方法已经被JDK声明废弃(@Deprecated)了。常见的线程对立的操做还有System.setIn()、Sytem.setOut()和System.runFinalizersOnExit()等。

线程安全的实现方法:

  • 互斥同步(Mutual Exclusion&Synchronization):是常见的一种并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。所以,在这4个字里面,互斥是因,同步是果;互斥是方法,同步是目的。在Java中,最基本的互斥同步手段就是synchronized关键字,还可使用java.util.concurrent(下文称J.U.C)包中的重入锁(ReentrantLock)来实现同步。
  • 非阻塞同步(Non-Blocking Synchronization):基于冲突检测的乐观并发策略,通俗地说,就是先进行操做,若是没有其余线程争用共享数据,那操做就成功了;若是共享数据有争用,产生了冲突,那就再采起其余的补偿措施(最多见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不须要把线程挂起,所以这种同步操做称为非阻塞同步(Non-Blocking Synchronization)。经常使用的有:测试并设置(Test-and-Set)、比较并交换(Compare-and-Swap,下文称CAS)。
  • 无同步方案:要保证线程安全,并非必定就要进行同步,二者没有因果关系。同步只是保证共享数据争用时的正确性的手段,若是一个方法原本就不涉及共享数据,那它天然就无须任何同步措施去保证正确性,所以会有一些代码天生就是线程安全的。可重入代码(Reentrant Code)和线程本地存储(Thread Local Storage)。

二、锁优化

自旋锁与自适应自旋:

  • 若是物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,咱们就可让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,咱们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
  • 在JDK 1.6中引入了自适应的自旋锁。自适应意味着自旋的时间再也不固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

锁消除:

  • 锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,可是被检测到不可能存在共享数据竞争的锁进行消除。
  • 锁消除的主要断定依据来源于逃逸分析的数据支持,若是判断在一段代码中,堆上的全部数据都不会逃逸出去从而被其余线程访问到,那就能够把它们当作栈上数据对待,认为它们是线程私有的,同步加锁天然就无须进行。

锁粗化:

  • 原则上,咱们在编写代码的时候,老是推荐将同步块的做用范围限制得尽可能小——只在共享数据的实际做用域中才进行同步,这样是为了使得须要同步的操做数量尽量变小,若是存在锁竞争,那等待锁的线程也能尽快拿到锁。
  • 大部分状况下,上面的原则都是正确的,可是若是一系列的连续操做都对同一个对象反复加锁和解锁,甚至加锁操做是出如今循环体中的,那即便没有线程竞争,频繁地进行互斥同步操做也会致使没必要要的性能损耗。

轻量级锁:

  • 轻量级锁是JDK 1.6之中加入的新型锁机制,它名字中的“轻量级”是相对于使用操做系统互斥量来实现的传统锁而言的,所以传统的锁机制就称为“重量级”锁。

偏向锁:

  • 这个锁会偏向于第一个得到它的线程,若是在接下来的执行过程当中,该锁没有被其余的线程获取,则持有偏向锁的线程将永远不须要再进行同步。

推荐博客:

http://www.javashuo.com/article/p-daisayll-cs.html

转载请于明显处标明出处:

http://www.javashuo.com/article/p-amblhnno-bd.html

相关文章
相关标签/搜索