Use Java threads to obtain excellent performance (II) -- use synchronous serialization threads to access key code parts

Source: Internet
Author: User
Summary
A multi-threaded program created by a developer sometimes generates an error value or produces other strange behaviors. Odd behavior occurs when a multi-threaded program does not use a synchronous serialization thread to access key code. What does synchronous serialization threads mean by accessing key code? This article explains synchronization, Java synchronization mechanism, and two problems that occur when developers do not use this mechanism correctly. Once you have read this article, you can avoid the strange behavior caused by the lack of synchronization in your multi-threaded Java program.
Is it difficult to create a multi-threaded Java program? You can answer only the information obtained from "getting excellent performance (I) with Java Threads", no. After all, I have shown you how to easily create thread objects, start threads related to these objects by calling the thread's start () method, and call other thread methods, for example, three overloaded join () Methods perform simple thread operations. So far, many developers still face difficulties in developing multi-threaded programs. Their programs often have unstable functions or produce incorrect values. For example, a multithreaded program may store incorrect employee information in a database, such as name and address. The name may belong to one employee, but the address may belong to another employee. What caused this strange behavior? There is a lack of synchronization: serialization, or sorting at the same time, threads access the code sequence of classes and field variable instances that allow multi-thread operations, and other shared resources. I call these code SEQUENCES A key part of the code.
Note: Unlike the class and instance field variables, threads cannot share local variables and parameters. The reason is: local variables and parameters are allocated in a thread method-called a stack. As a result, each thread receives its own copy of those variables. On the contrary, threads can share class fields and instance fields because those variables are not allocated in a thread method (called a stack. Instead, they are allocated as a part of the class (class field) or object (instance field) in the shared memory heap.
This article will teach you how to use synchronous serialization threads to access key code parts. I will use an example to explain why some multi-threaded programs must start with synchronization. Next I will discuss the synchronization mechanism of Java and the synchronized keyword about the monitor and lock. By studying the two problems caused by such incorrect use, I often find that the benefits of the synchronization mechanism are often denied due to incorrect use of the synchronization mechanism.
Read the entire series of thread programs:
· Part I: Describes threads, threads, and runnable
· Part II: using synchronous serialization threads to access key code
For synchronization needs
Why do we need synchronization? One answer, consider this example: You write a Java program that uses a pair of threads to simulate withdrawal/Deposit financial transactions. In that program, one thread processes deposits and other threads are processing withdrawals. Each thread operates on a pair of shared variables, classes, and instance field variables, which are used to identify the name and account of a financial transaction. For a correct financial transaction, each thread must assign values to the name and amount variables (and print those values at the same time) before other threads start to assign values to the name and amount variables (and print those values, simulate storage transactions ). The source code is as follows:
List 1. needforsynchronizationdemo. Java
// Needforsynchronizationdemo. Java
Class needforsynchronizationdemo
{
Public static void main (string [] ARGs)
{
Fintrans Ft = new fintrans ();
Transthread TT1 = new transthread (FT, "deposit thread ");
Transthread TT2 = new transthread (FT, "withdrawal thread ");
Tt1.start ();
Tt2.start ();
}
}
Class fintrans
{
Public static string transname;
Public static double amount;
}
Class transthread extends thread
{
Private fintrans ft;
Transthread (fintrans ft, string name)
{
Super (name); // Save the thread name
This. Ft = ft; // Save the reference to the financial transaction object
}
Public void run ()
{
For (INT I = 0; I <100; I ++)
{
If (getname (). Equals ("deposit thread "))
{
// Start of the key code part of the deposit thread
Ft. transname = "deposit ";
Try
{
Thread. Sleep (INT) (math. Random () * 1000 ));
}
Catch (interruptedexception E)
{
}
Ft. Amount = 2000.0;
System. Out. println (FT. transname + "" + ft. amount );
// The End Of the key code of the deposit thread
}
Else
{
// Start of the key code part of the withdrawal thread
Ft. transname = "withdrawal ";
Try
{
Thread. Sleep (INT) (math. Random () * 1000 ));
}
Catch (interruptedexception E)
{
}
Ft. Amount = 250.0;
System. Out. println (FT. transname + "" + ft. amount );
// The End Of the key code section of the withdrawal thread
}
}
}
}
The source code of needforsynchronizationdemo has two key parts: one can be understood as a deposit thread, and the other can be understood as a withdrawal thread. In the key code section of the deposit thread, the thread allocates the reference of the deposit String object to the shared variable transname and allocates 2000.0 to the shared variable amount. Similarly, in the key part of the withdrawal code, the thread allocates the reference of the withdrawal String object to transname and assigns 250.0 to amount. Print the variable content after each thread is allocated. When you run needforsynchronizationdemo, you may expect to output a list consisting of two rows, withdrawal 250.0 and deposit 2000.0. On the contrary, the output you receive is as follows:
Withdrawal 250.0
Withdrawal 2000.0
Deposit 2000.0
Deposit 2000.0
Deposit 250.0
The program is obviously faulty. The withdrawal thread should not simulate withdrawal of $2,000, and the deposit thread should not simulate deposits of $250. Each thread produces inconsistent output. What caused these contradictions? We believe that:
· On a single processor machine, threads share the processor. As a result, a thread can only run for a certain period of time. At other times, the JVM/operating system suspends the execution of that thread and allows other threads to execute it. This is a thread timing arrangement. On a multi-processor machine, each thread can own its own processor based on the number of threads and processors.
· On a single processor machine, the execution period of a thread is not long enough to complete its own key code before the key code part of other threads start to execute. On a multi-processor machine, threads can simultaneously execute their own key code. However, they may enter their key code at different times.
· The following situations may occur for single-processor or multi-processor machines: thread a allocates a value to the shared variable X in its key code section and decides to execute an input/output operation that requires 100 milliseconds. Next, thread B enters its key code section, assigns a different value to X, executes a 50 millisecond input/output operation, and assigns a value to the shared variables Y and Z. The input/output operations of thread a are completed, and their own values are allocated to Y and Z. Because X contains a value allocated by B, but y and z contain the value allocated by a, this is a contradiction.
How does this conflict occur in needforsynchronizationdemo? Assume that the deposit thread executes ft. transname = "deposit" and then calls thread. Sleep (). At that point, the deposit thread handed over the processor for a period of time to sleep and let the withdrawal thread execute. Assume that the deposit thread sleep for 500 milliseconds (thanks to math. Random (), a random value is selected from the range of 0 to 999 milliseconds ). During the deposit thread sleep, the withdrawal thread executes ft. transname = "withdrawal", sleep for 50 milliseconds (the withdrawal thread randomly chooses the sleep value), and then executes ft. amount = 250.0 and run system. out. println (FT. transname + "" + ft. amount)-all before the deposit thread wakes up. As a result, the withdrawal thread printed withdrawal 250.0, which is correct. When the deposit thread wakes up and executes ft. Amount = 2000.0, then it executes system. Out. println (FT. transname + "" + ft. amount ). It is incorrect to print withdrawal 2000.0 at this time. Although the deposit thread previously assigned a "deposit" reference to transname, this reference will then disappear when the withdrawal thread assigned the "withdrawal" reference to that shared variable. When the deposit thread wakes up, it cannot store the correct reference to transname, but it continues its execution by allocating 2000.0 to amount. Both variables do not have invalid values, but their combined values are contradictory. In this case, their values show an attempted withdrawal of $2,000.
A long time ago, computer scientists invented a term used to describe conflicting multi-threaded combinations. The term Race Condition-Each thread performs its own key code part before another thread enters the same key code part. As a demo of needforsynchronizationdemo, the thread execution sequence is unknown. It is not guaranteed that a thread can complete its own key code before other threads enter the key code section. Therefore, we have competing conditions that cause inconsistency. To prevent race conditions, each thread must complete its own key code before other threads enter the same key code section or other key code sections that operate on the same shared variable or resource. If a key part of the Code does not contain a serialization method (that is, only one thread can be accessed at a time), you cannot prevent the appearance of race conditions or inconsistencies. Fortunately, Java provides a method to access the serialization thread through its synchronization mechanism.
Note: For Java types, only long integer and Double Precision Floating Point variables tend to be different. Why? A 32-bit JVM generally uses two near 32-bit step sizes to access a 64-bit long integer variable or a 64-bit Double Precision Floating Point variable. One thread may wait for other threads to execute all two steps after completing the first step. Next, the first thread may wake up and complete the second step to generate a variable whose value is different from that of the first thread and that of the second thread. As a result, if at least one thread can modify a long integer variable or a double-precision floating-point variable, all threads that read and/or modify that variable must use synchronous serialization for access.

Java synchronization mechanism
Java provides a synchronization mechanism to prevent more than one thread from executing code in one or more key code parts at any point in time. This mechanism is built on the concept of monitors and locks. A monitor is protected as a package around key code parts, and a lock is used as a software entity that monitors to prevent multiple threads from entering the monitor. The idea is that when a thread wants to enter a key part of the code monitored by a monitor, the thread must obtain a lock for the monitor-related objects. (Each object has its own lock.) if some other threads keep this lock, JVM forces the request thread to wait in a wait area related to the Monitor/lock. When the threads in the monitor release the lock, JVM removes the waiting thread from the wait area of the monitor and allows that thread to obtain the lock and process the key code part of the monitor.
To work with the monitor/lock, JVM provides the monitorenter and monitorexit commands. Fortunately, you don't need to work in such a low level. Instead, you can use the Java synchronized keyword in the synchronized declaration and synchronization method.
Synchronous Declaration
Some key code components constitute a small part of their encapsulation method. To prevent multiple threads from accessing key code, you can use synchronized statements. This statement has the following syntax:
'Synchronized' ('objectidentifier ')'
'{'
// Key code
'}'
The synchronized Declaration starts with the keyword synchronized and uses an objectidentifier, which occurs between a pair of circular arc. Objectidentifier references a lock object related to the monitor described in the synchronized declaration. Finally, the key code part of the Java declaration appears between a pair of arms. How do you explain the synchronized statement? Take a look at the following code snippet:
Synchronized ("Sync object ")
{
// Access Shared variables and other shared resources
}
From a source code point of view, a thread attempts to enter the key code part of synchronized declaration protection. Internally, the JVM checks whether other threads control the lock related to the "Sync object" object. If no other thread controls the lock, JVM locks the request thread and allows that thread to enter the key code section between the curly arc. However, if other threads control the lock, the JVM forces the request thread to wait in a private wait area until the current thread in the key code completes the execution of the final declaration and passes through the final arc.
You can use the synchronized statement to remove the Race Condition of needforsynchronizationdemo. See exercise list 2 for how to eliminate it:
List 2. synchronizationdemo1.java
// Synchronizationdemo1.java
Class synchronizationdemo1
{
Public static void main (string [] ARGs)
{
Fintrans Ft = new fintrans ();
Transthread TT1 = new transthread (FT, "deposit thread ");
Transthread TT2 = new transthread (FT, "withdrawal thread ");
Tt1.start ();
Tt2.start ();
}
}
Class fintrans
{
Public static string transname;
Public static double amount;
}
Class transthread extends thread
{
Private fintrans ft;
Transthread (fintrans ft, string name)
{
Super (name); // Save the thread name save thread's name
This. Ft = ft; // Save the reference to the financial transaction object
}
Public void run ()
{
For (INT I = 0; I <100; I ++)
{
If (getname (). Equals ("deposit thread "))
{
Synchronized (FT)
{
Ft. transname = "deposit ";
Try
{
Thread. Sleep (INT) (math. Random () * 1000 ));
}
Catch (interruptedexception E)
{
}
Ft. Amount = 2000.0;
System. Out. println (FT. transname + "" + ft. amount );
}
}
Else
{
Synchronized (FT)
{
Ft. transname = "withdrawal ";
Try
{
Thread. Sleep (INT) (math. Random () * 1000 ));
}
Catch (interruptedexception E)
{
}
Ft. Amount = 250.0;
System. Out. println (FT. transname + "" + ft. amount );
}
}
}
}
}
Take a closer look at synchronizationdemo1. The run () method contains two key code parts clipped between synchronized (FT) {And. Each deposit and withdrawal thread must obtain a lock related to the fintrans object referenced by FT before any thread enters its key code section. If the deposit thread is in its key code section and the withdrawal thread wants to enter its key code section, the withdrawal thread should try to get the lock. Because the deposit thread controls the lock when executing its key code, JVM forces the withdrawal thread to wait until the deposit thread finishes executing the key code and releases the lock. (The lock is automatically released when the Execution leaves the key code part)
Tip: When you need to decide whether a thread controls the lock related to a given object, call the static Boolean holdslock (Object O) method of the thread. If the thread call controls the lock method related to the object, this method returns a Boolean true value. Otherwise, a false value is returned. For example, if you want to place system. Out. println (thread. holdslock (FT) at the end of main () method of synchronizationdemo1, holdslock () will return a false value. The returned dummy value is because the main thread that executes the main () method does not use the synchronization mechanism to obtain any lock. However, if you plan. out. println (thread. holdslock (FT) is placed in the run () synchronized (FT) Statement, holdslock () returns the true value because both the deposit thread and the withdrawal thread have to get the lock related to the fintrans object referenced by FT before those threads can enter its key code.
Synchronized Method
You can use synchronized Declaration through the source code of your program. However, you may also fall into excessive use of such declarations, resulting in low code efficiency. For example, suppose your program contains a method with two consecutive synchronized declarations, and each declaration attempts to obtain the same public object lock. Because it takes time to obtain and translate Object locks, repeated calls (in a loop) will reduce program performance. Each call to that method must obtain and release two locks. The program takes a lot of time to obtain and release the lock. To eliminate this problem, you should consider using the synchronization method.
A synchronization method is a class method whose header contains the synchronized keyword. For example, synchronized void print (string S ). When you synchronize a complete instance method, a thread must obtain the lock related to the object that appears in the method call. For example, if you call an FT. Update ("deposit", 2000.0) instance method and assume that update () is synchronous, a method must obtain a lock related to the object referenced by FT. To view the source code of a synchronization method of synchronizationdemo1, see list 3:
List 3. synchronizationdemo2.java
// Synchronizationdemo2.java
Class synchronizationdemo2
{
Public static void main (string [] ARGs)
{
Fintrans Ft = new fintrans ();
Transthread TT1 = new transthread (FT, "deposit thread ");
Transthread TT2 = new transthread (FT, "withdrawal thread ");
Tt1.start ();
Tt2.start ();
}
}
Class fintrans
{
Private string transname;
Private double amount;
Synchronized void Update (string transname, double amount)
{
This. transname = transname;
This. Amount = amount;
System. Out. println (this. transname + "" + this. amount );
}
}
Class transthread extends thread
{
Private fintrans ft;
Transthread (fintrans ft, string name)
{
Super (name); // Save the thread name
This. Ft = ft; // Save the reference to the financial transaction object
}
Public void run ()
{
For (INT I = 0; I <100; I ++)
If (getname (). Equals ("deposit thread "))
Ft. Update ("deposit", 2000.0 );
Else
Ft. Update ("withdrawal", 250.0 );
}
}
Although it is a little more concise than list 2, Table 3 achieves the same purpose. If the deposit thread calls the update () method, the JVM checks whether the withdrawal thread has obtained the lock related to the object referenced by FT. If so, the deposit thread will wait. Otherwise, the thread enters the key code section.
Synchronizationdemo2 demonstrates a synchronous instance method. However, you can also synchronize the class method. For example, the Java. util. Calendar class declares a public static synchronized Locale [] getavailablelocales () method. Because the class method does not have the concept of this reference, where can the class method obtain its lock? Class methods get their locks from class objects-each loaded class related to the class object, and get their locks from the class methods of those loaded classes. I call this lock class locks.
Some programs confuse the instance method and synchronization class method. To help you understand what happened in the program for calling synchronous instance methods in synchronous class methods, we should keep the following two points in mind:
1. Object locks and class locks have no relationship with each other. They are different entities. You obtain and release each lock independently. A synchronous instance method that calls a synchronous method obtains two locks. First, synchronize the instance method to obtain its object lock. Second, the method obtains the class lock of the synchronous class method.
2. synchronous methods can call the synchronization method of an object or use the object to lock a synchronization block. In that case, a thread initially acquires the class lock of the synchronization class method and then obtains the object lock of the object. Therefore, a synchronous method that calls the synchronous instance method also obtains two locks.
The following code snippet describes these two points of view:
Class locktypes
{
// Get the object lock right before the execution enters instancemethod ()
Synchronized void instancemethod ()
{
// Release the object lock when the thread leaves instancemethod ()
}
// Obtain the class lock just before the execution enters classmethod ()
Synchronized static void classmethod (locktypes lt)
{
Lt. instancemethod ();
// Obtain the object lock just before executing the key code.
Synchronized (LT)
{
// Key code
// Release the object lock when the thread leaves the key part of the code.
}
// Release the class lock when the thread leaves classmethod ()
}
}
The code snippet demonstrates the synchronous class method classmethod () that calls the synchronous instance method instancemethod (). By reading the annotation, you can see that classmethod () first obtains its class lock, and then obtains the object lock related to the locktypes object referenced by Lt.
Warning do not synchronize the run () method of a thread object because multiple threads need to execute run (). Because those threads attempt to synchronize the same object, only one thread can execute run () in a time period (). Result: before each thread can access run (), it must wait until the end of the previous thread.

Two Problems About synchronization mechanism
Despite its simplicity, developers often abuse the Java synchronization mechanism, which may lead to deadlocks from non-synchronization. This chapter will examine these issues and provide a pair of suggestions to avoid them.
Note: A thread problem related to the synchronization mechanism is the time cost related to lock acquisition and release. In other words, a thread takes time to obtain or release a lock. When a lock is obtained/released in a loop, the total time cost alone reduces the performance. For the old JVMs, the lock acquisition time cost often leads to significant performance loss. Fortunately, Sun Microsystem's hotspot JVM (which is loaded on j2se SDK) provides fast lock acquisition and release, greatly reducing the impact on these programs.
Not synchronized
When a thread exits a key part of code automatically or not (with one exception), it releases a lock so that another thread can enter. Assume that two threads want to enter the same key code part. To prevent two threads from simultaneously entering that key code part, each thread must strive to obtain the same lock. If each thread tries to get a different lock and succeeds, both threads enter the key code section, both threads have to wait for other threads to release its lock because other threads have obtained a different lock. The final result is: no synchronization. Example 4:
List 4. nosynchronizationdemo. Java
// Nosynchronizationdemo. Java
Class nosynchronizationdemo
{
Public static void main (string [] ARGs)
{
Fintrans Ft = new fintrans ();
Transthread TT1 = new transthread (FT, "deposit thread ");
Transthread TT2 = new transthread (FT, "withdrawal thread ");
Tt1.start ();
Tt2.start ();
}
}
Class fintrans
{
Public static string transname;
Public static double amount;
}
Class transthread extends thread
{
Private fintrans ft;
Transthread (fintrans ft, string name)
{
Super (name); // Save the thread name
This. Ft = ft; // Save the reference to the financial transaction object
}
Public void run ()
{
For (INT I = 0; I <100; I ++)
{
If (getname (). Equals ("deposit thread "))
{
Synchronized (this)
{
Ft. transname = "deposit ";
Try
{
Thread. Sleep (INT) (math. Random () * 1000 ));
}
Catch (interruptedexception E)
{
}
Ft. Amount = 2000.0;
System. Out. println (FT. transname + "" + ft. amount );
}
}
Else
{
Synchronized (this)
{
Ft. transname = "withdrawal ";
Try
{
Thread. Sleep (INT) (math. Random () * 1000 ));
}
Catch (interruptedexception E)
{
}
Ft. Amount = 250.0;
System. Out. println (FT. transname + "" + ft. amount );
}
}
}
}
}
When you run nosynchronizationdemo, you will see output similar to the following:
Withdrawal 250.0
Withdrawal 2000.0
Deposit 250.0
Withdrawal 2000.0
Deposit 2000.0
Although synchronized declaration is used, no synchronization occurs. Why? Check synchronized (this ). Because the keyword this points to the current object, the deposit thread attempts to obtain the lock related to the transthread object reference that is initialized to TT1. (In the main () method ). Similarly, the withdrawal thread attempts to obtain the lock related to the transthread object reference that is initialized to tt2. We have two different transthread objects, and each thread tries to obtain the locks related to its own transthread objects before entering its key code. Because threads obtain different locks, both threads can enter their own key code at the same time. The result is no synchronization.
Tip: To avoid synchronization, select an object that is public to all related threads. In that case, these threads compete to obtain the lock of the same object, and only one thread at a time can enter the relevant key code.
Deadlock
In some programs, the following situation may occur: Before thread B can enter the key part of code B, thread A obtains the lock required by thread B. Similarly, thread B obtains the lock required by thread a before thread a can enter the key code of thread. Because neither thread has its own lock, each thread must wait for its lock to be obtained. In addition, because no thread can execute, no thread can release the locks of other threads, and program execution is frozen. This behavior is called a deadlock ). Its example column is shown in table 5:
List 5. deadlockdemo. Java
// Deadlockdemo. Java
Class deadlockdemo
{
Public static void main (string [] ARGs)
{
Fintrans Ft = new fintrans ();
Transthread TT1 = new transthread (FT, "deposit thread ");
Transthread TT2 = new transthread (FT, "withdrawal thread ");
Tt1.start ();
Tt2.start ();
}
}
Class fintrans
{
Public static string transname;
Public static double amount;
}
Class transthread extends thread
{
Private fintrans ft;
Private Static string anothersharedlock = "";
Transthread (fintrans ft, string name)
{
Super (name); // Save the thread name
This. Ft = ft; // Save the reference to the financial transaction object
}
Public void run ()
{
For (INT I = 0; I <100; I ++)
{
If (getname (). Equals ("deposit thread "))
{
Synchronized (FT)
{
Synchronized (anothersharedlock)
{
Ft. transname = "deposit ";
Try
{
Thread. Sleep (INT) (math. Random () * 1000 ));
}
Catch (interruptedexception E)
{
}
Ft. Amount = 2000.0;
System. Out. println (FT. transname + "" + ft. amount );
}
}
}
Else
{
Synchronized (anothersharedlock)
{
Synchronized (FT)
{
Ft. transname = "withdrawal ";
Try
{
Thread. Sleep (INT) (math. Random () * 1000 ));
}
Catch (interruptedexception E)
{
}
Ft. Amount = 250.0;
System. Out. println (FT. transname + "" + ft. amount );
}
}
}
}
}
}
If you run deadlockdemo, you may see only one separate output line before the application is frozen. To restore deadlockdemo, press Ctrl-C (if you are using sun sdk1.4 in a Windows Command Prompt ).
What will lead to deadlocks? Check the source code carefully. The deposit thread must obtain two locks before it can enter its internal key code. External locks related to the fintrans object referenced by FT and internal locks related to the string object referenced by anothersharedlock. Similarly, the withdrawal thread must obtain two locks before it can enter its internal key code. External locks related to the string object referenced by anothersharedlock and internal locks related to the fintrans object referenced by FT. Assume that the execution commands of two threads are the external locks that each thread obtains. Therefore, the deposit thread acquires its fintrans lock and the withdrawal thread acquires its string lock. Now both threads execute their external locks, which are in their corresponding external key code. The two threads attempt to obtain the internal lock, so they can enter the corresponding internal key code section.
The deposit thread attempts to obtain the lock associated with the anothersharedlock reference object. However, because the withdrawal thread controls the lock, the deposit thread must wait. Similarly, the withdrawal thread attempts to obtain the lock related to the FT reference object. But the withdrawal thread cannot get the lock because the deposit thread (which is waiting) controls it. Therefore, the withdrawal thread must also wait. Neither thread can operate because neither thread can release the lock it controls. Two threads cannot release the lock they control because every thread is waiting. Every thread is deadlocked and the program is frozen.
Tip: To avoid deadlocks, carefully analyze your source code to see where threads may attempt to obtain each other's locks when a synchronous method calls other synchronous methods. You must do this because the JVM cannot detect and prevent deadlocks.
Review
To use threads to achieve excellent performance, you will encounter the situation that your multi-threaded program needs to serialize access to key code parts. Synchronization can effectively prevent inconsistencies in strange program behaviors. You can use synchronized Declaration to protect the part of a method or synchronize the entire method. However, check your code carefully to prevent synchronization failures or deadlocks.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.