Sike Synchronized underlying implementation, what are you afraid of the interview?

About synchronizedthe underlying implementation, there are many online articles a. But a lot of articles or authors did not look at the code, just based on other articles online summary copy is made, it is inevitable that some mistakes; or many points are in passing, as to why such a statement is not achieved, so that readers like me wanting more.

This series of articles will HotSpot synchronizedlock achieve a comprehensive analysis, including biased lock, lock lightweight, heavyweight lock lock, unlock, lock escalation process principle and source code analysis, hoping to study in synchronizedthe students the way of some help .

 

It took me about two weeks to see the realization of the code (it takes some time for so long Repentance ashamed, mainly for C ++, JVM underlying mechanism, JVM assembly code debugging and not cooked), the synchronizedcodes involved are basically read it again, which further comprising adding the JVM logs to verify his suspicions, in general currently synchronizedhave a more comprehensive and clear understanding of this, but it is limited, some of the details is inevitable that some omissions, but also hope please correct me.

This article will be synchronizedthe mechanism to be a general introduction, including the object header for carrying the lock state, the lock several forms, various forms of locking and unlocking the lock process, when it will lock escalation occurs. Note that this article is intended to introduce the background and concepts about some of the process in the time, mentioned only the main case, for implementation details, different branches of the runtime analysis are detailed in a later article.

I see the JVM version is jdk8u, specific version number, and the code can be here to see.

synchronized Profile

Java provides two basic semantic achieve synchronization: synchronizedmethods and synchronizedblocks, we look at a demo:

public class SyncTest {
    public void syncBlock(){
        synchronized (this){
            System.out.println("hello block");
        }
    }
    public synchronized void syncMethod(){
        System.out.println("hello method");
    }
}

When SyncTest.java be compiled into class files when synchronizedkeywords and synchronizedbytecode is slightly different methods, we can javap -v see the JVM bytecode class file corresponding to the command information, some of the information is as follows:

{ 
  Public void syncBlock (); 
    descriptor: () V 
    the flags: ACC_PUBLIC 
    Code: 
      Stack = 2, about locals =. 3, args_size. 1 = 
         0: aload_0 
         . 1: DUP 
         2: astore_1 
         . 3: // the monitorenter instruction enters the monitorenter sync block 
         4: getstatic Field, Java 2 // # / lang / the System.out: Ljava / IO / PrintStream; 
         . 7: LDC Block #. 3 // String Hello 
         . 9: invokevirtual. 4 // Method, Java # / IO / PrintStream.println: (Ljava / lang / String;) V 
        12 is: aload_1 
        13 is: the monitorexit instruction to exit the monitorexit sync block // 
        14: GOTO 22 is  
        . 17: astore_2
        18 is: aload_1
        19: monitorexit						  // monitorexit指令退出同步块
        20: aload_2
        21: athrow
        22: return
      Exception table:
         from    to  target type
             4    14    17   any
            17    20    17   any
 

  public synchronized void syncMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED      //添加了ACC_SYNCHRONIZED标记
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #5                  // String hello method
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
 
}

Chinese can be seen from the above comments, and those in synchronizedterms of keywords, javacat compile time, generates a corresponding monitorenterand monitorexitinstructions respectively synchronizedentering and exiting the sync block, there are two monitorexitinstruction reason: In order to ensure the case of Throws can release the lock, it javacadds an implicit try-finally block synchronization, finally will call in monitorexitorder to release the lock. For the synchronizedpurposes of the method, javacgenerated for a ACC_SYNCHRONIZEDkeyword in the JVM method call, found the method call is ACC_SYNCHRONIZEDmodified, it will first try to get a lock.

In the underlying JVM, for both synchronizedsemantics achieve substantially the same, will later select a detailed analysis.

Because this article aims to analyze the synchronizedrealization of the principle, and therefore its use for some of the problems will not go, I do not know friends can see this article .

Several forms of lock

The traditional lock (that is to say below heavyweight lock) depends on the synchronization function of the system, used on linux mutexmutex, the most dependent on the underlying implementation futex, on futexcan see my previous article , these synchronization functions are related to the user mode and kernel mode context switching, process switching, the higher the cost. For added a synchronizedkeyword but no more threads running competition, or close to two threads alternately performed using conventional lock mechanism is undoubtedly efficiency would be relatively low.

Prior to JDK 1.6, synchronizedonly the traditional locking mechanism, thus giving the developer left a synchronizedkey compared to other synchronization mechanisms bad performance impression.

In JDK 1.6 introduces two new locking mechanism: biased locking lock and lightweight, they are introduced to solve the problem without the performance overhead of multiple threads of competition or no competition at the scene due to the use of traditional locking mechanism brings.

Before looking to achieve these types of locking mechanisms, we start to understand the object under the head, which is the basis for a variety of locking mechanisms.

Object header

Because any object in Java can be used as a lock, so there must be a mapping between an object and store the information corresponding lock (such as the current thread which holds the lock, which threads are waiting). A very intuitive approach is to use a global map, to store the mapping relationship, but that there will be some issues: the need to map security thread to do different synchronizedinfluence each other, poor performance; In addition, when compared with the synchronization object for a long time, the map may take up more memory.

So the best way is to map this relationship is stored in the object header, because the object head itself also has some hashcode, GC-related data, so if you can lock information and object header information coexist just fine.

In the JVM, the object data in memory in addition to the objects will have their own head, for normal object, which object header has two types of information: mark wordand types of indicators. In addition to the data array will have a record in terms of length of the array.

The type of a pointer to the object's class object pointer, mark wordfor storing objects HashCode, GC generational age, lock status. In the 32-bit system, mark wordthe length of 32bit, a length in 64-bit systems 64bit. For the more data can be stored in a limited space, which is not fixed storage format, the following format for each state on the 32-bit system:

"Sike Synchronized underlying implementation - An Introduction"

We can see lock information is also present in the object mark wordof. When an object is biased locking state (biasable), mark wordstorage is biased thread ID; when the lock state is lightweight (lightweight locked), mark wordstored is a pointer to the thread stack Lock Recordpointer; state when the heavyweight lock (inflated2 ), the pointer to point monitor object heap.

Heavyweight lock

Heavyweight lock is a lock on the traditional sense, we often say that the use of synchronization mechanisms underlying operating system to implement thread synchronization in Java.

The heavyweight lock state, the object mark wordis a pointer to the heap object monitor.

Objects include a monitor so few key fields: cxq (ContentionList in the figure below), EntryList, WaitSet, owner.

Which cxq, EntryList, WaitSet are from the linked list structure ObjectWaiter, owner pointed thread holding the lock.

"Sike Synchronized underlying implementation - An Introduction"

When a thread attempts to acquire a lock, if the lock has been occupied, the thread will be packaged into a ObjectWaiter object into cxq tail of the queue, and then suspends the current thread. When the lock is released before the thread that holds the lock, all the elements will cxq mobile to EntryList go and wake EntryList head of the queue thread.

If a thread calls the sync blocks in Object#waitthe method, removes the thread from the corresponding ObjectWaiter WaitSet EntryList and added to, and then release the lock. When the thread is wait notify, corresponding ObjectWaiter from WaitSet will move to the EntryList.

These are just a snapshot of the heavyweight lock process, which involves a lot of details, such as ObjectMonitor objects come from? When the lock is released to move the elements to EntryList cxq the tail or the head? When notfiy, is ObjectWaiter EntryList move to the tail or the head?

For specific details, analyzes heavyweight lock in the article.

Lightweight lock

JVM developers found that in many cases, the Java program is running, the synchronization code in the block is no competition, the code sync blocks in different threads alternate execution. In this case, with heavyweight lock it is not necessary. Therefore JVM introduces the concept of lightweight lock.

Sync block before executing thread, the JVM will create a current in the stack frame of the thread Lock Record, comprising a header for storing objects  mark word(officially called Displaced Mark Word) and a pointer to the object. The next section is a diagram on the right Lock Record.

"Sike Synchronized underlying implementation - An Introduction"

Locking procedure

1. Create a thread stack Lock Record, which obj(in the above chart Object reference) field points to the locked object.

2. Direct by CAS instruction Lock Recordaddress stored in the object header mark word, if the object is in the locked state without modifying successful, on behalf of the lightweight thread lock is obtained. If it fails, go to step 3.

3. If the current thread already holds the lock, the lock is a representative of this re-entry. Disposed Lock Recordfirst portion ( Displaced Mark Word) is null, played a role of a counter weight. Then ended.

4. come this far shows that there is competition, the need for the expansion heavyweight lock.

Unlocking process

1. traversing thread stack to find all objfields equal to the current object's lock Lock Record.

2. If Lock Recorda Displaced Mark Wordis null, which is representative of a re-entry, the objsettings for the null continue.

3. If Lock Recorda Displaced Mark Wordnon-null, then the object header using the CAS instruction mark wordbe restored Displaced Mark Word. If successful, continue, otherwise the expansion heavyweight lock.

Biased locking

Java supports multithreaded language, so in order to ensure that the code can be run in a multi-threaded properly in many second party package, base library, that is, we often say that the thread-safe, will join as synchronizedsynchronization semantics so. However, in actual application is running, it is likely that only one thread will be dependent synchronization method call. For example, the following demo:

import java.util.ArrayList;
import java.util.List;

public class SyncDemo1 {

    public static void main(String[] args) {
        SyncDemo1 syncDemo1 = new SyncDemo1();
        for (int i = 0; i < 100; i++) {
            syncDemo1.addString("test:" + i);
        }
    }

    private List<String> list = new ArrayList<>();

    public synchronized void addString(String s) {
        list.add(s);
    }

}

在这个demo中为了保证对list操纵时线程安全,对addString方法加了synchronized的修饰,但实际使用时却只有一个线程调用到该方法,对于轻量级锁而言,每次调用addString时,加锁解锁都有一个CAS操作;对于重量级锁而言,加锁也会有一个或多个CAS操作(这里的’一个‘、’多个‘数量词只是针对该demo,并不适用于所有场景)。

在JDK1.6中为了提高一个对象在一段很长的时间内都只被一个线程用做锁对象场景下的性能,引入了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只会执行几个简单的命令,而不是开销相对较大的CAS命令。我们来看看偏向锁是如何做的。

对象创建

当JVM启用了偏向锁模式(1.6以上默认开启),当新创建一个对象的时候,如果该对象所属的class没有关闭偏向锁模式(什么时候会关闭一个class的偏向模式下文会说,默认所有class的偏向模式都是是开启的),那新创建对象的mark word将是可偏向状态,此时mark word中的thread id(参见上文偏向状态下的mark word格式)为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。

加锁过程

case 1:当该对象第一次被线程获得锁的时候,发现是匿名偏向状态,则会用CAS指令,将mark word中的thread id由0改成当前线程Id。如果成功,则代表获得了偏向锁,继续执行同步块中的代码。否则,将偏向锁撤销,升级为轻量级锁。

case 2:当被偏向的线程再次进入同步块时,发现锁对象偏向的就是当前线程,在通过一些额外的检查后(细节见后面的文章),会往当前线程的栈中添加一条Displaced Mark Word为空的Lock Record中,然后继续执行同步块的代码,因为操纵的是线程私有的栈,因此不需要用到CAS指令;由此可见偏向锁模式下,当被偏向的线程再次尝试获得锁时,仅仅进行几个简单的操作就可以了,在这种情况下,synchronized关键字带来的性能开销基本可以忽略。

case 3.当其他线程进入同步块时,发现已经有偏向的线程了,则会进入到撤销偏向锁的逻辑里,一般来说,会在safepoint中去查看偏向的线程是否还存活,如果存活且还在同步块中则将锁升级为轻量级锁,原偏向的线程继续拥有锁,当前线程则走入到锁升级的逻辑里;如果偏向的线程已经不存活或者不在同步块中,则将对象头的mark word改为无锁状态(unlocked),之后再升级为轻量级锁。

由此可见,偏向锁升级的时机为:当锁已经发生偏向后,只要有另一个线程尝试获得偏向锁,则该偏向锁就会升级成轻量级锁。当然这个说法不绝对,因为还有批量重偏向这一机制。

解锁过程

当有其他线程尝试获得锁时,是根据遍历偏向线程的lock record来确定该线程是否还在执行同步块中的代码。因此偏向锁的解锁很简单,仅仅将栈中的最近一条lock recordobj字段设置为null。需要注意的是,偏向锁的解锁步骤中并不会修改对象头中的thread id。

下图展示了锁状态的转换流程:

"Sike Synchronized underlying implementation - An Introduction"

另外,偏向锁默认不是立即就启动的,在程序启动后,通常有几秒的延迟,可以通过命令 -XX:BiasedLockingStartupDelay=0来关闭延迟。

批量重偏向与撤销

从上文偏向锁的加锁解锁过程中可以看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时将偏向锁撤销为无锁状态或升级为轻量级/重量级锁。safe point这个词我们在GC中经常会提到,其代表了一个状态,在该状态下所有线程都是暂停的(大概这么个意思),详细可以看这篇文章。总之,偏向锁的撤销是有一定成本的,如果说运行时的场景本身存在多线程竞争的,那偏向锁的存在不仅不能提高性能,而且会导致性能下降。因此,JVM中增加了一种批量重偏向/撤销的机制。

存在如下两种情况:(见官方论文第4小节):

1.一个线程创建了大量对象并执行了初始的同步操作,之后在另一个线程中将这些对象作为锁进行之后的操作。这种case下,会导致大量的偏向锁撤销操作。

2.存在明显多线程竞争的场景下使用偏向锁是不合适的,例如生产者/消费者队列。

批量重偏向(bulk rebias)机制是为了解决第一种场景。批量撤销(bulk revoke)则是为了解决第二种场景。

其做法是:以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时,class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其mark word的Thread Id 改成当前线程Id。

当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。

End

Java is synchronizedbiased lock, lock lightweight, heavyweight lock in three forms, corresponding to the lock held only by a thread, the thread alternately hold different locks, multi-threaded lock contention three cases. When the conditions are not met, the lock will tend to press the lock - Upgrade> sequence heavyweight lock -> lightweight lock. JVM kind of lock can also downgraded, but the conditions are very harsh, it is not within the scope of our discussion. This article is mainly for Java synchronizedto be a basic introduction, later there will be a more detailed analysis.

 

I compiled a free Java Advanced data, a total of 30G, needs its own collection, covering Java, Redis, MongoDB, MySQL, Zookeeper, Spring Cloud, Dubbo distributed high concurrency and other tutorials,
data collection micro letter: wwee19947

Guess you like

Origin www.cnblogs.com/yuxiang1/p/11305546.html