精华内容
下载资源
问答
  • 对象锁与类锁
    千次阅读
    2019-02-13 12:02:44

    HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Object Header)、实例数据(Instance Data)和对齐填充(Padding)。

    对象头(Object Header)

     JVM的对象头包括二/三部分信息:1、Mark Word;2、 类型指针;3、数组长度(只有数组对象才有)

     1、Mark Word

    用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits。

    Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的:

    锁状态25bit4bit1bit2bit
    23bit2bit是否偏向锁锁标志位
    无锁对象的HashCode分代年龄001
    偏向锁线程IDEpoch偏向时间戳分代年龄101
    轻量级锁指向栈中锁记录的指针00
    重量级锁指向重量级锁的指针10
    GC标记11

    其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。

    2、 类型指针 

    即是对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 并不是所有的虚拟机实现都必须在对象数据上保留类型指针,即查找对象的元数据信息并不一定要经过对象本身。

    3、数组长度 

    如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。 

    偏向锁

     引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要多次CAS获取锁的执行过程,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子操作(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。偏向锁则是在只有一个线程或者多个线程不同步执行同步块时进一步提高性能。

    轻量级锁

    轻量级锁是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景,如果线程阻塞需要线程之间完成线程间的通信,耗费性能(用户态和内核态状态切换),所以直接让线程进入自旋等待锁释放。

    1、偏向锁获取过程:

      (1)访问Mark Word中偏向锁的标识位(1bit)是否设置成1,锁标志位(2bit)是否为01——确认为可偏向状态。

      (2)如果为可偏向状态,则偏向锁线程ID是否指向当前线程,如果是,进入步骤(4),否则进入步骤(3)。

      (3)如果偏向锁线程ID并未指向当前线程,因为偏向锁不会主动释放,所以当前线程可以看到对象时偏向状态以及拥有该对象锁的线程,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新CAS操作偏向新的线程,然后执行(4);如果原来的线程依然存活,由偏向锁升级为轻量级锁(在下面进行分析)。

      (4)执行同步代码。

    2、偏向锁升级为轻量级锁过程

    偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,需要查看Java对象头中记录的偏向锁的线程是否存活,如果没有存活,那么修改对象头偏向锁标志位为0,其它线程可以竞争将其设置为偏向锁;如果存活,拷贝对象头中的Mark Word到该线程的栈帧中的锁记录里让lock record的指针指向锁对象头中的Mark Word,再让Mark Word指向指向lock record,唤醒线程继续执行原来线程的同步代码,则当前线程通过CAS操作竞争锁,竞争失败执行自旋操作继续竞争锁。

      ps: 全局安全点safepoint 参考 理解JVM的safepoint

    3、轻量级锁获取过程 

    线程的栈帧中有一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。 

    线程会拷贝对象头中的Mark Word到锁记录(Lock Record)中,然后使用CAS操作尝试将锁对象的Mark Word指针指向Lock Record,并将线程栈帧中的Lock Record里的owner指针指向Object的 Mark Word。如果更新成功,则表示获取了轻量级锁。

    4、轻量级锁膨胀重量级锁过程

    a线程获得锁,会在a线程的栈帧里创建lock record(锁记录),让lock record的指针指向锁对象的对象头中的mark word.再让mark word 指向lock record.这就是获取了锁。如果b线程在锁竞争时,发现锁已经被a线程占用,则b线程不进入内核态,让b线程自旋,执行空循环,等待a线程释放锁。如果发现a线程没有释放锁,这时候c线程也来竞争锁,那么这个时候轻量级锁就会膨胀为重量级锁。

     

    更多相关内容
  • 在本文中小编给的大家整理了关于Java锁的升级策略 偏向锁 轻量级锁 重量级锁的相关知识点内容,需要的朋友们参考下。
  • 目录由一个问题引发的思考多线程对于共享变量访问带来的安全性问题线程安全...对象都可以实现锁synchronized 锁的升级锁的种类偏向锁偏向锁的获取偏向锁的撤销轻量级锁轻量级锁的加锁**自旋锁**轻量级锁的解锁重量级锁...

    由一个问题引发的思考

    线程的合理使用能够提升程序的处理性能,主要有两个方面,

    1. 能够利用多核 cpu 以及超线程技术来实现线程的并行执行;
    2. 线程的异步化执行相比于同步执行来说,异步执行能够很好的优化程序的处理性能提升并发吞吐量

    同时,也带来了很多麻烦,举个简单的例子:

    多线程对于共享变量访问带来的安全性问题

    一个变量 i. 假如一个线程去访问这个变量进行修改,这个时候对于数据的修改和访问没有任何问题。但是如果多个线程对于这同一个变量进行修改,就会存在一个数据安全性问题

    image-20220116153316991

    线程安全性

    本质上是管理对于数据状态的访问,而且这个这个状态通常是共享的、可变的。

    1. 共享,是指这个数据变量可以被多个线程访问;
    2. 可变,指这个变量的值在它的生命周期内是可以改变的。

    一个对象是否是线程安全的,取决于它是否会被多个线程访问,以及程序中是如何去使用这个对象的。所以,如果多个线程访问同一个共享对象,在不需额外的同步以及调用端代码不用做其他协调的情况下,这个共享对象的状态依然是正确的(正确性意味着这个对象的结果与我们预期规定的结果保持一致),那说明这个对象是线程安全的。

    public class SynDemo {
        private static int count = 0;
    
        public static void incr(){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
        }
    
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 1000; i++) {
                new Thread(() -> SynDemo.incr()).start();
            }
            Thread.sleep(2000);
            System.out.println("运行结果"+count);
        }
    }
    
    //运行结果993
    
    

    在 Java 中如何解决由于线程并行导致的数据安全性问题呢?

    思考如何保证线程并行的数据安全性

    问题的本质在于共享数据存在并发访问。如果我们能够有一种方法使得线程的并行变成串行,那是不是就不存在这个问题呢?

    按照大家已有的知识,最先想到的应该就是锁吧。毕竟这个场景并不模式,我们在和数据库打交道的时候,就了解过悲观锁、乐观锁的概念。 什么是锁?它是处理并发的一种同步手段,而如果需要达到前面我们说的一个目的,那么这个锁一定需要实现互斥的特性。

    Java 提供的加锁方法就是 Synchroinzed 关键字。

    synchronized 的基本认识

    在多线程并发编程中 synchronized 一直是元老级角色,很多人都会称呼它为重量级锁。但是,随着 Java SE 1.6 对synchronized 进行了各种优化之后,有些情况下它就并不那么重,Java SE 1.6 中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁

    synchronized 的基本语法

    synchronized 有三种方式来加锁,分别是

    1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
    2. 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
    3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

    不同的修饰类型,代表锁的控制粒度

    synchronized 的应用

    修改前面的案例,使用 synchronized 关键字后,可以达到数据安全的效果

    public class SynDemo {
        private static int count = 0;
    
        public  static void incr(){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
        }
    
        public synchronized static void incr1(){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
        }
    
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 1000; i++) {
                //new Thread(() -> SynDemo.incr()).start();
                new Thread(() -> SynDemo.incr1()).start();
            }
            Thread.sleep(2000);
            System.out.println("运行结果"+count);
        }
    }
    

    思考锁是如何存储的

    可以思考一下,要实现多线程的互斥特性,那这把锁需要哪些因素?

    1. 锁需要有一个东西来表示,比如获得锁是什么状态、无锁状态是什么状态
    2. 这个状态需要对多个线程共享

    那么我们来分析,synchronized 锁是如何存储的呢?

    观察synchronized 的整个语法发现,synchronized(lock)是基于lock 这个对象的生命周期来控制锁粒度的,那是不是锁的存储和这个 lock 对象有关系呢?

    于是我们以对象在 jvm 内存中是如何存储作为切入点,去看看对象里面有什么特性能够实现锁

    对象在内存中的布局

    在 Hotspot 虚拟机中,对象在内存中的存储布局,可以分为三个区域:

    • 对象头(Header)、
    • 实例数据(Instance Data)、
    • 对齐填充(Padding)

    image-20220116154237658

    image-20220116154423740

    为什么任何对象都可以实现锁

    1. Java 中的每个对象都派生自 Object 类,而每个Java Object 在 JVM 内部都有一个 native 的 C++对象oop/oopDesc 进行对应。
    2. 线程在获取锁的时候,实际上就是获得一个监视器对象(monitor) ,monitor 可以认为是一个同步对象,所有的Java 对象是天生携带 monitor。在 hotspot 源码的markOop.hpp 文件中,可以看到下面这段代码。
    3. image-20220116154732414
    4. 多个线程访问同步代码块时,相当于去争抢对象监视器修改对象中的锁标识,上面的代码中ObjectMonitor这个对象和线程争抢锁的逻辑有密切的关系

    synchronized 锁的升级

    锁的种类

    jvm 存在偏向锁、轻量级锁、重量级锁。

    在分析这几种锁的区别时,我们先来思考一个问题,使用锁能够实现数据的安全性,但是会带来性能的下降。不使用锁能够基于线程并行提升程序性能,但是却不能保证线程安全性。这两者之间似乎是没有办法达到既能满足性能也能满足安全性的要求。

    hotspot 虚拟机的作者经过调查发现,

    • 大部分情况下,加锁的代码不仅仅不存在多线程竞争,而且总是由同一个线程多次获得。

    • 所以基于这样一个概率,是的 synchronized 在JDK1.6 之后做了一些优化,为了减少获得锁和释放锁带来的性能开销,引入了偏向锁、轻量级锁的概念。

    • 因此大家会发现在 synchronized 中,锁存在四种状态分别是:无锁、偏向锁、轻量级锁、重量级锁;

    • 锁的状态根据竞争激烈的程度从低到高不断升级。

    偏向锁

    前面说过,大部分情况下,锁不仅仅不存在多线程竞争,而是总是由同一个线程多次获得,为了让线程获取锁的代价更低就引入了偏向锁的概念。怎么理解偏向锁呢?

    当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的 ID,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了

    偏向锁的获取
    1. 首先获取锁 对象的 Markword,判断是否处于可偏向状态。(biased_lock=1、且 ThreadId 为空)

    2. 如果是可偏向状态,则通过 CAS 操作,把当前线程的 ID写入到 MarkWord

      1. 如果 cas 成功,那么 markword 就会变成这样。表示已经获得了锁对象的偏向锁,接着执行同步代码块
      2. 如果 cas 失败,说明有其他线程已经获得了偏向锁,这种情况说明当前锁存在竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁(这个操作需要等到全局安全点,也就是没有线程在执行字节码)才能执行
    3. 如果是已偏向状态,需要检查 markword 中存储的ThreadID 是否等于当前线程的 ThreadID

      1. 如果相等,不需要再次获得锁,可直接执行同步代码块
      2. 如果不相等,说明当前锁偏向于其他线程,需要撤销偏向锁并升级到轻量级锁
    偏向锁的撤销

    偏向锁的撤销并不是把对象恢复到无锁可偏向状态(因为 偏向锁并不存在锁释放的概念),而是在获取偏向锁的过程中,发现 cas 失败也就是存在线程竞争时,直接把被偏向 的锁对象升级到被加了轻量级锁的状态。对原持有偏向锁的线程进行撤销时,原获得偏向锁的线程,有两种情况:

    1. 原获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,那么这个时候会把对象头设置成无锁状态并且争抢锁的线程可以基于 CAS 重新偏向但前线程

    2. 如果原获得偏向锁的线程的同步代码块还没执行完,处于临界区之内,这个时候会把原获得偏向锁的线程升级为轻量级锁后继续执行同步代码块

      在我们的应用开发中,绝大部分情况下一定会存在 2 个以上的线程竞争,那么如果开启偏向锁,反而会提升获取锁的资源消耗。所以可以通过 jvm 参数UseBiasedLocking 来设置开启或关闭偏向锁

    流程图分析

    image-20220116160452699

    轻量级锁

    轻量级锁的加锁

    锁升级为轻量级锁之后,对象的 Markword 也会进行相应的的变化。升级为轻量级锁的过程:

    1. 线程在自己的栈桢中创建锁记录 LockRecord。
    2. 将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中。
    3. 将锁记录中的 Owner 指针指向锁对象。
    4. 将锁对象的对象头的 MarkWord替换为指向锁记录的指针。
    image-20220116160840118 image-20220116160856504
    自旋锁

    轻量级锁在加锁过程中,用到了自旋锁

    • 所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。
    • 注意,锁在原地循环的时候,是会消耗 cpu 的,就相当于在执行一个啥也没有的 for 循环。
    • 所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短的时间就能够获得锁了。

    自旋锁的使用,其实也是有一定的概率背景,在大部分同步代码块执行的时间都是很短的。

    • 所以通过看似无异议的循环反而能提升锁的性能。
    • 但是自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而会消耗 CPU 资源。
    • 默认情况下自旋的次数是 10 次,可以通过 preBlockSpin 来修改

    在 JDK1.6 之后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。

    • 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。
    • 如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源
    轻量级锁的解锁

    轻量级锁的锁释放逻辑其实就是获得锁的逆向逻辑,通过CAS 操作把线程栈帧中的 LockRecord 替换回到锁对象的MarkWord 中,

    如果成功表示没有竞争。如果失败,表示当前锁存在竞争,那么轻量级锁就会膨胀成为重量级锁

    流程图分析

    image-20220116160937683

    重量级锁

    当轻量级锁膨胀到重量级锁之后,意味着线程只能被挂起阻塞来等待被唤醒了。

    重量级锁的 monitor

    我们写一个java类,对行代码加锁,

    public abstract class User {
    
        public static void testSync() {
            synchronized (User.class) {
                System.out.println("我是锁");
            }
        }
    
        public static void main(String[] args) {
            User.testSync();
        }
    }
    

    使用 javac User.java 编译成class,然后使用javac -v User 反编译

    image-20220116163529560

    加了同步代码块以后,在字节码中会看到一个monitorenter 和 monitorexit。

    • 每一个 JAVA 对象都会与一个监视器 monitor 关联,我们可以把它理解成为一把锁,
    • 当一个线程想要执行一段被synchronized 修饰的同步方法或者代码块时,该线程得先获取到 synchronized 修饰的对象对应的 monitor。
    • monitorenter 表示去获得一个对象监视器。monitorexit 表示释放 monitor 监视器的所有权,使得其他被阻塞的线程可以尝试去获得
    • 这个监视器monitor 依赖操作系统的 MutexLock(互斥锁)来实现的, 线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能
    重量级锁的加锁

    任意线程对 Object(Object 由 synchronized 保护)的访问,

    • 首先要获得 Object 的监视器。
    • 如果获取失败,线程进入同步队列,线程变为 BLOCKED。
    • 当访问 Object 的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

    image-20220116163908166

    回顾线程的竞争机制

    再来回顾一下线程的竞争机制对于锁升级这块的一些基本流程。方便大家更好的理解

    加入有这样一个同步代码块,存在 Thread#1、Thread#2 等多个线程

    synchronized (lock) {
      
    // do something*
      
    }
    
    • 情况一:只有 Thread#1 会进入临界区;

    • 情况二:Thread#1 和 Thread#2 交替进入临界区,竞争不激烈;

    • 情况三:Thread#1/Thread#2/Thread3… 同时进入临界区,竞争激烈

    偏向锁

    1. 此时当 Thread#1 进入临界区时,JVM 会将 lockObject 的对象头 Mark Word 的锁标志位设为“01”,同时会用 CAS 操作把 Thread#1 的线程 ID 记录到 Mark Word 中,此时进入偏向模式。

    2. 所谓“偏向”,指的是这个锁会偏向于 Thread#1,若接下来没有其他线程进入临界区,则 Thread#1 再出入临界区无需再执行任何同步操作。

    3. 也就是说,若只有Thread#1 会进入临界区,实际上只有 Thread#1 初次进入临界区时需要执行 CAS 操作,以后再出入临界区都不会有同步操作带来的开销。

    轻量级锁

    1. 偏向锁的场景太过于理想化,更多的时候是 Thread#2 也会尝试进入临界区,
    2. 如果 Thread#2 也进入临界区但是Thread#1 还没有执行完同步代码块时,会暂停 Thread#1并且升级到轻量级锁。
    3. Thread#2 通过自旋再次尝试以轻量级锁的方式来获取锁

    重量级锁

    1. 如果 Thread#1 和 Thread#2 正常交替执行,那么轻量级锁基本能够满足锁的需求。
    2. 但是如果 Thread#1 和 Thread#2同时进入临界区,那么轻量级锁就会膨胀为重量级锁,
    3. 意味着 Thread#1 线程获得了重量级锁的情况下,Thread#2就会被阻塞

    Synchronized 结合 wait,notify,notifyAll

    前面我们在讲 synchronized 的时候,发现被阻塞的线程什么时候被唤醒,取决于获得锁的线程什么时候执行完同步代码块并且释放锁。那怎么做到显示控制呢?我们就需要借 助 一 个 信 号 机 制 : 在 Object 对 象 中 , 提 供 了wait/notify/notifyall,可以用于控制线程的状态

    wait/notify/notifyall 基本概念

    • wait:表示持有对象锁的线程 A 准备释放对象锁权限,释放 cpu 资源并进入等待状态。

    • notify:表示持有对象锁的线程 A 准备释放对象锁权限,通知 jvm 唤 醒 某 个 竞 争 该 对 象 锁 的 线 程 X 。 线 程 A synchronized 代码执行结束并且释放了锁之后,线程 X 直接获得对象锁权限,其他竞争线程继续等待(即使线程 X 同步完毕,释放对象锁,其他竞争线程仍然等待,直至有新的 notify ,notifyAll 被调用)。

    • notifyAll:notifyall 和 notify 的区别在于,notifyAll 会唤醒所有竞争同一个对象锁的所有线程,当已经获得锁的线程A 释放锁之后,所有被唤醒的线程都有可能获得对象锁权

    限需要注意的是:三个方法都必须在 synchronized 同步关键字 所 限 定 的 作 用 域 中 调 用 , 否 则 会 报 错

    java.lang.IllegalMonitorStateException ,意思是因为没有同步,所以线程对对象锁的状态是不确定的,不能调用这些方法。

    另外,通过同步机制来确保线程从 wait 方法返回时能够感知到感知到 notify 线程对变量做出的修改

    image-20220116164554208

    展开全文
  • 如果此对象的对象锁被其他调用者占用,则进入阻塞队列,等待此锁被释放(同步块正常返回或者抛异常终止,由JVM自动释放对象锁)。 注意,方法锁也是一种对象锁。当一个线程访问一个带 synchronized 方法时,由于对象...

    前言

    synchronized 用来处理多个线程同时访问同一个类的一个代码块、方法,甚至这个类。
    (1)修饰代码块时,需要一个reference对象作为锁的对象。
    (2)修饰方法时,默认是当前对线作为锁的对象。
    (3)修饰类时,默认是当前类的Class对象作为锁的对象。

    1、方法锁

    通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。

    synchronized 方法控制对类成员变量的访问:
    每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态,从而有效避免了类成员变量的访问冲突。

    2、对象锁

    当一个对象中有同步方法或者同步块,线程调用此对象进入该同步区域时,必须获得对象锁。如果此对象的对象锁被其他调用者占用,则进入阻塞队列,等待此锁被释放(同步块正常返回或者抛异常终止,由JVM自动释放对象锁)。
    注意,方法锁也是一种对象锁。当一个线程访问一个带 synchronized 方法时,由于对象锁的存在,所有加 synchronized 的方法都不能被访问(前提是在多个线程调用的是同一个对象实例中的方法)。
    对象锁有两种形式:

    public class object {
    	public synchronized void method(){
    		System.out.println("我是对象锁也是方法锁");
    	}
    }
    

    另外一种是:

    public class object {
    	public void method(){
    		synchronized(this){
    			System.out.println("我是对象锁");
    		}
    	}
    }
    
    

    3、类锁

    一个class其中的静态方法和静态变量在内存中只会加载和初始化一份,所以,一旦一个静态的方法被申明为synchronized,此类的所有的实例化对象在调用该方法时,共用同一把锁,称之为类锁。

    也是两种形式:

    public class object {
    	public static synchronized void method(){
    		System.out.println("我是第一种类锁");
    	}
    }
    
    

    第二种:

    public class object {
    	public void method(){
    		synchronized (object.this) {
    			System.out.println("我是第二种类锁");
    		}
    	}
    }
    
    
    展开全文
  • 一、对象头 在 JVM 中,对象在内存中分为三块区域: 对象头:标记字段和类型指针。一会介绍 实例数据:这部分主要是存放类的数据信息,父类的信息。 对齐填充:由于虚拟机要求对象起始地址必须是8字节的整数倍,...

    很久之前写的一篇文章,简单整理了下发表出来。整理的不够好,但是内容很充分很多内容是长期积累的舍不得删了,读者尽量把前面的看懂吧。源码部分可忽略

    一、对象头

    在 JVM 中,对象在内存中分为三块区域:

    • 对象头:标记字段类型指针。一会介绍
    • 实例数据:这部分主要是存放类的数据信息,父类的信息。
    • 对齐填充:由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。

    HotSpot虚拟机中,设计了一个OOP&Klass Model。它用来标识一个对象的特征,注意并不包括我们的数据。

    • OOP(Ordinary Object Pointer)指的是普通对象指针。他是一个完整的java对象
    • 而Klass用来描述对象实例的具体类型。
      • 它在每个类被JVM加载的时候创建,每个类对应一个instanceKlass,保存在方法区,用来在JVM层表示该Java类。

    当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据。

    以下面一段代码为例我们分析他们的对象头

    class Model {
        public static int a = 1;
        public int b;
    
        public Model(int b) {
            this.b = b;
        }
    }
    
    public static void main(String[] args) {
        int c = 10;
        Model modelA = new Model(2); //对应一个instanceOopDesc
        Model modelB = new Model(3);
    }
    

    图片

    OOP包含了markWord+Klass+实例数据。

    • markWord用于synchronized锁
    • Kclass指向方法区实例数据包含属性
    图片

    这就是一个简单的Java对象的OOP&Klass模型,即Java对象模型。

    图片

    上图标识:

    • p在
    • new Person()在中,它包含了对象头和实例对象
    • 上面对象头里有个Kclass,就是JVM加载的java类,它指向方法区。同一类型的java对象指向同一个Kclass
    // 对象头,它是其他对象类的基类
    class oopDesc {
      friend class VMStructs;
     private:
      volatile markOop  _mark; // mark word
      union _metadata {
        wideKlassOop    _klass; // kclass
        narrowOop       _compressed_klass;
      } _metadata; // 元数据
    }
    

    1.1 klass pointer

    Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据。如果对象是数组对象,那么对象头占用3个字宽(Word),如果对象是非数组对象,那么对象头占用2个字宽。(1word = 2 Byte = 16 bit)

    \1. 每个Class的属性指针(即静态变量)

    \2. 每个对象的属性指针(即对象变量)

    \3. 普通对象数组的每个元素指针

    当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针( JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。

    二、从synchonized看对象头

    • 对于同步方法,JVM采用ACC_SYNCHRONIZED标记符来实现同步。
    • 对于同步代码块。JVM采用monitorentermonitorexit两个指令来实现同步。

    方法级的同步是隐式的。同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。

    可以把执行monitorenter指令理解为加锁,执行monitorexit理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行monitorenter)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorexit指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。

    0、mark word

    我们到synchronized有锁升级的过程,他的原理就是操作对象头的mark word。

    mark word一般有64bit(在64位虚拟机下),各状态下每个bit的组成如图

    img

    如上图,关键字段分别指向的是

    • 线程id
    • 栈中锁记录的指针
    • 重量级锁monitor的指针

    在源码中各字段的bit:

    //  markOop.hpp 
    public:
    // Constants
    enum { 
        age_bits                 = 4, // 为什么最大年龄是15,就是因为只能是1111
        lock_bits                = 2,
        biased_lock_bits         = 1,
        max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
        hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits,
        cms_bits                 = LP64_ONLY(1) NOT_LP64(0),
        epoch_bits               = 2
    };
    
    // 锁状态的枚举:
    enum {  
        locked_value             = 0,//轻量级锁 000
        unlocked_value           = 1,// 无锁  001
        monitor_value            = 2,//重量级锁 010
        marked_value             = 3,//GC标记 011
        biased_lock_pattern      = 5//可偏向 101
    };
    

    32位虚拟机下,Mark Word是32bit大小的

    1 无锁

    无锁状态下就是我们上面的第一行,此时对象刚创建。

    最后的状态位为0 01

    img

    在这里插入图片描述

    2 偏向锁

    最后的状态位为1 01

    2.1 无锁升级偏向锁

    偏向锁解决一把锁与锁之间使用不冲突的情况,不用等待

    为什么要引入偏向锁?:在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。

    偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadID即可。

    开始获取锁:(结合这张图看https://img-blog.csdnimg.cn/20190323140321501.png)

    1)先看后两位:看后两位是否01(代表无锁或偏向锁),是的话看倒数第三位的偏向锁位

    2)无锁检查:偏向锁位为0,则直接去CAS。或者如果是1代表是当前线程ID,那么也获取成功。

    3)无锁时用CAS升级偏向锁:通过CAS把mark wordThread ID从0改为当前线程的。

    • 如果CAS换线程ID成功,获得偏向锁,执行同步代码
      • 复用该线程下次进入锁时,因为是自己的线程ID,直接获取到了锁(无操作,效率高)。
    • 如果CAS失败,说明自己栈帧里保存的mark word副本已经和对象头里mark word不符了,接下来看是否真要升级为轻量级锁(上个线程可能用完了但是没有擦除(偏向锁不会主动释放锁),所以先看前个线程是否存活,如果存活去看前线程的栈帧信息(从栈帧可以知道该线程执行到哪了,栈里是否有该方法或对象),)
      • 暂停持有锁的线程,检查原线程消亡/退出同步块没有,
        • 退出了的话线程ID位设置为0,重新走CAS逻辑
        • 没有退出,升级轻量级锁
    									是:获取到偏向锁,执行代码
    	倒数第三位是1(偏向锁)-检查Thread ID是否是自己
    									否:CAS尝试从0到自己,当然失败,于是撤销偏向锁,准备到轻量级锁(在正式到轻量之前还会检查一遍原线程状态)
    01
    	倒数第三位是0-CAS从0到当前线程
    

    总的来说就是看线程和栈帧

    2.2 偏向锁的升级

    验证偏向锁是否还在

    当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

    偏向锁撤销

    此时锁使用发生碰撞了,需要一个等待的情况,那就进入轻量级锁的步骤。

    发生偏向锁碰撞的时候,我们要升级轻量级锁,但升级之前,要先进行偏向锁的撤销

    撤销的要点:

    \1. 偏向锁的撤销动作必须等待全局安全点

    \2. 暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,如果偏向位是1的话就变成0

    \3. 撤销偏向锁,恢复到无锁(标志位为 01)或轻量级锁(标志位为 00)的状态

    此时谁都没有偏向锁

    2.3 偏向锁的设置

    偏向锁是在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。

    它同样是一个带有效益权衡性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问比如线程池,那偏向模式就是多余的。

    • 关闭偏向锁:可以通过-XX:-UseBiasedLocking = false来设置;
    • 偏向锁在1.6之后是默认开启的,但在应用程序启动几秒钟之后才激活,可以使用-XX:BiasedLockingStartupDelay=0 参数关闭延迟,如果确定应用程序中所有锁通常情况下处于竞争状态,可以关闭偏向锁。
    • 1.5中是关闭的,需要手动开启参数是xx:-UseBiasedLocking=true

    2.4 例子

    线程1复用自己偏向锁的情况对象头-Mark word
    访问同步块 A,检查 Mark 中是否有线程 ID101(无锁可偏向)
    尝试加偏向锁101(无锁可偏向)对象 hashCode
    成功101(无锁可偏向)线程ID
    执行同步块 A101(无锁可偏向)线程ID
    访问同步块 B,检查 Mark 中是否有线程 ID101(无锁可偏向)线程ID
    是自己的线程 ID,锁是自己的,无需做更多操作101(无锁可偏向)线程ID
    执行同步块 B101(无锁可偏向)线程ID
    执行完毕101(无锁可偏向)对象 hashCode

    3 轻量级锁

    img

    轻量级锁中是指向栈中锁记录的指针的

    偏向锁升级轻量级锁

    有几个概念区分一下:

    对象头的Mark Word

    线程的锁记录Lock Record中

    轻量级锁每个线程都会把Mark Word拷贝到他的线程中,重要的是谁能让MarkWord中的指针指向自己

    轻量级锁的释放更复杂,首先他释放时候得看对象头里的Mark Word是否还指向自己(可能已经重量级锁了),其次要判断当前线程Mark Word的信息与对象头的Mark Word信息是否一致(万一是其他的对象锁呢)

    当(关闭偏向锁功能)或者(多个线程竞争偏向锁导致偏向锁升级为轻量级锁),则会尝试获取轻量级锁,其步骤如下: 获取轻量级锁

    • 1、判断当前对象是否处于无锁状态(hashcode...-0-01),如果是无锁,则JVM首先将在当前线程的栈帧中建立一个名为锁记录Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(这份栈中的拷贝叫Displaced Mark Word),将对象的Mark Word复制到栈帧中的Lock Record中,将Lock Record中的owner指向当前对象从栈帧指向对象头,object,也就是互相引用了,这样别的线程就知道是不是自己持有了锁)。否则执行步骤(3);
      • Mark Word在对象头中,Displaced Mark Word在栈帧中,Lock Record在Displaced Mark Word中,owner在Lock Record中
    • 2、JVM利用CAS操作尝试将对象头的Mark Word(的除后2位的bit)更新为指向Lock Record的指针(从对象头指向栈帧),如果成功表示竞争到锁,则将锁标志位变成00(表示此对象处于轻量级锁状态),执行同步操作。;如果失败则执行步骤(3);
    • 3、如果前面执行成功了,那么就去执行业务代码了。如果1、2步失败,则判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,对象头的锁标志位变成10(代表我来过了),后面等待的线程将会进入阻塞状态。
    自旋

    有自旋锁的情况:

    • 线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;
    • 如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。
    • 但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

    自旋

    情景:

    开车去学校,一条路是多路灯路段,一条路是无路灯路段(绕一点远),你选择走哪个?

    答案:绕一点远的比较省时省油,因为车启动那一脚油门比较费油

    轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景(你没看错,是发生碰撞了,但是等会就获取到了,空转而不阻塞)。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋着等待锁释放。

    自旋的由来:

    • JDK1.6之前:当轻量级锁发发生竞争的时候会升级为重量级锁,但是重量级锁性能开销大,所以应该尽量避免升级为重量级锁。所以从轻量级升级为重量级的时候还会挣扎一下,避免升级为重量级。
    • JDK1.6后:引入了 自旋锁(默认开启,默认自旋次数为10)、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

    关于自旋的操作:

    • -XX:-UseSpinning 关闭自旋锁(1.6后默认开启)
    • -XX:preBlockSpin:调整自旋次数(默认10次)
    • 自适应自旋锁:可以按运行条件自动改变自旋次数

    为什么要挣扎:因为线程切换上下文耗时,线程的阻塞和唤醒需要CPU从用户态转为核心态。如果cas不到锁直接阻塞万一别的线程用锁时间很短呢,没必要阻塞,等一会即可。所谓挣扎就是在阻塞之前循环几次cas拿锁,实在cas不到再阻塞

    java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。

    1. 如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间;
    2. 如果对于那些需要同步的简单的代码块,获取锁挂起的耗时比用户代码执行的耗时还要长,这种同步策略显然非常糟糕的。

    自适应自旋锁

    在JDK 6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100次循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虛拟机就会变得越来越“聪明”了。

    1. 如果平均负载小于CPUs则一直自旋
    2. 如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞
    3. 如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞
    4. 如果CPU处于节电模式则停止自旋
    5. 自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差)
    6. 自旋时会适当放弃线程优先级之间的差异

    它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

    1. Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
    2. Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;
    3. Wait Set:那些调用wait方法被阻塞的线程被放置在这里;
    4. OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;
    5. Owner:当前已经获取到所资源的线程被称为Owner;
    6. !Owner:当前释放锁的线程。

    JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。

    OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中。

    处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。

    • Java 6 之后自旋锁是自适应的
    • Java 7 之后不能控制是否开启自旋功能
    轻量级锁的释放

    轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:

    • 取出在获取轻量级锁保存在Displaced Mark Word(就是自己线程栈帧中复制过mark word)中的数据。
    • 用CAS操作将取出的数据替换当前java对象的Mark Word中,如果成功,则说明释放锁成功,否则执行(3);
    • 如果CAS操作替换失败,说明有其他线程尝试获取该锁(被别人改为10了),则需要将轻量级锁需要膨胀升级为重量级锁。

    4、重量级锁

    重量级锁后Mark Word就不够用了,需要引入monitor来形成阻塞队列了,于此同时Mark Word对应的位也是monitor指针了

    img

    场景:好比等红灯时汽车是不是熄火,不熄火相当于自旋(等待时间短了划算),熄火了相当于阻塞(等待时间长了划算)

    自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。但是一直自旋也不好,所以要阻塞释放CPU给别人

    轻量级锁升级重量级锁

    monitor

    monitor是重量级锁里的东西

    重量级锁由monitor实现,当线程经monitor锁,得不到monitor锁的会阻塞。当拥有monitor锁的线程释放的时候会唤醒正在阻塞的线程来竞争锁。

    synchronized方法的反编译会出现
    {
        monitorenter;
        业务;
        monitorexit;
    }
    

    Synchronize的实现原理,无论是同步方法还是同步代码块,无论是ACC_SYNCHRONIZED还是monitorentermonitorexit都是基于Monitor实现的

    • 同步代码块monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;
    • 同步方法:依靠的是方法修饰符上的ACC_SYNCHRONIZED实现。synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象。(摘自:http://www.cnblogs.com/javaminer/p/3889023.html)

    线程尝试获取monitor的所有权,如果获取失败说明monitor被其他线程占用,则将线程加入到的同步队列中,等待其他线程释放monitor,当其他线程释放monitor后,有可能刚好有线程来获取monitor的所有权,那么系统会将monitor的所有权给这个线程,而不会去唤醒同步队列的第一个节点去获取,所以synchronized是非公平锁。如果线程获取monitor成功则进入到monitor中,并且将其进入数+1。

    重量级锁下,当一个线程想要执行一段被synchronized圈起来的同步方法或者代码块时,该线程得先获取到synchronized修饰的对象对应的monitor。

    img

    monitor并不是随着对象创建而创建的。我们是通过synchronized修饰符告诉JVM需要为我们的某个对象创建关联的monitor对象。每个线程都存在两个ObjectMonitor对象列表,分别为freeused列表。同时JVM中也维护着global locklist。当线程需要ObjectMonitor对象时,首先从线程自身的free表中申请,若存在则使用,若不存在则从global list中申请。

    ObjectMonitor() {
        _count        = 0; //用来记录该对象被线程获取锁的次数
        _waiters      = 0;
        _recursions   = 0; //锁的重入次数
        _owner        = NULL; //指向持有ObjectMonitor对象的线程 
        _WaitSetLock  = 0 ;
        
        _WaitSet      = NULL;  // .wait()方法//处于wait状态的线程,会被加入到_WaitSet
        _EntryList    = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
    }
    
    排队流程
    1. 当多个线程同时访问该方法,那么这些线程会先被放进_EntryList队列,此时线程处于blocking阻塞状态
    2. 当一个线程获取到了实例对象的监视器(monitor)锁,那么就可以进入running状态,执行方法,此时,ObjectMonitor对象的_owner指向当前线程,_count加1表示当前对象锁被一个线程获取
    3. 当running状态的线程调用wait()方法,那么当前线程释放monitor对象,进入waiting状态,ObjectMonitor对象的_owner变为null,_count减1,同时线程进入_WaitSet队列,直到有线程调用notify()方法唤醒该线程,则该线程重新获取monitor对象进入_Owner
    4. 如果当前线程执行完毕,那么也释放monitor对象,进入waiting状态,ObjectMonitor对象的_owner变为null,_count减1

    同时 Entrylist集合和 Waitsets集合中的线程会进入阻塞状阻塞的线程会进入内核调度状态,因为阻塞状态是通过 Linux的 pthread_ mutex_lock来实现的,所以线程要想阻塞就会发生用户态和内核态的切换此时会严重影响性能

    lock锁是await和signal

    synchronized是wait和notify

    monitor代码

    在HotSpot虚拟机中,monitor是由ObjectMonitor实现的。其源码是用c++来实现的

    // `src/share/vm/runtime/objectMonitor.hpp`
    ObjectMonitor() {
        _header       = NULL;
        _count        = 0;
        _waiters      = 0,
        _recursions   = 0; // 线程的重入次数
        _object       = NULL;//存储该monitor的对象 //即java对象引用了monitor,monitor又引用了java对象
        // _owner初始时为NULL。当有线程占有该monitor时,owner标记为该线程的唯一标识。当线程释放monitor时,owner又恢复为NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线程安全的。
        _owner        = NULL;//标识拥有该monitor的线程
        // 因为调用wait方法而被阻塞的线程会被放在该队列中。
        _WaitSet      = NULL;//处于wait状态的线程,会被加入到waitSet
        _WaitSetLock  = 0 ;
        _Responsible  = NULL ;
        _succ         = NULL ;
        // 竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。`_cxq`是一个临界资源,JVM通过CAS原子指令来修改`_cxq`队列。修改前`_cxq`的旧值填入了node的next字段,`_cxq`指向新值(新线程)。因此`_cxq`是一个后进先出的stack(栈)。
        _cxq          = NULL ;//多线程竞争锁时的单向列表
        FreeNext      = NULL ;
        // _cxq队列中有资格成为候选资源的线程会被移动到该队列中。
        _EntryList    = NULL ;//处于等待block状态的线程,会被加入到该列表 //从cxq移过来的
        _SpinFreq     = 0 ;
        _SpinClock    = 0 ;
        OwnerIsThread = 0 ;
        _previous_owner_tid = 0;
    }
    

    5、总结

    在这里插入图片描述

    轻量加锁成功演示

    下面演示两个线程不冲突地获取锁的过程,即轻量加锁的1、2步的操作

    线程 1对象 Mark Word线程 2
    访问同步块 A,把 Mark 复制到线程 1 的锁记录01(无锁)-
    CAS 修改 Mark 为线程 1 锁记录地址01(无锁)-
    1 成功(加锁)00(轻量锁)线程 1 锁记录地址-
    执行同步块 A00(轻量锁)线程 1 锁记录地址-
    访问同步块 B,把 Mark 复制到线程 1 的锁记录00(轻量锁)线程 1 锁记录地址-
    CAS 修改 Mark 为线程 1 锁记录地址00(轻量锁)线程 1 锁记录地址-
    2 失败(但是发现是自己的锁)00(轻量锁)线程 1 锁记录地址-
    锁重入00(轻量锁)线程 1 锁记录地址-
    执行同步块 B00(轻量锁)线程 1 锁记录地址-
    同步块 B 执行完毕00(轻量锁)线程 1 锁记录地址-
    同步块 A 执行完毕00(轻量锁)线程 1 锁记录地址-
    成功(解锁)01(无锁)-
    -01(无锁)访问同步块 A,把 Mark 复制到线程 2 的锁记录
    -01(无锁)CAS 修改 Mark 为线程 2 锁记录地址
    -00(轻量锁)线程 2 锁记录地址成功(加锁)
    -
    轻量加锁失败演示:锁膨胀(轻量->重量)

    下面演示轻量加锁第三步,由00轻量变10重量

    如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

    线程 1对象 Mark线程 2
    访问同步块,把 Mark 复制到线程 1 的锁记录01(无锁)-
    CAS 修改 Mark 为线程 1 锁记录地址01(无锁)-
    成功(加锁)00(轻量锁)线程 1 锁记录地址-
    执行同步块00(轻量锁)线程 1 锁记录地址-
    执行同步块00(轻量锁)线程 1 锁记录地址访问同步块,把 Mark 复制到线程 2
    执行同步块00(轻量锁)线程 1 锁记录地址CAS 修改 Mark 为线程 2 锁记录地址
    执行同步块00(轻量锁)线程 1 锁记录地址失败(发现别人已经占了锁)
    执行同步块00(轻量锁)线程 1 锁记录地址CAS 修改 Mark 为重量锁
    执行同步块10(重量锁)重量锁指针阻塞中
    执行完毕10(重量锁)重量锁指针阻塞中
    解锁(失败)10(重量锁)重量锁指针阻塞中
    释放重量锁,唤起阻塞线程竞争01(无锁)阻塞中
    -10(重量锁)竞争重量锁
    -10(重量锁)成功(加锁)
    -
    线程1 (cpu 1 上)重量自旋重试成功的情况对象Mark线程2 (cpu 2 上)
    -10(重量锁)-
    访问同步块,获取 monitor10(重量锁)重量锁指针-
    成功(加锁)10(重量锁)重量锁指针-
    执行同步块10(重量锁)重量锁指针-
    执行同步块10(重量锁)重量锁指针访问同步块,获取 monitor
    执行同步块10(重量锁)重量锁指针自旋重试
    执行完毕10(重量锁)重量锁指针自旋重试
    成功(解锁,把标志位变为01)01(无锁)自旋重试
    -10(重量锁)重量锁指针成功(加锁)
    -10(重量锁)重量锁指针执行同步块
    -
    线程 1(cpu 1上)重量自旋重试失败的情况对象Mark线程2(cpu 2 上)
    -10(重量锁)-
    访问同步块,获取 monitor10(重量锁)重量锁指针-
    成功(加锁)10(重量锁)重量锁指针-
    执行同步块10(重量锁)重量锁指针-
    执行同步块10(重量锁)重量锁指针访问同步块,获取 monitor
    执行同步块10(重量锁)重量锁指针自旋重试
    执行同步块10(重量锁)重量锁指针自旋重试
    执行同步块10(重量锁)重量锁指针自旋重试
    执行同步块10(重量锁)重量锁指针阻塞
    -

    6、jol实战验证

    <dependency>
        <groupId>org.openjdk.jol</groupId>
        <artifactId>jol-core</artifactId>
        <version>0.13</version>
    </dependency>
    

    对象头的大小:Java对象头一般占有两个机器码(注意是机器码不是字节码。在32位虚拟机中,1个机器码等于4字节,也就是32bit),

    但是如果对象是数组类型,则需要三个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。

    public static void main(String[] args) {
        // 声明一枚长度为3306的数组
        int[] intArr = newint[3306];
        // 使用jol的ClassLayout工具分析对象布局
        System.out.println(ClassLayout.parseInstance(intArr).toPrintable());
    }
        
    print:
    ------------就是对象刚创建好的状态-------------------------
    [I object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                      VALUE
      0       4     (object header)                   01000000 (00000001000000000000000000000000) (1)  // 这个1是偏向锁标志
      4       4     (object header)                   00000000 (00000000000000000000000000000000) (0)
      8       4     (object header)                   6d 0100 f8 (01101101000000010000000011111000) (-134217363) // 这行如果是非数组对象是没有的
      12      4     (object header)                   ea 0c 0000 (11101010000011000000000000000000) (3306)
         1613224int [I.<elements>                     N/A
    Instance size: 13240 bytes
    Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
    

    图片

    import org.openjdk.jol.info.ClassLayout;
    
    public class Main13 {
    	public static void main(String[] args) {
    
    		Main13 main13 = new Main13();
    		System.out.println(ClassLayout.parseInstance(main13).toPrintable());
    	}
    }
    
    //---------输出------------------
    Main13 object internals:
     OFFSET  SIZE   TYPE DESCRIPTION              VALUE
       0      4        (object header)         01 00 00 00 (00000001 00000000 00000000 00000000) (1)
       4      4        (object header)         00 00 00 00 (00000000 00000000 00000000 00000000) (0)
       8      4        (object header)         05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
       12     4        (loss due to the next object alignment)
         // 0-11共16个字节,即64位×2=8×2
         // 如果加个int a成员变量,最后一行改为
         /*
          12     4    int Main13.a                                  0
          */
         // 如果加个boolean a,最后一行改为如下,会有字节对齐
         /*
         12     1   boolean Main13.a                                  false
         13     3           (loss due to the next object alignment)
         */
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    // 对齐填充:
        对齐填充并不是必然存在的,也没有什么特别的意义,他仅仅起着占位符的作用,由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头正好是8字节的倍数,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
    

    8、synchronized使用技巧

    锁消除

    Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间

    锁消除是指虚拟机即时编译器(JIT)在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是程序员自己应该是很清楚的,怎么会在明知道不存在数据争用的情况下要求同步呢?实际上有许多同步措施并不是程序员自己加入的,同步的代码在Java程序中的普遍程度也许超过了大部分读者的想象。下面这段非常简单的代码仅仅是输出3个字符串相加的结果,无论是源码字面上还是程序语义上都没有同步。

    为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这时JVM会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。 如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于我们程序员来说这还不清楚么?我们会在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?我们虽然没有显示使用锁,但是我们在使用一些JDK的内置API时,如StringBuffer、Vector、HashTable等,这个时候会存在隐形的加锁操作。比如StringBuffer的append()方法,Vector的add()方法:

    // 在运行这段代码时,JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外,所以JVM可以大胆地将vector内部的加锁操作消除。
    public static void vectorTest(){
        Vector<String> vector = new Vector<String>();
        for(int i = 0 ; i < 10 ; i++){
            vector.add(i + "");
        }
    }
    
    
    public static void main(){contactStr("aa", "bb", "cc");}
    public static String contactStr(String s1, String s2, String s3) {
        return new StringBuffer().append(s1)
            .append(s2)
            .append(s3)
            .toString();
        // StringBuffer的append()是一个同步方法,锁就是this也就是(new StringBuilder())。虚拟机发现它的动态作用域被限制在concatString( )方法内部。也就是说, new StringBuilder()对象的引用永远不会“逃逸”到concatString ( )方法之外,其他线程无法访问到它,因此,虽然这里有锁,但是可以被安全地消除掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。
    }
    
    锁粗化

    按理来说,同步块的作用范围应该尽可能小,仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。

    但是加锁解锁也需要消耗资源,如果存在一个线程一系列的连续加锁解锁操作,甚至加锁操作是出现在循环体中的,可能会导致不必要的性能损耗。

    锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。

    什么是锁粗化?JVM会探测到一连串细小的操作都使用同一个对象加锁,将同步代码块的范围放大,放到这串操作的外面,这样只需要加一次锁即可。

    public class Demo01 {
        public static void main(String[] args) {
            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < 100; i++) {
                // 
                b.append("aa");
            }
            System.out.println(sb.toString());
        }
    }
    

    32位的mark word

    32位虚拟机下,Mark Word是32bit大小的,其存储结构如下:

    img

    源码

    术语参考: http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html

    JVM源码下载

    http://openjdk.java.net/ --> Mercurial --> jdk8 --> hotspot --> zip

    C++ IDE(Clion )下载 https://www.jetbrains.com/

    oop体系:

    当一个线程尝试访问synchronized修饰的代码块时,它首先要获得锁,那么这个锁到底存在哪里呢?是存在锁对象的对象头中的。

    HotSpot采用instanceOopDescarrayOopDesc来描述对象头。arrayOopDesc对象用来描述数组类型。instanceOopDesc的定义的在Hotspot源码的 instanceOop.hpp 文件中。arrayOopDesc的定义对应 arrayOop.hpp

    // 
    class instanceOopDesc : public oopDesc {
        public:
        // aligned header size.
        static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }
        // If compressed, the offset of the fields of the instance may not be aligned.
        static int base_offset_in_bytes() {
            // offset computation code breaks if UseCompressedClassPointers
            // only is true
            return (UseCompressedOops && UseCompressedClassPointers) ?
                klass_gap_offset_in_bytes() :
            sizeof(instanceOopDesc);
        }
        static bool contains_field_offset(int offset, int nonstatic_field_size) {
            int base_in_bytes = base_offset_in_bytes();
            return (offset >= base_in_bytes &&
                    (offset-base_in_bytes) < nonstatic_field_size * heapOopSize);
        }
    };
    

    下面列出的是整个Oops模块的组成结构,其中包含多个子模块。每一个子模块对应一个类型,每一个类型的OOP都代表一个在JVM内部使用的特定对象的类型。

    //定义了oops共同基类
    typedef class   oopDesc*                    oop;
    //表示一个Java类型实例
    typedef class   instanceOopDesc*            instanceOop;
    //表示一个Java方法
    typedef class   methodOopDesc*              methodOop;
    //表示一个Java方法中的不变信息
    typedef class   constMethodOopDesc*         constMethodOop;
    //记录性能信息的数据结构
    typedef class   methodDataOopDesc*          methodDataOop;
    //定义了数组OOPS的抽象基类
    typedef class   arrayOopDesc*               arrayOop;
    //表示持有一个OOPS数组
    typedef class   objArrayOopDesc*            objArrayOop;
    //表示容纳基本类型的数组
    typedef class   typeArrayOopDesc*           typeArrayOop;
    //表示在Class文件中描述的常量池
    typedef class   constantPoolOopDesc*        constantPoolOop;
    //常量池告诉缓存
    typedef class   constantPoolCacheOopDesc*   constantPoolCacheOop;
    //描述一个与Java类对等的C++类
    typedef class   klassOopDesc*               klassOop;
    //表示对象头
    typedef class   markOopDesc*                markOop;
    
    基类

    从上面的代码中可以看到,有一个变量opp的类型是oppDesc ,OOPS类的共同基类型为oopDesc

    // oop.hpp  // instanceOopDesc继承自oopDesc
    class oopDesc {//对象头的父类
        friend class VMStructs;
        private:
        volatile markOop _mark;// Mark World
        union _metadata { //类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址
            Klass*  _klass;// 类型指针 Klass
            narrowKlass _compressed_klass;// 压缩类指针
        } _metadata;
        // Fast access to barrier set. Must be initialized.
        static BarrierSet* _bs;
        // 省略其他代码
    };
    

    **在Java程序运行过程中,每创建一个新的对象,在JVM内部就会相应地创建一个对应类型的OOP对象。**在HotSpot中,根据JVM内部使用的对象业务类型,具有多种oopDesc的子类。除了oppDesc类型外,opp体系中还有很多instanceOopDescarrayOopDesc 等类型的实例,他们都是oopDesc的子类。

    图片

    这些OOPS在JVM内部有着不同的用途,例如**,instanceOopDesc表示类实例,arrayOopDesc表示数组。**也就是说,当我们使用new创建一个Java对象实例的时候,JVM会创建一个instanceOopDesc对象来表示这个Java对象。同理,当我们使用new创建一个Java数组实例的时候,JVM会创建一个arrayOopDesc对象来表示这个数组对象。

    在HotSpot中,oopDesc类定义在oop.hpp中,instanceOopDesc定义在instanceOop.hpp中,arrayOopDesc定义在arrayOop.hpp中。

    简单看一下相关定义:

    class instanceOopDesc : public oopDesc {
    }
    
    class arrayOopDesc : public oopDesc {
    }
    

    通过上面的源码可以看到,instanceOopDesc实际上就是继承了oopDesc,并没有增加其他的数据结构,也就是说**instanceOopDesc中包含两部分数据:markOop _markunion _metadata。**

    这里的markOop你可能又熟悉了,这不就是OOPS体系中的一部分吗,上面注释中已经说过,他表示对象头。 _metadata是一个联合体,这个字段被称为元数据指针。指向描述类型Klass对象的指针。

    HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头、实例数据和对齐填充。在虚拟机内部,一个Java对象对应一个instanceOopDesc的对象,该对象中有两个字段分别表示了对象头和实例数据。那就是_mark_metadata

    文章开头我们就说过,之所以我们要写这篇文章,是因为对象头中有和锁相关的运行时数据,这些运行时数据是synchronized以及其他类型的锁实现的重要基础。因为本文主要介绍的oop-klass模型,在这里暂时不对对象头做展开,下一篇文章介绍。

    前面介绍到的_metadata是一个共用体,其中_klass是普通指针,_compressed_klass是压缩类指针。在深入介绍之前,就要来到oop-Klass中的另外一个主角klass了。

    klass

    每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据。

    klass体系

    //klassOop的一部分,用来描述语言层的类型
    class  Klass;
    //在虚拟机层面描述一个Java类
    class   instanceKlass;
    //专有instantKlass,表示java.lang.Class的Klass
    class     instanceMirrorKlass;
    //专有instantKlass,表示java.lang.ref.Reference的子类的Klass
    class     instanceRefKlass;
    //表示methodOop的Klass
    class   methodKlass;
    //表示constMethodOop的Klass
    class   constMethodKlass;
    //表示methodDataOop的Klass
    class   methodDataKlass;
    //最为klass链的端点,klassKlass的Klass就是它自身
    class   klassKlass;
    //表示instanceKlass的Klass
    class     instanceKlassKlass;
    //表示arrayKlass的Klass
    class     arrayKlassKlass;
    //表示objArrayKlass的Klass
    class       objArrayKlassKlass;
    //表示typeArrayKlass的Klass
    class       typeArrayKlassKlass;
    //表示array类型的抽象基类
    class   arrayKlass;
    //表示objArrayOop的Klass
    class     objArrayKlass;
    //表示typeArrayOop的Klass
    class     typeArrayKlass;
    //表示constantPoolOop的Klass
    class   constantPoolKlass;
    //表示constantPoolCacheOop的Klass
    class   constantPoolCacheKlass;
    

    oopDesc是其他oop类型的父类一样,Klass类是其他klass类型的父类。

    图片

    Klass向JVM提供两个功能:

    • 实现语言层面的Java类(在Klass基类中已经实现)
    • 实现Java对象的分发功能(由Klass的子类提供虚函数实现)

    文章开头的时候说过:之所以设计oop-klass模型,是因为HotSopt JVM的设计者不想让每个对象中都含有一个虚函数表。

    HotSopt JVM的设计者把对象一拆为二,分为klassoop,其中oop的职能主要在于表示对象的实例数据,所以其中不含有任何虚函数。而klass为了实现虚函数多态,所以提供了虚函数表。所以,关于Java的多态,其实也有虚函数的影子在。

    _metadata是一个共用体,其中_klass是普通指针,_compressed_klass是压缩类指针。这两个指针都指向**instanceKlass对象,它用来描述对象的具体类型。**

    instanceKlass

    JVM在运行时,需要一种用来标识Java内部类型的机制。在HotSpot中的解决方案是:为每一个已加载的Java类创建一个instanceKlass对象,用来在JVM层表示Java类。

    来看下instanceKlass的内部结构:

    //类拥有的方法列表
    objArrayOop     _methods;
    //描述方法顺序
    typeArrayOop    _method_ordering;
    //实现的接口
    objArrayOop     _local_interfaces;
    //继承的接口
    objArrayOop     _transitive_interfaces;
    //域
    typeArrayOop    _fields;
    //常量
    constantPoolOop _constants;
    //类加载器
    oop             _class_loader;
    //protected域
    oop             _protection_domain;
    ....
    

    可以看到,一个类该具有的东西,这里面基本都包含了。

    这里还有个点需要简单介绍一下。

    在JVM中,对象在内存中的基本存在形式就是oop。那么,对象所属的类,在JVM中也是一种对象,因此它们实际上也会被组织成一种oop,即klassOop。同样的,对于klassOop,也有对应的一个klass来描述,它就是klassKlass,也是klass的一个子类。klassKlass作为oop的klass链的端点。关于对象和数组的klass链大致如下图:

    图片

    在这种设计下,JVM对内存的分配和回收,都可以采用统一的方式来管理。oop-klass-klassKlass关系如图:

    图片

    内存存储

    关于一个Java对象,他的存储是怎样的,一般很多人会回答:对象存储在堆上。稍微好一点的人会回答:对象存储在堆上,对象的引用存储在栈上。今天,再给你一个更加显得牛逼的回答:

    对象的实例(instantOopDesc)保存在堆上,对象的元数据(instantKlass)保存在方法区,对象的引用保存在栈上。

    其实如果细追究的话,上面这句话有点故意卖弄的意思。因为我们都知道。方法区用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 所谓加载的类信息,其实不就是给每一个被加载的类都创建了一个 instantKlass对象么。

    talk is cheap ,show me the code

    class Model {
        public static int a = 1;
        public int b;
    
        public Model(int b) {
            this.b = b;
        }
    }
    
    public static void main(String[] args) {
        int c = 10;
        Model modelA = new Model(2);
        Model modelB = new Model(3);
    }
    

    存储结构如下:

    图片

    总结

    每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了两部分信息,方法头以及元数据。对象头中有一些运行时数据,其中就包括和多线程相关的锁的信息。元数据其实维护的是指针,指向的是对象所属的类的instanceKlass

    重量级锁Moniter的实现原理

    操作系统中的管程

    如果你在大学学习过操作系统,你可能还记得管程(monitors)在操作系统中是很重要的概念。同样Monitor在java同步机制中也有使用。

    管程 (英语:Monitors,也称为监视器) 是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。这些共享资源一般是硬件设备或一群变量。管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。与那些通过修改数据结构实现互斥访问的并发程序设计相比,管程实现很大程度上简化了程序设计。 管程提供了一种机制,线程可以临时放弃互斥访问,等待某些条件得到满足后,重新获得执行权恢复它的互斥访问。

    Moniter相关方法

    获得锁

    图片

    图片

    图片

    释放锁

    图片

    图片

    除了enter和exit方法以外,objectMonitor.cpp中还有

    void      wait(jlong millis, bool interruptable, TRAPS);
    void      notify(TRAPS);
    void      notifyAll(TRAPS);
    等方法。
    
    展开全文
  • 锁升级过程(偏向锁/轻量级锁/重量级锁

    千次阅读 多人点赞 2021-04-09 13:00:09
    锁从宏观上分类,分为悲观锁与乐观锁。 乐观锁 乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低。每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有...
  • 锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)
  • Java中的偏向锁,轻量级锁, 重量级锁解析

    万次阅读 多人点赞 2018-08-13 18:39:49
    参考文章 聊聊并发(二)Java SE1.6中的Synchronized Lock Lock Lock: Enter! 5 Things You Didn’t Know About Synchronization in Java and Scala ...Java 中的 在 Java 中主要2种加锁机制: synchr...
  • 文章目录1 synchronized的优化1.1 CAS操作1.1.1 CAS带来的ABA问题1.1.2 自旋会浪费大量的处理器资源1.1.3 公平性1.2 Java对象头1.2.1 偏向1.2.1 偏向的获取1.2.1 偏向的撤销1.2.3 偏向的获得和撤销流程 ...
  • 偏向锁,轻量级锁与重量级锁的区别与膨胀

    万次阅读 多人点赞 2017-03-19 20:44:01
    一直被这三个锁的膨胀问题所困扰,不知道到底实在什么时候会有偏向锁升级到轻量级锁,什么时候由轻量级锁升级到重量级锁。找到好久,也没有找到简洁明了的答案。  综合多个方面的描述综合自己的理解,特地记录下来...
  • Synchronized原理(轻量级锁篇)

    千次阅读 2021-09-26 18:15:24
    Synchronized原理(轻量级锁篇) 简述 介绍 轻量级锁是JDK1.6之中加入的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就称为“重量级”锁。首先需要强调一点...
  • 由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32...
  • 文章目录一、简介二、Java对象头中的Mark Word三、偏向锁四、轻量级锁五、重量级锁六、自旋锁七、锁升级过程 一、简介 在讲解这些锁概念之前,我们要明确的是这些锁不等同于Java API中的ReentratLock这种锁,这些锁...
  • 结合网上查询的资料说说自己的见解 Synchronized升级的过程: 一个对象A刚开始实例化的...此时A是偏向第一个线程T1,T1在修改对象头成为偏向的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再...
  • 轻量级锁、偏向锁、重量级锁详情

    千次阅读 热门讨论 2018-11-17 13:48:47
    这篇文章是上篇文章是否真的理解了偏向锁、轻量级锁、重量级锁(锁膨胀)、自旋锁、锁消除、锁粗化,知道重偏向吗?的补充,对于偏向锁,网上有些对于它的原理解读过于简单,简单得似乎是错误的,最显眼的是对于Mark...
  • Java中有着各种锁机制,今天我们要说的就是其中两种状态,轻量级锁与重量级锁,小伙伴们知道它们的区别分别有哪些吗?下面来了解一下吧。首先我们了解一下有哪些锁状态吧锁的状态总共有四种:无锁状态、偏向锁、轻量...
  • Java对象锁的理解

    万次阅读 热门讨论 2018-07-16 14:57:27
    以前理解Java的锁机制认为:锁的类型分为‘类锁’,’方法锁‘,’对象锁‘。 1.类锁(synchronize(静态对象)):类的所有对象都要竞争锁。 2.方法锁(在方法前加synchronize): 同一对象同一方法需要竞争锁。 3....
  • synchronized原理,偏向锁,轻量级锁,重量级锁,锁升级
  • java中每个对象都可作为锁,锁有四种级别,按照量级从轻到重分为:无锁、偏向锁、轻量级锁、重量级锁。并且锁只能升级不能降级。 在讲这三个锁之前,我先给大家讲清楚自旋和对象头的概念。 自旋 现在假设有这么一...
  • 详细介绍了MySQL数据库中的全局锁、表级锁和行级锁的概念、使用方式,以及相关特性。
  • 偏向锁、轻量级锁和重量级锁

    千次阅读 2021-05-26 09:51:52
    重量级锁需要通过操作系统在用户态与核心态之间切换,就像它的名字是一个重量级操作,这也是synchronized效率不高的原因,JDK1.6对synchronized进行了优化,引入了偏向锁与轻量级锁,提高了性能降低了资源消耗。...
  • 偏向锁、轻量级锁、重量级锁解析

    千次阅读 2021-02-19 21:22:17
    Java 对象头 在32位虚拟机和64位虚拟机的 Mark Word 所占用的字节大小不一样,32位虚拟机的 Mark Word 和 class Pointer ...偏向锁、轻量级锁的状态转化及对象Mark Word的关系 偏向锁的获得和撤销流程 时序图 流程图
  • 重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 之关联,对象与其 monitor 之间的关系有存在多种实现...
  • 前面讲到了重量级锁是悲观锁的一种,自旋锁、轻量级锁与偏向锁属于乐观锁,所以现在你就能够大致理解了他们的适用范围,但是具体如何使用这几种锁呢,就要看后面的具体分析他们的特性; java中的锁 自旋锁...
  • 悲观锁与乐观锁详解

    千次阅读 2021-05-28 16:29:13
    悲观 悲观顾名思义是从悲观的角度去思考问题,解决问题。它总是会假设当前情况是最坏的情况,在每次去拿数据的时候,都会认为数据会被别人改变,因此在每次进行拿数据操作的时候都会加锁,如此一来,如果此时有...
  • Java对象头 普通对象 数组对象 其中 Mark Word在64 位虚拟机中的结构为 Mark Word后三位 ...若锁还未释放,另一线程就来竞争锁资源,则偏向锁直接升级为重量级锁。 调用hashcode方法,因为没有空间存储hashcod
  • 重量级锁相比轻量级锁的多了一步小明呼唤小红的步骤,但是却省掉了小红反复去敲门的过程,但是能保证小红去厕所时厕所一定是没人的。 将上面例子中的小明、小红理解为两个线程,上厕所理解为执行同步代码,门锁...
  • 采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其他线程想再获取这个【对象锁】时就会阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心上下文切换。 synchronized实际是用...
  • 自旋可以使线程在没有取得的时候,不被挂起,而转去执行一个空循环,(即所谓的自旋,就是自己执行空循环),若在若干个空循环后,线程如果可以获得,则继续执行。若线程依然不能获得,才会被挂起。 使用...
  • 对象头:synchronized用的锁是存在Java对象头里的。Java对象头里的Mark Word里... 轻量级锁 => 重量级锁 。锁可以升级但是不能降级。升级的目的是为了提高获得锁和释放锁的效率。 偏向锁 当一个线程访问同步块获.

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 232,924
精华内容 93,169
关键字:

对象锁与类锁

友情链接: BKSTR.rar