Java Concurrent Programming Series 2: Thread Concepts and Basic Operations

Java Concurrent Programming Series 2: Thread Concepts and Basic Operations

Great ideals can only be achieved through selfless struggle and sacrifice.

This is the second in the Java Concurrent Programming series on Threads.The main content is as follows:

  • Thread concept
  • Thread-based operations

Thread concept

A process represents a running program, and a running Java program is a process.In Java, when we start the main function, we start a JVM process, and the main function is in a thread called the main thread.
The process-thread relationship is illustrated below:

As can be seen from the figure above, there are multiple threads in a process, and multiple threads share the method area resources of the process's heap, but each thread has its own program counters and stack area.

Thread-based operations

Thread Creation and Running

There are three ways to create threads in Java: inherit the Thread class and override the run method, implement the run method of the Runnable interface, and use the FutureTask method.
First, look at the implementation of inheriting the Thread pattern, with the following code examples:

public class ThreadDemo {
    public static class DemoThread extends Thread {
        @Override
        public void run() {
            System.out.println("this is a child thread.");
        }
    }
    public static void main(String[] args) {
        System.out.println("this is main thread.")
        DemoThread thread = new DemoThread();
        thread.start();
    }
}

The DemoThread class in the code above inherits the Thread class and overrides the run method.An instance of DemoThread is created in the main function and the thread is started by calling its start method.

tips: instead of executing immediately after calling the start method, the thread is ready, that is, it has already acquired resources other than CPU resources and waits to acquire CPU resources before it is truly running.
With inheritance, the benefit is that the current thread can be obtained through this. The disadvantage is that Java does not support multiple inheritance. If you inherit the Thread class, you can no longer inherit other classes.Moreover, tasks are heavily coupled with code, and a thread class can only perform one task, but there is no limit to using Runnable.
To see how to implement the run method of the Runnable interface, the code example is as follows:

public class RunnableDemo {
    public static class DemoRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("this is a child thread.");
        }
    }
    public static void main(String[] args) {
        System.out.println("this is main thread.");
        DemoRunnable runnable = new DemoRunnable();
        new Thread(runnable).start();
        new Thread(runnable).start();
    }
}

The above code shares a single Runnable logic between the two threads, and you can add parameters to the RunnableTask to differentiate tasks if you want.In Java8, you can use Lambda expressions to simplify the above code:

 public static void main(String[] args) {
    System.out.println("this is main thread.");
    Thread t = new Thread(() -> System.out.println("this is child thread"));
    t.start();
}

One disadvantage of both approaches is that the task does not return a value. See the third, using FutureTask.The code example is as follows:

public class CallableDemo implements Callable<JsonObject> {
    @Override
    public JsonObject call() throws Exception {
        return new JsonObject();
    }
    public static void main(String[] args) {
        System.out.println("this is main thread.");
        FutureTask<JsonObject> futureTask = new FutureTask<>(new CallableDemo());   // 1. Reusable FutureTask
        new Thread(futureTask).start();
        try {
            JsonObject result = futureTask.get();
            System.out.println(result.toString());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        // 2. One-time FutureTask
        FutureTask<JsonObject> innerFutureTask = new FutureTask<>(() -> {
            JsonObject jsonObject = new JsonObject();
            jsonObject.addProperty("name", "Dali");
            return jsonObject;
        });
        new Thread(innerFutureTask).start();

        try {
            JsonObject innerResult = innerFutureTask.get();
            System.out.println(innerResult.toString());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

As in the code above, CallableDemo implements the call method of the Callable interface, creates a FutureTask in the main function using an instance of CallableDemo, creates a thread using the created FutureTask object as a task, starts it, and finally waits for the task to finish executing and returns the result through FutureTask.
Similarly, the above procedures are appropriate for tasks that need to be reused, and for one-time tasks, Lambda can be used to simplify the code, such as Note 2.

Waiting for thread termination

In a project, you often encounter a scenario where you need to wait for something to complete before continuing.There is a join method in the Thread class that can be used to handle this scenario.Go directly to the code example:

    public static void main(String[] args) throws InterruptedException {
        System.out.println("main thread starts");
        Thread t1 = new Thread(() -> System.out.println("this is thread 1"));
        Thread t2 = new Thread(() -> System.out.println("this is thread 2"));
        t1.start();
        t2.start();
        System.out.println("main thread waits child threads to be over");
        t1.join();
        t2.join();
        System.out.println("child threads are over");
    }

The code above starts two threads in the main thread and then calls their join methods separately. The main thread is blocked after calling t1.join() and returns when it finishes execution. Then the main thread is blocked again after calling t2.join(), and returns when T2 finishes execution.The results of the above code are as follows:

main thread starts
main thread waits child threads to be over
this is thread 1
this is thread 2
child threads are over

It is important to note that thread 1 will be blocked when it calls thread 2's join method, and that when another thread interrupts thread 1 by calling its interrupt method, thread 1 will throw an InterruptedException exception and return.

Sleep Threads

There is a static sleep method in the Thread class. When an executing thread invokes the Thread sleep method, the calling thread temporarily relinquishes execution for the specified time, that is, it does not participate in CPU scheduling during this time, but the monitor resources it owns, such as locks, are still not relinquished.When the specified sleep time is reached, the function returns normally, the thread is ready, and then waits for the CPU to schedule execution.

tips: wait s and sleep s are often used to compare during interviews and need to be aware of the differences.
Calling the wait() method of an object is equivalent to having the current thread surrender the monitor of the object, then enter a wait state, waiting for subsequent acquisition of the object's lock; the notify() method wakes up a thread waiting for the monitor of the object, and only one thread can wake up if multiple threads are waiting for the monitor of the object, specifying which line to wake upCheng is unknown.
Calling the wait() and notify() methods of an object requires that the current thread own the monitor for that object, so calling the wait() and notify() methods must be done within a synchronized block or synchronized method.

Take a look at a code example of thread sleep:

private static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
       // Acquire exclusive locks
       lock.lock();
       System.out.println("thread1 get to sleep");
        try {
            Thread.sleep(1000);
            System.out.println("thread1 is awake");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    });
    Thread t2 = new Thread(() -> {
        // Acquire exclusive locks
        lock.lock();
        System.out.println("thread2 get to sleep");
        try {
            Thread.sleep(1000);
            System.out.println("thread2 is awake");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    });

    t1.start();
    t2.start();
}

The code above creates an exclusive lock, then creates two threads, each acquiring the lock internally, then sleeping, and releasing the lock when sleep is over.The results are as follows:

thread1 get to sleep
thread1 is awake
thread2 get to sleep
thread2 is awake

From the execution results, thread 1 acquires the lock first, then sleeps, then wakes up, and then it is thread 2's turn to acquire the lock, that is, thread 1 did not release the lock during thread 1 sleep.
It is important to note that if a child thread interrupts it during sleep, the child threads throw an InterruptedException exception at the call to the sleep method.

Thread Yield CPU

There is a static yielding method in the Thread class. When a thread calls the yielding method, it actually implies that the thread scheduler's current thread requests to be freed from its CPU, and if the thread has an unused time slice it will be discarded, which means that the thread scheduler can schedule the next round of the thread.
When a thread invokes the yield method, the current thread gives CPU usage and is ready. The thread scheduler gets a thread with the highest priority from the thread-ready queue. Of course, it may also schedule to the thread that just released the CPU to get CPU execution.
See the code example:

public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 10; i++) {
            if (i == 8) {
                System.out.println("current thread: " + Thread.currentThread() + " yield cpu");
            }
            Thread.yield(); // 2
        }
        System.out.println("current thread: " + Thread.currentThread() + " is over");
    });

    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 10; i++) {
            if (i == 8) {
                System.out.println("current thread: " + Thread.currentThread() + " yield cpu");
            }
            Thread.yield(); // 1
        }
        System.out.println("current thread: " + Thread.currentThread() + " is over");
    });
    t1.start();
    t2.start();
}

In the code above, two threads function the same way, running multiple times, and the output of two lines of the same thread is sequential, but the overall order is uncertain, depending on how the thread scheduler is dispatched.
When you comment out the codes 1 and 2 above, you will find that there is only one result, as follows:

current thread: Thread[Thread-1,5,main] yield cpu
current thread: Thread[Thread-0,5,main] yield cpu
current thread: Thread[Thread-1,5,main] is over
current thread: Thread[Thread-0,5,main] is over

As a result, the Thread.yiled method works so that two threads discard the CPU during execution and then schedule another thread, where the two threads feel a little humble toward each other, ultimately because only two threads have executed two tasks.

The difference between tips:sleep and yield:
When a thread invokes the sleep method, the calling thread blocks the suspend for the specified time during which the thread dispatcher does not dispatch the thread.When the yield method is called, the thread simply gives up the rest of its time and is not blocked and suspended. Instead, the thread scheduler may schedule to the current thread's execution the next time it schedules, out of a ready state.

Thread Interrupt

Thread interrupts in Java are a mode of interthread collaboration.Each threaded object has a boolean-type identifier (returned by the isInterrupted() method) that represents whether there is an interrupt request (interrupt() method).For example, if thread T1 wants to interrupt thread t2, simply set the interrupt identifier of the thread T2 object to true in thread t1, then thread 2 can choose to process the interrupt request at the appropriate time, or even ignore it, as if the thread had not been interrupted.
Thread interrupts are also mentioned in the previous section, so there is no longer code to expand them.

Java Concurrent Programming Outline

Continue to attach a systematic learning outline for Java programming for your reference:
Java Concurrent Programming.png

[Reference]

  1. Beauty of Java Concurrent Programming

This article was originally created by Mini Public No. [Dali King's Technology Blog], and scanner focuses on getting more original technical articles.
Dali

Tags: Java Programming Lambda jvm

Posted on Sat, 21 Mar 2020 13:04:54 -0400 by Xil3