1 introduction

Prior to JDK5, Java multithreading and its performance had always been a weakness, with only limited methods such as synchronized, thread.sleep (), object.wait /notify, while synchronized was particularly inefficient and expensive.

After JDK5, there are significant improvements over previous versions, not only in Java syntax, including: The introduction of concurrent programming master Doug Lea’s java.util. Concurrent package (J.U.C) and support for modern CPU CAS primitives has greatly improved performance. There are also more choices in degree of freedom, and the efficiency of J.U.C is far better than that of synchronized in high concurrency environment.

In JDK6 (Mustang Mustang), a number of significant improvements have been made to the inherent mechanics of synchronized, including CAS, biased locking, and lightweight locking, making synchronized almost as efficient as J.U.C. Therefore, in the current JDK7 era, synchronized has become the first choice in general situations. In some special scenarios, such as interruptable lock, conditional lock, and stop if failure occurs after waiting for a certain period of time, J.U.C is applicable. Therefore, for multithreading research, it is necessary to understand its principles and their applicable scenarios.

2 Basic Concepts

2.1 the thread

Threads are attached to a process, which is the smallest unit of resource allocation. A process can generate multiple threads, which have shared process resources. There are few unique resources per thread, such as a thread control block that controls the thread’s execution, stack space that holds local variables and a few parameters, and so on. Threads have three states: ready, blocked, and running, and can switch between them. Because multiple threads share process resources, thread conflicts and inconsistencies occur when they operate on the same shared variable/object.

In a multi-threaded concurrent environment, these two problems are essentially solved:




In a nutshell: how threads communicate correctly with each other. While this is about how to ensure this at the Java level, it involves the Java virtual machine, the Java memory model, and the fact that high-level languages like Java are ultimately mapped to the CPU for execution (key reasons: Today’s cpus have caches and are multi-core), which, while obscure, is essential to a deep grasp of multithreading, so it takes a little more time.

2.2 the lock

When multiple threads of the same Shared variables/object, even the simplest operations, such as: i++, on processing actual also involves reading, since the increase and assignment of the three operation, that is to say, this time difference exists between, leading to multiple threads in such as programmers envisioned to sequentially, dislocation, leading to the final results do not agree with expectations.

Multithreaded synchronization in Java is embodied through the concept of locks. A lock is not an object, not a concrete thing, but a mechanism name. The locking mechanism must ensure the following two features:





2.3 Suspend, Hibernate, Block, and non-block

Suspend: When a thread is suspended, it loses CPU usage until it is woken up by another thread (user thread or scheduling thread).

Sleep: Also loses CPU usage time, but after the specified Sleep time, it will automatically activate, no need to wake up (the whole wake up appears to be automatic, but in fact, the daemon thread to wake up, but no programmer manual intervention).

Block: When a thread is executing and the required resources are not available, the thread is suspended until an actionable condition is met.

Non-blocking: When a thread is executing and the required resources are not available, instead of being suspended and waiting, the thread continues to perform other tasks until the condition is met and is notified (again by the daemon thread) to do the task.





Explicit suspension in Java was previously represented by Thread’s suspend method. This concept has since disappeared because the suspend/resume methods are now obsolete and prone to deadlocks. A deadlock occurs when the suspend thread holds an object lock that its resume thread needs to use.

So, in the current JDK version, hanging is the system behavior of the JVM, with no programmer intervention. Sleeping does not release the lock, but it must wake up after some time, so it does not deadlock. Now, when we talk about suspension, we usually don’t mean active suspension in the writer’s program, but rather controlled by the operating system’s thread scheduler.

So, we often say “thread lock on application after the failure will be suspended, and then wait for scheduling” that has a certain ambiguity, because the “hang up” is the operating system level hangs, is actually when applying for resource failure block, and Java thread hangs in the (may have to get the lock, or may not have locks, word has nothing to do with the lock) is not a concept, It’s easy to confuse, so when I say suspend later in this article, I generally refer to the operation of the operating system, not suspend() as in Thread.

Accordingly, it is necessary to mention wait/notify for java.lang.object. These methods are also wait/notify, but they assume that the lock has been acquired and will be released during the wait. To call a wait method, a thread must acquire the lock on the object. After calling the wait method, the current thread releases the lock on the object and sleeps. In any case, the JVM will release the lock from the current thread before sleeping/suspending), and only wake up in the following cases: Another thread calls notify or notifyAll of the object, the current thread is interrupted, and the specified time is up when wait is called.

2.4 Kernel mode and user mode

These are two operating system concepts, but understanding them helps us understand Java’s threading mechanism.

Some system-level calls, such as: remove clock, creating processes such as instruction of these systems, if the underlying system level instruction can be arbitrary application access, the consequences are dangerous, the system may collapse at any time, so will be performed by the CPU instruction set to multiple privilege levels, when hardware execute each instruction will check the privilege of instruction, such as: Intel x86 cpus have four privilege levels from 0 to 3. Level 0 has the highest privilege and level 3 has the lowest privilege.

According to the security of this system call, operating systems are divided into two types: kernel-mode and user-mode. Kernel-mode commands have privileges of 0, user-mode commands have privileges of 3.





Once you understand the concepts of kernel-mode and user-mode, what is the efficiency impact of switching between the two states?





So what do these two forms of operating systems have to do with our topic of threading? Here’s the key. Java does not have its own threading model, but uses the operating system’s native threads!

If you implement your own threading model, some problems are particularly complex and difficult to solve, such as: How to handle blocking, how to properly allocate threads between multiple cpus, how to lock, including creating and destroying threads, all need to be done by Java itself. Prior to JDK1.2, Java used its own implementation of the threading model, then abandoned it in order to use the operating system’s threading model. Therefore create, destroy, scheduling, congestion, etc to the operating system to do these things, and matters of the threads in the operating system belongs to the call of the system level, it needs to be done in the kernel mode, so if frequently executing thread hangs, scheduling, and will often cause between kernel mode and user mode switch, affect efficiency (of course, The operating system’s threading operations are not directly accessible to the outside world (including the Java virtual machine), but open to the outside world an interface called “lightweight processes,” which have a one-to-one relationship with kernel threads on Windows and Linux, not described here).





3 Thread Advantage

Despite the challenges, multithreading has some advantages that keep it in use. These advantages are:





3.1 Better resource utilization

The CPU can do something else while waiting for IO. This doesn’t have to be disk IO. It can also be IO for the network, or user input. In general, network and disk IO is much slower than CPU and memory IO.

3.2 Simpler programming

In a single-threaded application, if you want to write a program that manually handles the order in which multiple IO are read and processed, you must record the state of each file read and processed. Instead, you can start two threads, each handling a file read and processing operation. The thread is blocked while waiting for disk to read the file. While waiting, other threads can use the CPU to process the file that has been read. As a result, the disk is always busy reading different files into memory. This leads to an increase in disk and CPU utilization. And because only one file needs to be logged per thread, this approach is also easy to implement programmatically.

3.3 Program response faster

Another common goal of turning a single-threaded application into a multi-threaded application is to achieve a more responsive application. Imagine a server application that listens for incoming requests on a port. When a request comes in, it processes the request and then goes back to listen.

If a request takes a lot of time to process, the new client cannot send the request to the server during this time. Requests can only be received if the server is listening.

In another design, the listener thread passes the request to the worker thread pool and then immediately returns to listen. The worker thread can process the request and send a reply to the client.

4 Thread Cost

Using multithreading often results in higher throughput and shorter response times, but using multithreading does not necessarily run faster than single-threaded programs, depending on the capabilities of our programmers and application scenarios. Don’t multithread for the sake of multithreading, but to consider the specific application scenarios and development strength, using multithreading is to get faster processing speed and use idle processing power, if it does not bring any benefits also brought complexity and some time bombs, that is not good? Use multithreading only if the benefits to us far outweigh the costs. Sometimes the performance gains from multithreading may not be worth the overhead, and a program that is not well designed for concurrency may be even slower than using a single thread.

4.1 More complex design

Multi-threaded programs when access to a Shared variable data, we need to carefully handle, can appear otherwise difficult to find bugs, generally, a multithreaded application is often more complex than single thread programming will (though some single thread handler may be more complex than multi-threaded programs), and the error is difficult to reproduce (because of thread scheduling disorder, Some bugs depend on the execution timing of a particular thread.

4.2 Context switch Cost

When the CPU switches from executing a thread to executing another thread, it needs to store the local data of the current thread, program Pointers, etc., and then load the local data of another thread, program Pointers, etc., and finally start executing. This switch is called a “context switch.” The CPU executes a thread in one context and then switches to another context to execute another thread.





4.3 Increasing resource consumption

Threads need some resources from the computer to run. In addition to the CPU, the thread needs some memory to maintain its local stack. It also requires some resources from the operating system to manage threads. We could try writing a program that creates 100 threads that do nothing but wait and see how much memory the program uses as it runs.

5 Creating and Running

There are two ways to write code that a Thread executes at runtime: you can create an instance of a Thread subclass and override the run method, or you can create classes that implement the Runnable interface.

5.1 Subclassing Thread

Create an instance of the Thread subclass and override the run method, which is executed after calling the start() method. Examples are as follows:





You can create and run the Thread subclass as follows:





The start method returns as soon as the thread is started, rather than waiting for the run method to finish executing. It is as if the run method were executed on a different CPU. When the run method is executed, the string MyThread RUNNING is printed.

5.2 Implementing the Runnable Interface

A second way to write thread execution code is to create a new instance of a class that implements the java.lang.Runnable interface, with methods that can be called by threads. Here’s an example:





To enable the Thread to execute the run() method, you need to pass in an instance object of MyRunnable in the constructor of the Thread class. The following is an example:





When a thread runs, it calls the Run method that implements the Runnable interface. In the example above, “MyRunnable Running” will be printed.

5.3 Subclass or Implement the Runnable Interface?

There is no definitive answer as to which of the two approaches is better, and they both fit the bill. Personally, I prefer to implement the Runnable interface. Because thread pools effectively manage threads that implement the Runnable interface, if the pool is full, new threads queue up for execution until the pool is free. If threads are implemented by implementing Thread subclasses, this is a bit more complicated.

Sometimes we want to implement both the Runnable interface and the Thread subclass together. For example, an instance that implements a Thread subclass can execute multiple threads that implement the Runnable interface. A typical application is thread pools.

5.4 Common error: Call run() instead of start()

A common mistake to make when creating and running a thread is to call the thread’s run() method instead of its start() method, as shown below:





At first you won’t notice anything wrong, because the run() method is called exactly as you expect. In fact, however, the run() method is not executed by the newly created thread, but by the current thread that created the new thread. That is, executed by the thread executing the above two lines of code. To cause the run() method to be executed by the new thread you create, you must call the start() method of the new thread.

5.5 the thread name

When creating a thread, you can give the thread a name. It helps us distinguish between different threads. For example, if more than one thread writes to system.out, we can easily identify which thread is writing out by thread name. Examples are as follows:





Note that the MyRunnable class does not have a getName() method because MyRunnable is not a subclass of Thread. You can get a reference to the current thread by:





Therefore, we can get the name of the current thread with the following code:





First print the name of the thread that executes the main() method. This thread is allocated by the JVM. Then start 10 threads named 1 to 10. Each thread outputs its own name and exits.




Note that although the order in which threads are started is orderly, the order in which they are executed is not. That is, thread 1 is not necessarily the first thread to print its name to the console. This is because threads execute in parallel rather than sequentially. The JVM, along with the operating system, determines the order in which threads are executed, and the order in which they are started is not always the same.

5.6 Relationship between Main Threads and child threads

The Main thread is a non-daemon thread and cannot be set as a daemon thread

This is because the Main thread is created by the Java virtual machine at startup time. When the main method starts executing, the main thread is already created and running. For a running Thread, the Thread. SetDaemon () throws an Exception Exception in the Thread “is the main” Java. Lang. IllegalThreadStateException.

2. The Main thread ends, and other threads can run normally

The main thread, which is just a normal non-daemon thread used to start applications, cannot be set as a daemon thread; Other than that, it’s no different than any other non-daemon thread. The main thread completes, and other threads can execute normally.

This is actually very reasonable, according to the operating system theory, process is the basic unit of resource allocation, thread is the basic unit of CPU scheduling. As far as the CPU is concerned, there is no Java main thread and child thread. The resources of the process are shared by the thread, and the thread can execute as long as the process exists. In other words, the thread is strongly dependent on the process. In other words:





3. The Main thread terminates, and other threads can terminate immediately if and only if the child threads are daemons

When a Java virtual machine (equivalent to a process) exits, all living threads in the virtual machine are daemons. The virtual machine does not exit as long as there are non-daemons alive. Instead, it waits for non-daemons to complete. Conversely, if all the threads in the virtual machine are daemons, the Java virtual machine exits regardless of whether those threads are alive or dead.

Let’s talk about concurrency and parallelism

The difference between concurrency and parallelism is that one processor handles multiple tasks at the same time versus multiple processors or multi-core processors handling multiple different tasks at the same time. The former is logically simultaneous while the latter is physically simultaneous.








The figure above shows an eight-operation task running on a two-core CPU creating four threads. Assuming that there are two threads per core, then the two threads on each CPU will run alternately, and the operations between the two cpus will run in parallel. As far as a single CPU is concerned, two threads can solve the problem of not being smooth caused by thread blocking. It does not improve the running efficiency by itself. It is the parallel operation of multiple cpus that really solves the running efficiency problem, which is the difference between concurrent and parallel.