zoukankan      html  css  js  c++  java
  • 深入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原则
  • 相关阅读:
    Atitit attilax要工作研究的要素 纪要 方案 趋势 方向 概念 理论
    Atitit 常见每日流程日程日常工作.docx v7 r8f
    Atitit it 互联网 软件牛人的博客列表
    Atitit 信息链(Information Chain)的概念理解 attilax总结
    Atitit 知识点的体系化 框架与方法 如何了解 看待xxx
    Atitit 聚合搜索多个微博 attilax总结
    Atitit 企业知识管理PKM与PIM
    Atitit 项目沟通管理 Atitit 沟通之道 attilax著.docx
    Atitit 项目管理软件 在线服务 attilax总结 1. 项目管理协作的历史 1 1.1. Worktile 406k 1 1.2. Teambition  584k in baidu
    Atitit.每周末总结 于每周一计划日程表 流程表 v8 import 上周遗漏日志补充 检查话费 检查流量情况 Crm问候 Crm表total and 问候
  • 原文地址:https://www.cnblogs.com/f-zhao/p/6189104.html
Copyright © 2011-2022 走看看