JVM之高效并发篇

十二、Java内存模型与线程

12.1 Java内存模型

12.1.1主内存与工作内存

Java内存模型的主要目的是定义程序中各种变量的访问规则, 即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。

Java内存模型规定了所有的变量都存储在主内存(Main Memory)中,每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如下所示:

 

主内存直接对应于物理硬件的内存, 而为了获取更好的运行速度, 虚拟机(或者是硬件、 操作系统本身的优化措施) 可能会让工作内存优先存储于寄存器和高速缓存中。

12.1.2 内存间交互操作

一个变量如何从主内存拷贝到工作内存、 如何从工作内存同步回主内存这一类的实现细节, Java内存模型中定义了以下8种操作来完成。 Java虚拟机实现时必须保证下面提及的每一种操作都是原子的、 不可再分的:

  1. lock(锁定): 作用于主内存的变量, 它把一个变量标识为一条线程独占的状态。
  2. unlock(解锁) : 作用于主内存的变量, 它把一个处于锁定状态的变量释放出来, 释放后的变量才可以被其他线程锁定。
  3. read(读取) : 作用于主内存的变量, 它把一个变量的值从主内存传输到线程的工作内存中, 以便随后的load动作使用。
  4. load(载入) : 作用于工作内存的变量, 它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  5. use(使用) : 作用于工作内存的变量, 它把工作内存中一个变量的值传递给执行引擎, 每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  6. assign(赋值) : 作用于工作内存的变量, 它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  7. store(存储) : 作用于工作内存的变量, 它把工作内存中一个变量的值传送到主内存中, 以便随后的write操作使用。
  8. write(写入) : 作用于主内存的变量, 它把store操作从工作内存中得到的变量的值放入主内存的变量中。

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操作) 。

12.1.3 volatile型变量的特殊规则

关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制。当一个变量被定义成volatile之后, 它将具备两项特性:

1.保证此变量对所有线程的可见性

当一条线程修改了这个变量的值, 新值对于其他线程来说是可以立即得知的。

public class VolatileTest {
    public static volatile int race = 0;
    public static void increase() {
        race++;
    }
    private static final int THREADS_COUNT = 20;

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }
        // 等待所有累加线程都结束
        while (Thread.activeCount() > 1){
            Thread.yield();
        }
        System.out.println(race);
    }
}

当getstatic指令把race的值取到操作栈顶时, volatile关键字保证了race的值在此时是正确的, 但是在执行iconst_1、 iadd这些指令的时候, 其他线程可能已经把race的值改变了, 而操作栈顶的值就变成了过期的数据, 所以putstatic指令执行后就可能把较小的race值同步回主内存之中。

volatile变量只能保证可见性,在不符合以下两条规则的运算场景中, 我们仍然要通过加锁(使用synchronized、 java.util.concurrent中的锁或原子类) 来保证原子性:

  1. 运算结果并不依赖变量的当前值, 或者能够确保只有单一的线程修改变量的值。
  2. 变量不需要与其他的状态变量共同参与不变约束。

2.禁止指令重排序优化

普通的变量仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果, 而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。

public class Singleton {
    private volatile static Singleton instance;
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    } 
    public static void main(String[] args) {
        Singleton.getInstance();
    }
}

其中对instance变量赋值部分的字节码如下:

0x01a3de0f: mov $0x3375cdb0,%esi             ;...beb0cd75 33
                                         ; {oop('Singleton')}
0x01a3de14: mov %eax,0x150(%esi)             ;...89865001 0000
0x01a3de1a: shr $0x9,%esi                     ;...c1ee09
0x01a3de1d: movb $0x0,0x1104800(%esi)          ;...c6860048 100100
0x01a3de24: lock addl $0x0,(%esp)              ;...f0830424 00
                                          ;*putstatic instance
                                          ; - Singleton::getInstance@24

有volatile修饰的变量, 赋值后(前面mov%eax, 0x150(%esi)这句便是赋值操作) 多执行了一个“lock addl$0x0, (%esp)”操作, 这个操作的作用相当于一个内存屏障(Memory Barrier或Memory Fence, 指重排序时不能把后面的指令重排序到内存屏障之前的位置), 只有一个处理器访问内存时, 并不需要内存屏障; 但如果有两个或更多处理器访问同一块内存, 且其中有一个在观测另一个, 就需要内存屏障来保证一致性。

volatile变量读操作的性能消耗与普通变量几乎没有什么差别, 但是写操作则可能会慢上一些, 因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。 不过即便如此, 大多数场景下volatile的总开销仍然要比锁来得更低。

假定T表示一个线程, V和W分别表示两个volatile型变量, 那么在进行read、 load、 use、 assign、 store和write操作 时需要满足如下规则:

  • 只有当线程T对变量V执行的前一个动作是load的时候, 线程T才能对变量V执行use动作;并且,只有当线程T对变量V执行的后一个动作是use的时候, 线程T才能对变量V执行load动作。 线程T对变量V的use动作可以认为是和线程T对变量V的load、 read动作相关联的, 必须连续且一起出现。

    这条规则要求在工作内存中, 每次使用V前都必须先从主内存刷新最新的值, 用于保证能看见其他线程对变量V所做的修改。

  • 只有当线程T对变量V执行的前一个动作是assign的时候, 线程T才能对变量V执行store动作; 并且, 只有当线程T对变量V执行的后一个动作是store的时候, 线程T才能对变量V执行assign动作。 线程T对变量V的assign动作可以认为是和线程T对变量V的store、 write动作相关联的, 必须连续且一起出现。

    这条规则要求在工作内存中, 每次修改V后都必须立刻同步回主内存中, 用于保证其他线程可以看到自己对变量V所做的修改。

  • 假定动作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修饰的变量不会被指令重排序优化, 从而保证代码的执行顺序与程序的顺序相同。

12.1.4 long和double型变量的特殊规则

虚拟机允许将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现自行选择是否要保证64位数据类型的load、 store、 read和write这四个操作的原子性, 这就是所谓的“long和double的非原子性协定”(Non-Atomic Treatment of double and long Variables)。

12.1.5 原子性、 可见性与有序性

1.原子性(Atomicity)

由Java内存模型来直接保证的原子性变量操作包括read、 load、 assign、 use、 store和write这六个, 基本数据类型的访问、 读写都是具备原子性的(例外就是long和double的非原子性协定)。

Java内存模型还提供了lock和unlock操作来保证一个更大范围的原子性,尽管虚拟机未把lock和unlock操作直接开放给用户使用, 却提供了更高层次的字节码指令monitorentermonitorexit来隐式地使用这两个操作。 这两个字节码指令反映到Java代码中就是同步块——synchronized关键字, 因此在synchronized块之间的操作也具备原子性。

2.可见性(Visibility)

Java内存模型是通过在变量修改后将新值同步回主内存, 在变量读取前从主内存刷新变量值,这种依赖主内存作为传递媒介的方式来实现可见性的。

除了volatile之外, Java还有两个关键字能实现可见性, 它们是synchronizedfinal。 同步块的可见性是由“对一个变量执行unlock操作之前, 必须先把此变量同步回主内存中(执行store、 write操作) ”这条规则获得的。

final关键字的可见性是指: 被final修饰的字段在构造器中一旦被初始化完成, 并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情, 其他线程有可能通过这个引用访问到“初始化了一半”的对象) , 那么在其他线程中就能看见final字段的值。

3.有序性(Ordering)

如果在本线程内观察, 所有的操作都是有序的; 如果在一个线程中观察另一个线程,所有的操作都是无序的。

12.1.6 先行发生原则

  1. 程序次序规则(Program Order Rule) : 在一个线程内, 按照控制流顺序, 书写在前面的操作先行发生于书写在后面的操作。
  2. 管程锁定规则(Monitor Lock Rule) : 一个unlock操作先行发生于后面对同一个锁的lock操作。
  3. volatile变量规则(Volatile Variable Rule) : 对一个volatile变量的写操作先行发生于后面对这个变量的读操作, 这里的“后面”同样是指时间上的先后。
  4. 线程启动规则(Thread Start Rule) : Thread对象的start()方法先行发生于此线程的每一个动作。
  5. 线程终止规则(Thread Termination Rule) : 线程中的所有操作都先行发生于对此线程的终止检测, 可以通过Thread::join()方法是否结束、 Thread::isAlive()的返回值等手段检测线程是否已经终止执行。
  6. 线程中断规则(Thread Interruption Rule) : 对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生, 可以通过Thread::interrupted()方法检测到是否有中断发生。
  7. 对象终结规则(Finalizer Rule) : 一个对象的初始化完成(构造函数执行结束) 先行发生于它的finalize()方法的开始。
  8. 传递性(Transitivity) : 如果操作A先行发生于操作B, 操作B先行发生于操作C, 那就可以得出操作A先行发生于操作C的结论。

时间先后顺序与先行发生原则之间基本没有因果关系,衡量并发安全问题的时候不要受时间顺序的干扰, 一切必须以先行发生原则为准

12.2 Java与线程

12.2.1 线程的实现

线程是Java里面进行处理器资源调度的最基本单位,实现线程主要有三种方式: 使用内核线程实现(1:1实现)、使用用户线程实现(1:N实现)、使用用户线程加轻量级进程混合实现(N:M实现)

1.内核线程实现

内核线程(Kernel-Level Thread, KLT) 就是直接由操作系统内核(Kernel,下称内核)支持的线程,这种线程由内核来完成线程切换, 内核通过操纵调度器(Scheduler) 对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就称为多线程内核(Multi-Threads Kernel)。

程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口—轻量级进程(Light Weight Process,LWP)。轻量级进程就是我们通常意义上所讲的线程, 每个轻量级进程都由一个内核线程支持。

轻量级进程也具有它的局限性: 首先, 由于是基于内核线程实现的, 所以各种线程操作, 如创建、 析构及同步, 都需要进行系统调用。 而系统调用的代价相对较高, 需要在用户态(User Mode) 和内核态(Kernel Mode) 中来回切换。 其次, 每个轻量级进程都需要有一个内核线程的支持, 因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间) , 因此一个系统支持轻量级进程的数量是有限的。

2.用户线程实现

狭义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到用户线程的存在及如何实现的。用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都需要由用户程序自己去处理。

3.混合实现

在这种混合实现下, 既存在用户线程, 也存在轻量级进程。用户线程还是完全建立在用户空间中, 因此用户线程的创建、 切换、 析构等操作依然廉价, 并且可以支持大规模的用户线程并发。 而操作系统支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级进程来完成, 这大大降低了整个进程被完全阻塞的风险。

4.Java线程的实现

从JDK 1.3起, “主流”平台上的“主流”商用Java虚拟机的线程模型普遍都被替换为基于操作系统原生线程模型来实现, 即采用1: 1的线程模型。

以HotSpot为例, 它的每一个Java线程都是直接映射到一个操作系统原生线程来实现的, 而且中间没有额外的间接结构, 所以HotSpot自己是不会去干涉线程调度的(可以设置线程优先级给操作系统提供调度建议), 全权交给底下的操作系统去处理。

12.2.2 Java线程调度

线程调度是指系统为线程分配处理器使用权的过程, 调度主要方式有两种, 分别是协同式(Cooperative Threads-Scheduling)线程调度和抢占式(Preemptive Threads-Scheduling)线程调度

使用协同式调度的多线程系统, 线程的执行时间由线程本身来控制, 线程把自己的工作执行完了之后, 要主动通知系统切换到另外一个线程上去。 使用抢占式调度的多线程系统, 那么每个线程将由系统来分配执行时间, 线程的切换不由线 程本身来决定。

Java语言一共设置了10个级别的线程优先级(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY) 。 在两个线程同时处于Ready状态时, 优先级越高的线程越容易被系统选择执行。

12.2.3 状态转换

Java语言定义了6种线程状态, 在任意一个时间点中, 一个线程只能有且只有其中的一种状态, 并且可以通过特定的方法在不同状态之间转换。 这6种状态分别是:

  1. 新建(New) : 创建后尚未启动的线程处于这种状态。

  2. 运行(Runnable) : 包括操作系统线程状态中的Running和Ready, 也就是处于此状态的线程有可能正在执行, 也有可能正在等待着操作系统为它分配执行时间。

  3. 无限期等待(Waiting) : 处于这种状态的线程不会被分配处理器执行时间, 它们要等待被其他线程显式唤醒。 以下方法会让线程陷入无限期的等待状态: ■没有设置Timeout参数的Object::wait()方法; ■没有设置Timeout参数的Thread::join()方法; ■LockSupport::park()方法。

  4. 限期等待(Timed Waiting): 处于这种状态的线程也不会被分配处理器执行时间, 不过无须等待被其他线程显式唤醒, 在一定时间之后它们会由系统自动唤醒。 以下方法会让线程进入限期等待状态:

    ■Thread::sleep()方法; ■设置了Timeout参数的Object::wait()方法; ■设置了Timeout参数的Thread::join()方法; ■LockSupport::parkNanos()方法; ■LockSupport::parkUntil()方法。

  5. 阻塞(Blocked) : 线程被阻塞了, “阻塞状态”与“等待状态”的区别是“阻塞状态”在等待着获取到一个排它锁, 这个事件将在另外一个线程放弃这个锁的时候发生; 而“等待状态”则是在等待一段时间, 或者唤醒动作的发生。 在程序等待进入同步区域的时候, 线程将进入这种状态。

  6. 结束(Terminated) : 已终止线程的线程状态, 线程已经结束执行。

12.3Java与协程(了解即可)

十三、 线程安全与锁优化

13.1 线程安全

按照线程安全的“安全程度”由强至弱来排序,Java语言中各种操作共享的数据分为以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

13.1.1 线程安全的实现方法

1.互斥同步

同步是指在多个线程并发访问共享数据时, 保证共享数据在同一个时刻只被一条(或者是一些,当使用信号量的时候) 线程使用。 而互斥是实现同步的一种手段, 临界区(Critical Section) 、 互斥量(Mutex) 和信号量(Semaphore) 都是常见的互斥实现方式。

synchronized关键字经过Javac编译之后, 会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。 在执行monitorenter指令时, 首先要去尝试获取对象的锁。 如果这个对象没被锁定, 或者当前线程已经持有了那个对象的锁, 就把锁的计数器的值增加一, 而在执行monitorexit指令时会将锁计数器的值减一。 一旦计数器的值为零, 锁随即就被释放。

  1. 被synchronized修饰的同步块对同一条线程来说是可重入的。 这意味着同一线程反复进入同步块也不会出现自己把自己锁死的情况。
  2. 被synchronized修饰的同步块在持有锁的线程执行完毕并释放锁之前, 会无条件地阻塞后面其他线程的进入。

持有锁是一个重量级(Heavy-Weight) 的操作。 Java的线程是映射到操作系统的原生内核线程之上的, 如果要阻塞或唤醒一条线程, 则需要操作系统来帮忙完成, 这就不可避免地陷入用户态到核心态的转换中, 进行这种状态转换需要耗费很多的处理器时间。

除了synchronized关键字以外,java.util.concurrent.locks.Lock接口便成了Java的另一种全新的互斥同步手段。 基于Lock接口, 用户能够以非块结构(Non-Block Structured) 来实现互斥同步。

重入锁(ReentrantLock)是Lock接口最常见的一种实现,与synchronized相比增加了一些高级功能,主要有以下三项:

  1. 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。可中断特性对处理执行时间非常长的同步块很有帮助。
  2. 公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁是非公平的,ReentrantLock在默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。不过一旦使用了公平锁,将会导致ReentrantLock的性能急剧下降,会明显影响吞吐量。
  3. 锁绑定多个条件: 一个ReentrantLock对象可以同时绑定多个Condition对象。在synchronized中,锁对象的wait()跟它的notify()或者notifyAll()方法配合可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外添加一个锁;而ReentrantLock则无须这样做,多次调用newCondition()方法即可。

Lock应该确保在finally块中释放锁, 否则一旦受同步保护的代码块中抛出异常, 则有可能永远不会释放持有的锁。使用synchronized的话则可以由Java虚拟机来确保即使出现异常, 锁也能被自动释放。

2.非阻塞同步

互斥同步面临的主要问题是进行线程阻塞和唤醒所带来的性能开销,因此这种同步也被称为阻塞同步(Blocking Synchronization)。

基于冲突检测的乐观并发策略,先进行操作, 如果没有其他线程争用共享数据, 那操作就直接成功了; 如果共享的数据的确被争用, 产生了冲突, 那再进行其他的补偿措施, 最常用的补偿措施是不断地重试, 直到出现没有竞争的共享数据为止。 这种乐观并发策略的实现不再需要把线程阻塞挂起, 因此这种同步操作被称为非阻塞同步(Non-Blocking Synchronization) , 使用这种措施的代码也常被称为无锁(Lock-Free)编程。

**CAS(Compare-and-Swap,比较并交换 )**指令需要有三个操作数, 分别是内存位置(在Java中可以简单地理解为变量的内存地址, 用V表示)、旧的预期值(用A表示)和准备设置的新值(用B表示)。CAS指令执行时,当且仅当V符合A时, 处理器才会用B更新V的值,否则它就不执行更新。但是,不管是否更新了V的值,都会返回V的旧值,上述的处理过程是一个原子操作,执行期间不会被其他线程中断。

CAS它存在一个逻辑漏洞: 如果一个变量V初次读取的时候是A值, 在这段期间它的值曾经被改成B,后来又被改回为A, 那CAS操作就会误认为它从来没有被改变过。 这个漏洞称为CAS操作的“ABA问题”。J.U.C包为了解决这个问题, 提供了一个带有标记的原子引用类AtomicStampedReference(比较鸡肋,直接用互斥同步就行), 它可以通过控制变量值的版本来保证CAS的正确性。

13.2 锁优化

13.2.1 自旋锁与自适应自旋

如果两个或以上的线程同时并行执行,我们可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只须让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁

自旋等待的时间必须有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程。自旋次数的默认值是十次,用户也可以使用参数-XX: PreBlockSpin来自行更改。

自适应自旋:自旋的时间由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。例如,在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功, 进而允许自旋等待持续相对更长的时间, 比如持续100次忙循环。

13.2.2 锁消除和锁粗化

锁消除:虚拟机即时编译器在运行时, 对一些代码要求同步, 但是对被检测到不可能存在共享数据竞争的锁进行消除。

锁粗化:如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化) 到整个操作序列的外部。

13.2.3 轻量级锁

轻量级锁是相对于使用操作系统互斥量来实现的传统锁而言的,设计的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

HotSpot虚拟机的对象头(Object Header)分为两部分,第一部分用于存储对象自身的运行时数据,称为“Mark Word” 。

Mark Word被设计成一个非固定的动态数据结构,在32位的HotSpot虚拟机中,对象未被锁定的状态下,Mark Word的32个比特空间里的25个比特将用于存储对象哈希码,4个比特用于存储对象分代年龄,2个比特用于存储锁标志位,还有1个比特固定为0(这表示未进入偏向模式)。对象除了未被锁定的正常状态外,还有轻量级锁定、重量级锁定、 GC标记、 可偏向等几种不同状态:

 轻量级锁的工作过程: 在代码即将进入同步块的时候, 如果此同步对象没有被锁定(锁标志位为“01”状态) , 虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record) 的空间, 用于存储锁对象目前的Mark Word的拷贝(官方为这份拷贝加了一个Displaced前缀, 即Displaced Mark Word)。

虚拟机将使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针。 如果这个更新动作成功, 即代表该线程拥有了这个对象的锁, 并且对象Mark Word的锁标志位(Mark Word的最后两个比特) 将转变为“00”, 表示此对象处于轻量级锁定状态。

如果更新失败了, 那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。 虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧, 如果是, 说明当前线程已经拥有了这个对象的锁, 那直接进入同步块继续执行, 否则说明这个锁对象已经被其他线程抢占了。 如果出现两条以上的线程争用同一个锁的情况, 那轻量级锁就不再有效, 必须要膨胀为重量级锁, 锁标志的状态值变为“10”, 此时Mark Word中存储的就是指向重量级锁(互斥量) 的指针, 后面等待锁的线程也必须进入阻塞状态。

轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁, 在整个同步周期内都是不存在竞争的”这一经验法则。

13.2.4 偏向锁

偏向锁会偏向于第一个获得它的线程, 如果在接下来的执行过程中, 该锁一直没有被其他的线程获取, 则持有偏向锁的线程将永远不需要再进行同步。

当锁对象第一次被线程获取的时候, 虚拟机将会把对象头中的标志位设置为“01”、 把偏向模式设置为“1”, 表示进入偏向模式。 同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中。 如果CAS操作成功, 持有偏向锁的线程以后每次进入这个锁相关的同步块时, 虚拟机都可以不再进行任何同步操作(例如加锁、 解锁及对Mark Word的更新操作等) 。

一旦出现另外一个线程去尝试获取这个锁的情况, 偏向模式就马上宣告结束。 根据锁对象目前是否处于被锁定的状态决定是否撤销偏向(偏向模式设置为“0”) , 撤销后标志位恢复到未锁定(标志位为“01”) 或轻量级锁定(标志位为“00”) 的状态, 后续的同步操作就按照上面介绍的轻量级锁那样去执行。

当对象进入偏向状态的时候, Mark Word大部分的空间(23个比特) 都用于存储持有锁的线程ID了, 这部分空间占用了原有存储对象哈希码的位置, 那原来对象的哈希码怎么办呢?

作为绝大多数对象哈希码来源的Object::hashCode()方法, 返回的是对象的一致性哈希码(Identity Hash Code) , 这个值是能强制保证不变的, 它通过在对象头中存储计算结果来保证第一次计算之后, 再次调用该方法取到的哈希码值永远不会再发生改变。 因此, 当一个对象已经计算过一致性哈希码后, 它就再也无法进入偏向锁状态了; 而当一个对象当前正处于偏向锁状态, 又收到需要计算其一致性哈希码请求[1]时, 它的偏向状态会被立即撤销, 并且锁会膨胀为重量级锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值