Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

Introduction to the principle of Synchronized

2025-01-17 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/03 Report--

The main content of this article is "introduction to the principle of Synchronized". Interested friends may wish to have a look. The method introduced in this paper is simple, fast and practical. Let's let the editor take you to learn "introduction to the principles of Synchronized".

1. CAS

CAS full name: CompareAndSwap, hence the name thought: compare and exchange. His main idea is: * * I need to modify a value. I will not modify it directly. Instead, I will pass in the current value I think and the value to be modified. If it is really what I think it is in memory, then modify it. Otherwise, the modification fails. * * his thought is a kind of optimistic lock.

A picture explains his workflow:

Now that we know how it works, let's listen to a scenario: now there is a number of int type that equals 1, and there are three threads that need to increment it.

In general, we think of the procedure as follows: the thread reads the variable from the main memory, goes to its own workspace, then performs the variable increment, and then writes back to the main memory, but there is a security problem in the multithreaded state. If we ensure the security of variables, the common practice is to ThreadLocal or lock directly. (brothers who don't know anything about ThreadLocal, read my article to understand ThreadLocal design ideas.)

At this point, let's think about what we need to do if we use the CAS above to modify the value.

First, we need to pass in the value that the current thread thinks, and then pass in the value that we want to modify. If the value in memory is equal to our expected value at this time, modify it, otherwise the modification will fail. Does this solve the problem of multithreaded modification, and it does not use the locks provided by the operating system.

The above process is actually the underlying implementation of the AtomicInteger-like self-increment operation, which ensures the atomicity of an operation. Let's look at the source code.

Public final int incrementAndGet () {return unsafe.getAndAddInt (this, valueOffset, 1) + 1;} public final int getAndAddInt (Object var1, long var2, int var4) {int var5; do {/ / read the latest value from memory var5 = this.getIntVolatile (var1, var2); / / modify} while (! this.compareAndSwapInt (var1, var2, var5, var5 + var4)) Return var5;}

Implementing CAS uses the Unsafe class, and you can tell by its name that it's not safe, so JDK doesn't recommend it. Compared to the modification process of a variable performed by multiple threads above, the operation of this class only adds a spin, which is constantly fetching the latest value in memory and then performing a self-increment operation.

A brother may have said how the getIntVolatile and compareAndSwapInt operations ensure atomicity.

For getIntVolatile, reading the address in memory is an operation, and the atomicity is obvious.

For compareAndSwapInt, its atomicity is guaranteed by CPU, implemented through a series of CPU instructions, and its C++ underlying layer is implemented by Atomic::cmpxchg_ptr.

This is the end of CAS, but there is also an ABA problem. If you are interested, you can learn about the multithreading knowledge section of my article. There is a detailed explanation in it.

We can guarantee the atomicity of the operation through CAS, so we need to consider one thing, how the lock is implemented. Compared with case in life, we implement a lock through a set of passwords or a key, as well as a key, a lock object used by a block of synchronized code in the computer.

So how do other threads determine that the current resource has been occupied?

The implementation in the computer is often realized by judging a variable, the unlocked state is 0, the locked state is 1, and so on to determine whether the resource is locked or not. when a thread releases the lock, it only needs to change the value of this variable to 0, which means there is no lock.

We just need to ensure the atomicity of the variable modification, and the CAS just now can solve this problem.

As for the question of where the lock variable is stored, it is the following, the memory layout of the object.

two。 Memory layout

Brothers, it should be clear that all the objects we create are stored in the heap, and what we end up with is a reference pointer to an object. So there is a question that will be born, when the object created by JVM opens up a space, what is in this space? This is the content of our point.

Let's first come to a conclusion: there are two types of objects in Java, one is a normal object, the other is an array

Object memory layout

Let's explain its meaning one by one.

* * vernacular version: * * there are two more fields in the object header. Mark Word mainly stores lock information, GC information and so on (the implementation of lock upgrade). The Klass Point represents a class pointer that points to the definition and structure information of the class in the method area. Instance Data represents the member variables of the class. When we first learned the basics of Java, we all heard the teacher say that the non-static member properties of an object will be stored in the heap, and this is the Instance Data of the object. Compared to the object, the array adds an additional attribute of array length

What is the last data on it?

Let's take a scene to show the reason: * imagine if you and your girlfriend are going to go out on the weekend and your girlfriend asks you to put on lipstick, will you only wear lipstick at this time? Of course not, but take all the necessities with you in case you have to go home and get your things as soon as you go out! What is the name of this kind of behavior? Plan ahead, yes, warm man behavior. Don't you get it? Let's have another case. You are ready to start a business, the capital is very abundant, you need to register a domain name, you only register one? No, but register all the relevant ones to prevent buying domain names at high prices in the future. One truth.

As far as CPU is concerned, it is impossible to take what it needs when calculating and processing data, which is very harmful to its performance. So there is a protocol that when CPU reads data, it not only takes the data it needs, but also gets a row of data, which is the cache row, and the row is 64 bytes.

So? Through this feature, you can play some weird tricks, such as the following code.

Public class CacheLine {private volatile Long L1, L2;}

Let's give a scenario: two threads T1 and T2 operate on L1 and L2, respectively, so whether L2 needs to re-read the main memory value after T1 has modified L1. The answer is yes, according to our understanding of the cache line above, L1 and L2 must be on the same cache line. According to the cache consistency protocol, when the data is modified, other CPU needs to re-read the data in the main memory. This leads to the problem of pseudo-sharing.

So why does the object header require the existence of a pair of its data?

The HotSpot virtual machine requires that the memory size of each object must be guaranteed to be an integral multiple of 8 bytes, so it is supplemented for those that are not 8 bytes. The reason is also because of the cache line.

Object = object header + instance data

3. No lock

We talked about the implementation idea of the lock in the computer and the layout of the object in memory, and then we talked about its specific lock implementation. The object header in the object memory model is used to lock the object, and the exclusive locking operation of the resource is realized by modifying the lock flag bit and the biased lock log bit. Next, let's take a look at its memory structure diagram.

The image above shows the performance of the object head in memory (64 bits). JVM achieves "no lock" by modifying the lock log bit and bias lock bit in the object header.

For the concept of no lock, before 1.6, that is, all objects are created in an unlocked state, but after 1.6, the bias lock is opened, and the object is automatically upgraded to the current thread's bias lock after a few seconds (4 seconds 5s). (with or without synchronized).

Let's verify that the memory layout is printed through the jol-core tool. Note: the data information printed by this tool is reversed, that is, the last few bits are in front, as can be seen in the following case

Scene: create two objects, one at the beginning and the other five seconds later, to compare their memory layout

Object object = new Object (); System.out.println (ClassLayout.parseInstance (object). ToPrintable ()); / / unlocked try {TimeUnit.SECONDS.sleep (5);} catch (InterruptedException e) {e.printStackTrace ();} Object o = new Object (); System.out.println ("bias lock open"); System.out.println (ClassLayout.parseInstance (o). ToPrintable ()); / / bias lock opening after five seconds

We can see that the object created by the thread turned on is in the unlocked state, while the thread created after 5 seconds is in the biased lock state.

Similarly, when we encounter a synchronized block, we automatically upgrade to a biased lock instead of applying for a lock with the operating system.

After saying this, bring up a face-to-face question. Explain what unlocked is.

From the perspective of object memory structure, it is the embodiment of a lock log bit; in terms of its semantics, no lock is more abstract, because in the past, the concept of lock is often closely related to the lock of the operating system, so the new CAS-based bias lock, lightweight lock and so on are also called lock-free. At the starting point of the synchronized upgrade-no lock. This thing is more difficult to explain, can only say that it is not locked. However, it may be more comfortable to understand it from the object memory model during the interview.

4. Bias lock

In the actual development, there is often less competition for resources, so there is a bias lock, so the name implies that the current resources are biased towards the thread, thinking that all future operations come from changing the thread. Let's look at the bias lock from the memory layout of the object.

Object header description: the biased lock flag bit is modified to 1 through CAS, and the thread pointer of the thread is stored.

When lock contention occurs, it is not lock contention, that is, when the resource is used by multiple threads, the biased lock will escalate.

There is a point during the upgrade-the global security point, at which point the bias lock is undone.

Global Security Point-similar to CMS's stop the world, it ensures that no thread is operating on the resource at this time. This point in time is called a global security point.

You can turn off the delay of the bias lock through XX:BiasedLockingStartupDelay=0 to make it take effect immediately.

Turn off the bias lock through XX:-UseBiasedLocking=false.

5. Lightweight lock

When talking about lightweight locks, we need to understand these issues. What is a lightweight lock and what heavyweight lock? Why is it heavy? why is it light?

The lightweight and heavyweight standards rely on the operating system as the standard. Have you ever called the lock resources of the operating system during the operation? if so, it is heavyweight, if not, lightweight.

Next let's take a look at the implementation of lightweight locks.

The thread acquires the lock to determine whether the current thread is in a lock-free or lock-biased state. If so, copy the object header of the current object through CAS to Lock Recoder and place it in the current stack frame (for brothers who are not clear about the JVM memory model, it is enough to read this article here to get started with JVM.

The object header of the current object is set to Lock Recoder in the stack frame through CAS, and the lock flag bit is set to 00

If the modification fails, determine whether the thread in the current stack frame is itself. If you acquire the lock directly, if it is not upgraded to a heavy lock, the subsequent thread will block.

We mentioned a Lock Recoder above, which is used to hold the data in the object header of the current object, and the data saved in the object header of the object becomes the pointer to the current Lock Recoder.

Let's look at a code simulation.

Public class QingLock {public static void main (String [] args) {try {/ / sleep for 5 seconds, turn on the bias lock, you can use the JVM parameter TimeUnit.SECONDS.sleep (5);} catch (InterruptedException e) {e.printStackTrace ();} An o = new A (); / / Let the thread execute CountDownLatch countDownLatch = new CountDownLatch (1) alternately New Thread (()-> {o.test (); countDownLatch.countDown ();}, "1"). Start (); new Thread ()-> {try {countDownLatch.await ();} catch (InterruptedException e) {e.printStackTrace ();} o.test () }, "2"). Start ();}} class A {private Object object = new Object (); public void test () {System.out.println ("to enter synchronization code block *"); System.out.println (ClassLayout.parseInstance (object). ToPrintable ()); System.out.println ("enter synchronization code block *"); for (int I = 0; I

< 5; i++) { synchronized (object){ System.out.println(ClassLayout.parseInstance(object).toPrintable()); } } }} 运行结果为两个线程交替前后 轻量级锁强调的是线程交替使用资源,无论线程的个数有几个,只要没有同时使用就不会升级为重量级锁 在上面的关于轻量级锁加锁步骤的讲解中,如果线程CAS修改失败,则判断栈帧中的owner是不是自己,如果不是就失败升级为重量级锁,而在实际中,JDK加入了一种机制自旋锁,即修改失败以后不会立即升级而是进行自旋,在JDK1.6之前自旋次数为10次,而在1.6又做了优化,改为了自适应自旋锁,由虚拟机判断是否需要进行自旋,判断原因有:当前线程之前是否获取到过锁,如果没有,则认为获取锁的几率不大,直接升级,如果有则进行自旋获取锁。 6. 重量级锁 前面我们谈到了无锁-->

Bias lock-> lightweight lock, now let's finally talk about heavyweight lock.

This lock is very common in our development process, and threads preempt most of the resources at the same time, so synchronized will be upgraded directly to a heavy lock. Let's code simulation to see the state of its object head.

Code simulation

Public class WeightLock {public static void main (String [] args) {AA = new A (); for (int I = 0; I

< 2; i++) { new Thread(()->

{a.test ();}, "thread" + I) .start ();}

Both are unlocked before entering the code block

Start the loop and enter the code block

After taking a look, the object locks the mark.

Compared with the figure above, you can see that the lock has become a heavy lock when the thread competes. Next, let's take a look at the implementation of heavyweight locks.

6.1 Analysis of Java assembly code

We first analyze the underlying implementation of synchronzied from the Java bytecode. Its main implementation logic is to rely on a monitor object. After the current thread encounters monitorenter, it adds one to an attribute recursions of the current object (which will be explained in detail below). When it encounters monitorexit, the attribute minus one represents the release of the lock.

Code

Object o = new Object (); synchronized (o) {}

Assembly coding

The above picture is the assembly code of the above four lines of code, and we can see that at the bottom of synchronized are two assembly instructions.

Monitoreneter starts on behalf of synchronized block

Monitorexit represents the end of the synchronized block

I have a brother to say. Why are there two monitorexit? This is also an interview question I once encountered.

The first monitorexit represents the normal exit of the synchronized block.

The second monitorexit represents the abnormal exit of the synchronized block

It is understandable that when an exception occurs in a synchronized block, the current thread cannot keep the lock out of use by other threads. So there are two monitorexit.

Now that the synchronization code block is understood, let's take a look at the synchronization method.

Code

Public static void main (String [] args) {} public synchronized void test01 () {}

Assembly coding

We can see that the synchronization method adds an ACC_SYNCHRONIZED flag that calls monitorenter before the synchronization method executes and the monitorexit instruction when it is finished.

6.2Code for C++

In the introduction to Java assembly code, we mentioned two instructions monitorenter and monitorexit. In fact, they come from a C++ object monitor. Every time an object is created in Java, a monitor object is implicitly created. They are bound to the current object and are used to monitor the status of the current object. In fact, binding is not correct. The actual process is as follows: the thread itself maintains two MonitorList lists, namely free and used. When the thread encounters synchronization code blocks or synchronization methods, it will apply for a monitor from the free list. If the first thread is no longer idle, it will directly obtain a monitor from the global (JVM).

Let's take a look at C++ 's description of this object.

ObjectMonitor () {_ header = NULL; _ count = 0; _ waiters = 0, _ recursions = 0; / / number of reentrants _ object = NULL; / / Store the Monitor object _ owner = NULL; / / the object that owns the Monitor object _ WaitSet = NULL; / / Thread wait collection (Waiting) _ WaitSetLock = 0; _ Responsible = NULL _ succ = NULL; _ cxq = NULL; / / unidirectional linked list FreeNext = NULL; _ EntryList = NULL; / / blocked linked list (Block) _ SpinFreq = 0; _ SpinClock = 0; OwnerIsThread = 0; _ previous_owner_tid = 0;}

Thread locking model

Locking process:

The newest thread enters the _ cxp stack, attempts to acquire the lock, executes the code if the current thread acquires the lock, and adds it to the EntryList blocking queue if it does not acquire the lock

If the current thread of the executing process is suspended (wait), it is added to the WaitSet waiting queue, waiting to be woken up to continue execution

When the synchronous code block is finished, get a thread execution from _ cxp or EntryList

Monitorenter locking implementation

CAS modifies the _ owner of the current monitor object to the current thread. If the modification is successful, perform the operation.

If the modification fails, determine whether the _ owner object is the current thread, and if so, increase the number of _ recursions reentrants by one

If the current implementation acquires the lock for the first time, set _ recursions to one

Wait for the lock to be released

Blocking and acquisition lock implementation

Encapsulates the current thread as a node node with the state set to ObjectWaiter::TS_CXQ

Add it to the _ cxp stack, try to acquire the lock, and if the acquisition fails, suspend the current thread, waiting for wake-up

After waking up, execute the rest of the code from the starting point of the hang

Monitorexit release lock implementation

Reduce the number of _ recursions reentrants of the current thread by one. If the current number of reentrants is 0, exit directly and wake up other threads.

At this point, I believe you have a deeper understanding of the "introduction to the principle of Synchronized", you might as well come to the actual operation! Here is the website, more related content can enter the relevant channels to inquire, follow us, continue to learn!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report