深入了解java同步、锁紧机构

时间:2021-11-02 14:40:24

该薄膜还具有从本文试图一个高度来认识我们共同的同步(synchronized)和锁(lock)机制。

我们假定读者想了解更多的并发知识推荐一本书《java并发编程实战》,这是一个经典的书,英语水平良好的学生也可以读《Concurrent programming in Java - design principles and patterns》由Doug Lea亲自操刀。Doug Lea是并发方面的大神,jdk的并发包就是由他完毕的。

我们都知道在java中被synchronized修饰的代码被称为同步代码块。同步代码块意味着同一时刻仅仅有一个线程运行。其它线程都被排斥在该同步块之外,而且訪问也是依照某种顺序运行的。实际上synchronized是基于监视器实现的,每个实例和类都拥有一个监视器,通常我们说的“锁”的动作就是获取该监视器。

因此通常我们讲synchronized是基于JVM层面的,使用的是对象内置的锁。静态方法锁住的是该class的监视器。实例方法锁住的是相应实例的监视器。

同步是使用monitorenter和monitorexit指令实现的。monitorenter尝试获取对象的锁,假设该对象没被锁定或者当前线程已经获取了锁。则把锁的计数器+1,相同monitorexit把锁的计数器-1。

因此synchronized对于同一个线程是可重入的。

监视器支持两种线程:相互排斥(sync)和协作。java通过对象的锁实现对临界区的相互排斥訪问。使用Object的wait(),notify(),notifyAll()方法来实现。

乐观锁和悲观锁

这两个名字非常多地方都出现过,所谓的乐观锁就是当去做某个改动或其它操作的时候它觉得不会有其它线程来做相同的操作(竞争)。这是一种乐观的态度。一般是基于CAS原子指令来实现的。关于CAS能够參见这篇文章java并发包的CAS操作。CAS通常不会将线程挂起,因此有时性能会好一些。(线程的切换是挺耗性能的一个操作)。

悲观锁,依据乐观锁的定义非常easy理解悲观锁是觉得肯定有其它线程来争夺资源,因此无论究竟会不会发生争夺。悲观锁总是会先去锁住资源。

曾经的synchronized都是会堵塞线程的,就是说会发生上下文切换。从用户态切换到内核态。由于这样的方式有时候太耗费资源,因此后来又出现了自旋锁。所谓自旋事实上就是假设锁已经被其它线程占有,当前线程并不会挂起,而是做空操作,自旋事实上从某种程度来说是乐观锁,由于它总是觉得下次会得到锁的。因此自旋锁适合在竞争不激烈的情况下使用,据了解眼下的jvm针对synchronized已经有了这方面的优化。

自旋的使用也是分场景的。有可能线程自旋非常久也没获取到锁。那么CPU就白白被浪费了,还不如挂起线程,因此有出现了自适应的自旋锁,它会更具历史的自旋是否获取到锁的记录来推断自旋的时间或者是否须要自旋。

轻量级锁

轻量级锁的概念是相对须要相互排斥操作的重量级锁而言,轻量级锁的目的是降低多线程的相互排斥几率。并非要取代相互排斥。

要想了解轻量级锁和后面讲到的偏向锁必须先了解下对象头的内存布局。以下这张图就是Object Header的内存布局:

深入了解java同步、锁紧机构

初始都是01表示无锁。00表示轻量级锁,10表示重量级锁等等。在代码进入同步块的时候,假设此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象眼下的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀。即Displaced Mark Word)。然后虚拟机尝试利用CAS操作将对象的轻量级指针指向栈的lock record,假设更新成功当前线程获取到锁,而且标记为00轻量级锁。

假设这个更新操作失败了。虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,假设是就说明当前线程已经拥有了这个对象的锁。那就能够直接进入同步块继续运行,否则说明这个锁对象已经被其它线程抢占了。假设有两条以上的线程争用同一个锁。那轻量级锁就不再有效。要膨胀为重量级锁。锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(相互排斥量)的指针,后面等待锁的线程也要进入堵塞状态。 

偏向锁

偏向锁就是偏心的意思,当锁被某个线程第一次获取到得时候。会在对象头记录获取到该锁的线程id,以后每次该线程进入同步块的时候都不须要加锁,假设一旦有其它线程获取到该锁,则偏向锁模式宣告失败,锁撤销回未锁定或轻量级锁状态。偏向锁的作用就是全然消除锁。连CAS操作都不做。

以下来看一下线程在进入同步块和出同步块的状态转换。

  当多个线程同一时候请求某个对象监视器时。对象监视器会设置几种状态用来区分请求的线程:

  • Contention List:全部请求锁的线程将被首先放置到该竞争队列
  • Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List
  • Wait Set:那些调用wait方法被堵塞的线程被放置到Wait Set
  • OnDeck:不论什么时刻最多仅仅能有一个线程正在竞争锁,该线程称为OnDeck
  • Owner:获得锁的线程称为Owner
  • !Owner:释放锁的线程
以下是一位网友画得图非常形象:

深入了解java同步、锁紧机构

新请求的线程会被放置到ContentionList中。当某个Owner释放锁的时候。假设EntryList是空则Owner会从ContentionList中移动线程到EntryList。

显然,ContentionList结构事实上是个Lock-Free的队列,由于仅仅有Owner才会从ContentionList取节点。

EntryList与ContentionList逻辑上同属等待队列,ContentionList会被线程并发訪问,为了减少对ContentionList队尾的争用,而建立EntryList。Owner线程在unlock时会从ContentionList中迁移线程到EntryList,并会指定EntryList中的某个线程(一般为Head)为Ready(OnDeck)线程。Owner线程并非把锁传递给OnDeck线程,仅仅是把竞争锁的权利交给OnDeck,OnDeck线程须要又一次竞争锁。

这样做尽管牺牲了一定的公平性,但极大的提高了总体吞吐量,在Hotspot中把OnDeck的选择行为称之为“竞争切换”。

可重入锁

可重入锁的最大优点是能够避免思索。由于对于已经获取到锁的线程。不须要再一次去获取锁了,仅仅须要将计数器+1就可以。实际上synchronized也是可重入锁的一种。可是本节我们要讲的是并发包中的ReentrantLock及事实上现。synchronized是JVM层面提供的锁。而在java的语言层面jdk也为我们提供了很优秀的锁,这些锁都在java.util.concurren包中。

先来看一下JVM提供的锁和并发包中的锁有哪些差别:

1.synchronized的加锁和释放都是由JVM提供,不须要我们关注,而lock的加锁和释放所有由我们去控制,通常释放锁的动作要在finally中实现。

2.synchronized仅仅有一个状态条件。也就是每一个对象仅仅有一个监视器,假设须要多个Condition的组合那么synchronized是无法满足的。而lock则提供了多条件的相互排斥。很灵活。

3.ReentrantLock 拥有Synchronized同样的并发性和内存语义,此外还多了 锁投票,定时锁等候和中断锁等候。

在解说ReentrantLock之前。先来看下不AtomicInteger源码大体了解下它的实现原理。

/**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
     //该方法相似同步版本号的i++。先将当前值+1,然后返回,
     //能够看到是一个for循环,仅仅有当compareAndSet成功才会返回
     //那么什么时候成功呢?
    public final int incrementAndGet() {
        for (;;) {
            int current = get();//volatile类型的变量。因此每次获取都是最新值
            int next = current + 1;//加1操作
            if (compareAndSet(current, next))//关键的是if中的方法
	    //假设compareAndSet成功,则整个加操作成功,假设失败,则说明有其它线程已经改动了value
	    //那么会进行下一轮的加1操作,直到成功
                return next;
        }
    }
/**
     * Gets the current value.
     *
     * @return the current value
     */
     //get方法很easy,返回value,这个value是类的成员变量。而且是volatile的
    public final int get() {
        return value;
    }

    /**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return true if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        //继续跟踪unsafe的方法,发现并没提供,实际上该方法是个基于本地类库的原子方法,使用一个指令就可以完毕操作。
	//假设内存中的值和预期的值同样,也就是没有其它线程改动过该值,则更新该值为预期的值,返回成功,否则返回失败
	return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
能够预见的是假设竞争很激烈,则失败的概率会大大添加。性能也会受到影响。实际上并发包中的锁大多是基于CAS操作完毕的。本节打算解说可重入锁,但很多事情还是需要知道,刚刚好再次写入介绍ReentrantLock该。

版权声明:本文博客原创文章,博客,未经同意,不得转载。