The basic concept of multithreading

By default, a Java program starts with only one Main Thread. What if we were to use the main thread to do something at the same time? For example, in a window, draw two rows of circles at the same time, one at a height of 10 pixels and one at a height of 50 pixels. If you can only write simultaneous programs in one main thread, you can only draw a circle at height 10, then quickly draw a circle at height 50, pan and draw a circle at height 10,…. And so on:

Suppose, let’s say, if we have two threads, one drawing his circle at height 10, and the other drawing his circle at height 50, without interfering with each other. Then the program is easy to write, we just need to consider how a thread on its own height to draw a circle. This is the simple introduction of multithreading in Java.

To create a new thread in Java, you inherit the Runnable interface, which has only one run method to implement. The run method is the entry point of an executable thread, and the contents of the run method are the contents of the execution of an executable thread.

public class CirclePainter implements Runnable {
    public CirclePainter(int x, int y, int r, int offset) {
        ....
    } 

    public void run() {
        while(...). {.. Draw a circle of radius r at (x, y)... Shift offset}}}Copy the code

** The JVM itself is a virtual system. The.class class is the program that the JVM can execute. It is generally assumed that the JVM has only one CPU to execute the executable. **

** If you want to use more cpus on a virtual system, you can create an execution program, write the execution code for each execution program, and then start the execution thread

Thread painterThread1 = new Thread(new CiclePainter(50, 10, 10));
Thread painterThread2 = new Thread(new CiclePainter(50, 50, 10));
painterThread1.start();
painterThread2.start();
Copy the code

New multi-threaded Thread, the entry to the execution is run method, once the execution of the run method, the Thread will be recycled, if the execution of repeated calls will occur errors.

The thread.start () method executes the code in the run method, which is defined in Thread’s run() method. Thread also inherits the Runnable interface:

public class Thread implements Runnable { ... private Runnable target; . public voidrun() {
        if(target ! = null) { target.run(); }}... }Copy the code

Obviously, we could inherit Thread and implement its run method to create a new Thread. However, it is important to note that once we inherit Thread, this must be a Thread, so we cannot inherit from other classes. So generally we inherit the Runnable interface.

Thread synchronization

When performing multithreading, if two or more threads are operating on the same shared code or data, you need to pay attention. This can cause thread synchronization problems, resulting in unpredictable program results. This problem is caused by “Race condition”.

For a simple example, if we develop a simple stack class:

public class Stack {
    private int[] data;
    private int index;
    public Stack(int capacity) {
        data = new int[capacity];
    }
    public void put(int d) {
        data[index] = d;
        index++;
    }
    public int pop() {
        index--;
        returndata[index]; }}Copy the code

This program works fine when executed with a single thread, but race conditions can occur when executed with multiple threads. Suppose that in the case of multiple threads, when a thread’s run reaches a put method,

public class Some implements Runnable { private Stack stack; . public voidrun() {... stack.put(d); . }}Copy the code

Given that index is 2, after the first line of put, index++ should follow, but suppose another thread is performing pop() at this point:

public class Other implements Runnable { private Stack stack; . public voidrun() {... int p = stack.pop(); . }}Copy the code

So index– becomes 1, and let’s say, after that, we switch back to put, index++, and index becomes 2. But in fact, the put method should have been 3 after execution, so at this time, there is a program error due to race conditions, resulting in unpredictable running results.

In fact, we can imagine that the index and fetch operations of the PUT and POP methods should not be separable and need to be performed in one go. But with multithreading, it’s possible to break that order

So naturally thought, if you want to solve this problem, you need to this a few steps can’t split operation in an area must be one-time execution of the code, this is the concept of thread synchronization, thread synchronization area, all code must be performed in a single, when in its execution, there will be no other thread insert in.

The synchronized keyword allows you to specify the range of code that needs to be synchronized. Basically, declare synchronized before a method so that the method’s code is in the synchronized region.

public class Stack {
    private int[] data;
    private int index;
    public Stack(int capacity) {
        data = new int[capacity];
    }
    public synchronized void put(int d) {
        data[index] = d;
        index++;
    }
    public synchronized int pop() {
        index--;
        returndata[index]; }}Copy the code

In fact, each object contains a lock object, also known as a lock, and the executing thread must acquire a unique lock on that object to enter a synchronized region. Suppose one thread is synchronized, and another thread wants to enter the execution area because the lock has been removed, so it can only wait for the other thread to finish executing the code and release the code, thus achieving thread synchronization. In the above example, it is obvious that the stack lock must be obtained before the put method can be executed.

So in this example, if you’re doing put, you can’t do POP, and if you’re doing POP, you can’t do PUT. It won’t cause the same error.

Furthermore, there is no need to declare the whole method synchronized if we know exactly what blocks are shared, as in the previous example, where we know exactly what blocks are shared:

public void put(int d) { ... synchronized(this) { data[index] = d; index++; }... } public intpop() {... synchronized(this) { index--;returndata[index]; }... }Copy the code

Further, synchronized statements can provide finer control, providing locking for different objects as shown in the following example:

public class Material {
    private int data1 = 0;
    private int data2 = 0;
    private Object lock1 = new Object();
    private Object lock2 = new Object();

    public void doSome() {... synchronized(lock1) { ... data1++; . }... } public voiddoOther() {... synchronized(lock2) { ... data2--; . }... }}Copy the code

In this example, multiple doSome methods cannot execute simultaneously because lock1 is locked, and doOther methods cannot execute simultaneously because lock2 is locked, but doSome and doOther execute simultaneously without interference because they have different locks and do not affect each other.

Wait, notify, notifyAll

Wait, notify, and notifyAll are methods provided by object. They are automatically inherited when defining their own classes. In object, wait, notify, and notifyAll are defined as final, so we cannot modify or redefine them. The purpose of these three methods is to notify the competing object of the lock, or to release the lock of the object.

When an executor enters a synchronized region, the lock is acquired. During synchronized code, the lock is released by using the object’s wait method, and the executor is placed in the object’s wait set. At this point, other threads can compete for the locked target and enter the synchronized area to execute code.

Programs placed in a wait set do not participate in typesetting. Instead, they wait for notify or interrupt to be called before scheduling, and wait for the specified time to wait.

When notifyAll is called, a random thread is removed from the wait set of the object for scheduling, restoring the runnable state. When notifyAll is executed, all threads are removed from the wait set for scheduling.

As a simple example, these methods are like asking someone to do something. If you don’t want them to do something for a while, you tell them to wait and call notify when it is their turn to do something.

The best example of these approaches is the producer-consumer model. The producer will produce the goods to the clerk, and the consumer will take the goods from the clerk. The clerk can only hold a certain number of goods, and when the quantity exceeds the limit, the producer will ask the customer to wait for the production. If there are no goods, the consumer will ask the producer to wait.

Let’s look at the program code in detail:

package Thread;

public class Producer implements Runnable {
	
	private Clerk clerk; 
    
    public Producer(Clerk clerk) { 
        this.clerk = clerk; 
    } 
    
    public void run() { 
        System.out.println(
                "Producers start producing integers......"); // Produce integers from 1 to 10for(int product = 1; product <= 10; Thread.sleep((int) (math.random () * 3000)); thread.sleep ((int) (math.random () * 3000)); } catch(InterruptedException e) { e.printStackTrace(); } // Give the product to the clerk. } } public static void main(String[] args) { Clerk clerk = new Clerk(); Thread producerThread = new Thread(new Producer(clerk)); Thread consumerThread = new Thread(new Consumer(clerk)); producerThread.start(); consumerThread.start(); }}Copy the code

consumers

package Thread;

public class Consumer implements Runnable {
private Clerk clerk; 
    
    public Consumer(Clerk clerk) { 
        this.clerk = clerk; 
    } 
    
    public void run() { 
        System.out.println(
                "Consumers start to consume round numbers......"); // Consume 10 integersfor(int i = 1; i <= 10; Thread.sleep((int) (math.random () * 3000)); thread.sleep ((int) (math.random () * 3000)); } catch(InterruptedException e) { e.printStackTrace(); } // get the integer clerk.getProduct(); }}}Copy the code

The shop assistant

package Thread; Public class Clerk {private int product = -1; private int product = -1; // This method is called by the producer to public synchronized voidsetProduct(int product) { 
        while(this.product ! = -1) {try {// There is no room to collect the product at present, please wait!wait(a); } catch(InterruptedException e) { e.printStackTrace(); } } this.product = product; System.out.printf("Producer setting (%d)%n", this.product); // a consumer in the notification waiting area can continue to work notify(); } // This method is called by the consumer to public synchronized intgetProduct() { 
        while(this.product == -1) {try {// The product is out of stock, please wait!wait(a); } catch(InterruptedException e) { e.printStackTrace(); } } int p = this.product; System.out.printf("Consumer takes (%d)%n", this.product); this.product = -1; // a producer in the notification wait area can continue to work notify();returnp; }}Copy the code

Results of program execution:

The life cycle of a thread

When you instantiate a thread object, you must call it with the start method. The start method can only be executed once. If the thread method is executed repeatedly, an exception will be raised. Wait for CPU allocation to execute.

The executor has a priority. You can set the priority by specifying setPriority.

Once the executing program is executed, it enters the dead state. You can use the isAlive method to check whether the program isAlive. If you call the start method again after the program is dead, an exception will be thrown.

When an execution program is blocked due to IO waiting or thread.sleep, the execution program enters the blocked state. When the blocking condition disappears, the execution program enters the runnable state and waits for CPU scheduling

When the program enters the synchronized area, it must enter the lock pool for lock competition before entering the executable state and entering the CPU scheduling.

However, after the wait method is invoked, the locked object is released and enters the wait pool. The notify or notifyAll methods are used to enter the lock pool to compete with each other. After the lock is obtained, the object enters the executable state to obtain the CPU scheduling.