Java memory model JMM six in-depth understanding of synchronized (1)

1. Mutual exclusion synchronization

Earlier we learned what is the basic application of thread safety and synchronization, so how can we achieve thread safety ? Mutual exclusion synchronization is the most common method of concurrent thread security. Synchronization refers to ensuring that shared data is only used by one thread at the same time when multiple threads concurrently access shared data. Mutual exclusion is a means to achieve synchronization. Critical sections, mutexes, and semaphores are typical implementations of mutual exclusion. Therefore: mutual exclusion is the method, and synchronization is the purpose. In Java, the most basic means of mutual exclusion synchronization is to use synchronized.

 

Second, the implementation of the synchronized bytecode level

The Java virtual machine implements synchronized code block synchronization and method synchronization based on entering and exiting the Monitor object, but the performance of the two at the bytecode level is slightly different.

1. Synchronized synchronization block: View the synchronized synchronization block code compiled by javac through the disassembly of the javap command. You can see that the monitorenter and monitorexit bytecode instructions are inserted at the beginning and end of the synchronized block (including abnormal exit). Both instructions require a reference-type parameter to specify the object to lock and unlock. This also means that synchronized locks objects rather than code fragments. In Java, any object has a Monitor (lock) associated with it. When executing the monitorenter instruction, first try to obtain the lock (Monitor) of the object. If the lock object is not locked or is owned by the current thread, Then the lock counter is incremented by 1. Accordingly, when monitorexit is executed , the lock counter is decremented by 1. When the counter is 0, the lock is released. If the acquisition of the object lock fails, the current thread will block and wait until it is released by the owning thread.

 

2. Synchronized method: At the bytecode level, it may be translated into ordinary method call and return instructions such as: invokevirtual, areturn instructions, but by setting the ACC_SYNCHRONIZED flag in the access_flags field of the method in the method table of the Class file To indicate that the method is a synchronized method, and use the object that calls the method or the internal object of the Class to which the method belongs to represent Klass as the lock object. The specific implementation details can also be implemented using the monitorenter and monitorexit bytecode instructions.

 

3. The underlying implementation principle and lock optimization of synchronized

Since Java threads are mapped to the native threads of the operating system, and the implementation of synchronized will block/wake up other threads that fail to acquire the object lock, the operations of blocking and waking up threads need to be completed by the operating system, and need to be executed from the user mode. Converting to the core state requires a lot of processor time. For simple synchronous code, the state transition may take longer than the user code execution time, so synchronized is a heavyweight lock in Java .

JDK 1.6 introduces a large number of optimizations to the implementation of locks, such as spin locks, adaptive spin locks, lock elimination, lock coarsening, biased locks, lightweight locks and other technologies to reduce the overhead of lock operations. 

Locks mainly exist in four states, which are in the order of upgrade: no lock state, biased lock state, lightweight lock state, and heavyweight lock state. They will be gradually upgraded with the fierce competition. Note that locks can be upgraded but not downgraded. This strategy is to improve the efficiency of acquiring and releasing locks.

 

3.1 Object Header object header

Because synchronization-related lock information is stored in the object header as runtime data, before studying synchronized, you must first understand the object header. In the JVM, the layout of objects in memory is divided into three areas: object header, instance data and alignment padding.

 

Instance variable: stores the attribute data information of the class, including the attribute information of the parent class. If the instance part of the array also includes the length of the array, this part of the memory is aligned by 4 bytes.

Padding data: Since the virtual machine requires that the starting address of the object must be an integer multiple of 8 bytes. Padding data is not required, it is just for byte alignment.

 

HotSpot虚拟机的对象头分为两部分:Mark Word(标记字段)、Klass Pointer(类型指针)。但如果对象是数组类型,还会有一个额外的部分用于存储数组长度。对象头每一部分一般都占一个机器码字宽(32位虚拟机中一个字宽位4字节,32bit,64位虚拟机则为8字节,64bit)。                对象头结构:

 
长度 内容 说明
32/64bit Mark Word 存储对象的hashCode、锁信息或分代年龄或GC标志等信息
32/64bit Klass Pointer 对象指向它的类元数据的指针,JVM通过这个指针确定该对象是哪个类的实例
32/64bit Array length 数组的长度(如果当前对象是数组才有这部分)

 

3.1.1 Mark Word

从对象头的结构可以看出, Mark Word才是我们研究锁的关键部分,对象头信息是与对象自身定义的数据无关的额外存储成本,但是考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说,Mark Word会随着程序的运行发生变化,变化状态如下(32位虚拟机):

 从对象头的Mark Word的运行时数据结构可以看出,在不同的锁状态下,其存放的相关信息各不相同。下面我们一一进行分析。

 

3.2 无锁态

在32位的HotSpot的虚拟机中, Mark Word的32个bit用于存储对象的哈希码(hashCode),4个bit存储对象的分代年龄,1个bit固定为0,标识为非偏向锁,2个bit用于存储无锁标志位。

 

3.3 轻量级锁

了解轻量级锁之前,从Mark Word可以看出,在轻量级锁状态下,Mark Word中保存的将是指向线程栈中锁记录的指针,那么什么是锁记录呢?

锁记录(以下称Lock Record)是线程私有的数据结构,每一个线程都有一个可用Lock Record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个Lock Record关联(通过对象头中的Mark Word指向Lock Record的起始地址),同时Lock Record中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。如下图所示为Lock Record的内部结构: 

Owner:初始时为NULL表示当前没有任何线程拥有该Lock Record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;

EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住Lock Record失败的线程。

RcThis:表示blocked或waiting在该Lock Record上的所有线程的个数。

Nest:用来实现重入锁的计数。

HashCode:保存从对象头Mark Word拷贝过来的HashCode值(可能还包含GC age)。

Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程,1表示要唤醒一个继任线程来竞争锁。

  

轻量级锁具体实现:

轻量级锁是JDK1.6中才加入的锁机制,它并不是用来替代重量级锁的,而是为了在没有多线程竞争(多线程交替执行)的条件下,减少相较于传统的重量级锁使用操作系统互斥量实现产生的大量性能消耗。这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程交替执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。当存在锁竞争的情况下,执行CAS指令失败的线程将调用操作系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒。在JDK1.6中默认是开启轻量级锁机制的。

 

当一个线程欲进入同步代码块的时候,轻量级锁的加锁(monitorenter)过程为(源码在synchronizer.cpp文件的ObjectSynchronizer::slow_enter):

(1)判断锁对象的对象头的Mark Word是否是无锁态。

        a. 如果是无锁态:线程首先从自己的可用Lock Record列表中取得一个空闲的Lock Record,初始Nest和Owner值分别被预先设置为1和该线程自己的标识。一旦Lock Record准备好,复制对象头中的Mark Word到该Lock Record中。然后通过CAS原子指令尝试将对象的Mark Word更新为指向该Lock Record的指针。如果该CAS操作成功,表示竞争到轻量级锁,则将锁标志位设为00(表示此对象处于轻量级锁状态)。如果CAS操作失败,则表示存在其他线程竞争锁的情况,那么重新执行加锁过程,即从(1)重新开始开始。

 

(2) 如果锁对象的对象头的Mark Word处于轻量级锁态: 但是Owner中保存的线程标识为获取锁的当前线程自己,这就是重入锁的情况,只需要简单的将Nest加1即可。不需要任何原子操作,效率非常高。

 

(3) 如果锁对象的对象头的Mark Word处于轻量级锁态: 但是Owner的值为Null.当一把锁上存在被阻塞或等待的线程,并且锁的前一个拥有者刚刚释放锁时就会出现这种状态。此时多个线程通过CAS原子指令在多线程竞争状态下都试图将Owner设置为自己的标识来获得锁,竞争失败的线程则会进入到(4)的执行路径。

 

(4)如果锁对象的对象头的Mark Word处于轻量级锁态,并且Owner的值不为Null,也不是当前想获取锁的线程自己:在调用操作系统的重量级的互斥锁(即膨胀为重量级锁)之前先自旋一定的次数,当达到一定的次数时如果仍然没有成功获得锁,则开始准备进入阻塞状态,首先将rcThis的值原子性的加1,由于在加1的过程中可能会被其他线程破坏Mark Word和monitor record之间的关联关系,所以在原子性加1后需要再一次比较以确保Mark Word的值没有被改变,当发现Mark Word被改变后则重新执行加锁过程,即从(1)重新开始开始。这次就可能会执行到(3) ,即Owner为NULL,如果在(3)中锁再次竞争失败则进入到阻塞状态而不是又进入(4)形成死循环。

 

轻量级锁释放(monitorexit)过程如下(源码在synchronizer.cpp文件的ObjectSynchronizer::fast_exit完成):

(1)首先检查该对象是否处于轻量级锁状态并且该线程是这个锁的拥有者,如果发现不对则抛出异常;

 

(2)检查Nest字段是否大于1,如果大于1则简单的将Nest减1并继续拥有锁,如果等于1,则进入到第(3)步;

 

(3)检查rcThis是否大于0,如果是则设置Owner为NULL然后唤醒一个正在阻塞或等待的线程再一次试图获取锁,如果等于0则进入到第(4)步

 

(4)通过将对象的Mark Word置换回原来的HashCode值,解除和Lock Record之间的关联来释放锁,同时将Lock Record放回到线程的可用Lock Record列表中。

 

轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内线程都是交替执行,不存在竞争的”,这是一个经验数据,如果不存在竞争,轻量级锁使用CAS操作从而避免了使用操作系统互斥量的重量级锁开销,但是如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作的消耗,因此在有竞争的情况下,轻量级锁会比重量级锁更慢开销更大。所以应该视具体情况分析是否适用轻量级锁。 

 

3.4 偏向锁

偏向锁也是JDK1.6中引入的一项锁优化, 它的目的是消除无竞争情况下锁的性能问题,因为研究发现,在大多数情况下,锁不但不存在多线程竞争,而且总是由同一个线程多次获得。因此为了减少同一线程多次获取/释放轻量级锁时的多次CAS操作的代价而引入偏向锁。因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟,后续有CAS详解。偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活。

偏向锁的核心思想是:如果一个线程获得了锁,那么该锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。

关于源码,在HotSpot中,偏向锁的入口位于synchronizer.cpp文件的ObjectSynchronizer::fast_enter函数。 

偏向锁的加锁过程如下: 

(1)检测Mark Word是否为可偏向状态,即是否为偏向锁为1,锁标识位为01 

(2)若为可偏向状态,则测试线程ID是否指向当前线程,如果是,则直接执行同步代码块,否则进入步骤(3)。

(3)如果线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行线程(4);

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

 

偏向锁的释放采用了一种只有出现竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争(例如上面加锁的第四步)。 偏向锁的撤销需要等待全局安全点(这个时间点是上没有正在执行的代码)。其步骤如下:

(1)暂停/挂起拥有偏向锁的线程(不会释放锁),判断锁对象是否还处于被锁定状态;

(2)如果锁对象没有处于被锁定状态(表示之前拥有偏向锁的线程已经执行完毕),那么撤销偏向锁后恢复到无锁态(标志位为“01”) 

(3)如果锁对象还是处于被锁定状态(表示之前拥有偏向锁的线程仍然还在运行),那么撤销偏向锁后恢复到轻量级锁状态。思考:就算持有偏向锁的线程依然活着,但是已经离开了synchronized同步块,是否也可以恢复到无锁态?

(4)唤醒被暂停/挂起的线程。

 

偏向锁可以提高带有同步但无多线程竞争的程序性能,但是如果程序中大多数的锁都总是被多个不同的线程访问或者锁竞争比较激烈,那么偏向模式就是多余的。 所以是否适用偏向锁,需具体问题具体分析。

 

3.5 重量级锁

重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,故切换成本非常高。

当锁处于重量级锁态时,Mark Word存储的就是指向monitor对象的指针。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)

 

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;  //记录重入的次数
    _object       = NULL;
    _owner        = NULL;  //当前拥有锁的线程
    _WaitSet      = NULL; //调用了wait()方法的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;//处于等待锁被挂起的线程列表,JDK8默认策略下,是一个后进先出(LIFO)的队列,每次放入和取出都操作队头
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁挂起状态的线程,有资格成为候选的线程会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }
 As can be seen from the above constructor of ObjectMonitor, there are three important queues in ObjectMonitor, _cxq , _WaitSet and _EntryList , which are used to store the list of ObjectWaiter objects. The object stored in the ObjectWaiter object is the thread (thread object). Each thread waiting for the lock is encapsulated into an ObjectWaiter object. ObjectWaiter is an object of a doubly linked list structure. _owner points to the thread holding the ObjectMonitor object, that is, the thread that currently owns the lock. The following figure shows the flow of threads competing for heavyweight locks under the default settings of JDK8 (Policy is 2, QMode is 0).
According to the flow diagram of competing threads in the above figure, it can be concluded that:
(1) Spin : The thread that fails to compete for the lock for the first time will not immediately enter the blocking state. This is because the thread switches back and forth between the user state and the kernel state, which seriously affects the performance of the lock. By spinning for an appropriate time, when the Owner thread releases the lock, the lock can be obtained immediately, thus avoiding state switching. This is not unfair locks . Obviously, spin only makes sense on multiprocessors. Spinning is a double-edged sword. If the spinning time is too long, the overall performance will be affected, and if the spinning time is too short, the purpose of delay blocking will not be achieved. Obviously, the selection of the spin cycle is very important, but it is related to many scenarios such as the operating system, hardware system, system load, etc. It is difficult to choose. It is generally accepted that spinlocks are not scalable.
(2)阻塞:自旋之后依然竞争锁失败的线程在被加入 cxq队列头部的过程中,都将继续尝试获取锁,如果在被放入 cxq队列中之后依然没能成功竞争到锁,将调用park将其挂起等待被唤醒。
(3)wait():当竞争到锁的线程(就是Owner)调用wait()方法,将会把owner线程封装成ObjectWaiter对象放入 _WaitSet中,并将owner线程通过park()方法挂起,从而让出锁。
(4)notify():当执行notify方法时,取出 _WaitSet列表中的第一个,根据Policy的不同,将这个线程放入 _EntryList或者 _cxq队列中的起始或末尾位置。JDK8默认Policy为2.即默认:如果_EntryList为空就放入_EntryList,否则放入cxq头部。而notifyAll()则相当于的将_WaitSet列表中的所有对象取出来调用notify().
  • Policy == 0:放入_EntryList队列的排头位置; 
  • Policy == 1:放入_EntryList队列的末尾位置; 
  • Policy == 2:_EntryList队列为空就放入_EntryList,否则放入_cxq队列的排头位置;
  • Policy == 3:放入_cxq队列中末尾位置;
(5)锁释放:当Owner释放锁时,根据不同的策略(由QMode指定),从 cxqEntryList中获取头节点,通过unpark唤醒其代表的线程,线程唤醒之后,继续通过CAS指令去竞争锁。竞争失败的继续保留在原来的列表中的原位置。JDK8默认QMode为0,即:取_EntryList的首元素,如果_EntryList为空就将cxq的首元素取出来放入_EntryList,然后再从_EntryList中取出来。
  • QMode = 2,并且_cxq非空:取_cxq队列排头位置的ObjectWaiter对象并立即唤醒其中的线程并返回。
  • QMode = 3,并且_cxq非空:把_cxq队列首元素放入_EntryList的尾部;
  • QMode = 4,并且_cxq非空:把_cxq队列首元素放入_EntryList的头部; 
  • QMode = 0,不做什么,继续往下执行。
  • 根据QMode不同策略操作完成(除开QMode==2立即返回)之后的后续操作为:取_EntryList的首元素,如果_EntryList为空就将cxq的首元素取出来放入_EntryList,然后再从_EntryList中取出来。
(1) Spin : The thread that fails to compete for the lock for the first time will not immediately enter the blocking state. This is because the thread switches back and forth between the user state and the kernel state, which seriously affects the performance of the lock. By spinning for an appropriate time, when the Owner thread releases the lock, the lock can be obtained immediately, thus avoiding state switching. This is not unfair locks . Obviously, spin only makes sense on multiprocessors. Spinning is a double-edged sword. If the spinning time is too long, the overall performance will be affected, and if the spinning time is too short, the purpose of delay blocking will not be achieved. Obviously, the selection of the spin cycle is very important, but it is related to many scenarios such as the operating system, hardware system, system load, etc. It is difficult to choose. It is generally accepted that spinlocks are not scalable. (2) Blocking: The thread that still fails to compete for the lock after spinning will continue to try to acquire the lock during the process of being added to the head of the cxq queue. If it still fails to compete for the lock after being put into the cxq queue, the Call park to suspend it waiting to be woken up. (3) wait(): When the thread competing for the lock (that is, the Owner) calls the wait() method, the owner thread will be encapsulated into an ObjectWaiter object and placed in _WaitSet , and the owner thread will be suspended through the park() method, thereby Let go of the lock. (4) notify(): When the notify method is executed, take out the first one in the _WaitSet list, and put the thread into _EntryList or _cxq according to the policy The start or end position in the queue. The default Policy of JDK8 is 2. That is, the default: if _EntryList is empty, put it in _EntryList, otherwise put it in the cxq header. And notifyAll() is equivalent to calling notify() by taking out all objects in the _WaitSet list.
  • Policy == 0: put into the head position of the _EntryList queue; 
  • Policy == 1: put it at the end of the _EntryList queue; 
  • Policy == 2: If the _EntryList queue is empty, put it into _EntryList, otherwise put it into the head position of the _cxq queue;
  • Policy == 3: put it at the end of the _cxq queue;
  • Policy == 0: put into the head position of the _EntryList queue; 
  • Policy == 1: put it at the end of the _EntryList queue; 
  • Policy == 2: If the _EntryList queue is empty, put it into _EntryList, otherwise put it into the head position of the _cxq queue;
  • Policy == 3: put it at the end of the _cxq queue;
(5) Lock release: When the Owner releases the lock, according to different strategies (specified by QMode), the head node is obtained from cxq or EntryList , and the thread represented by it is awakened through unpark. After the thread wakes up, it continues to compete for the lock through the CAS instruction . Those that fail the competition remain in their original positions in the original list. The default QMode of JDK8 is 0, that is, take the first element of _EntryList, if _EntryList is empty, take the first element of cxq into _EntryList, and then take it out from _EntryList.
  • QMode = 2, and _cxq is not empty: Take the ObjectWaiter object at the head of the _cxq queue and immediately wake up the thread in it and return.
  • QMode = 3, and _cxq is not empty: put the first element of the _cxq queue at the end of _EntryList;
  • QMode = 4, and _cxq is not empty: put the first element of the _cxq queue into the head of _EntryList; 
  • QMode = 0, do nothing, continue to execute.
  • According to the different strategies of QMode, the follow-up operation after the completion of the operation (except for QMode==2 to return immediately) is: take the first element of _EntryList, if _EntryList is empty, take the first element of cxq and put it into _EntryList, and then from _ Take it out from the EntryList.
  • QMode = 2, and _cxq is not empty: Take the ObjectWaiter object at the head of the _cxq queue and immediately wake up the thread in it and return.
  • QMode = 3, and _cxq is not empty: put the first element of the _cxq queue at the end of _EntryList;
  • QMode = 4, and _cxq is not empty: put the first element of the _cxq queue into the head of _EntryList; 
  • QMode = 0, do nothing, continue to execute.
  • According to the different strategies of QMode, the follow-up operation after the completion of the operation (except for QMode==2 to return immediately) is: take the first element of _EntryList, if _EntryList is empty, take the first element of cxq and put it into _EntryList, and then from _ Take it out from the EntryList.
The locking process of heavyweight locks is as follows:

(1)检查是否是无锁状态,如果是,则走轻量级锁或偏向锁加锁过程,如果不是,就检查是否就是当前线程持有锁,如果是则执行重入锁逻辑,如果不是则调用inflate方法开始锁膨胀(源码在synchronized.cpp).

(2)膨胀:检查是否已经是重量级锁状态,如果是则返回相应的ObjectMonitor对象执行步骤(5)。如果不是则执行步骤(3);

(3)检查是否正处于膨胀中状态(即其他线程正在膨胀中),如果是则当前线程进行自旋等待膨胀完成,完成后获取到相应的ObjectMonitor对象执行步骤(5)。

(4)如果既不是重量级锁状态也不是正处于膨胀中,那么目前就是轻量级锁状态了,开始膨胀至重量级锁:通过CAS指令将OjectMonitor的状态设置为INFLATING,标识当前锁正在膨胀中,

         如果CAS失败,说明同一时刻其他线程已经在膨胀了,则当前线程进行自旋等待膨胀完成,完成后获取到相应的ObjectMonitor对象执行步骤(5)。

         如果CAS成功则表示设置了对象头指向的ObjectMonitor对象。返回相应的ObjectMonitor对象执行步骤(5)

(5)线程获取到相应的ObjectMonitor对象之后,真正的锁竞争才开始(源码在ObjectMonitor.cpp的enter函数),即通过CAS指令尝试将monitor的_owner字段设置为当前线程,

         如果CAS成功则表是竞争到锁或者是重入。如果CAS失败表示竞争锁失败指向步骤(6)。

(6)将当前竞争失败的线程封装成ObjectWaiter对象的节点,通过CAS将其添加到cxq列表的头部,放入失败之后立即尝试竞争锁TryLock,竞争失败继续尝试放入cxq列表。

         如此交替,如果没有竞争到锁导致被加入到cxq列表之后,最后执行一次TryLock,如果依然竞争失败,则执行park将线程挂起(这里有自旋逻辑,如果满足自旋条件则挂起操作将有超时时间,到达超时时间后自动唤醒)等待被唤醒,唤醒之后继续尝试,           如果失败继续挂起,如此交替。

 

3.6 自旋锁与自适应自旋

在上面的轻量级锁与重量级锁的加锁过程中,当出现锁竞争的时候,都有提到自旋操作,自旋的含义和作用也在重量级锁的流程图解释中给予了说明,即就是为了避免不必要的挂起和恢复线程时状态转换造成的性能消耗。其实现方式大概就是执行多次无意义的空循环,以达到不放弃处理器的执行时间。自旋锁在JDK1.4.2中就被引入,但默认是关闭的。在JDK1.6中就已经默认开启了,自旋时间周期的选择将决定自旋是否真正在起到提高性能的作用,因为长时间的自旋将导致白白的消耗处理器资源,这对提高性能来说反而起到了反作用。自旋次数的默认值是10次,一旦达到这个限定的阈值,线程将退出自旋进入阻塞状态。值得庆幸的是,在JDK1.6中还引入了自适应的自旋锁,其意味着自旋时间不再试固定的次数,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来动态决定。比如:在同一个锁对象上,上一次的自旋等待成功获得了锁,并且持有锁的线程处于运行中,那么虚拟机将认为这一次自旋也很可能会再次成功,进而可能将允许自旋的次数更多一些,比如100次循环。但是如果对于莫一个锁,自旋很少成功过(比如一些花费时间很长的同步块),那么在以后要获取这个锁时将可能直接省略掉自旋的过程,避免浪费处理器资源。

 

篇幅太长。。。。下篇继续。。。。。

 

关于以上synchronized的源码研究,可以参考如下文章:

https://www.jianshu.com/p/c5058b6fe8e5

http://blog.csdn.net/boling_cavalry/article/details/77793224

http://www.woowen.com/java/2017/01/01/JAVA%20Synchronized/

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326372192&siteId=291194637