Java Deep Adventures (iii)--java threads?: Basic concepts, visibility and synchronization

Source: Internet
Author: User
Tags thread class

Developing high-performance concurrent applications is not an easy task. Examples of such applications include high-performance Web servers, game servers, and search engine crawlers. Such an application might require processing thousands of requests at the same time. For such applications, a multithreaded or event-driven architecture is generally used. For Java, threading support is provided inside the language. But Java's multi-threaded application development will encounter many problems. The first is hard to write correctly, and secondly it is hard to test whether it is correct, and finally the problem is difficult to debug. A multithreaded application may run for several days without problems, and then suddenly there is a problem, and then it cannot be reproduced again. In addition to correctness, it is more complex to consider the throughput and performance optimizations of the application. This article focuses on the basic concepts of threading in Java, visibility, and thread synchronization related content.

Basic Java Threading Concepts

The two more confusing concepts in the operating system are process (processes) and threads (thread). The process in the operating system is the organizational unit of the resource. The process has an address space containing the program's contents and data, as well as other resources, including open files, sub-processes, and signal handlers. The address spaces of different processes are isolated from each other. The thread represents the execution flow of the program and is the basic unit of CPU dispatch. Threads have their own program counters, registers, stacks, and frames. The motivation for introducing threads is the presence of blocking I/O in the operating system. When a thread executes an I/O that is blocked, other threads in the same process can use the CPU to compute. In this way, the implementation efficiency of the application is improved. Threading concepts are supported in mainstream operating systems and programming languages.

Part of the Java program is single-threaded. The machine instructions for the program are executed sequentially in the order given in the program. The Java language provides the Java.lang.Thread class to provide abstractions for threads. There are two ways to create a new thread: One is to inherit the Java.lang.Thread class and overwrite the run () method, The other is to provide an object in the constructor for a class that implements the Java.lang.Runnable interface when creating an object of the Java.lang.Thread class. After the object of the Java.lang.Thread class is obtained, the execution of the thread can be initiated by invoking its start () method.

After a thread has been created successfully and started, it can be in a different state. This thread may be running on CPU time, or it may be in a ready state, waiting to be scheduled for execution, and possibly blocking on a resource or event. Multiple ready-State threads compete for CPU time to get the chance of being executed, while the CPU uses some algorithm to schedule thread execution. The running order of different threads is indeterminate, and the logic in multithreaded programs cannot depend on the scheduling algorithm of CPU.

Visibility of

The problem with visibility (visibility) is the source of errors in Java multithreaded applications. In a single-threaded program, if you first change the value of a variable and then read the value of the variable, the value read is the value written by the last write operation. This means that the result of the preceding operation is definitely visible to the subsequent operation. However, in a multithreaded program, if you do not use a certain synchronization mechanism, there is no guarantee that the value written by one thread will be visible to another thread. There are several possible reasons for this:

    • Cache inside the CPU: The CPU now generally has a few levels of cache in the hierarchy. The CPU directly operates the data in the cache and synchronizes the data in the cache with the main memory when needed. So at some point, the data in the cache may be inconsistent with the data in main memory. The new value of the write operation performed by a thread may currently be stored in the CPU's cache and has not been written back into main memory. At this time, another thread reads the old value in main memory.
    • Instruction execution order of the CPU: At some point the CPU may change the order in which instructions are executed. This can lead to a thread prematurely seeing the new value after the write operation of another thread finishes.
    • Compiler code rearrangement: For performance tuning purposes, the compiler may rearrange the generated target code at compile time.

The reality is that different CPUs may have different architectures, which are particularly complex in multicore processors and multiprocessor systems. The goal of Java is to "write once, run everywhere", so it is necessary to Java Program Access and operation of main memory in a way to make sure that the same program on different CPU architecture running results are consistent. The Java memory model is introduced for this purpose. JSR 133 further corrects the problem in the previous memory model. In all, the Java memory model describes the relationship of shared variables in the program and the underlying details of writing and reading these variable values in main memory. The Java memory model defines the meaning of the synchronized, volatile, and final keywords in the Java language for variable read and write operations in main memory. Java developers Use these keywords to describe the behavior that the program expects, and the compiler and the JVM are responsible for ensuring that the generated code behaves at run time in accordance with the description of the memory model. For example, for variables declared volatile, before reading, the JVM will ensure that the cached values in the CPU are first invalidated and re-read from main memory, and the new values are written to main memory immediately after the write. The synchronized and Volatile keywords also impose additional restrictions on the code rearrangement of compiler optimizations. For example, the compiler cannot remove the code from the synchronized block. Read and write operations on volatile variables cannot be rearranged with other read and write operations.

An important concept in the Java memory model is to define the order in which it occurred before (Happens-before). If an action occurs before another action in the order in which it occurred before, the result of the previous action is definitely visible for the latter action in the case of multithreading. The most common sequence of "before" includes: the Unlocking of a monitor on an object must occur before the next lock operation on the same monitor, and a write to a variable that is declared volatile must occur before a subsequent read operation. With the "in place" order, the behavior of the multithreaded procedure at run time is predictable on the critical part. The compiler and the JVM ensure that the order "occurs before" is guaranteed. For example, here's an easy way to do this:

public void Increase () {    this.count++;}

This is a common counter increment method, this.count++ is actually this.count = This.count + 1, which consists of a read operation and a write operation to the variable This.count. If you are in a multithreaded situation, the order in which the two operations are performed by two threads is not expected. If the initial value of This.count is 1, two threads may have read the value of 1, and then set the This.count value to 2 successively, resulting in an error. The reason for the error is that one of the threads that writes to This.count is not visible to another thread, and another thread does not know that the value of This.count has changed. If you add the Synchronized keyword to the increase () method declaration, a "before occurs" order is enforced between the operations of the two threads. A thread needs to obtain a lock on the current object first to execute, and to complete the write operation to the This.count at the time it has the lock. Another thread can execute only after the current thread has freed the lock. This guarantees that the two-thread call to the increase () method can only be done sequentially, ensuring visibility of the operations between the threads.

If the value of a variable can be read by multiple threads and can be written by at least one thread lock, and there is no defined "in the previous" order between these read and write operations, then there is a data contention on this variable (race). The existence of data competition is the first problem to be solved in the application of Java multithreading. The solution is to define "what happened before" in the order of synchronized and volatile keywords.

Locks in Java

When data competition exists, the simplest solution is to lock. The lock mechanism restricts only one thread at a time to access the critical section that produces competing data. The synchronized keyword in the Java language can be used to lock a code block or method. Any Java object has its own monitor that can be locked and unlocked. When a block or method of code protected by the Synchronized keyword is executed, it indicates that the current thread has successfully acquired the lock on the object's monitor. The locks acquired by the current thread are automatically freed when the code block or method executes properly or exits unexpectedly. A thread can add multiple locks on a Java object. The JVM also guarantees that the value of the variable is synchronized with the contents of main memory after the lock is acquired and the lock is released.

Synchronization of Java Threads

In some cases, it is not sufficient to rely solely on mutually exclusive access to data between threads. Some threads have a collaborative relationship that requires a certain protocol to work together to accomplish a task, such as a typical producer-consumer model. In this case, you need to use the wait-notification mechanism between the threads provided by Java. When the condition required by the thread is not satisfied, it enters the wait state, while the other thread is responsible for notifying the waiting thread at the appropriate time. The Wait/notify/notifyall method group in the Java.lang.Object class in Java is the completion of synchronization between threads.

When calling the wait method above a Java object, first check to see if the current thread has acquired a lock on the object. If not, the java.lang.IllegalMonitorStateException exception is thrown directly. If there is a lock, add the current thread to the object's waiting set and release the lock it owns. The current thread is blocked from continuing execution until it is removed from the object's wait collection. There are many reasons for a thread to be removed from the object's wait collection: When the Notify method on the object is called, the thread is selected, the Notifyall method on the object is called, the thread is interrupted, and for a wait operation with a timeout limit, when the time limit is exceeded The JVM internally implements operations that are not normally under normal conditions.

From the above description, you can get a few conclusions: the Wait/notify/notifyall operation needs to be placed in the synchronized code block or method, in order to ensure that the execution of Wait/notify/notifyall, The current thread has acquired the required lock. When you are unsure of the number of threads in a waiting set for an object, it is best to use Notifyall instead of notify. Notifyall can cause a performance impact if the thread wakes up unnecessarily, but it is simpler to use. Because a thread may be unexpectedly awakened in an unusual situation, it is generally necessary to put the wait operation in a loop and check that the required logical condition is satisfied. The typical usage pattern is as follows:

1 PrivateObject lock =NewObject ();2 synchronized(lock) {3      while(/*when the logical condition is not satisfied*/) { 4        Try { 5 lock.wait (); 6}Catch(Interruptedexception e) {}7     } 8     //processing Logic9}

A private object lock is used as the lock object in the code above, and the advantage is that it avoids the use of other code errors.

Break Thread in

The interrupt () method of a Thread object allows an interrupt request to be made to the thread. An interrupt request is a way of collaborating between threads. Thread A is aware of request thread B when thread A makes an interrupt request by calling the interrupt () method of thread B. Thread B should be able to handle this interrupt request at a convenient time, which is not required, of course. When an interrupt occurs, there is a tag in the thread object to record the current interrupt state. The isinterrupted () method can be used to determine if an interrupt request has occurred. If the thread is in a blocking state when the interrupt request occurs, the interrupt request causes the thread to exit the blocking state. A condition that can cause a thread to be blocked is when the thread enters the waiting set of an object by calling the Wait () method, or temporarily sleeps through the sleep () method, or waits for another thread to complete by invoking the join () method. In the case of a thread blocking, when the interrupt occurs, the java.lang.InterruptedException is thrown, and the code enters the appropriate exception handling logic. In fact, when invoking the Wait/sleep/join method, this exception must be caught. Breaking a thread that is in the waiting set of an object causes the thread to be removed from the wait collection so that it can continue executing the processing logic of the java.lang.InterruptedException exception after the lock is acquired again.

The task can be canceled by a thread break. The interrupt token of the current thread can be checked periodically during the execution of the task, and if the thread receives an interrupt request, the execution of the task can be terminated. When encountering an java.lang.InterruptedException exception, do not take any processing after capturing it. If you do not want to handle this exception at this level, throw the exception again. When a thread in a blocking state is interrupted and throws an Java.lang.InterruptedException exception, the interrupt status token in its object is emptied. If the java.lang.InterruptedException exception is caught but cannot be re-thrown, the token needs to be reset by calling the interrupt () method again.

Resources
    • Modern operating system?
    • Java Language Specification (Third edition) Chapter 17th: Threads and locks
    • Java Memory Model FAQ
    • Fixing the Java Memory Model, Part 1 & Part 2

Java Deep Adventures (iii)--java threads?: Basic concepts, visibility and synchronization

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.