深入理解Java虚拟机读书笔记(五)

一、Java内存模型与线程

  1. Java内存模型
    Java内存模型的主要目标是定义程序中各个变量(不包括局部变量和方法参数,因为它们是线程私有的,不会被共享,不存在竞争问题)的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。
    Java内存模型规定了所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存保存了该线程使用到的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主存中的变量。不同线程之间无法访问其他线程工作内存中的变量,线程间变量的值传递均需通过主存来完成。


    交互关系.png
  2. 内存间交互操作
    (1)Java内存模型定义的8种操作
    ① lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
    ② unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
    ③ read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
    ④ load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
    ⑤ use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
    ⑥ assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
    ⑦ store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
    ⑧ write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中


    关系图.png

    (2)8种操作必须满足的规则
    ① 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主存读取了但是工作内存不接受,或者从工作内存发起会写了但是主存不接受的情况。
    ② 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
    ③ 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
    ④ 一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
    ⑤ 一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
    ⑥ 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
    ⑦ 如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
    ⑧ 对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)。

  3. 先行发生原则
    (1)程序次序规则:在一个线程内,按照程序代码顺序,前面的操作先行于后面的操作,准确的说是控制流顺序,因为要考虑到分支和循环结构。
    (2)管程锁定规则:一个unlock操作先行发生于后面(时间上)对同一个锁的lock操作。
    (3)volatile变量规则:对一个volatile变量的写操作先行发生于后面(时间上)对这个变量的读操作。
    (4)线程启动规则:Thread的start( )方法先行发生于这个线程的每一个操作。
    (5)线程终止规则:线程的所有操作都先行于此线程的终止检测。可以通过Thread.join( )方法结束、Thread.isAlive( )的返回值等手段检测线程的终止。
    (6)线程中断规则:对线程interrupt( )方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupt( )方法检测线程是否中断
    (7) 对象终结规则:一个对象的初始化完成先行于发生它的finalize()方法的开始。
    (8)传递性:如果操作A先行于操作B,操作B先行于操作C,那么操作A先行于操作C。

结论:先行发生原则是判断数据是否存在竞争、线程是否安全的主要依据。它与时间先后顺序没有太大的关系,所以衡量并发安全问题的时候不要受到时间顺序的影响,一切以先行发生原则为准。

  1. Java线程调度
    线程调度是指系统为线程分配处理器使用权的过程,主要有以下两种调度方式:
    (1)协同式线程调度:线程的执行时间由线程本身控制,线程执行完任务,主动通知操作系统切换到另一个线程。
    (2)抢占式线程调度:由操作系统来分配时间,线程的执行时间是系统可控的,不会导致进程阻塞。Java使用的就是这种调度方式。

  2. 线程状态转换


    线程的状态转换.png

二、线程安全与锁优化

  1. Java语言中各种操作共享的数据可分为以下5类:
    (1)不可变:一定是线程安全的。在Java中,如果共享数据是基本类型,且被final关键字修饰,则它是不可变的。如果共享数据是一个对象,就要保证对象的行为不会对其状态产生影响。如把对象中带有状态的变量都声明为final,这样在构造函数结束之后,它就是不可变的。String、枚举类型、Long、Double等包装类型、BigDecimal、BigInteger等大数据类型都是不可变的。
    (2)绝对线程安全
    (3)相对线程安全:Java语言中,大部分线程安全类型都属于这一类,如Vector、HashTable、Collections的synchronizedCollection()方法包装的集合。
    (4)线程兼容:对象本身不是线程安全的,但可以通过在调用端正确使用同步手段来保证对象在并发环境可安全使用。
    (5)线程对立:无论调用端是否采取了同步措施,都无法在多线程情况下并发使用。如suspend()和resume()方法。

  2. 线程安全的实现方法
    (1)互斥同步:同步是指多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个(或者是一些,在使用信号量时)线程使用。互斥是实现同步的一种手段,主要的互斥实现方式有临界区、互斥量、信号量等。互斥是方法,同步是目的。
    (2)非阻塞同步:基于冲突检测的乐观并发策略,就是先进行操作,如果没有其他线程争用共享数据,则操作成功。否则,采取其他补偿措施(如不断重试)。CAS操作就是属于上述的一种实现手段,但它可能会导致ABA问题,可通过AtomicStampedReference控制变量的版本值,保证CAS的正确性。
    (3)无同步方案
    ① 可重入代码:可在代码执行的任何时刻中断它,转而去执行另一段代码,控制权返回后,原来的程序不会错误。这类代码都是线程安全的。
    ② 线程本地存储:ThreadLocal

  3. 锁优化
    (1)自旋锁与自适应自旋
    如果物理机有一个以上的处理器,能让两个或两个以上的线程同时并行执行,则可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看线程是否会很快释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。自旋等待的时间有一定的限度。
    自适应意味自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者状态决定的。如果在同一个锁上,自旋等待刚刚获得过锁,并且持有锁的线程正在运行中,则认为这次自旋很可能再成功,并且允许其自旋等待更长时间。反之,若某个锁的自旋很少成功过,则以后可能忽略自旋过程。
    (2)锁消除:通过逃逸分析,对检测到不可能存在共享数据竞争的锁进行消除。
    (3)锁粗化:如果一连串的操作都是针对同一个对象进行加锁,则可以将加锁范围扩展到整个操作序列的外部。
    (4)轻量级锁:如果锁在同步周期不存在竞争关系,轻量级锁使用CAS操作避免互斥量的开销。
    (5)偏向锁:锁会偏向与第一个获得它的线程,如果接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程永远不需要再同步。偏向锁可以提高带有同步带无竞争的程序性能。
    如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁则是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。

你可能感兴趣的:(深入理解Java虚拟机读书笔记(五))