📜 ⬆️ ⬇️

Java multithreading

Hello! In this article I will briefly tell you about the processes, threads, and the basics of multi-threaded programming in the Java language.
The most obvious area of ​​application for multithreading is programming interfaces. Multithreading is indispensable when it is necessary that the graphical interface continues to respond to user actions during the execution of some information processing. For example, a thread that is responsible for the interface may wait for the completion of another thread that downloads a file from the Internet, and at this time output some animation or update the progress bar. In addition, it can stop the download file if the “cancel” button was pressed.

Another popular and perhaps one of the most hardcore applications of multithreading is games. In games, various streams may be responsible for working with the network, animation, physics, etc.

Let's start. First about the processes.

Processes


A process is a collection of code and data that shares a common virtual address space. Most often, one program consists of one process, but there are exceptions (for example, the Chrome browser creates a separate process for each tab, which gives it some advantages, such as the independence of tabs from each other). Processes are isolated from each other, therefore direct access to the memory of another process is impossible (interaction between processes is carried out using special means).
')
For each process, the OS creates a so-called “virtual address space” to which the process has direct access. This space belongs to the process, contains only its data and is at its complete disposal. The operating system is responsible for how the virtual space of the process is projected onto the physical memory.

The scheme of this interaction is presented in the picture. The operating system operates on so-called memory pages, which are simply an area of ​​a certain fixed size. If the process becomes out of memory, the system allocates additional pages to it from the physical memory. Virtual memory pages can be projected onto physical memory in random order.



When the program is started, the operating system creates a process, loading the code and data of the program into its address space, and then starts the main thread of the created process.

Streams


One thread is one unit of code execution. Each thread sequentially executes instructions of the process to which it belongs, in parallel with other threads of this process.

It is necessary to separately discuss the phrase “in parallel with other streams”. It is known that there is one execution unit per processor core at a time. That is, a single-core processor can process commands only sequentially, one at a time (in the simplified case). However, the launch of several parallel threads is possible in systems with single-core processors. In this case, the system will periodically switch between threads, alternately letting it run one or the other. Such a scheme is called pseudo-parallelism. The system remembers the state (context) of each thread before switching to another thread, and restores it on returning to the execution of the thread. The context of the thread includes such parameters as the stack, the set of values ​​of the processor registers, the address of the command being executed, and so on ...

Simply put, with pseudo-parallel execution of threads, the processor is torn between executing multiple threads, performing part of each of these in turn.

Here's what it looks like:



The colored squares in the figure are processor instructions (green is the main thread's instructions, blue ones are side). The execution goes from left to right. After launching a sidestream, its instructions begin to run along with the instructions of the main thread. The number of instructions to be executed for each approach is not defined.

The fact that the instructions of parallel streams are executed interleaved, in some cases, can lead to data access conflicts. The following article will be devoted to the problems of thread interaction, but for now on how threads are started in Java ...

Running threads


Each process has at least one running thread. The thread that starts the program is called the main one. In Java, after creating a process, the execution of the main thread begins with the main () method. Then, as required, in other places specified by the programmer, and when the conditions set by him are fulfilled, other side threads are launched.

In Java, a stream is represented as a child object of the class Thread. This class encapsulates the standard thread handling mechanisms.

You can launch a new stream in two ways:

Method 1

Create an object of the class Thread, passing to it in the constructor something that implements the Runnable interface. This interface contains the run () method that will be executed in the new thread. The thread finishes execution when its run () method completes.

It looks like this:

class SomeThing //,   Runnable implements Runnable //(  run()) { public void run() //       { System.out.println("   !"); } } public class Program //   main() { static SomeThing mThing; //mThing -  ,   Runnable public static void main(String[] args) { mThing = new SomeThing(); Thread myThready = new Thread(mThing); //  "myThready" myThready.start(); //  System.out.println("  ..."); } } 


To heighten the shortening of the code, you can pass into the constructor of the Thread class an object of an unnamed inner class that implements the Runnable interface:

 public class Program //   main(). { public static void main(String[] args) { //  Thread myThready = new Thread(new Runnable() { public void run() //       { System.out.println("   !"); } }); myThready.start(); //  System.out.println("  ..."); } } 


Method 2

Create a descendant of the class Thread and override its run () method:

 class AffableThread extends Thread { @Override public void run() //       { System.out.println("   !"); } } public class Program { static AffableThread mSecondThread; public static void main(String[] args) { mSecondThread = new AffableThread(); //  mSecondThread.start(); //  System.out.println("  ..."); } } 


In the example above, another stream is created and started in the main () method. It is important to note that after calling the mSecondThread.start () method, the main thread continues its execution, not waiting for the thread generated by it to end. And those instructions that follow the start () method call will be executed in parallel with the mSecondThread flow instructions.

To demonstrate the parallel work of streams, let's consider a program in which two streams argue on the philosophical question “what was before, an egg or a chicken?”. The main stream is sure that the first was a chicken, which he will report every second. The second stream once a second will refute his opponent. Total dispute will last 5 seconds. The stream that is the last to speak its answer to this, without a doubt, burning philosophical question will win. The example uses tools that have not yet been mentioned (isAlive () sleep () and join ()). They are given comments, and in more detail they will be further disassembled.

 class EggVoice extends Thread { @Override public void run() { for(int i = 0; i < 5; i++) { try{ sleep(1000); //   1  }catch(InterruptedException e){} System.out.println("!"); } // «»  5  } } public class ChickenVoice //   main() { static EggVoice mAnotherOpinion; //  public static void main(String[] args) { mAnotherOpinion = new EggVoice(); //  System.out.println(" ..."); mAnotherOpinion.start(); //  for(int i = 0; i < 5; i++) { try{ Thread.sleep(1000); //   1  }catch(InterruptedException e){} System.out.println("!"); } // «»  5  if(mAnotherOpinion.isAlive()) //       { try{ mAnotherOpinion.join(); //    . }catch(InterruptedException e){} System.out.println("  !"); } else //     { System.out.println("  !"); } System.out.println(" !"); } } :  ... ! ! ! ! ! ! ! ! ! !   !  ! 


In the above example, two streams in parallel within 5 seconds output information to the console. It is impossible to accurately predict which thread will finish speaking last. You can try, and you can even guess, but there is a high probability that the same program will have another “winner” at the next launch. This is due to the so-called "asynchronous code execution." Asynchrony means that it cannot be said that any instruction of one thread will be executed sooner or later than another instruction. Or, in other words, parallel threads are independent of each other, except when the programmer himself describes dependencies between threads using the means of the language provided for this.

Now a little about the completion of the processes ...

Process Completion and Demons


In Java, a process terminates when its last thread terminates. Even if the main () method has already completed, but the threads generated by it are still running, the system will wait for them to complete.

However, this rule does not apply to a special type of thread - demons. If the last normal process thread has ended, and only the daemons have remained, they will be forcibly terminated and the process will end. Most often, daemon threads are used to perform background tasks that serve the process throughout its life.

It is quite simple to declare a thread as a daemon - you need to call its setDaemon(true) method before starting the stream;
You can check whether a thread is a daemon by calling its boolean isDaemon() method;

Termination of threads


In Java, there are (existed) means to force a thread to terminate. In particular, the Thread.stop () method terminates the thread immediately after its execution. However, this method, as well as Thread.suspend (), which suspends the thread, and Thread.resume (), which continues to execute the thread, have been declared obsolete and their use is now highly undesirable. The fact is that the flow can be “killed” during the execution of the operation, the break of which in half a word will leave some object in the wrong state, which will lead to the appearance of a hard-to-catch and randomly occurring error.

Instead of the forced termination of a stream, a scheme is used in which each thread is responsible for its termination. A thread can stop either when it finishes the execution of the run () method, (main () for the main thread) or by a signal from another thread. And how to react to such a signal is, again, the flow itself. After receiving it, the thread can perform some operations and complete the execution, or it can completely ignore it and continue execution. Description of the response to the signal to terminate the stream lies on the programmer’s shoulders.

Java has a built-in stream notification mechanism called Interruption, and we’ll soon consider it, but first look at the following program:

Incremenator - a stream that every second adds or subtracts one from the value of the static variable Program.mValue. The Incremenator contains two closed fields, mIsIncrement and mFinish. What action is performed is determined by the mIsIncrement boolean variable - if it is true, then one is added, otherwise - subtraction. And thread termination occurs when the mFinish value becomes true.

 class Incremenator extends Thread { //   volatile -   private volatile boolean mIsIncrement = true; private volatile boolean mFinish = false; public void changeAction() //    { mIsIncrement = !mIsIncrement; } public void finish() //   { mFinish = true; } @Override public void run() { do { if(!mFinish) //    { if(mIsIncrement) Program.mValue++; // else Program.mValue--; // //    System.out.print(Program.mValue + " "); } else return; //  try{ Thread.sleep(1000); //   1 . }catch(InterruptedException e){} } while(true); } } public class Program { //,    public static int mValue = 0; static Incremenator mInc; //   public static void main(String[] args) { mInc = new Incremenator(); //  System.out.print(" = "); mInc.start(); //  //    //   i*2  for(int i = 1; i <= 3; i++) { try{ Thread.sleep(i*2*1000); //   i*2 . }catch(InterruptedException e){} mInc.changeAction(); //  } mInc.finish(); //    } } :  = 1 2 1 0 -1 -2 -1 0 1 2 3 4 


You can interact with the stream using the changeAction () method (for changing from subtraction to addition and vice versa) and using the finish () method (to end the stream).

In the declaration of the variables mIsIncrement and mFinish, the keyword volatile (changeable, not permanent) was used. It must be used for variables that are used by different threads. This is due to the fact that the value of a variable declared without volatile may be cached separately for each thread, and the value from this cache may differ for each of them. Declaring a variable with the volatile keyword disables such caching for it and all requests to the variable will be sent directly to memory.

This example shows how to organize the interaction between threads. However, there is one problem with this approach to terminating the stream — the Incremenator checks the value of the mFinish field once a second, so it can take up to a second of time between the finish () method and the actual completion of the stream. It would be great if, when receiving a signal from the outside, the sleep () method returned execution and the thread immediately started its completion. To perform such a scenario, there is a built-in flow alert tool called Interruption.

Interruption


The Thread class contains a hidden Boolean field, similar to the mFinish field in the Incremenator program, which is called the interrupt flag. You can set this flag by calling the thread's interrupt () method. There are two ways to check if this flag is set. The first way is to call the bool isInterrupted () method of the thread object, the second is to call the static method bool Thread.interrupted (). The first method returns the state of the interrupt flag and leaves this flag intact. The second method returns the state of the flag and resets it. Notice that Thread.interrupted () is a static method of the Thread class, and calling it returns the value of the interrupt flag of the thread from which it was called. Therefore, this method is called only from within the thread and allows the thread to check its interrupt state.

So back to our program. The interrupt mechanism will allow us to solve the problem of falling asleep flow. Methods that suspend the execution of a thread, such as sleep (), wait () and join (), have one feature - if the interrupt () method of this thread is called during their execution, they will wait for the end of the wait time to throw an InterruptedException exception.

Alter the program Incremenator - now instead of terminating the stream using the finish () method, we will use the standard interrupt () method. And instead of checking the mFinish flag, we will call the bool Thread.interrupted () method;
This is what the Incremenator class will look like after adding interrupt support:

 class Incremenator extends Thread { private volatile boolean mIsIncrement = true; public void changeAction() //    { mIsIncrement = !mIsIncrement; } @Override public void run() { do { if(!Thread.interrupted()) //  { if(mIsIncrement) Program.mValue++; // else Program.mValue--; // //    System.out.print(Program.mValue + " "); } else return; //  try{ Thread.sleep(1000); //   1 . }catch(InterruptedException e){ return; //    } } while(true); } } class Program { //,    public static int mValue = 0; static Incremenator mInc; //   public static void main(String[] args) { mInc = new Incremenator(); //  System.out.print(" = "); mInc.start(); //  //    //   i*2  for(int i = 1; i <= 3; i++) { try{ Thread.sleep(i*2*1000); //   i*2 . }catch(InterruptedException e){} mInc.changeAction(); //  } mInc.interrupt(); //   } } :  = 1 2 1 0 -1 -2 -1 0 1 2 3 4 


As you can see, we got rid of the finish () method and implemented the same thread termination mechanism using the built-in interrupt system. In this implementation, we have one advantage - the sleep () method will return control (generate an exception) immediately after the thread is interrupted.

Notice that the sleep () and join () methods are wrapped in try-catch constructs. This is a necessary condition for the operation of these methods. The code that calls them must catch the InterruptedException exception, which they throw when interrupted while waiting.

With the start and end of the threads figured out, then I will talk about the methods used when working with threads.

Thread.sleep () method


Thread.sleep () is a static method of class Thread, which suspends the execution of the thread in which it was called. During the execution of the sleep () method, the system stops allocating CPU time to the thread, distributing it among other threads. The sleep () method can be executed either for a specified amount of time (milliseconds or nanoseconds) or until it is stopped by an interrupt (in this case, it throws an InterruptedException exception).

 Thread.sleep(1500); //   Thread.sleep(2000, 100); // 2   100  


Although the sleep () method can take a nanosecond waiting time, you should not take it seriously. In many systems, the waiting time is still rounded to milliseconds or even to their tens.

Yield () method


The static method Thread.yield () forces the processor to switch to processing other system threads. The method can be useful, for example, when the flow is waiting for the occurrence of an event and it is necessary that the test of its occurrence occurs as often as possible. In this case, you can put the event check and the Thread.yield () method into a loop:

 //   while(!msgQueue.hasMessages()) //     { Thread.yield(); //    } 


Join () method


Java has a mechanism that allows one thread to wait for the completion of another. For this, the join () method is used. For example, in order for the main thread to wait for the completion of the side stream myThready, you need to execute the instruction myThready.join () in the main thread. As soon as the stream myThready is completed, the join () method returns control and the main thread can continue execution.

The join () method has an overloaded version that takes a wait time as a parameter. In this case, join () returns control either when the expected stream ends, or when the wait time ends. Like the Thread.sleep () method, the join method can wait for milliseconds and nanoseconds - the arguments are the same.

Using the task to wait for a stream, you can, for example, update an animated picture while the main (or any other) stream is waiting for the completion of a sidestream that performs resource-intensive operations:

 Thinker brain = new Thinker(); //Thinker -   Thread. brain.start(); // "". do { mThinkIndicator.refresh(); //mThinkIndicator -  . try{ brain.join(250); //    . }catch(InterruptedException e){} } while(brain.isAlive()); // brain ... //brain   ( ). 


In this example, the brain stream thinks about something, and it is assumed that it takes him a long time. The main stream waits for it a quarter of a second and, in the event that there is not enough time for meditation, it updates the “reflection indicator” (some animated picture). As a result, while thinking, the user observes on the screen an indicator of the thinking process, which lets him know that the electronic brains are busy with something.

Thread priorities


Each thread in the system has its own priority. The priority is a number in the thread object, a higher value of which means a higher priority. The system primarily performs threads with a higher priority, and threads with a lower priority receive CPU time only when their more privileged brethren are idle.

You can work with the priorities of the stream using two functions:

void setPriority(int priority) - sets the priority of the stream.
Possible priority values ​​are MIN_PRIORITY, NORM_PRIORITY, and MAX_PRIORITY.

int getPriority() - gets the priority of the stream.

Some useful methods of the class Thread


This is almost everything. Finally, here are some useful methods for working with threads.

boolean isAlive() - returns true if myThready () is executed and false if the stream has not been started yet or has been terminated.

setName(String threadName) - Sets the name of the thread.
String getName() - Get the name of the stream.
The name of the thread is its associated string, which in some cases helps to understand which thread performs some action. Sometimes it is useful.

static Thread Thread.currentThread() is a static method that returns the thread object in which it was called.

long getId() - returns the stream identifier. The identifier is a unique number assigned to the stream.

Conclusion


I note that the article does not talk about all the nuances of multi-threaded programming. And the code given in the examples, for complete correctness lacks some of the nuances. In particular, synchronization is not used in the examples. Synchronization of threads is a topic, without studying which, programming the correct multithreaded applications will not work. You can read about it, for example, in the book “Java Concurrency in Practice” or here (all in English).

The article examined the main means of working with threads in Java. If this article turns out to be useful, then in the next one I will talk about the problems of sharing flows to resources and methods for solving them.

All the best.

Source: https://habr.com/ru/post/164487/


All Articles