JVM锁

4 篇文章 0 订阅
4 篇文章 0 订阅

转载地址:https://www.jianshu.com/p/ec28e3a59e80

                  https://www.cnblogs.com/sten/p/5651484.html

                  https://www.cnblogs.com/f-zhao/p/6189104.html?utm_source=itdadao&utm_medium=referral

一、Java对象结构

概述

对象实例由对象头、实例数据组成,其中对象头包括markword和类型指针,如果是数组,还包括数组长度;

| 类型 | 32位JVM | 64位JVM|
| ------ ---- | ------------| --------- |
| markword | 32bit | 64bit |
| 类型指针 | 32bit |64bit ,开启指针压缩时为32bit |
| 数组长度 | 32bit |32bit |

header.png

compressed_header.png

可以看到

  • 开启指针压缩时,markword占用8bytes,类型指针占用8bytes,共占用16bytes;
  • 未开启指针压缩时,markword占用8bytes,类型指针占用4bytes,但由于java内存地址按照8bytes对齐,长度必须是8的倍数,因此会从12bytes补全到16bytes;
  • 数组长度为4bytes,同样会进行对齐,补足到8bytes;

另外从上面的截图可以看到,开启指针压缩之后,对象类型指针为0xf800c005,但实际的类型指针为0x7c0060028;那么指针是如何压缩的呢?实际上由于java地址一定是8的倍数,因此将0xf800c005*8即可得到实际的指针0x7c0060028,关于指针压缩的更多知识可参考官方文档

markword结构

markword的结构,定义在markOop.hpp文件:

  32 bits:
  --------
  hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
  JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
  size:32 ------------------------------------------>| (CMS free block)
  PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)

  64 bits:
  --------
  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
  size:64 ----------------------------------------------------->| (CMS free block)

  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
 [ptr             | 00]  locked             ptr points to real header on stack
 [header      | 0 | 01]  unlocked           regular object header
 [ptr             | 10]  monitor            inflated lock (header is wapped out)
 [ptr             | 11]  marked             used by markSweep to mark an object
                                              

由于目前基本都在使用64位JVM,此处不再对32位的结构进行详细说明:

偏向锁标识位锁标识位锁状态存储内容
001未锁定hash code(31),年龄(4)
101偏向锁线程ID(54),时间戳(2),年龄(4)
00轻量级锁栈中锁记录的指针(64)
10重量级锁monitor的指针(64)
11GC标记空,不需要记录信息

此处,有几点要注意:

  • 如果对象没有重写hashcode方法,那么默认是调用os::random产生hashcode,可以通过System.identityHashCode获取;os::random产生hashcode的规则为:next_rand = (16807seed) mod (2*31-1),因此可以使用31位存储;另外一旦生成了hashcode,JVM会将其记录在markword中;
  • GC年龄采用4位bit存储,最大为15,例如MaxTenuringThreshold参数默认值就是15;
  • 当处于轻量级锁、重量级锁时,记录的对象指针,根据JVM的说明,此时认为指针仍然是64位,最低两位假定为0;当处于偏向锁时,记录的为获得偏向锁的线程指针,该指针也是64位;
 We assume that stack/thread pointers have the lowest two bits cleared.
ObjectMonitor* monitor() const {
    assert(has_monitor(), "check");
    // Use xor instead of &~ to provide one extra tag-bit check.
    return (ObjectMonitor*) (value() ^ monitor_value);//monitor_value=2,value最右两位为10,因此异或之后最右两位为0
  }
JavaThread* biased_locker() const {
    assert(has_bias_pattern(), "should not call this otherwise");
    return (JavaThread*) ((intptr_t) (mask_bits(value(), ~(biased_lock_mask_in_place | age_mask_in_place | epoch_mask_in_place))));
//~(biased_lock_mask_in_place | age_mask_in_place | epoch_mask_in_place)为11111111111111111111110010000000,计算后的结果中,低10位全部为0;
  }

由于java中内存地址都是8的倍数,因此可以理解为最低3bit为0,因此假设轻量级和重量级锁的最低2位为0是成立的;但为什么偏向锁的最低10位都是0?查看markOop.hpp文件,发现有这么一句话:

// Alignment of JavaThread pointers encoded in object header required by biased locking
  enum { biased_lock_alignment    = 2 << (epoch_shift + epoch_bits)
//epoch_shift+epoch_bits=10
  };

thread.hpp中重载了operator new:

void* operator new(size_t size) { return allocate(size, true); }

// ======= Thread ========
// Support for forcing alignment of thread objects for biased locking
void* Thread::allocate(size_t size, bool throw_excpt, MEMFLAGS flags) {
  if (UseBiasedLocking) {
    const int alignment = markOopDesc::biased_lock_alignment;//10
    size_t aligned_size = size + (alignment - sizeof(intptr_t));
    void* real_malloc_addr = throw_excpt? AllocateHeap(aligned_size, flags, CURRENT_PC)
                                          : os::malloc(aligned_size, flags, CURRENT_PC);
    void* aligned_addr     = (void*) align_size_up((intptr_t) real_malloc_addr, alignment);
    assert(((uintptr_t) aligned_addr + (uintptr_t) size) <=
           ((uintptr_t) real_malloc_addr + (uintptr_t) aligned_size),
           "JavaThread alignment code overflowed allocated storage");
    if (TraceBiasedLocking) {
      if (aligned_addr != real_malloc_addr)
        tty->print_cr("Aligned thread " INTPTR_FORMAT " to " INTPTR_FORMAT,
                      real_malloc_addr, aligned_addr);
    }
    ((Thread*) aligned_addr)->_real_malloc_address = real_malloc_addr;
    return aligned_addr;
  } else {
    return throw_excpt? AllocateHeap(size, flags, CURRENT_PC)
                       : os::malloc(size, flags, CURRENT_PC);
  }
}

如果开启了偏移锁,在创建线程时,线程地址会进行对齐处理,保证低10位为0

实例数据

实例数据中主要包括对象的各种成员变量,包括基本类型和引用类型;static类型的变量会放到java/lang/Class中,而不会放到实例数据中;
对于引用类型的成员(包括string),存储的指针;对于基本类型,直接存储内容;通常会将基本类型存储在一起,引用类型存储在一起;
例如类Test的成员定义如下:

 private static Test t1=new Test();
 private Test t2;
 private int a=5;
 private Integer b=7;
 private String c="112";
 private BigDecimal d=new BigDecimal("5");
 private long e=9l;

body.png

可以看到long e、int a为基本类型,存储在一起;其它的引用类型存储在一起;int占用4bytes,不足8bytes,自动补足到8bytes;

 

二、JVM锁说明

以前Synchronised关键字加锁效率问题,经常受到吐槽。后来java的开发团队进行了优化,引入了偏向锁、自旋锁、轻量锁,性能有了很大的提升。下面我们来分析下这里面的过程和原理。

最初阶段,我们的代码段或者方法加syncronized关键字,如下:

syncronized(object) {
           // do something 
      }

执行这段代码时,其实相当于,在这段代码前面加了monitorenter指令;后面加了monitorexit指令。如下:

 monitorenter
  // do something
  monitorexit

 这两指令都得和操作系统连接,相对来说是比较重的,比较耗时的。

但是,在代码里加synchronised 关键字时,只是全面考虑,很多情况只有一个线程在执行,如果在有这种情况下,就有些浪费了;另外,有时,即使多线程访问,他的等待时间是很短的,比monitorenter相关指令还轻,这也是可想办法优化的。

在叙述相关优化锁之前,我们先说下java 对象头部分的 Mark Word。在32位虚拟机上,他的长度是32位;在64位虚拟机上,他的长度是64位。

普通情况下,Mark Word的格式如下:

hash:25 ------------>| age:4    | biased_lock:1 lock:2

最后部分2个比特,是固定的,表示对象锁的状况

01 表示无锁,或者偏向锁; 00表示轻量级锁;10表示重量级锁

第三部分是标识是否偏向锁。当最后部分为01时,第三部分为0表示普通无锁对象,第三部分为1表示偏向锁;最后部分为其他值,第三部分不存在。

age表示分代年龄;hash表示对象hash值。

普通无锁:对象hash:25 ------------>| age:4    | 0 | 01

偏向锁:偏向锁的线程和时间戳:25 ------------>| age:4    | 1 | 01

轻量锁:指向栈中锁记录的指针:30  | 00

重量锁:指向互斥量(重量级锁)的指针:30 | 10

java1.6之后,对象有4中状态:无锁态,偏向锁态,轻量锁态,重量锁态。

java程序执行,没有遇到同步块时,都是无锁态。Mark Word如上普通无锁的情况。

遇到同步块之后,Mark Word从普通无锁变成偏向锁的情况,通过CAS加锁。如果是相同线程在次进入,不受影响。执行完成,CAS解锁,Mark Word又变成普通无锁的情况。

如果在偏向锁中,遇到锁竞争的情况。先线程停止,通过CAS锁升级到轻量锁,成功的一方继续执行,另一方开始自旋操作。待他完了继续执行。这里的自旋有个智能学习的策略。假设每个同步块的初始自旋次数是20次,A同步块的线程自旋时,每次很小的自旋次数就获得了执行时间,那后面他的自旋信用会很高,会超过20次往上升;B同步块的线程自旋时,每次都需要很大的自旋次数才能获得执行时间,后面就会把他的自旋次数从20往下降或者不给自旋机会。

当然,自旋失败后,锁得再次升级,通过CAS操作从轻量锁升级到重量锁,这就回到一开始的moniterenter指令和moniterexit指令了。

jvm还有其他一些锁的优化措施。

锁清除:在某些情况下,同步块中的代码不可能被其他线程访问修改的时候,这时候的锁是可以清除的。

锁粗化:在一些代码中,会频繁的加锁,加锁,加锁;当然,有时并不是程序员这么编写的,因为我们的jdk api中也会加些锁操作。我们得频繁的做互斥操作,可以把几个同步块合并成一个同步块来优化执行。

三、深入JVM-锁与并发

一、锁在Java虚拟机中的实现与优化

1.1 偏向锁

偏向锁是JDK 1.6 提出的一种锁优化方式。其核心思想是,如果程序没有竞争,则取消之前已经取得锁的线程同步操作。也就说,若某一锁被线程获取后,便进入偏向模式,当线程再次请求这个锁时,无需进行相关的同步操作,从而节省了操作时间。如果在此之前有其他线程进行了锁请求,则锁退出偏向模式。在JVM中使用-XX:+UseBiasedLocking可以设置启用偏向锁。

当锁对象处于偏向模式时,对象头会记录获取锁的线程

[JavaThread* | epoch | age | 1 | 01]

这样,当该线程再次尝试获得锁时,通过Mark Word的线程信息就可以判断当前线程是否持有偏向锁。

偏向锁在锁竞争激烈的场合没有太强的优化效果,因为大量的竞争会导致持有锁的线程不停地切换,锁也很难一直保持在偏向模式,此时,使用锁偏向不仅得不到性能的优化,反而有可能降低系统性能。

1.2 轻量级锁

如果偏向锁失败,Java虚拟机会让线程申请轻量级锁。轻量级锁在虚拟机内部,使用一个称谓BasicObjectLock的对象实现,这个对象内部由一个BasicLock对象和一个持有该锁的Java对象指针组成。BasicObjectLock对象放置在Java栈的栈帧中。在BasicLock对象内部还维护者displaced_header字段,他用于备份对象头部的Mark Word。

当一个线程持有一个对象的锁时,对象头部Mark Word如下:

[ptr       | 00]  locked

末尾两位比特为00,整个Mark Word为指向BasicLock对象的指针。由于BasicObjectLock对象在线程栈中,因此该指针必然指向持有该锁的线程栈空间。当需要判断某一线程是否持有该对象锁时,也只需简单的判断对象头的指针是否在当前线程的栈地址范围即可。同时,BasicLock对象的displaced_header字段,备份了元对象的Mark Word内存。BasicObjectLock对象的obj字段则指向该对象。

1.3 锁膨胀

当轻量级锁失败,虚拟机就会使用重量级锁。在使用重量级锁时,对象的Mark Word如下:

[ptr        | 10] monitor

末尾的2比特标记位被置为10。整个Mark Word表示指向monitor对象的指针。

1.4 自旋锁

锁膨胀后,进入ObjectMonitor的enter(),线程很可能会在操作系统层面被挂起,这样线程上下文切换的性能损失就比较大。因此,在锁膨胀后,虚拟机会做最后的争取,希望线程可以尽快进入临界区而避免被操作系统挂起。一种较为有效的手段就是使用自旋锁。

自旋锁可以使线程在没有取得锁时,不被挂起,而转而去执行一个空循环(即所谓的自旋),在若干个空循环后,线程如果可以获得锁,则继续执行。若线程依然不能获得锁,才会被挂起。

使用自旋锁后,线程被挂起的几率相对减少,线程执行的连贯性相对加强。因此,对于那些锁竞争不是很激烈,锁占用时间很短的并发线程,具有一定的积极意义,但对于锁竞争激烈,单线程锁占用时间长的并发程序,自旋锁在自旋等待后,往往依然无法获得对应的锁,不仅仅白白浪费了CPU时间,最终还是免不了执行被挂起的操作,反而浪费了系统资源。

在JDK 1.6 中,Java虚拟机提供-XX:+UseSpinning参数来开启自旋锁,使用-XX:PreBlockSpin参数来设置自旋锁的等待次数。

在JDK 1.7中,自旋锁的参数被取消,虚拟机不再支持由用户配置自旋锁。自旋锁总是会执行,自旋次数也由虚拟机自行调整。

1.5 锁消除

锁消除是Java虚拟机在JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。通过锁消除,可以节省毫无意义的请求锁时间。

二、锁在应用层的优化思路

2.1 减少锁持有时间

public Matcher matcher(CharSequence input) {
    if (!compiled) {
        synchronized(this) {
            if (!compiled)
                compile();
        }
    }
    Matcher m = new Matcher(this, input);
    return m;
}

2.2 减小锁粒度

典型的场景就是ConcurrentHashMap类的实现。ConcurrentHashMap将整个HshMap分成若干段(Segment),每个段都是一个子HashMap。

如果需要在ConcurrentHashMap中增加一个新的表项,并不是将整个HashMap加锁,而是首先根据hashcode得到该表项应该被存放到哪个段中,然后对该段加锁,并完成put()操作。在多线程环境中,如果多个线程同时进行put()操作,只要被加入的表项不存放在同一个段中,则线程间可以做到真正的并行。

2.3 锁分离

锁分离是减小锁粒度的一个特例。他依据应用程序的功能特点,将一个独占锁分成多个锁。一个典型的案例就是java.util.concurrent.LinkedBlockingQueue的实现。

在LinkedBlockingQueue的实现中,take()函数和put()函数分别实现了从队列中取得数据和往队列中zeng增加数据的功能。虽然两个函数都对当前队列进行了修改操作,但由于LinkedBlockingQueue是基于链表的,因此,两个操作分别作用于队列的前端和尾端,从理论上来说,两者并不冲突。

    public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            while (count.get() == 0) {
                notEmpty.await();
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }
public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        // Note: convention in all put/take/etc is to preset local var
        // holding count negative to indicate failure unless set.
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            /*
             * Note that count is used in wait guard even though it is
             * not protected by lock. This works because count can
             * only decrease at this point (all other puts are shut
             * out by lock), and we (or some other waiting put) are
             * signalled if it ever changes from capacity. Similarly
             * for all other uses of count in other wait guards.
             */
            while (count.get() == capacity) {
                notFull.await();
            }
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }

2.4 锁粗化

通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早的获得资源执行任务。但是,凡事都有一个度,如果对同一个锁不停地进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化。

为此,虚拟机在遇到一连串连续的对同一锁不断进行请求和释放的操作时,便会把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步次数。这个操作叫做锁的粗化。

三、无锁

可以使用yi'z一种称为非阻塞同步的方法,这种方法不需要使用“锁”(因此称之为无锁),但是依然能确保数据和程序在高并发环境下,保持多线程间的一致性。

3.1 理解CAS

CAS算法的过程是这样:它包含3个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。仅当V值=E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。

3.2 原子操作

为了能让CAS操作被Java应用程序充分使用,在JDK的java.util.concurrent.atomic包下,有一组使用无锁算法实现的原子操作类,主要有AtomicInteger、AtomicIntegerArray、AtomicLong、AtomicLongArray和AtomicReference等。他们分别封装了对整数、整数数组、长整型、长整型数组和普通对象的多线程安全操作。

3.3 LongAdder

在JDK 1.8中引入了LongAdder。结合减小锁粒度与ConcurrentHashMap的实现,我们可以想到一种对传统AtomicInteger等原子类的改进思路。虽然在CAS操作中没有锁,但是像减小锁粒度这种分离热点的思路依然可以使用。一种可行的方案就是仿造ConcurrentHashMap,将热点数据分离。比如,可以将AtomicInteger的内部核心数据value分离成一个数组,每个线程访问时,通过哈希等算法映射到其中一个数字进行计数,而最终的技术结果,则为这个数组的求和累加。其中,热点数据value被分离成多个单元cell,每个cell独自维护内部的值,当前对象的实际值由所有的cell累计合成,这样,热点就进行了有效的分离,提高了并行度。LongAdder正是使用了这种思想。

四、理解Java内存模型

  • 原子性
  • 有序性
  • 可见性
  • Happens-Before原则

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值