实战java虚拟机06- 锁与并发

实战java虚拟机
深入理解java虚拟机

锁时多线程软件开发的必要工具之一,它的基本作用是保护临界资源不会 被多个线程同时访问而遭到破坏。通过锁,可以让线程排队,一个一个进入临界资源访问目标对象,使目标对象的状态总是一致的,这也是锁存在的价值。
锁的类型
- 互斥同步锁
互斥同步是通过进行线程阻塞和唤醒来实现锁功能,也称为阻塞同步。从处理方式上说,它属于一种悲观的并发策略(悲观锁),总是认为只要不去做正确的同步措施(如加锁),那就肯定会出现问题,无论共享数据是否存在竞争,它都要进行加锁。java中的悲观锁就是Synchronized,RetreenLock。

公平锁是指多个线程在等待同一个锁时,按照申请锁的时间顺序依次获得锁。
Synchronized,RetreenLock默认都是非公平的,RetreenLock可以通过带布尔值的构造参数,构造公平锁

非阻塞同步锁
非阻塞同步是一种基于冲突检测的乐观并发策略。通俗的就是先进行操作,如果没有其他线程争夺共享数据,那么操作就成功了,如果有争用,且产生了冲突,那就采取其他的补偿措施(最常见的补偿措施就是不断重试,直到成功为止。)。


锁优化

在了解了对象头Mark Word的基本概念后(HotSpot虚拟机对象探秘 ),就可以深入虚拟机内部,一探虚拟机对锁的实现方式。
在多线程程序中,线程之间的竞争是不可避免的,而且是一种常态,如何高效率的处理多线程的竞争,是java虚拟机的一个重要使命。如果将所有线程竞争都交由操作系统处理,那么并发性能将是非常低下的。为此,虚拟机在操作系统挂起线程前,会先尽可能的在虚拟机层面解决竞争关系,尽可能避免真实的竞争发生。同时,在竞争不激烈的场合,也会试图消除不必要的竞争。

偏向锁
它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。其核心思想是,在无竞争的情况下把整个同步都消除掉。

-XX:+UseBiasedLocking ://启用关闭偏向锁
-XX:BiasedLockingStartupDelay=0 ://虚拟机启动后n秒,启用偏向锁,默认是4秒

也就是,若某一个锁被线程获取后,锁对象便进入偏向模式,此时锁对象的对象头Mark Word结构如下:

[JavaThread*:23 | epoch:2 | age:4 | 1 | 01 ]

,当线程在再次请求这个锁时(与锁对象头中的线程进行比较),

  • 同一个线程,则无需再进行相关的同步操作,从而节省了操作时间。
  • 其他线程进行了锁请求(有了竞争),便退出偏向模式。(偏向锁膨胀为轻量级锁,撤销偏向锁)

偏向锁可以提高带有同步但无竞争的程序性能。
如果程序中大多数的锁总是被多个不同的线程访问,那偏向模式就是多余的。 在具体问题具体分析的前提
下,有时候使用参数-XX:-UseBiasedLocking来禁止偏向锁优化反而可以提升性能。


轻量级锁
如果偏向锁失败,java虚拟机会让线程申请轻量级锁。
它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就称为“重量级”锁。 首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

当一个线程持有轻量级锁时,锁对象的对象头Mark Word结构如下:

[ptr | 00] locked

轻量级锁加锁过程:
1. 在代码进入同步块时,如果同步对象(锁对象)没有被锁定(即锁标志位”01”),虚拟机首先在当前线程的栈帧中创建一个名为锁记录的空间(Lock Record),用于存储锁对象当前的Mark Word的拷贝(官方把这份拷贝前增加了一个Displaced前缀,即Displaced Mark Word)
这里写图片描述
2. 然后虚拟机将使用CAS操作尝试锁对象的Mark Word更新为指向Lock Record的指针。
3. 如果更新成功了,那么线程将拥有锁对象,并将锁对象的Mark Word锁标志位更新为“00”(轻量级锁标识)。
这里写图片描述
4. 如果更新失败了,虚拟机会检查锁对象的Mark Word是否指向当前线程,如果是则说明了当前线程已经拥有了该锁,直接进入同步块继续执行,否则说明这个锁对象被其他线程抢占了。如果有两个以上线程征用一个锁,则膨胀为重量级锁。

轻量级锁解锁过程

  1. 解锁过程也是通过CAS操作来进行的。如果对象的Mark Word仍然指向着线程的锁记录(Lock Record),那就用CAS操作把锁对象当前的Mark Word和线程中复制的 Displaced Mark Word 替换回来。
  2. 如果替换成功,整个解锁过程完成了。
  3. 如果替换失败了,说明其它线程尝试获取该锁,那就要在释放锁的同时,唤起被挂起的线程。

轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。 如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。


Synchronized

引用Oracle官方文档:https://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html

Synchronization is built around an internal entity known as the intrinsic lock or monitor lock. (The API specification often refers to this entity simply as a “monitor.”) Intrinsic locks play a role in both aspects of synchronization: enforcing exclusive access to an object’s state and establishing happens-before relationships that are essential to visibility.

Every object has an intrinsic lock associated with it. By convention, a thread that needs exclusive and consistent access to an object’s fields has to acquire the object’s intrinsic lock before accessing them, and then release the intrinsic lock when it’s done with them. A thread is said to own the intrinsic lock between the time it has acquired the lock and released the lock. As long as a thread owns an intrinsic lock, no other thread can acquire the same lock. The other thread will block when it attempts to acquire the lock.

 

英语三级半水准的翻译,大致意思是说:
synchronized是通过一个叫做ntrinsic lock or monitor lock的东西实现的。。巴拉巴拉。
每个java对象都有一个intrinsic lock与之关联。当一个线程去独占访问对象的fileds时,需要先行的申请这个对象的intrinsic lock,并在在完成操作后释放它们。也就是说在这个线程执行期间拥有intrinsic lock。在该线程执行期间,其他线程无法获得这些锁,当这些线程尝试获得锁时将会阻塞。


synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。
如果Java程序中的synchronized明确指定了对象参数,那就是这个对象的reference;如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象。

根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁。 如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就被释放。 如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。(注意:synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题



引用:https://blog.csdn.net/uftjtt/article/details/80250182
这里写图片描述
如果一个顾客想要进入这个特殊的房间,他首先需要在走廊(Entry Set)排队等待。调度器将基于某个标准(比如 FIFO)来选择排队的客户进入房间。如果,因为某些原因,该客户客户暂时因为其他事情无法脱身(线程被挂起),那么他将被送到另外一间专门用来等待的房间(Wait Set),这个房间的可以可以在稍后再次进入那件特殊的房间。
监视器是一个用来监视这些线程进入特殊的房间的。他的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码。


重量锁
在使用重量锁时,对象的Mark Word结构:

[ptr | 10] monitor

整个MarkWord指向monitor对象的指针,在使用轻量级锁处理失败后,虚拟机指向如下操作:

lock->set_displaced_header(markOopDesc::unused_mark());
ObjectSynchronizer::inflate(THREAD,obj())->enter(THREAD);
  1. 放弃Lock Record备份的对象头信息。
  2. 正式启用重量级锁(启动过程包含2步:首先通过inflate()膨胀,目标是获取ObjectMonitor,然后使用enter()尝试进入)
    enter()方法调用过程中,线程很可能在操作系统层面被挂起,重量级锁的线程切换和调度成本较高

自旋锁
膨胀后,进入ObjectMonitor的enter(),线程很可能在操作系统层面被挂起,这样切换线程的性能损失比较大。
因此在膨胀之后,虚拟机会做最后的争取,希望线程可以尽快进入临界资源而避免被操作系统挂起。一种较为有效的手段就是采用自旋锁。
自旋锁可以使线程在没有获取锁时,不被挂起,而转去执行一个空旋转(即所谓的自旋),若干个空旋转循环后,线程如果可以获取锁,则往后执行,如果不行,才会被挂起。

-XX:+UseSpinning ;//开启自旋锁
-XX:+PerBlockSpin;//自旋次数,默认是10

自适应自旋锁
自适应意味着自旋的时间不再固定了,有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机就会变得越来越“聪明”了。
在JDK1.7中,自旋参数被取消,虚拟机不再支持由用户配置自旋锁。自旋锁总会执行,且自旋次数也是由虚拟机自信调整。

锁消除
锁消除是指虚拟机即时编译器(JIT)在运行时,通过对运行上下文的扫描,去除不可能存在的共享资源竞争的锁。通过锁取消,可以节省毫无意义的请求锁时间。
锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。

逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术,它与类型继承关系分析一样,并不是直接优化代码的手段,而是为其他优化手段提供依据的分析技术。逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸。
甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。如果能证明一个对象不会逃逸到方法或线程之外,也就是别的方法或线程无法通过任何
途径访问到这个对象,则可能为这个变量进行一些高效的优化。

-XX:+DoEscapeAnalysis //开启逃逸分析
-XX:+EliminateLocks //开启锁消除

这里写图片描述


锁在应用层的优化思路

减少锁的持有时间
before:

    public synchronized void syncMethod(){
        othercode1();
        mutextMethod(); //需要同步
        othercode2();
    }

after后::

    public  void syncMethod(){
        othercode1();
        synchronized (this){
            mutextMethod(); //需要同步
        }
        othercode2();
    }

Spring使用编程式事务,采用同种原理减少对database的connection连接时间;
这里写图片描述

减少锁粒度
所谓减少锁粒度,就是指缩小对象的范围,从而减少锁冲突的可能性,进而提高系统的并发能力。
典型的应用场景就是ConcurrentHashMap类的实现。对于一个普通的集合对象的多线程同步来说,最常用的方式就是对get()和add()方法进行同步。每当对get()和add()操作时,总是获取集合对象锁。而ConcurrentHashMap,很好的拆分锁对象的方式提高系统吞吐量。Concurrent将整个HashMap分成若干个段(Segment),每个段都是一个HashMap.针对每个segment分别加锁。
这里写图片描述

锁分离
锁分离是减少锁粒度的一个特例,它更具应用程序的特点,将一个独占锁分成多个锁。典型案例就是LinkedBlockingQueue的实现。
在LinkedBlockingQueue实现中,take()和put()函数分别实现了从队列中获取数据或往队列中增加数据功能。
如果使用独占锁,则要求在两个操作时,同时获取当前队列的独占锁,那么take()和put()操作不能真正的并发,在运行时,会彼此等待对方释放资源。这种情况下,锁竞争会相对比较激烈,从而影响程序在高并发时的性能。
JDK实现时,将take()和put()”拆分为两把锁”(Condition 将 Object 监视器方法分解成截然不同的对象)。

/** Main lock guarding all access */
final ReentrantLock lock = new ReentrantLock();
/** Condition for waiting takes */
private final Condition notEmpty = lock.newCondition();
/** Condition for waiting puts */
private final Condition notFull = lock.newCondition();

锁粗化
原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。
大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
before:

    public  void syncMethod(){
        synchronized (lock){
            othercode1(); //需要同步1
        }
        synchronized (lock){
            othercode2(); //需要同步2
        }
    }

after:

    public  void syncMethod(){
        synchronized (lock){
            othercode1(); //需要同步1
            othercode2(); //需要同步2
        }
    }

无锁
- CAS: CAS (Compare-and-Swaap)指令有3个操作数:CAS有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做并返回false。(它是通过sun.misc.Unsafe类实现的,Unsafe提供了硬件级别的原子操作。)上述是一个原子操作。

CAS的“ABA”问题:如果一个变量V初次读取的值是A,并且在准备赋值的时候仍然是A值,那么我们就可以认为这个值没有被其他线程更改过吗??如果期间被其他的线程改为B,后又被改为A,那么CAS会错误的认为它没有更改过。 解决方法:JUC提供了一个带有标记的原子引用类:AtomicStampeReference,它通过控制变量值的版本来保证CAS的准确性。不过这个目前来说,这个类比较“鸡肋”,大部分ABA问题不会影响程序并发的正确性,如果解决ABA锁,改用传统同步互斥锁比原子类更好。

  • 原子操作Atomic:AtomicInteger,AtomicLong,AtomicLongArray,AtomicReference<V>
    public final int getAndIncrement() {
        for (;;) { //死循环--不断尝试
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next)) //使用cas
                return current;
        }
    }
  • LongAdder:JDK8引入juc.LongAdder类,它的原理结合减小锁粒度与ConcurrentHashMap实现,将AtomicInteger的内部核心数据value分离成一个数组,每个线程访问时,通过哈希等算法映射到其中一个数字进行计算,而最终的计数结果,为这个数组的求和累加。
    这里写图片描述
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值