java锁升级

1、理解锁的基础知识

        如果想要透彻的理解java锁的来龙去脉,需要先了解以下基础知识。

1,1、锁的类型

        锁从宏观上分类,分为悲观锁乐观锁

乐观锁
        乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。

        java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

        自旋锁、轻量级锁与偏向锁属于乐观锁。

悲观锁
        悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。

AQS 说明:

        Jdk的并发包提供了各种锁及同步机制,事实上现的核心类是AbstractQueuedSynchronizer。我们简称为AQS框架,它为不同场景提供了实现锁及同步机制的基本框架,为同步状态的原子性管理、线程的堵塞、线程的解除堵塞及排队管理提供了一种通用的机制。

    AQS将线程封装到一个Node里面,并维护一个CHL Node FIFO队列。它是一个非堵塞的FIFO队列,也就是说在并发条件下往此队列做插入或移除操作不会堵塞,是通过自旋锁和CAS保证节点插入和移除的原子性,实现无锁高速插入。

        事实上AbstractQueuedSynchronizer主要就是维护了一个state属性、一个FIFO队列和线程的堵塞与解除堵塞操作。 详细说明:Java并发框架——什么是AQS框架 - 走看看   

        state表示同步状态。它的类型为32位整型。对state的更新必须要保证原子性。这里的队列是一个双向链表,每一个节点里面都有一个prev和next,它们各自是前一个节点和后一个节点的引用。须要注意的是此双向链表除了链头其它每一个节点内部都包括一个线程,而链头能够理解为一个空节点。

1.2、java线程阻塞的代价

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

        如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间;
        如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。
        synchronized会导致争用不到锁的线程进入阻塞状态,所以说它是java语言中一个重量级的同步操纵,被称为重量级锁,为了缓解上述性能问题,JVM从1.5开始,引入了轻量锁与偏向锁,默认启用了自旋锁,他们都属于乐观锁。

        明确java线程切换的代价,是理解java中各种锁的优缺点的基础之一。

1.3、markword

        在介绍java锁之前,先说下什么是markword,markword是java对象数据结构中的一部分。markword数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,它的最后2bit是锁状态标志位,用来标记当前对象的状态,对象的所处的状态,决定了markword存储的内容,如下表所示:

状态 偏向锁位 1bit(是否偏向锁) 锁标志位 2bit 存储内容
未锁定 0 01 对象哈希码、对象分代年龄
可偏向 1 01 偏向线程ID、偏向时间戳、对象分代年龄
轻量级锁定 - 00 指向锁记录的指针
膨胀(重量级锁定) - 10 执行重量级锁定的指针
GC标记 - 11 空(不需要记录信息)

64位虚拟机在不同状态下markword结构如下图所示:

java锁升级_第1张图片

 jvm开启/关闭偏向锁

  • 开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
  • 关闭偏向锁:-XX:-UseBiasedLocking
  • 如果对象调用了hashcode()方法,则该对象会默认关闭偏向锁,应为hashcode为31位,对象头里存了hashcode后,就无法再存线程id的54位了,索引会关闭偏向锁。

2、锁升级

锁的状态:

  • 无锁状态
  • 偏向锁状态
  • 轻量级锁状态
  • 重量级锁状态

四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。

        要注意的是,这四种状态都不是Java语言中的锁,而是Jvm为了提高锁的获取与释放效率而做的优化(使用synchronized时)。

 2.1、偏向锁

        Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。 
        偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁

        它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。

偏向锁的实现

偏向锁获取过程:

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

2、如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。

3、如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4。

4、如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)

5、执行同步代码。

注意:第四步中到达安全点safepoint会导致stop the word,时间很短。

偏向锁的释放:

        偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

偏向锁的适用场景

        始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作; 
        在有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向所的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用。

2.2、轻量级锁

        轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁; 

轻量级锁的加锁过程:

       1、 在进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图: 
  

        2、拷贝对象头中的Mark Word复制到锁记录中;

        3、拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。

        4、如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示。 


  

         5、如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

轻量级锁的释放

        释放锁线程视角:由轻量锁切换到重量锁,是发生在轻量锁释放锁的期间,之前在获取锁的时候它拷贝了锁对象头的markword,在释放锁的时候如果它发现在它持有锁的期间有其他线程来尝试获取锁了,并且该线程对markword做了修改,两者比对发现不一致,则切换到重量锁。

因为重量级锁被修改了,所以display mark word和原来的markword不一样了。

怎么补救,就是进入mutex前,compare一下obj的markword状态。确认该markword是否被其他线程持有。

此时如果线程已经释放了markword,那么通过CAS后就可以直接进入线程,无需进入mutex,就这个作用。

尝试获取锁线程视角:如果线程尝试获取锁的时候,轻量锁正被其他线程占有,那么它就会修改markword,修改重量级锁,表示该进入重量锁了。

还有一个注意点:等待轻量锁的线程不会阻塞,它会一直自旋等待锁,并如上所说修改markword。

这就是自旋锁,尝试获取锁的线程,在没有获得锁的时候,不被挂起,而转而去执行一个空循环,即自旋。在若干个自旋后,如果还没有获得锁,则才被挂起,获得锁,则执行代码。
 

2.3、重量级锁

Synchronized的作用: 它可以把任意一个非NULL的对象当作锁。

1、作用于方法时,锁住的是对象的实例(this);
2、当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8则是metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
3、synchronized作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。

Synchronized的实现

这里写图片描述

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

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内核函数实现的)。

Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。

2.4、总结

这里写图片描述
        上面几种锁都是JVM自己内部实现,当我们执行synchronized同步块的时候jvm会根据启用的锁和当前线程的争用情况,决定如何执行同步操作;

        在所有的锁都启用的情况下线程进入临界区时会先去获取偏向锁,如果已经存在偏向锁了,则会尝试获取轻量级锁,如果以上两种都失败,则启用自旋锁,如果自旋也没有获取到锁,则使用重量级锁,没有获取到锁的线程阻塞挂起,直到持有锁的线程执行完同步块唤醒他们;

        偏向锁是在无锁争用的情况下使用的,也就是同步开在当前线程没有执行完之前,没有其它线程会执行该同步快,一旦有了第二个线程的争用,偏向锁就会升级为轻量级锁,一点有两个以上线程争用,就会升级为重量级锁;

如果线程争用激烈,那么应该禁用偏向锁。
 

你可能感兴趣的:(Java基础,#,线程,并发,java,jvm)