At least few Java programmers refuse from multithreading and libraries that support it, but those who have found time to study the issue in depth are even less. Instead, we learn about flows only as much as we need for a specific task, adding new tricks to our toolkit only when necessary. So you can create and run decent applications, but you can do better. Understanding the features of the compiler and the Java virtual machine will help you write more efficient, productive code.
In this issue of the
“5 Things ...” series , I will introduce some of the subtle aspects of multi-threaded programming, including synchronized methods, volatile variables, and atomic classes. It focuses specifically on how some of these constructs interact with the JVM and the Java compiler, and how various interactions can affect application performance.
Translator’s note: I’m one of those people who didn’t know these five things about multithreaded programming, so I thought that this article was worth it to be made public here, but that’s why I could make some mistakes in translation, so the amendments are welcome with enthusiasm.
Translator's note2: in the comments, knowledgeable people share links and information on the topic, no less interesting than the content of the article)
')
1. Synchronized method or synchronized block?
You may have already thought about whether to declare synchronized the whole method or only that part of it that needs to be secure. In such situations, it is useful to know that when the Java compiler converts the source code into byte code, it works with synchronized methods and synchronized blocks in very different ways.
When the JVM executes the synchronized method, the executing thread determines that the method_info of this method has the ACC_SYNCHRONIZED flag. Then it automatically sets a lock on the object, calls the method and removes the lock. If an exception is thrown, the thread automatically releases the lock.
On the other hand, the synchronized block bypasses the support of object locking requests and exception handling built into the JVM, so this must be explicitly described in bytecode. If you look at the byte-code for the block, you will see a bunch of additional operations in it in comparison with the method. Listing 1 shows a call for both.
Listing 1. Two approaches to synchronization.package com.geekcap ;
public class SynchronizationExample {
private int i ;
public synchronized int synchronizedMethodGet ( ) {
return i ;
}
public int synchronizedBlockGet ( ) {
synchronized ( this ) {
return i ;
}
}
}
The synchronizedMethodGet () method generates the following bytecode:
0: aload_0
1: getfield
2: nop
3: iconst_m1
4: ireturn
Here is the bytecode for the synchronizedBlockGet () method:
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: getfield
6: nop
7: iconst_m1
8: aload_1
9: monitorexit
10: ireturn
11: astore_2
12: aload_1
13: monitorexit
14: aload_2
15: athrow
Creating a synchronized block yielded 16 strings of bytecode, while the synchronized method produced only 5.
2. "Intraflow" (ThreadLocal) variables.
If you want to keep one instance of a variable for all instances of a class, you use static class variables. If you want to keep an instance variable for each thread, use threading variables. ThreadLocal variables differ from ordinary variables in that each thread has its own, individually initialized instance of a variable, which it gets access to via get () or set () methods.
Suppose you are developing a multi-threaded code tracer whose goal is to uniquely determine the path of each stream through your code. The problem is that you need to coordinate several methods in several classes across multiple threads. Without ThreadLocal this would be intractable. When a thread starts to run, it would be necessary to generate a unique marker to identify it with the tracer, and then pass this marker to each method during the trace.
With ThreadLocal this is easier. The thread initializes the ThreadLocal variable at the beginning of execution, and then accesses it from each method in each class, and the variable will only keep trace information for the thread that is currently executing. When its execution is complete, the thread can pass its individual trace record to the control object responsible for maintaining all the records.
Using ThreadLocal makes sense when you need to store instances of a variable for each thread.
3. Volatile variables.
I estimate that only half of all Java developers know that Java has the keyword volatile. Of these, only about 10 percent know what it means, and even less know how to use it effectively. In short, defining a variable with the keyword volatile (“changeable”) means that the value of the variable will change in different threads. To fully understand what volatile means, first, you need to understand how threads operate on regular, non-volatile, variables.
In order to improve performance, the Java language specification allows the JRE to store a local copy of a variable in each stream that references it. You can consider these “intra-stream” copies of variables similar to a cache, which helps to avoid checking the main memory every time you need access to the value of a variable.
But imagine what happens in the following case: two threads start, and the first one reads variable A as 5, while the second one reads 10. If variable A changes from 5 to 10, then the first thread will not know about the change, so it will have wrong value A. However, if variable A is marked as volatile, then at any time, when a thread accesses its value, it will receive a copy of A and read its current value.
If the variables in your application do not change, then the intra-stream cache makes sense. Otherwise, it’s very useful to know what the volatile keyword can do for you.
4. Volatile vs. synchronized.
If a variable is declared as volatile, this means that it is expected to change in several threads. Naturally, you think that the JRE will impose some form of synchronization for volatile variables. For better or worse, the JRE implicitly provides synchronization when accessing volatile variables, but with one very big caveat: reading volatile variables is synchronized and writing to volatile variables is synchronized, and non-atomic operations are not.
Which means that the following code is not safe for threads:
myVolatileVar ++;
This code can also be written as follows:
int temp = 0 ;
synchronize ( myVolatileVar ) {
temp = myVolatileVar ;
}
temp ++;
synchronize ( myVolatileVar ) {
myVolatileVar = temp ;
}
In other words, if the volatile variable is updated implicitly, that is, the value is read, changed, and then assigned as new, the result will be non-thread safe between two synchronous operations. You can choose whether to use synchronization or rely on JRE support for automatic synchronization of volatile variables. The best approach depends on your case: if the assigned value of a volatile variable depends on its current value (for example, during an increment operation), you need to use synchronization if you want the operation to be thread-safe.
5. Updates of atomic fields.
When you need a primitive type that performs increment and decrement operations, it is much better to choose it among the new atomic classes in the java.util.concurrent.atomic package than to write the synchronized block yourself. Atomic classes guarantee that certain operations will be performed thread-safe, such as increment and decrement, update, and add (add) value operations. The list of atomic classes includes AtomicInteger, AtomicBoolean, AtomicLong, AtomicIntegerArray, and so on.
A peculiar challenge to the programmer in using atomic classes is that all operations of a class, including get, set, and the family of get-set operations are also atomic. This means that read and write operations that do not change the value of an atomic variable are synchronized, and not just important read-update-write operations. If you want more detailed control over the deployment of synchronized code, then a workaround is to use an atomic field update.
Using atomic update.
Atomic updates such as AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, and AtomicReferenceFieldUpdater are essentially shells that apply to volatile fields. Inside, Java class libraries use them. Although they are not often used in application code, you have no reason not to start lightening your life with them.
Listing 2 shows an example of a class that uses atomic updates to modify a book that someone is reading:
Listing 2. Book class.package com.geeckap.atomicexample ;
public class Book
{
private String name ;
public Book ( )
{
}
public Book ( String name )
{
this . name = name ;
}
public String getName ( )
{
return name ;
}
public void setName ( String name )
{
this . name = name ;
}
}
The Book class is just a POJO (plain old Java object is a plain old Java object) that has only one field: name.
Listing 3. The MyObject class.package com.geeckap.atomicexample ;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater ;
/ **
*
* @author shaines
* /
public class MyObject
{
private volatile Book whatImReading ;
private static final AtomicReferenceFieldUpdater < MyObject, Book > updater =
AtomicReferenceFieldUpdater. newUpdater (
MyObject. class Book . class , "whatImReading" ) ;
public Book getWhatImReading ( )
{
return whatImReading ;
}
public void setWhatImReading ( Book whatImReading )
{
//this.whatImReading = whatImReading;
updater. compareAndSet ( this , this . whatImReading , whatImReading ) ;
}
}
The MyObject class in Listing 3 represents, as you would expect, the get and set methods, but the set method does something else. Instead of simply providing his internal link to the specified book (which would have been done with commented out code in Listing 3), he uses AtomicReferenceFieldUpdater.
AtomicReferenceFieldUpdater
Javadoc defines an AtomicReferenceFieldUpdater like this:
A reflection-based utility that allows reference fields of designated classes. This is a subject that is not subject to any updates.
(A reflection-based utility that permits atomic updates to assigned volatile reference fields of assigned classes. This class is intended for use in atomic data structures in which several reference fields of the same record are independent subjects for atomic updates)
kill me, I don't know how is it normal to translateIn Listing 3, AtomicReferenceFieldUpdater is created by calling the newUpdater method, which takes three parameters.
• the class of the object containing the field (in this case, MyObject)
• the class of the object that will be updated atomically (in this case, Book)
• field name for atomic update
What is significant here is that the getWhatImReading method is executed without synchronization of any kind, while setWhatImReading is performed as an atomic operation.
Listing 4 shows how to use setWhatImReading () and proves that the variable changes correctly:
Listing 4. Atomic update test case.package com.geeckap.atomicexample ;
import org.junit.Assert ;
import org.junit.Before ;
import org.junit.Test ;
public class AtomicExampleTest
{
private MyObject obj ;
@Before
public void setUp ( )
{
obj = new MyObject ( ) ;
obj. setWhatImReading ( new Book ( "Java 2 From Scratch" ) ) ;
}
@Test
public void testUpdate ( )
{
obj. setWhatImReading ( new Book (
"Pro Java EE 5 Performance Management and Optimization" ) ) ;
Assert . assertEquals ( "Incorrect book name" ,
"Pro Java EE 5 Performance Management and Optimization" ,
obj. getWhatImReading ( ) . getName ( ) ) ;
}
}
Finally.
Multithreaded programming is always a challenge, but since the Java platform evolved, it has gained support that simplifies some multithreaded programming tasks. In this article, I looked at five things you might not know about writing multi-threaded applications on the Java platform, including the difference between synchronized methods and code blocks, the value of using ThreadLocal variables, the widespread misunderstanding of volatile (including the danger of relying on volatile when it is necessary to use synchronization), and a brief overview of the subtleties of the atomic classes. Who wants to know more, see the
Links section
(on the author’s site) .