Thread

Threads are lightweight process that can run concurrently. They can be created by extending the Thread class:

class MyThread extends Thread {
    public void run() {
        System.out.println("My thread is running");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}

With modern java you can compile and run it with a single command:

java Main.java

Another way to create thread is to implement Runnable

class MyThread implements Runnable {
    public void run() {
        System.out.println("Thread via Runnable");
    }
}

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyThread());
        thread.start();
    }
}

It can be further simplified using lambda:

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("Thread as lambda expression");
        });
        thread.start();
    }
}
A java thread might run on different cores through out it's lifetime.

join()

Unlike goroutines, java program won't exit until all threads are finished (unless the threads are daemon threads).

But if you want to wait for a particular thread to finish, use join():

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("Thread started");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread finished");
        });

        thread.start();

        thread.join();
        System.out.println("Main thread finished");
    }
}

Expected output:

Thread started
Thread finished
Main thread finished

You can also pass a timeout when join:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("Thread started");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread finished");
        });

        thread.start();

        thread.join(1000);

        System.out.println(thread.isAlive() ? "Thread is alive" : "Thread is dead");

        System.out.println("Main thread finished");
    }
}

Expected output:

Thread started
Thread is alive
Main thread finished
Thread finished

The problem of multithreading

A race condition occurs when multiple threads try to access and modify shared data concurrently in a naive way:

public class Main {
    private static int counter = 0;

    public static void main(String[] args) {

        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(() -> {
                for (int j = 0; j < 10; j++) {
                    counter++;
                    System.out.println(Thread.currentThread().getName() + " " + counter);
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();
        }
    }
}

The output can be 38, 47 or other numbers.