In this tutorial, we will explore the concept of threads in Java, which are essential for building responsive and efficient applications. Understanding how to create and manage threads is crucial for developing multi-threaded applications that can handle multiple tasks concurrently.
Threads allow a program to execute multiple operations simultaneously. In Java, you can create threads by either extending the Thread class or implementing the Runnable interface. Each approach has its own use cases and advantages, which we will discuss in detail. Additionally, managing concurrency issues is vital to ensure that threads do not interfere with each other, leading to unexpected behavior.
When you extend the Thread class, your class becomes a thread itself. This approach is straightforward but has limitations because Java does not support multiple inheritance. Therefore, if your class needs to extend another class, implementing the Runnable interface is a better choice.
1class MyThread extends Thread {2public void run() {3for (int i = 1; i <= 5; i++) {4System.out.println("Thread " + Thread.currentThread().getId() + ": " + i);5};6}78public static void main(String[] args) {9MyThread thread1 = new MyThread();10thread1.start();1112for (int i = 1; i <= 5; i++) {13System.out.println("Main Thread: " + i);14}15}16}
Thread 10: 1 Main Thread: 1 Thread 10: 2 Main Thread: 2 Thread 10: 3 Main Thread: 3 Thread 10: 4 Main Thread: 4 Thread 10: 5 Main Thread: 5
Implementing the Runnable interface allows your class to define a task that can be executed by a thread. This approach is more flexible as it separates the task from the thread management, making it easier to share tasks among multiple threads.
1class MyTask implements Runnable {2public void run() {3for (int i = 1; i <= 5; i++) {4System.out.println("Thread " + Thread.currentThread().getId() + ": " + i);5}6}78public static void main(String[] args) {9MyTask task = new MyTask();10Thread thread1 = new Thread(task);11thread1.start();1213for (int i = 1; i <= 5; i++) {14System.out.println("Main Thread: " + i);15}16}17}
Thread 10: 1 Main Thread: 1 Thread 10: 2 Main Thread: 2 Thread 10: 3 Main Thread: 3 Thread 10: 4 Main Thread: 4 Thread 10: 5 Main Thread: 5
Concurrency issues arise when multiple threads access shared resources simultaneously. Common problems include race conditions, deadlocks, and livelocks.
A race condition occurs when the outcome of a program depends on the sequence or timing of uncontrollable events such as thread scheduling. This can lead to inconsistent results.
1class Counter {2private int count = 0;34public void increment() {5count++;6}78public int getCount() {9return count;10}11}1213class MyThread extends Thread {14private Counter counter;1516public MyThread(Counter counter) {17this.counter = counter;18}1920public void run() {21for (int i = 0; i < 1000; i++) {22counter.increment();23}24}2526public static void main(String[] args) throws InterruptedException {27Counter counter = new Counter();28MyThread thread1 = new MyThread(counter);29MyThread thread2 = new MyThread(counter);3031thread1.start();32thread2.start();3334thread1.join();35thread2.join();3637System.out.println("Final Count: " + counter.getCount());38}39}
Final Count: 1950
A deadlock occurs when two or more threads are blocked forever, waiting for each other. This typically happens when threads hold locks on resources and try to acquire additional locks held by other threads.
1class Resource {2public synchronized void method1(Resource other) {3System.out.println("Thread " + Thread.currentThread().getId() + " in method1");4other.method2(this);5}67public synchronized void method2(Resource other) {8System.out.println("Thread " + Thread.currentThread().getId() + " in method2");9}10}1112class MyThread extends Thread {13private Resource resource1;14private Resource resource2;1516public MyThread(Resource resource1, Resource resource2) {17this.resource1 = resource1;18this.resource2 = resource2;19}2021public void run() {22resource1.method1(resource2);23}2425public static void main(String[] args) {26Resource resource1 = new Resource();27Resource resource2 = new Resource();2829MyThread thread1 = new MyThread(resource1, resource2);30MyThread thread2 = new MyThread(resource2, resource1);3132thread1.start();33thread2.start();34}35}
Thread 10 in method1 Thread 11 in method1
A livelock occurs when two or more threads keep changing their state in response to each other without making any progress. This is similar to a deadlock but involves active thread behavior.
1class Task {2private boolean flag = true;34public synchronized void setFlag(boolean flag) {5this.flag = flag;6}78public synchronized boolean getFlag() {9return flag;10}11}1213class MyThread extends Thread {14private Task task;1516public MyThread(Task task) {17this.task = task;18}1920public void run() {21while (true) {22if (task.getFlag()) {23System.out.println("Thread " + Thread.currentThread().getId() + " is running");24try {25sleep(100);26} catch (InterruptedException e) {27e.printStackTrace();28}29task.setFlag(false);30}31}32}3334public static void main(String[] args) {35Task task = new Task();36MyThread thread1 = new MyThread(task);37MyThread thread2 = new MyThread(task);3839thread1.start();40thread2.start();41}42}
Thread 10 is running Thread 11 is running Thread 10 is running Thread 11 is running
The isAlive() method of the Thread class can be used to check if a thread is still running.
1class MyThread extends Thread {2public void run() {3for (int i = 1; i <= 5; i++) {4System.out.println("Thread " + Thread.currentThread().getId() + ": " + i);5try {6sleep(100);7} catch (InterruptedException e) {8e.printStackTrace();9}10}11}1213public static void main(String[] args) throws InterruptedException {14MyThread thread = new MyThread();15thread.start();1617while (thread.isAlive()) {18System.out.println("Main Thread: " + thread.isAlive());19sleep(200);20}2122System.out.println("Main Thread: " + thread.isAlive());23}24}
Main Thread: true Thread 10: 1 Main Thread: true Thread 10: 2 Main Thread: true Thread 10: 3 Main Thread: true Thread 10: 4 Main Thread: true Thread 10: 5 Main Thread: false
Let's create a simple application that simulates a bank account with concurrent deposits and withdrawals.
1class BankAccount {2private int balance = 0;34public synchronized void deposit(int amount) {5balance += amount;6System.out.println("Deposited " + amount);7}89public synchronized void withdraw(int amount) {10if (balance >= amount) {11balance -= amount;12System.out.println("Withdrew " + amount);13} else {14System.out.println("Insufficient funds");15}16}1718public int getBalance() {19return balance;20}21}2223class DepositTask implements Runnable {24private BankAccount account;25private int amount;2627public DepositTask(BankAccount account, int amount) {28this.account = account;29this.amount = amount;30}3132public void run() {33for (int i = 0; i < 5; i++) {34account.deposit(amount);35}36}37}3839class WithdrawTask implements Runnable {40private BankAccount account;41private int amount;4243public WithdrawTask(BankAccount account, int amount) {44this.account = account;45this.amount = amount;46}4748public void run() {49for (int i = 0; i < 5; i++) {50account.withdraw(amount);51}52}53}5455public class BankApp {56public static void main(String[] args) throws InterruptedException {57BankAccount account = new BankAccount();5859DepositTask depositTask = new DepositTask(account, 100);60WithdrawTask withdrawTask = new WithdrawTask(account, 50);6162Thread depositThread = new Thread(depositTask);63Thread withdrawThread = new Thread(withdrawTask);6465depositThread.start();66withdrawThread.start();6768depositThread.join();69withdrawThread.join();7071System.out.println("Final Balance: " + account.getBalance());72}73}
Deposited 100 Withdrew 50 Deposited 100 Withdrew 50 Deposited 100 Withdrew 50 Deposited 100 Withdrew 50 Deposited 100 Withdrew 50 Final Balance: 250
Thread is straightforward but limits inheritance. Implementing Runnable provides more flexibility and is generally preferred.isAlive() to check if a thread is still running.In the next tutorial, we will explore Java Lambdas. Lambdas provide a concise way to represent one-method interfaces (functional interfaces) and are integral to modern Java programming. This will help you write more functional-style code and take full advantage of Java 8 and later features. Stay tuned!