-
Since multiple threads of the same process share the same storage space, while bringing convenience, it also brings about the serious problem of access conflicts. The Java language provides a special mechanism to resolve this conflict, effectively preventing the same data object from being accessed by multiple threads at the same time.
Since we can use the private keyword to ensure that the data object can only be accessed by methods, we only need to propose a mechanism for methods. This mechanism is the synchronized keyword, which includes two usages: synchronized method and synchronized block.
1. synchronized method: declare the synchronized method by adding the synchronized keyword in the method declaration. like:
public synchronized void accessVal(int newVal);
The synchronized method controls access to class member variables: each class instance corresponds to a lock. Each synchronized method must obtain the lock of the class instance that calls the method before it can be executed. Otherwise, the thread to which it belongs is blocked. Once the method is executed, it will exclusively occupy the class member variable. The lock is not released until returning from this method. After that, the blocked thread can obtain the lock and re-enter the executable state. This mechanism ensures that for each class instance at the same time, at most one of all its member functions declared as synchronized is in the executable state (because at most one can obtain the lock corresponding to the class instance) (pay attention to this statement! Because It is a lock for a class instance, so for an object at a time, only one synchronized method is executed by one thread at a time), thus effectively avoiding access conflicts of class member variables (as long as all methods that may access class member variables are declared as synchronized).
In Java, not only class instances, each class also corresponds to a lock. In this way, we can also declare the static member functions of the class as synchronized to control its access to the static member variables of the class.
Defects of the synchronized method: If a large method is declared as synchronized, the efficiency will be greatly affected. Typically, if the thread class method run() is declared as synchronized, it will continue to run during the entire life cycle of the thread. Will cause its calls to any synchronized methods of this class to never succeed. Of course, we can solve this problem by putting the code that accesses class member variables into a special method, declaring it as synchronized, and calling it in the main method, but Java provides us with a better solution, that is synchronized block.
2. synchronized block: declare the synchronized block through the synchronized keyword. The syntax is as follows:
synchronized(syncObject) {
//Code to allow access control
}
A synchronized block is a code block in which the code must obtain the lock of the object syncObject (as mentioned before, it can be a class instance or class) before it can be executed. The specific mechanism is the same as mentioned above. Because it can target any code block and specify the locked object arbitrarily, it has high flexibility.
Thread blocking (synchronization)
In order to solve access conflicts to shared storage areas, Java introduced a synchronization mechanism. Now let us examine the access of shared resources by multiple threads. Obviously, the synchronization mechanism is no longer enough, because the resources required at any time may not be ready. In order to be accessed, in turn, there may be more than one resource ready at the same time. To solve the access control problem in this case, Java introduced support for blocking mechanism.
Blocking refers to pausing the execution of a thread to wait for a certain condition to occur (such as a resource being ready). Students who have studied operating systems must be familiar with it. Java provides a large number of methods to support blocking. Let's analyze them one by one.
1. sleep() method: sleep() allows you to specify a period of time in milliseconds as a parameter. It causes the thread to enter the blocking state within the specified time and cannot get CPU time. Once the specified time passes, the thread re-enters the executable state. state.
Typically, sleep() is used when waiting for a resource to be ready: after the test finds that the condition is not met, let the thread block for a period of time and then retest until the condition is met.
2. suspend() and resume() methods (easy to cause deadlock, outdated): The two methods are used together. suspend() causes the thread to enter a blocking state and will not automatically recover. Its corresponding resume() must be called. , can the thread re-enter the executable state. Typically, suspend() and resume() are used when waiting for a result produced by another thread: after the test finds that the result has not been produced, the thread is blocked, and after another thread produces the result, call resume() to resume it. .
3. yield() method: yield() causes the thread to give up the currently allocated CPU time, but does not cause the thread to block, that is, the thread is still in an executable state and may be allocated CPU time again at any time. The effect of calling yield() is equivalent to the scheduler deeming that the thread has executed enough time to move to another thread.
4. wait() and notify() methods: The two methods are used together. wait() causes the thread to enter a blocking state. It has two forms. One allows specifying a period of time in milliseconds as a parameter, and the other does not. Parameters, the former will re-enter the executable state when the corresponding notify() is called or the specified time is exceeded, and the latter must be called when the corresponding notify() is called.
At first glance, they appear to be no different from the suspend() and resume() method pairs, but in fact they are completely different. The core difference is that all the methods described above will not release the occupied lock (if it is occupied) when blocking, while this opposite rule is the opposite.
The above core differences lead to a series of detailed differences.
First of all, all the methods described above belong to the Thread class, but this pair belongs directly to the Object class, that is, all objects have this pair of methods. This may seem incredible at first, but in fact it is very natural, because when this pair of methods blocks, the occupied lock must be released, and the lock is possessed by any object. Calling the wait() method of any object causes the thread to block. and the lock on the object is released. Calling the notify() method of any object will cause a randomly selected thread blocked by calling the wait() method of the object to be unblocked (but it will not be executed until the lock is obtained).
Secondly, all the methods described above can be called at any location, but this pair of methods (wait() and notify()) must be called in a synchronized method or block. The reason is also very simple, only in a synchronized method or block Only the current thread occupies the lock and the lock can be released. In the same way, the lock on the object that calls this pair of methods must be owned by the current thread, so that the lock can be released. Therefore, the pair of method calls must be placed in a synchronized method or block whose locked object is the object that calls the pair of methods. If this condition is not met, although the program can still compile, an IllegalMonitorStateException exception will occur at runtime.
The above characteristics of the wait() and notify() methods determine that they are often used together with synchronized methods or blocks. Comparing them with the inter-process communication mechanism of the operating system will reveal their similarities: synchronized methods or blocks provide Similar to the functions of operating system primitives, their execution will not be interfered by the multi-threading mechanism, and this counterpart is equivalent to the block and wakeup primitives (this pair of methods are both declared synchronized). Their combination allows us to implement a series of exquisite inter-process communication algorithms on the operating system (such as semaphore algorithms), and can be used to solve various complex inter-thread communication problems.
Two final points about the wait() and notify() methods:
First: The unblocked thread caused by calling the notify() method is randomly selected from the threads blocked by calling the wait() method of the object. We cannot predict which thread will be selected, so be particularly careful when programming. , to avoid problems arising from this uncertainty.
Second: In addition to notify(), there is also a method notifyAll() that can also play a similar role. The only difference is that calling the notifyAll() method will remove all threads blocked by calling the wait() method of the object at once. All unblocked. Of course, only the thread that acquires the lock can enter the executable state.
When talking about blocking, we have to talk about deadlock. A brief analysis can reveal that both the suspend() method and the call of the wait() method without specifying a timeout period may cause deadlock. Unfortunately, Java does not support deadlock avoidance at the language level, and we must be careful to avoid deadlocks in programming.
Above we have analyzed the various methods of thread blocking in Java. We focused on the wait() and notify() methods because they are the most powerful and the most flexible to use, but this also leads to their The efficiency is lower and it is more error-prone. In actual use, we should use various methods flexibly to better achieve our goals.
daemon thread
Daemon threads are a special type of thread. The difference between them and ordinary threads is that they are not the core part of the application. When all non-daemon threads of an application terminate, even if there are still daemon threads running, the application will Terminate, on the other hand, the application will not terminate as long as there is a non-daemon thread running. Daemon threads are generally used to provide services to other threads in the background.
You can determine whether a thread is a daemon thread by calling the method isDaemon(), or you can call the method setDaemon() to set a thread as a daemon thread.
thread group
Thread group is a concept unique to Java. In Java, thread group is an object of class ThreadGroup. Each thread belongs to a unique thread group. This thread group is specified when the thread is created and cannot be used during the entire life cycle of the thread. Change. You can specify the thread group to which the thread belongs by calling the Thread class constructor containing a ThreadGroup type parameter. If not specified, the thread defaults to the system thread group named system.
In Java, all thread groups must be created explicitly except the pre-built system thread groups. In Java, each thread group except the system thread group belongs to another thread group. You can specify the thread group to which it belongs when creating a thread group. If not specified, it belongs to the system thread group by default. . In this way, all thread groups form a tree with the system thread group as the root.
Java allows us to operate on all threads in a thread group at the same time. For example, we can set the priority of all threads in it by calling the corresponding method of the thread group, and we can also start or block all threads in it.
Another important role of Java's thread group mechanism is thread safety. The thread group mechanism allows us to distinguish threads with different security characteristics through grouping, handle threads in different groups differently, and support the adoption of unequal security measures through the hierarchical structure of thread groups. Java's ThreadGroup class provides a large number of methods to facilitate us to operate each thread group in the thread group tree and each thread in the thread group.
Thread State At a given point in time, a thread can only be in one state.
NEW
Threads that have not yet been started are in this state.
RUNNABLE
Threads executing in the Java virtual machine are in this state.
BLOCKED
A thread that is blocked and waiting for a monitor lock is in this state.
WAITING
A thread that waits indefinitely for another thread to perform a specific operation is in this state.
The thread status of a waiting thread. A thread is in a waiting state because it called one of the following methods:
Object.wait without timeout value
Thread.join without timeout value
LockSupport.park
A thread in the wait state is waiting for another thread to perform a specific operation. For example, a thread that has called Object.wait() on an object is waiting for another thread to call Object.notify() or Object.notifyAll() on that object. The thread that has called Thread.join() is waiting for the specified thread to terminate.
TIMED_WAITING
A thread that is waiting for another thread to perform an operation that depends on the specified wait time is in this state.
The thread state of a waiting thread with a specified wait time. A thread is in a timed wait state by calling one of the following methods with a specified positive wait time:
Thread.sleep
Object.wait with timeout value
Thread.join with timeout value
LockSupport.parkNanos
LockSupport.parkUntil
TERMINATED
An exited thread is in this state.