Java deep adventure (3) -- Java thread: basic concepts, visibility, and Synchronization

Source: Internet
Author: User

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 may need to process thousands of requests at the same time. For such applications, the multi-thread or event-driven architecture is generally used. For Java, thread support is provided within the language. However, Java multi-threaded application development may encounter many problems. First, it is difficult to write correctly, second, it is difficult to test whether it is correct, and finally it is difficult to debug when a problem occurs. A multi-threaded application may have been running for several days, and then suddenly the problem occurs, but it cannot be reproduced again. In addition to correctness, it is more complicated to consider application throughput and performance optimization. This article describes the basic concepts, visibility, and thread synchronization of threads in Java.

Basic concepts of Java threads

In the operating system, the two concepts that are easy to confuse are process and thread ). The process in the operating system is the organizational unit of the resource. A process has an address space that contains program content and data, as well as other resources, including open files, sub-processes, and signal processors. Address spaces of different processes are isolated from each other. The thread represents the execution process of the program and the basic unit of CPU scheduling. A thread has its own program counters, registers, stacks, and frames. Thread introduction is motivated by the existence of blocking I/O in the operating system. When the I/O executed by a thread is blocked, other threads in the same process can use the CPU for computing. In this way, the application execution efficiency is improved. The thread concept is supported in mainstream operating systems and programming languages.

Some Java programs are single-threaded. The machine commands of the program are executed in the order specified in the program. The Java language provides the java. Lang. Thread class to abstract threads. There are two ways to create a new thread: one is to inherit Java. lang. thread class and override the run () method. The other method is to create Java. lang. when the object of the thread class is used, Java is provided in the constructor. lang. the Class Object of the runnable interface. After obtaining the java. Lang. Thread class object, you can start the thread execution by calling its start () method.

After a thread is created and started, it can be in different States. This thread may be running in CPU time, ready for scheduling, or blocked on a resource or event. Multiple ready threads compete for the CPU time to get the chance to be executed, while the CPU uses an algorithm to schedule thread execution. The running sequence of different threads is uncertain. The logic in multi-threaded programs cannot depend on the scheduling algorithm of the CPU.

Visibility

Visibility is the root cause of errors in Java multi-threaded applications. In a single-threaded program, if you change the value of a variable first and then read the value of the variable, the read value is the value written in the previous write operation. That is to say, the results of the previous operation are certainly visible to the subsequent operations. However, in a multi-threaded program, if a synchronization mechanism is not used, the value written by a thread cannot be visible to another thread. The reasons for this situation may be as follows:

  • Internal CPU cache: The current CPU generally has several levels of Hierarchical Cache. The CPU directly operates on the data in the cache and synchronizes the data in the cache with the primary storage as needed. Therefore, the cached data may be inconsistent with the data in the primary storage at some time points. The new value of the write operation executed by a thread may still be stored in the CPU cache and has not been written back to the primary memory. At this time, the read operation of another thread will still read the old value in the main memory.
  • CPU command execution sequence: in some cases, the CPU may change the command execution sequence. This may cause a thread to prematurely see the new value after the write operation of another thread is completed.
  • Compiler code re-arrangement: For the purpose of performance optimization, the compiler may re-arrange the generated target code during compilation.

The reality is that different CPUs may adopt different architectures, and such problems become especially complex in multi-core processors and multi-processor systems. The goal of Java is to "write once and run everywhere". Therefore, it is necessary to standardize the way Java programs access and operate the primary storage, to ensure that the running results of the same program on different CPU architectures are consistent. The Java memory model is introduced for this purpose. JSR 133 further corrected the problems in the previous memory model. In general, the Java Memory Model describes the relationship between shared variables in the program and the underlying details of writing and reading values of these variables in the main memory. The Java memory model defines the significance of keywords such as synchronized, volatile, and final in the Java language to the variable read/write operations in the primary memory. Java developers use these keywords to describe the expected behavior of the program, and the compiler and JVM are responsible for ensuring that the generated code behavior conforms to the memory model description at runtime. For example, for variables declared as volatile, before reading, the JVM will ensure that the cached value in the CPU will first become invalid and then read the value from the primary memory. After writing, the new value is immediately written to the primary storage. The synchronized and volatile keywords also impose additional restrictions on code shuffling during Compiler optimization. For example, the compiler cannot remove the code from the synchronized block. Read/write operations on volatile variables cannot be rearranged with other read/write operations.

An important concept in the Java memory model is to define the order of "happening before (happens-before. If an action occurs before another action in the "before" Order, the result of the previous action is visible to the latter action in the case of multithreading. The most common "occur before" sequence includes: the unlock operation on the monitor on an object must occur before the next lock operation on the same monitor; write operations on variables declared as volatile certainly occur before subsequent read operations. With the "happening before" sequence, the behavior of multi-threaded programs at runtime is predictable in the key aspect. The compiler and JVM will ensure that the order of "previous occurrence" is guaranteed. For example, the following simple method:

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

This is a common method for increasing counters. This. Count ++ is actually this. Count = This. Count + 1, which is composed of a read operation and write operation on the variable this. Count. In the case of multiple threads, the order in which two threads execute these two operations is unpredictable. If the initial value of this. Count is 1, both threads may read the value of 1, and set the value of this. Count to 2 successively to generate an error. The cause of the error is that the write operation on this. Count by one thread is invisible to the other thread, and the value of this. Count has changed. If the synchronized keyword is added to the increase () method declaration, a "previous occurrence" order is forcibly defined between the operations of the two threads. A thread must first obtain the lock on the current object to execute the operation, and complete the write operation on this. count during the time it has the lock. The other thread can be executed only after the current thread releases the lock. In this way, the call to the increase () method by two threads can only be completed in sequence, and the operation visibility between threads can be ensured.

If the value of a variable may be read by multiple threads and written by at least one thread lock, and there is no defined sequence between these read/write operations that occurs before, data race exists in this variable ). The existence of data competition is the primary problem to be solved in Java multi-threaded applications. The solution is to use the synchronized and volatile keywords to define the sequence of occurrence.

Java locks

When data competition exists, the simplest solution is locking. The lock mechanism allows only one thread to access the critical section of competing data at a time. The synchronized keyword in Java can be used to lock a code block or method. Any Java object has its own monitor, which can be locked or unlocked. When a code block or method protected by the synchronized keyword is executed, it indicates that the current thread has successfully obtained the lock on the monitor of the object. When the code block or method is successfully executed or an exception exits, the lock obtained by the current thread is automatically released. A thread can lock a Java object multiple times. At the same time, JVM ensures that the value of the variable is synchronized with the content in the primary storage before the lock is obtained and after the lock is released.

Java Thread Synchronization

In some cases, it is not enough to rely solely on mutually exclusive access to data between threads. Some threads have a cooperative relationship and need to collaborate to complete a task according to certain protocols, such as a typical producer-consumer mode. In this case, the waiting-notification mechanism between threads provided by Java is used. When the conditions required by the thread are not met, the thread enters the waiting state. The other thread is responsible for sending a notification at the right time to wake up the waiting thread. The wait/notify/yyall method group in the Java. Lang. Object Class in Java completes synchronization between threads.

When calling the wait method on a Java object, first check whether the current thread has obtained the lock on the object. If no, java. Lang. illegalmonitorstateexception is thrown directly. If there is a lock, add the current thread to the waiting set of the object and release the lock. The current thread is blocked and cannot be executed until it is removed from the waiting set of the object. There are many reasons why a thread is removed from the object's waiting set: When the notify method on the object is called, the thread is selected; The yyall method on the object is called; the thread is interrupted. For wait operations with time-out restrictions, when the time limit is exceeded, the operations within the JVM are not normal.

From the above description, we can draw several conclusions: the wait/notify/notifyall operation must be placed in the synchronized code block or method to ensure that when wait/notify/notifyall is executed, the current thread has obtained the required lock. When you are unsure about the number of threads in the waiting set of an object, it is best to use policyall instead of policy. Although policyall will cause the thread to be awakened without necessary performance impact, it is easier to use. Because the thread may be accidentally awakened under abnormal circumstances, you generally need to put the wait operation in a loop and check whether the required logical conditions are met. The typical usage mode is as follows:

Private object lock = new object (); synchronized (LOCK) {While (/* When the logical conditions are not met */) {try {lock. wait ();} catch (interruptedexception e) {}}// processing logic}

The above Code uses a private object lock as the Lock Object. The advantage is that this object can be used without other code errors.

Interrupt thread

The interrupt () method of a thread object can send an interrupt request to the thread. Interrupt requests are a way of collaboration between threads. When thread a sends an interrupt request by calling the interrupt () method of thread B, thread a is concerned with requesting thread B. Thread B should handle this interrupt request at convenience. Of course this is not necessary. When an interruption occurs, the thread object will have a mark to record the Current Interruption status. The isinterrupted () method can be used to determine whether a request is interrupted. If the thread is in the blocking status when the interrupt request occurs, this interrupt request will cause the thread to exit the blocking status. This may cause the thread to be in a blocking state: When the thread calls the wait () method to enter the waiting set of an object, or uses the sleep () method to temporarily sleep, or wait for another thread to complete through the join () method. When a thread is congested, a java. Lang. interruptedexception is thrown when the interrupt occurs, and the Code enters the corresponding exception handling logic. In fact, this exception must be caught when the wait/sleep/join method is called. Interrupting a thread in the waiting set of an object will remove the thread from the waiting set so that it can continue to execute Java after obtaining the lock again. lang. the processing logic of interruptedexception exceptions.

The task can be canceled by interrupting the thread. During task execution, you can regularly check the interrupt mark of the current thread. If the thread receives the interrupt request, it can terminate the execution of the task. If an exception occurs in Java. Lang. interruptedexception, do not capture it and do not process it. If you do not want to handle this exception at this level, throw the exception again. When a thread in the blocking status is interrupted and a java. Lang. interruptedexception exception is thrown, the interruption status mark in the object is cleared. If a java. Lang. interruptedexception exception is caught but cannot be thrown again, you need to call the interrupt () method again to reset the flag.

 

 

Reproduced from: http://www.infoq.com/cn/articles/cf-java-thread authorCheng Fu

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.