Skip to main content Link Menu Expand (external link) Document Search Copy Copied

Multithreading and Concurrency: Creating and Running Threads

Header Image

Ahoy, mateys! Welcome aboard our pirate-themed Java adventure. In this article, we’ll be exploring the treasure trove of multithreading and concurrency. Specifically, we’ll delve into the depths of creating and running threads. This mighty skill will help ye navigate the choppy waters of complex Java applications with ease.

Why Use Threads?

Imagine ye be the captain of a fine pirate ship, and ye have a crew of sailors workin’ hard to keep the ship afloat. Now, imagine each sailor bein’ a thread. Ye want them to perform tasks simultaneously so ye can reach yer destination more quickly. If ye only had one sailor doin’ all the tasks, it’d take ye ages to reach the treasure!

This be the same reason we use threads in Java. A thread allows ye to execute multiple tasks concurrently, improvin’ the performance of yer application. So let’s hoist the Jolly Roger and dive into creating and running threads!

Creating Threads

In Java, ye can create threads in two ways: extendin’ the Thread class or implementin’ the Runnable interface. Let’s take a closer look at both of these methods.

Extendin’ the Thread Class

To create a thread by extendin’ the Thread class, ye need to:

  1. Create a new class that extends the Thread class.
  2. Override the run() method, where ye place the code to be executed in the thread.
  3. Create an instance of the new class and call the start() method on it.

Here be an example:

class PirateSailor extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println("Ahoy! I'm sailor " + i + "!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class PirateShip {
    public static void main(String[] args) {
        PirateSailor sailor1 = new PirateSailor();
        PirateSailor sailor2 = new PirateSailor();
        
        sailor1.start();
        sailor2.start();
    }
}

In this example, we create a new class PirateSailor that extends the Thread class. We override the run() method, which prints a message and sleeps for 1 second. Then, in the PirateShip class, we create two instances of PirateSailor and call the start() method on each of them.

Implementin’ the Runnable Interface

To create a thread by implementin’ the Runnable interface, ye need to:

  1. Create a new class that implements the Runnable interface.
  2. Implement the run() method, where ye place the code to be executed in the thread.
  3. Create an instance of the new class and pass it to the Thread constructor, then call the start() method on the Thread object.

Here be an example:

class PirateSailor implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println("Ahoy! I'm sailor " + i + "!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class PirateShip {
    public static void main(String[] args) {
        PirateSailor sailor1 = newPirateSailor();
        PirateSailor sailor2 = new PirateSailor();

        Thread thread1 = new Thread(sailor1);
        Thread thread2 = new Thread(sailor2);

        thread1.start();
        thread2.start();
    }
}

In this example, we create a new class PirateSailor that implements the Runnable interface. We implement the run() method, which prints a message and sleeps for 1 second. Then, in the PirateShip class, we create two instances of PirateSailor, pass them to the Thread constructor, and call the start() method on each Thread object.

Joinin’ Threads

Sometimes, ye may want to wait for a thread to finish before continuin’ with yer main thread. To do this, ye can use the join() method. The join() method ensures that the main thread waits for the specified thread to finish before movin’ on.

Here be an example:

public class PirateShip {
    public static void main(String[] args) {
        PirateSailor sailor1 = new PirateSailor();
        PirateSailor sailor2 = new PirateSailor();

        Thread thread1 = new Thread(sailor1);
        Thread thread2 = new Thread(sailor2);

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("All sailors have finished their tasks! Time to set sail!");
    }
}

In this example, we use the join() method to wait for both thread1 and thread2 to finish before printin’ the message, “All sailors have finished their tasks! Time to set sail!”.

Now that ye know how to create and run threads, ye be well on yer way to masterin’ the art of multithreading and concurrency. There be more to learn, so stay tuned for our next adventure, where we’ll explore synchronization and locks.

In the meantime, may the wind be ever in yer sails, and may yer Java threads run swiftly and smoothly!

Synchronization and Locks

After creatin’ and runnin’ threads, ye might encounter situations where multiple threads try to access shared resources, like treasure chests or rum barrels. This can lead to problems like data inconsistency or unexpected behavior. To avoid these issues, we use synchronization and locks to ensure that only one thread can access the shared resource at a time.

Synchronized Keyword

In Java, ye can use the synchronized keyword to create a synchronized block or method. When a thread enters a synchronized block or method, it acquires a lock on the shared resource. Other threads must wait until the lock is released to access the resource.

Here be an example:

class TreasureChest {
    private int goldCoins;

    public TreasureChest(int goldCoins) {
        this.goldCoins = goldCoins;
    }

    public synchronized void addGold(int gold) {
        goldCoins += gold;
        System.out.println("Gold added! There are now " + goldCoins + " gold coins in the chest.");
    }

    public synchronized void removeGold(int gold) {
        if (goldCoins >= gold) {
            goldCoins -= gold;
            System.out.println("Gold removed! There are now " + goldCoins + " gold coins in the chest.");
        } else {
            System.out.println("Not enough gold coins in the chest!");
        }
    }
}

class PirateSailor implements Runnable {
    private TreasureChest treasureChest;

    public PirateSailor(TreasureChest treasureChest) {
        this.treasureChest = treasureChest;
    }

    @Override
    public void run() {
        treasureChest.addGold(10);
        treasureChest.removeGold(5);
    }
}

public class PirateShip {
    public static void main(String[] args) {
        TreasureChest treasureChest = new TreasureChest(50);
        PirateSailor sailor1 = new PirateSailor(treasureChest);
        PirateSailor sailor2 = new PirateSailor(treasureChest);

        Thread thread1 = new Thread(sailor1);
        Thread thread2 = new Thread(sailor2);

        thread1.start();
        thread2.start();
    }
}

In this example, we have a TreasureChest class with a shared resource, goldCoins. We use the synchronized keyword to ensure that only one thread can access the addGold() and removeGold() methods at a time.

Lock Interface and Its Implementations

The Lock interface in Java provides more flexible lockin’ mechanisms than the synchronized keyword. Ye can use the ReentrantLock class, which implements the Lock interface, to create a lock object.

Here be an example:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class TreasureChest {
    private int goldCoins;
    private Lock lock = new ReentrantLock();

    public TreasureChest(int goldCoins) {
        this.goldCoins = goldCoins;
    }

    public void addGold(int gold) {
        lock.lock();
        try {
            goldCoins += gold;
            System.out.println("Gold added! There are now " + goldCoins + " gold coins in the chest.");
        } finally {
            lock.unlock();
        }
    }

    public void removeGold(int gold) {
        lock.lock();
        try {
            if (goldCoins >= gold) {
                goldCoins -= gold;
                System.out.println("Gold removed! There are now " + goldCoins + " gold coins in the chest.");
            } else {
                System.out.println("Not enough goldcoins in the chest!");
            }
        } finally {
            lock.unlock();
        }
    }
}

class PirateSailor implements Runnable {
    private TreasureChest treasureChest;

    public PirateSailor(TreasureChest treasureChest) {
        this.treasureChest = treasureChest;
    }

    @Override
    public void run() {
        treasureChest.addGold(10);
        treasureChest.removeGold(5);
    }
}

public class PirateShip {
    public static void main(String[] args) {
        TreasureChest treasureChest = new TreasureChest(50);
        PirateSailor sailor1 = new PirateSailor(treasureChest);
        PirateSailor sailor2 = new PirateSailor(treasureChest);

        Thread thread1 = new Thread(sailor1);
        Thread thread2 = new Thread(sailor2);

        thread1.start();
        thread2.start();
    }
}

In this example, we’ve replaced the synchronized keyword with a ReentrantLock. The lock.lock() method is called before accessin’ the shared resource, and lock.unlock() is called in the finally block to release the lock.

Deadlock

Deadlock occurs when two or more threads are waitin’ for each other to release a lock, and none of them can proceed. It be like two pirate ships circlin’ each other, neither willin’ to fire the first shot. To avoid deadlock, use proper lock orderin’, lock timeouts, or try to minimize the use of nested locks.

Here be a simple example of a deadlock:

class SailorA {
    synchronized void method1(SailorB sailorB) {
        System.out.println("Sailor A: Boardin' sailor B's ship.");
        sailorB.board();
    }

    synchronized void board() {
        System.out.println("Sailor A: Boarded by sailor B.");
    }
}

class SailorB {
    synchronized void method1(SailorA sailorA) {
        System.out.println("Sailor B: Boardin' sailor A's ship.");
        sailorA.board();
    }

    synchronized void board() {
        System.out.println("Sailor B: Boarded by sailor A.");
    }
}

public class DeadlockExample {
    public static void main(String[] args) {
        SailorA sailorA = new SailorA();
        SailorB sailorB = new SailorB();

        // Thread 1
        Runnable runA = () -> sailorA.method1(sailorB);
        // Thread 2
        Runnable runB = () -> sailorB.method1(sailorA);

        new Thread(runA).start();
        new Thread(runB).start();
    }
}

In this example, deadlock occurs when sailorA acquires the lock on method1() and sailorB acquires the lock on method1() as well. Both threads are waitin’ for each other to release the lock before they can proceed, resultin’ in a deadlock.

Thread Safety

Now that ye know how to create threads and keep the shared treasure locked, we must ensure that our code is thread-safe. Thread safety be the concept of writin’ code that can be executed safely by multiple threads without causin’ unexpected behavior or data corruption.

Here be some techniques for makin’ yer code thread-safe:

1. Immutability

By creatin’ immutable objects, ye can ensure that their state can’t be changed after construction. This eliminates the need for synchronization, as there be no risk of data corruption.

Consider the followin’ immutable ImmutableCoordinates class:

final class ImmutableCoordinates {
    private final int x;
    private final int y;

    public ImmutableCoordinates(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }
}

The ImmutableCoordinates class be immutable because its fields are marked final and there be no methods that can modify their values.

2. Local Variables

Local variables be thread-safe, as they be created on the stack and can’t be accessed by other threads. Each thread has its own stack, so ye don’t need to worry about synchronization.

Here be an example of usin’ local variables:

public class PirateCalculator {
    public int calculateTreasureValue(int goldCoins, int gems) {
        int goldValue = goldCoins * 10;
        int gemsValue = gems * 50;
        return goldValue + gemsValue;
    }
}

In this example, goldValue and gemsValue be local variables, so they be thread-safe.

3. Thread-Local Storage

Thread-local storage be a mechanism that allows ye to store data specific to a particular thread. Ye can use the ThreadLocal class to create thread-local variables that store a value for each thread.

Here be an example of usin’ thread-local storage:

class PirateNameGenerator {
    private static final ThreadLocal<String> pirateName = new ThreadLocal<>();

    public void setPirateName(String name) {
        pirateName.set(name);
    }

    public String getPirateName() {
        return pirateName.get();
    }
}

In this example, the pirateName variable be thread-local, so each thread can have its own pirate name without interferin’ with others.

4. Atomic Operations

Atomic operations be those that can be performed in a single, uninterruptible step. Usin’ atomic operations can help ye avoid synchronization issues, as they can’t be interrupted by other threads.

Java provides the AtomicInteger, AtomicLong, and AtomicBoolean classes for performin’ atomic operations on integers, longs, and booleans, respectively.

Here be an example of usin’ atomic operations:

import java.util.concurrent.atomic.AtomicInteger;

class PirateCounter {
    private AtomicInteger piratesCount = new AtomicInteger(0);

    public void addPirate() {
        piratesCount.incrementAndGet();
    }

    public int getPiratesCount() {
        return piratesCount.get();
    }
}

In this example, we use the AtomicInteger class to perform atomic operations on the piratesCount variable, ensurin’ thread safety.

By usin’ these techniques, ye can sail the treacherous waters of multithreaded programming with confidence, keepin’ yer code safe from data corruption and other unexpected perils.

Atomic Variables

In the vast ocean of concurrent programming, ye need a way to keep yer treasure count accurate while the pirate crew works simultaneously. This be where atomic variables come in handy. They be thread-safe variable types that allow ye to perform simple operations in an atomic manner, ensurin’ that no two threads can interfere with each other’s work.

Java provides the java.util.concurrent.atomic package, which includes classes like AtomicInteger, AtomicLong, AtomicBoolean, and AtomicReference for handlin’ atomic operations on integers, longs, booleans, and object references, respectively.

Let’s explore some common atomic operations:

Increment and Decrement

Increasin’ or decreasin’ an atomic variable be as easy as hoistin’ the Jolly Roger. Ye can use the incrementAndGet() and decrementAndGet() methods to do so:

import java.util.concurrent.atomic.AtomicInteger;

class PirateCrew {
    private AtomicInteger crewSize = new AtomicInteger(0);

    public void recruitPirate() {
        crewSize.incrementAndGet();
    }

    public void maroonPirate() {
        crewSize.decrementAndGet();
    }

    public int getCrewSize() {
        return crewSize.get();
    }
}

In this example, crewSize be an atomic variable that can be safely incremented or decremented by multiple threads.

Compare and Set

Sometimes, ye need to compare the current value of an atomic variable with an expected value and set a new value if the comparison succeeds. The compareAndSet() method be perfect for this:

import java.util.concurrent.atomic.AtomicBoolean;

class TreasureChest {
    private AtomicBoolean isOpen = new AtomicBoolean(false);

    public boolean tryOpenChest() {
        return isOpen.compareAndSet(false, true);
    }

    public void closeChest() {
        isOpen.set(false);
    }
}

In this example, we use an AtomicBoolean to represent the state of a treasure chest. The tryOpenChest() method will only open the chest if it be currently closed, and multiple threads can safely attempt to open the chest without interference.

Accumulate

If ye need to update an atomic variable based on its current value, ye can use the accumulateAndGet() method:

import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.IntBinaryOperator;

class PirateScore {
    private AtomicInteger score = new AtomicInteger(0);

    public void addToScore(int points) {
        IntBinaryOperator accumulator = (current, update) -> current + update;
        score.accumulateAndGet(points, accumulator);
    }

    public int getScore() {
        return score.get();
    }
}

In this example, we use the accumulateAndGet() method to add points to a pirate’s score. The method accepts an IntBinaryOperator that describes how the current value should be updated.

By usin’ atomic variables, ye can avoid the need for explicit synchronization in many cases, makin’ yer concurrent code more efficient and easier to maintain. So, hoist the colors and sail towards the horizon with the power of atomic variables on yer side!

Volatile Keyword

When navigatin’ the choppy waters of multithreadin’, ye might sometimes come across the volatile keyword. It be a simple but powerful tool that guarantees visibility and ordering of changes to a shared variable among multiple threads. By markin’ a variable as volatile, ye tell the Java memory model to avoid any optimizations that may cause threads to cache the variable’s value, ensurin’ that all threads always read the freshest value directly from the main memory.

Consider this example of a thread monitorin’ a pirate ship’s cannon status:

class CannonStatus {
    private volatile boolean isLoaded = false;

    public void loadCannon() {
        isLoaded = true;
    }

    public void fireCannon() {
        if (isLoaded) {
            // Fire the cannon!
            System.out.println("Boom!");
            isLoaded = false;
        } else {
            System.out.println("Cannon is not loaded!");
        }
    }
}

In this example, we mark the isLoaded variable as volatile to ensure that all threads see the most recent value. Without the volatile keyword, a thread may cache the isLoaded value, causin’ it to miss updates made by other threads.

It be important to note that volatile only guarantees visibility and ordering, not atomicity. If ye need to perform compound operations on shared variables (like incrementin’ a counter or updatin’ a value based on its current state), ye should use atomic variables or synchronization techniques like locks or synchronized blocks.

Sailin’ to Concurrency Horizons

Now that ye be equipped with the knowledge of multithreadin’, synchronization, thread safety, atomic variables, and the volatile keyword, ye be ready to venture forth into the vast sea of concurrent Java programming. Remember to hoist yer Jolly Roger and keep a weather eye on the horizon for potential threadin’ pitfalls. Happy sailin’, and may ye find the treasure ye seek in the world of Java concurrency!