User Viacheslav
Viacheslav
Level 3
St. Petersburg

Better together: Java and the Thread class. Part I — Threads of execution

Published in the Java Developer group

Introduction

Multithreading was built into Java from the very beginning. So, let's briefly look at this thing called multithreading. Better together: Java and the Thread class. Part I — Threads of execution - 1We take the official lesson from Oracle as a reference point: "Lesson: The "Hello World!" Application". We'll slightly alter the code of our Hello World program as follows:

class HelloWorldApp {
    public static void main(String[] args) {
        System.out.println("Hello, " + args[0]);
    }
}
args is an array of input parameters passed when the program is started. Save this code to a file with a name that matches the class name and has the extension .java. Compile it using the javac utility: javac HelloWorldApp.java. Then, we run our code with some parameter, for example, "Roger": java HelloWorldApp Roger Better together: Java and the Thread class. Part I — Threads of execution - 2Our code currently has a serious flaw. If you don't pass any argument (i.e. execute just "java HelloWorldApp"), then we get an error:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
        at HelloWorldApp.main(HelloWorldApp.java:3)
An exception (i.e. an error) occurred in the thread named "main". So, Java has threads? This is where our journey begins.

Java and threads

To understand what a thread is, you need to understand how a Java program starts. Let's change our code as follows:

class HelloWorldApp {
    public static void main(String[] args) {
		while (true) { 
			// Do nothing
		}
	}
}
Now let's compile it again with javac. For convenience, we'll run our Java code in a separate window. On Windows, this can be done like this: start java HelloWorldApp. Now we'll use the jps utility to see what information Java can tell us: Better together: Java and the Thread class. Part I — Threads of execution - 3The first number is the PID or Process ID. What is a process?

A process is a combination of code and data sharing a common virtual address space.
With processes, different programs are isolated from each other as they run: each application uses its own area in memory without interfering with other programs. To learn more, I recommend reading this tutorial: Processes and Threads. A process cannot exist without a thread, so if a process exists, then it has at least one thread. But how does this come about in Java? When we start a Java program, execution begins with the main method. It's as if we're stepping into the program, so this special main method is called the entry point. The main method must always be "public static void", so that the Java virtual machine (JVM) can start executing our program. For more information, Why is the Java main method static?. It turns out that the Java launcher (java.exe or javaw.exe) is a simple C application: it loads the various DLLs that actually comprise the JVM. The Java launcher makes a specific set of Java Native Interface (JNI) calls. JNI is a mechanism for connecting the Java virtual machine's world with the world of C++. So, the launcher is not the JVM itself, but rather a mechanism to load it. It knows the correct commands to execute in order to start the JVM. It knows how to use JNI calls to set up the necessary environment. Setting up this environment includes creating the main thread, which is called "main", of course. To better illustrate which threads exist in a Java process, we use the jvisualvm tool, which is included with the JDK. Knowing the pid of a process, we can immediately see information about that process: jvisualvm --openpid <process id> Better together: Java and the Thread class. Part I — Threads of execution - 4Interestingly, each thread has its own separate area in the memory allocated to the process. This memory structure is called a stack. A stack consists of frames. A frame represents the activation of a method (an unfinished method call). A frame can also be represented as a StackTraceElement (see the Java API for StackTraceElement). You can find more information about the memory allocated to each thread in the discussion here: "How does Java (JVM) allocate stack for each thread". If you look at the Java API and search for the word "Thread", you'll find the java.lang.Thread class. This is the class that represents a thread in Java, and we will need to work with it. Better together: Java and the Thread class. Part I — Threads of execution - 5

java.lang.Thread

In Java, a thread is represented by an instance of the java.lang.Thread class. You should immediately understand that instances of the Thread class are not themselves threads of execution. This is just a kind of API for the low-level threads managed by the JVM and the operating system. When we start the JVM using the Java launcher, it creates a main thread called "main" and a few other housekeeping threads. As stated in the JavaDoc for the Thread class: When a Java Virtual Machine starts up, there is usually a single non-daemon thread. There are 2 types of threads: daemons and non-daemons. Daemon threads are background (housekeeping) threads that do some work in the background. The word "daemon" refers to Maxwell's demon. You can learn more in this Wikipedia article. As stated in the documentation, the JVM continues executing the program (process) until:
  • The Runtime.exit() method is called
  • All NON-daemon threads finish their work (without errors or with thrown exceptions)
An important detail follows from this: daemon threads can be terminated at any point. As a result, there are no guarantees about the integrity of their data. Accordingly, daemon threads are suitable for certain housekeeping tasks. For example, Java has a thread that is responsible for processing finalize() method calls, i.e. threads involved with the Garbage Collector (gc). Each thread is part of a group (ThreadGroup). And groups can be part of other groups, forming a certain hierarchy or structure.

public static void main(String[] args) {
	Thread currentThread = Thread.currentThread();
	ThreadGroup threadGroup = currentThread.getThreadGroup();
	System.out.println("Thread: " + currentThread.getName());
	System.out.println("Thread Group: " + threadGroup.getName());
	System.out.println("Parent Group: " + threadGroup.getParent().getName());
}
Groups bring order to thread management. In addition to groups, threads have their own exception handler. Take a look at an example:

public static void main(String[] args) {
	Thread th = Thread.currentThread();
	th.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
		@Override
		public void uncaughtException(Thread t, Throwable e) {
			System.out.println("An error occurred: " + e.getMessage());
		}
	});
    System.out.println(2/0);
}
Division by zero will cause an error that will be caught by the handler. If you don't specify your own handler, then the JVM will invoke the default handler, which will output the exception's stack trace to StdError. Each thread also has a priority. You can read more about priorities in this article: Java Thread Priority in Multithreading. Better together: Java and the Thread class. Part I — Threads of execution - 6

Creating a thread

As stated in the documentation, we have 2 ways to create a thread. The first way is to create your own subclass. For example:

public class HelloWorld{
    public static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("Hello, World!");  
        }
    }
    
    public static void main(String[] args) {
        Thread thread = new MyThread();
        thread.start();
    }
}
As you can see, the work of the task happens in the run() method, but the thread itself is started in the start() method. Do not confuse these methods: if we call the run() method directly, then no new thread will be started. It is the start() method that asks the JVM to create a new thread. This option where we inherit Thread is already bad in that we're include Thread in our class hierarchy. The second drawback is that we're starting to violate the "single responsibility" principle. That is, our class is simultaneously responsible for controlling the thread and for some task to be performed in this thread. What's the right way? The answer is found in the same run() method, which we override:

public void run() {
	if (target != null) {
		target.run();
	}
}
Here, target is some java.lang.Runnable, which we can pass when creating an instance of the Thread class. This means we can do this:

public class HelloWorld{
    public static void main(String[] args) {
        Runnable task = new Runnable() {
            public void run() {
                System.out.println("Hello, World!");
            } 
        };
        Thread thread = new Thread(task);
        thread.start();
    }
}
Runnable has also been a functional interface since Java 1.8. This makes it possible to write even more beautiful code for a thread's task:

public static void main(String[] args) {
	Runnable task = () -> { 
		System.out.println("Hello, World!");
	};
	Thread thread = new Thread(task);
	thread.start();
}

Conclusion

I hope this discussion clarifies what a thread is, how threads come into existence, and what basic operations can be performed with threads. In the next part, we'll try to understand how threads interact with one another and explore the thread life cycle. Better together: Java and the Thread class. Part II — Synchronization Better together: Java and the Thread class. Part III — Interaction Better together: Java and the Thread class. Part IV — Callable, Future, and friends Better together: Java and the Thread class. Part V — Executor, ThreadPool, Fork/Join Better together: Java and the Thread class. Part VI — Fire away!
Comments (2)
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION
Andrei Level 41
13 May 2021
It was OK, definitely needs multiple reads to better understand the concept.
Vesa Level 41, Kaliningrad, Russia
28 May 2020