Java multithreading: a new understanding of threads

Posted by coolbeansdude51 on Thu, 04 Jun 2020 15:32:41 +0200

preface

For Java multithreading, in fact, it has been some scattered learning. Multithreading is often asked in the interview. I really want to summarize this certification this time, and I really don't want to fall in this field. According to "Java high concurrent programming details" a book to learn and summarize.

Let's start with a simple example

@Slf4j
public class StepIn {

    public static void main(String[] args) {
    	//Start a thread
        new Thread(){
            @Override
            public void run() {
                enjoyMusic();
            }
        }.start();
        browseNews();
    }

    private static void browseNews(){
        while(true){
            log.info("Uh-hh,the good news.");
            sleep(1);
        }
    }

    private static void enjoyMusic(){
        while(true){
            log.info("Uh-hh,the nice music.");
            sleep(1);
        }
    }

    private static void sleep(int seconds){
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

After running, use JConsole to connect to the specified JVM process. The results are as follows

It is normal to see that there will be a main thread and Thread-0 thread started by itself, as well as other daemons (what is daemons will be discussed later). Each thread has its own local variable table, program counter, and life cycle.

Thread life cycle

Does the start method of Thread mean that the Thread has started executing? The life cycle of a Thread can be roughly divided into the following five main stages, as shown in the figure below

The life cycle of a thread can be roughly divided into five main stages: NEW, RUNNABLE, RUNNING, BLOCKED, TERMINATED.

NEW status

When a Thread object is created through new Thread(), no Thread start operation is performed. At this time, the Thread is not in the execution state. Because start is not called, the Thread can only be in the NEW state.

RUNNABLE state

When the thread object enters the RUNNABLE state, it must call the start method. At this time, a thread is actually created in the JVM. After the thread is started, it cannot be executed immediately. It must be selected by the CPU. If it is selected by the CPU, it will enter the Running state.

By the way, a thread cannot enter the TERMINATED state directly from the RUNNABLE state.

RUNNING status

Only when the CPU selects this thread can it really enter the RUNNING state, and then it can actually execute its own code logic. A thread in RUNNING state may send the following transitions

Enter the TERMINATED state directly from RUNNING, such as calling stop method (although not recommended)

Enter the BLOCKED state from the RUNNING state, such as calling the sleep or wait method, or performing a BLOCKED IO state to obtain a lock resource,

Enter the RUNNING state into the RUNNING state, for example, the scheduler polling of the CPU causes the thread to give up execution, or the CPU's execution right is given up due to the active call of the yield method.

BLOCKED status

From RUNNING to BLOCKED, we have already introduced it. The BLOCKED state can be switched to other states as follows:

1. Enter the TERMINATED state directly, such as calling the stop method

2. Directly enter the RUNNABLE state, and the operation of thread blocking ends, such as reading the desired byte, or completing the sleep for a specified time, or acquiring a lock resource, or being interrupted during the blocking process (such as other threads calling interrupt method).

TERMINATED status

This is the final state of a thread, and there will be no switching. Thread running error or JVM Crash will cause all threads to enter this state directly

Why there should be a Runnable interface

When it comes to creating threads in several ways, our first reaction is to inherit the Thread class and implement the Runnable interface. However, why there are two ways? Why not implement a Runnable interface directly?

In fact, there are two reasons: one is that if a class already inherits a class, it is difficult to inherit the Thread class, so there is a Runnable interface, but there is another reason. This reason needs to start from the following program, which we will discuss later.

Let's take a look at the source code in the start method

/**
 * Causes this thread to begin execution; the Java Virtual Machine
 * calls the <code>run</code> method of this thread.
 * <p>
 * The result is that two threads are running concurrently: the
 * current thread (which returns from the call to the
 * <code>start</code> method) and the other thread (which executes its
 * <code>run</code> method).
 * <p>
 * It is never legal to start a thread more than once.
 * In particular, a thread may not be restarted once it has completed
 * execution.
 *
 * @exception  IllegalThreadStateException  if the thread was already
 *               started.
 * @see        #run()
 * @see        #stop()
 */
public synchronized void start() {
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}

private native void start0();

It can be seen that the start source code is relatively simple, and the core is nothing more than calling a local method of start0(). But when we inherit Thread, we copy the run method instead of the start method? In fact, we can see this from the comments: Causes this thread to begin execution; the Java Virtual Machine calls the run method of this thread.

1. After the Thread is new, the threadStatus inside the Thread is 0, which only means that the Thread is initialized and not started

2. You cannot start a thread twice, otherwise the IllegalThreadStateException will appear.

3, group.add(this); after the thread starts, it will be added to a thread group.

4. The thread has ended (i.e. the thread has finished running) or started. If the threadStatus is not 0, it cannot be started again.

From the template method

An example of a simple template method

There is an abstract class

@Slf4j
public abstract class AbstractTemplate {

    //Specific implementation is handed over to subclass
    public abstract String sayHello(String name);

    public void printHelloMessage(String name){
        log.info("print hello message : {}",sayHello(name));
    }

}

Two implementation classes

public class TemplateOne extends AbstractTemplate {
    public String sayHello(String name) {
        return "Greetings in Chinese: Hello"+name;
    }
}
public class TemplateTwo extends AbstractTemplate {
    public String sayHello(String name) {
        return "English say Hello : "+name;
    }
}

Operation class

public class TemplateTest {
    public static void main(String[] args) {
        AbstractTemplate template = new TemplateOne();
        template.printHelloMessage("liman");

        AbstractTemplate templateTwo = new TemplateTwo();
        templateTwo.printHelloMessage("liman");
    }
}

Operation result:


In fact, this is relatively simple. It can't be that the real logic is handed over to the subclass for implementation. Then when the concrete method is called through the parent class, the implementation logic of the subclass will be automatically called. In fact, this makes full use of the characteristics of polymorphism.

Inherit Thread and implement Runnable interface

Now back to our previous question, what's the difference between the two? Is there anything else worth considering besides convenient inheritance? Let's start with the run method in the Thread class

@Override
public void run() {
    //If the Runnable interface is passed during Thread construction, the run method corresponding to the Runnable interface will be called
    if (target != null) {
        target.run();
    }
    //If you do not pass an instance that implements the Runnable interface, you need to copy the run method yourself
}

Take a look at one of the Thread constructors

public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

You can see that the Thread constructor here takes a Runnable parameter.

Before, there was a saying that there were several ways to create threads. Some people answered that there are two ways: one is to implement the Runnable interface, the other is to inherit the Thread class. At least for now, it seems to be a little superficial. In the previous start method, we can see that run is just a template method, and the local method start0 calls back the run method of the corresponding subclass. In fact, the two ways are the same. In essence, the only way to create a Thread is to construct a Thread class. There are two ways to create a Thread execution unit: one is to rewrite the run method of the Thread, the other is to implement the run method of the Runnable interface, and the second method uses the Runnab instance as the Thread construction parameter.

Inherit Thread

@Slf4j
public class TicketWindow extends Thread {

    private final String name;
    private static final int MAX = 500;
    private static int index = 1; //Decorate with static

    public TicketWindow(String name){
        this.name = name;
    }

    @Override
    public void run() {
        while(index<=MAX){
            log.info("Name of counter:{},Current number:{}",name,index++);
        }
    }

    public static void main(String[] args) {
        TicketWindow ticketWindow0 = new TicketWindow("No.1 calling machine");
        TicketWindow ticketWindow1 = new TicketWindow("Call number two");
        TicketWindow ticketWindow2 = new TicketWindow("Call three");
        TicketWindow ticketWindow3 = new TicketWindow("Call four");
        TicketWindow ticketWindow4 = new TicketWindow("Call five");
        ticketWindow0.start();
        ticketWindow1.start();
        ticketWindow2.start();
        ticketWindow3.start();
        ticketWindow4.start();
    }
}

In this case, there will be differences. If the index is decorated with static, the consumption of a number by multiple TicketWindow will be avoided to some extent. If the static decoration is removed, multiple TicketWindow will print the same index value.

Implement the runnable interface

@Slf4j
public class TicketWindowRunnable implements Runnable {

    private final String name;
    private static final int MAX = 500;
    private int index = 1;//Not decorated with static
    
    public TicketWindowRunnable(String name){
        this.name = name;
    }

    public void run() {
        while(index<=MAX){
            log.info("Name of counter:{},Current number:{}",name,index++);
        }
    }
}

After the Runnable interface is implemented, it will be much better. Index can share index to a certain extent without static decoration, which can separate business data and business operation. However, if the index value is too large, there will be thread safety problems.

Topics: Java jvm Programming