[Java Study Notes - Concurrent Programming] Threads and Tasks

foreword

Recently, I was looking at some Java 15 concurrency, thread scheduling, and some implementation solutions. Although many things are still 1.5, they are still very rewarding.

1. Threads and tasks

In Java, threads are used to execute tasks, and threads can be said to be containers for tasks. Without the physical start of the thread (start0), no tasks will be executed.

If you have read the source code of Thread, you can know that the implementation of threads in Java is very closed, and its mechanism comes from the low-level p thread method of c. In the source code, through the native keyword, relying on the JNI interface, call other languages ​​to achieve access to the bottom layer.

2. The basic interface of Java threads and tasks

In the concurrent and lang packages, there are several basic interfaces that need to be understood first:

  • Runnable
  • Callable
  • Future

Runnable

For tasks or threads , the most basic and primary function is to run , so in Java1.0, there is only the Runnable interface. The specification of the Runnable interface is also very simple:

public interface Runnable {
    
    
    public abstract void run();
}

There is no return value and no exception thrown, it is simply executed, and the code for task execution is implemented in the run function. If you need to obtain the task execution result, you must write a callback .

Implement the Runnable interface, and abstract personalized and simplified parallel tasks that require new threads to execute according to actual business needs.

Thread is an implementation class that implements Runnable, so I personally understand that from a Java perspective, threads are actually a special task.

  • Runnable is suitable as an implemented interface to be implemented by task classes (because Thread and Executor can only enter Runnable).

Callable

But if it's just this, it doesn't meet our requirements for task management . We want to obtain the task exception thrown by the thread execution task (note that it is not a thread exception, the two are essentially different) and the returned result more conveniently. Therefore, there is a Callable interface in Java1.5.

The Callable interface specification is also not complicated:

public interface Callable<V> {
    
    
    V call() throws Exception;
}

Compared with Runnable, we can see obvious changes. First of all, in the process of the thread executing the task, we can catch the exception thrown by the task . Second, we can get the return value of the input type.

  • Callable is more suitable to be written into a task as a task content, because it can easily handle throwing exceptions in run. (This will be reflected in FutureTask)

Future

The Future interface is also provided in Java1.5 to manage tasks in more detail.

public interface Future<V> {
    
    

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();
	//阻塞(等待)获取计算结果
    V get() throws InterruptedException, ExecutionException;
    //超时报错
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

In this interface specification, we can monitor the state of the thread. First, a specification for manually terminating threads is provided. Secondly, what is more useful is that with the get function, it means that we can block and obtain the calculation results of the thread at any time and place (any row).

3. Basic implementation classes of Java threads and tasks

After understanding these basic interfaces, let's look at several Java thread implementation classes (Thread) and classic task (FutureTask) implementation classes to see how Java uses these specifications.

Thread

Thread is the simplest, most direct, and lowest-level class in Java to create a thread object and open a thread.

Note that the run function must be distinguished from the start function. The start function is to physically start a thread, and the run function is just to call our implementation of Runnable (the code executed in the thread, that is, the task).

	//创建对象
    Thread thread = new Thread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            //代码实现
            //如果想让线程回传结果,只能在这里面写回调函数。
        }
    });
    //开启线程
    thread.start();

In the above code, we can roughly understand that a task (implementation of Runnable) is placed in a thread object, and then the start function is called to let the thread calculate the task.

Looking at the source code, we know that the constructor of Thread can only pass in the implementation of Runnable. Therefore, if you do not use the thread pool, you must implements Runnable when you write your own business task class (Task).

Let's take a look, mentioned above, where the thread is actually started:

    public synchronized void start() {
    
    

        if (threadStatus != 0)
            throw new IllegalThreadStateException();
            
        group.add(this);

        boolean started = false;
        try {
    
    
        	//这里是真正物理层开启线程的地方 start0() 函数~~~
            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();
    
    //仅仅是调用实现
    @Override
    public void run() {
    
    
        if (target != null) {
    
    
            target.run();
        }
    }

We see that it is the start0() function in start() that actually starts the thread at the physical layer, which is the JNI interface mentioned above, and calls other languages ​​to access the bottom layer. In other words, the thread is actually created and run by start().

The implementation of the Runnable run function is called by the run function in the Thread, but how the Thread's run is executed in the thread started by start0 is still unclear to me. Further study is required.

In addition to start0, there are many other operations implemented by non-Java, such as:

    private native void setPriority0(int newPriority);
    private native void stop0(Object o);
    private native void suspend0();
    private native void resume0();
    private native void interrupt0();
    private static native void clearInterruptEvent();
    private native void setNativeName(String name);

FutureTask

FutureTask is the basic implementation of Runnable and Future. In fact, it is the basic management of an asynchronous task. We can roughly read the implementation details and provide ideas for us to realize our own Task.

Let's take a look at the implementation inheritance relationship of this class:

insert image description here
It is clear that FutureTask is actually a combination of Runnable and Future. (The Task concept mentioned earlier is also reflected here).

    private volatile int state;
    private static final int NEW          = 0;
    private static final int COMPLETING   = 1;
    private static final int NORMAL       = 2;
    private static final int EXCEPTIONAL  = 3;
    private static final int CANCELLED    = 4;
    private static final int INTERRUPTING = 5;
    private static final int INTERRUPTED  = 6;

    /** The underlying callable; nulled out after running */
    private Callable<V> callable;
    /** The result to return or exception to throw from get() */
    private Object outcome; // non-volatile, protected by state reads/writes
    /** The thread running the callable; CASed during run() */
    private volatile Thread runner;
    /** Treiber stack of waiting threads */
    private volatile WaitNode waiters;

In this class, you can see the state state (the state of the task) that the task has, including a Callable (task content), output result (any object), and Thread (the Thread that executes the task) and WaitNode (this will be mentioned later) .

Of course, in actual business, a simple Task may only need to have a distinct id, a handle for judging execution, and a lock to satisfy the basic functions.

In the constructor of FutureTask, you can clearly see the initialization process of the object and the construction essence of this class.

    public FutureTask(Callable<V> callable) {
    
    
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;
    }

    public FutureTask(Runnable runnable, V result) {
    
    
    	//Executors 运用了 RunnableAdapter 将 Runnable 转为 Callable<T>
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;   
    }

It can be seen that even with the second constructor, Runnable is converted into Callable internally.

Take another look at the run function:

    public void run() {
    
    
        if (state != NEW ||
            !RUNNER.compareAndSet(this, null, Thread.currentThread()))
            return;
        try {
    
    
            Callable<V> c = callable;
            if (c != null && state == NEW) {
    
    
                V result;
                boolean ran;
                //这里就是为什么推荐使用Callable作为输入,因为方便catch异常,
                //不然只能在 Runnable 的run中回调。
                try {
    
    
                	//执行自己实现的call函数
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
    
    
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
    
    
            runner = null;
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

We can see the most basic, in the interrupt condition, the task cannot be executed repeatedly, and this object cannot execute other threads. After that, it is also the normal implementation process.

The use of FutureTask is also very simple, here it is recommended to enter Callable:

    FutureTask<String> f = new FutureTask<>(new Callable<String>() {
    
    
        @Override
        public String call() throws Exception {
    
    
            return null;
        }
    });
    Thread t = new Thread(f);
    t.start();

It is also very obvious here that a task is put into a thread to execute, and various states of the task are obtained.

Four. Summary

In the learning of Java multithreading, it is necessary to understand the difference between threads and tasks, the essential difference between Runnable and Callable (not the appearance of the code), and the connection between them.

Callable is more suitable to be written into the task as a task content (because exceptions can be easily handled in run), and Runnable is suitable to be implemented by the task class as an implemented interface (because Thread and Executor can only input Runnable). This is fully reflected in the FutureTask class, let's experience it again:

  • Why implements Runnable is recommended
	//新建任务
    FutureTask<String> f = new FutureTask<>(new Callable<String>() {
    
    
        @Override
        public String call() throws Exception {
    
    
            return null;
        }
    });
    //为什么推荐 implements Runnable
    Thread t = new Thread(f);
    //物理开启线程
    t.start();
  • Why is Callable input to the constructor recommended.
    public void run() {
    
    
        if (state != NEW ||
            !RUNNER.compareAndSet(this, null, Thread.currentThread()))
            return;
        try {
    
    
            Callable<V> c = callable;
            if (c != null && state == NEW) {
    
    
                V result;
                boolean ran;
                //这里就是为什么推荐使用Callable作为输入,因为方便catch异常,
                //不然只能在 Runnable 的run中回调。
                try {
    
    
                	//执行自己实现的call函数
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
    
    
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
    
    
            runner = null;
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

Always remember that a thread is a container for a task, and the physical startup of the thread is separate from the implementation code of the task. In this way, we can have a deeper understanding of the nature of Java multithreading, and it will be helpful for us to understand the thread pool later.

Guess you like

Origin blog.csdn.net/weixin_43742184/article/details/113388988