Master concurrent programming in Java: an in-depth analysis of multi-threading and locking mechanisms

Part 1: Multithreading Basics

Introduction

Multi-threaded programming is an important and powerful feature in Java, which allows us to perform multiple tasks at the same time, improving program performance and response speed. This article will provide an in-depth analysis of multi-threaded programming in Java and help you understand the basic concepts of multi-threading and how to implement them in Java.

1. What is a thread?

In Java, a thread is the smallest unit of code execution. A thread can be viewed as an independent execution path that can run in parallel, enabling the program to perform multiple tasks simultaneously. Typically, a Java program has at least one main thread, and then multiple secondary threads can be created to perform other tasks.

2. Create thread

In Java, there are two ways to create threads: inheriting the Thread class and implementing the Runnable interface. Here are two examples of creating threads:

2.1 Use the Thread class to create threads
class MyThread extends Thread {<!-- -->
    public void run() {<!-- -->
        //Code executed by thread
    }
}

public class Main {<!-- -->
    public static void main(String[] args) {<!-- -->
        MyThread thread = new MyThread();
        thread.start(); // Start thread
    }
}
2.2 Create threads using Runnable interface
class MyRunnable implements Runnable {<!-- -->
    public void run() {<!-- -->
        //Code executed by thread
    }
}

public class Main {<!-- -->
    public static void main(String[] args) {<!-- -->
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start(); // Start thread
    }
}

3. Thread life cycle

Threads have different states in Java, which constitute the life cycle of the thread. Common thread states include:

  • New: The thread is created but has not been started yet.
  • Runnable: The thread is executing or waiting for CPU execution.
  • Blocked: The thread waits for a certain condition to be met, such as waiting for an I/O operation to complete.
  • Waiting: A thread waits indefinitely for notification from another thread.
  • Timed Waiting: The thread automatically resumes after waiting for a period of time.
  • Terminated: The thread completes execution or an exception occurs, and execution is terminated.

4. Thread priority

Each thread has a priority level that tells the operating system scheduler how important the thread is. Thread priorities range from 1 to 10, with the default being 5. High-priority threads have more opportunities to obtain CPU execution time, but the absolute execution order is not guaranteed.

Thread thread = new Thread();
thread.setPriority(Thread.MAX_PRIORITY); // Set the highest priority of the thread

5. Thread synchronization

Multi-threaded programming often needs to deal with access issues of shared resources. In this case, thread synchronization needs to be used to ensure data consistency. Java provides the synchronized keyword and Lock interface to implement thread synchronization.

5.1 Using the synchronized keyword
class Counter {<!-- -->
    private int count = 0;

    public synchronized void increment() {<!-- -->
        count + + ;
    }

    public synchronized int getCount() {<!-- -->
        return count;
    }
}

public class Main {<!-- -->
    public static void main(String[] args) {<!-- -->
        Counter counter = new Counter();

        //Create multiple threads and start them
        for (int i = 0; i < 5; i + + ) {<!-- -->
            Thread thread = new Thread(() -> {<!-- -->
                for (int j = 0; j < 1000; j + + ) {<!-- -->
                    counter.increment();
                }
            });
            thread.start();
        }
    }
}

In the above example, we use the synchronized keyword to ensure the mutually exclusive execution of the increment() and getCount() methods, thus avoiding race conditions.

5.2 Using Lock interface

Using the Lock interface allows for more flexible thread synchronization operations:

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

class Counter {<!-- -->
    private int count = 0;
    private Lock lock = new ReentrantLock();

    public void increment() {<!-- -->
        lock.lock();
        try {<!-- -->
            count + + ;
        } finally {<!-- -->
            lock.unlock();
        }
    }

    public int getCount() {<!-- -->
        lock.lock();
        try {<!-- -->
            return count;
        } finally {<!-- -->
            lock.unlock();
        }
    }
}

public class Main {<!-- -->
    public static void main(String[] args) {<!-- -->
        Counter counter = new Counter();

        //Create multiple threads and start them
        for (int i = 0; i < 5; i + + ) {<!-- -->
            Thread thread = new Thread(() -> {<!-- -->
                for (int j = 0; j < 1000; j + + ) {<!-- -->
                    counter.increment();
                }
            });
            thread.start();
        }
    }
}

Using the Lock interface requires manual acquisition and release of locks, but provides more control and flexibility.

Conclusion

This part introduces the basic knowledge of multithreading in Java, including thread creation, life cycle, priority and thread synchronization. In the next section, we’ll continue to delve into more advanced multithreading topics, including thread pools, concurrent collections, and inter-thread communication. Read on to find out more.

Part 2: Advanced Multithreading

6. Thread pool

Thread pool is a mechanism for managing and reusing threads. It can effectively control the number of threads and reduce the overhead of thread creation and destruction. Java provides the java.util.concurrent.Executors class to create different types of thread pools. Here is an example using a thread pool:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {<!-- -->
    public static void main(String[] args) {<!-- -->
        ExecutorService executor = Executors.newFixedThreadPool(3); // Create a fixed-size thread pool

        for (int i = 0; i < 5; i + + ) {<!-- -->
            Runnable task = () -> {<!-- -->
                System.out.println("Thread " + Thread.currentThread().getId() + " is running.");
            };
            executor.execute(task); // Submit the task to the thread pool for execution
        }

        executor.shutdown(); // Close the thread pool
    }
}

Using a thread pool can avoid manual management of thread creation and destruction, improving code maintainability and performance.

7. Concurrent collection

Java provides some thread-safe concurrent collection classes, such as ConcurrentHashMap, CopyOnWriteArrayList, etc., which can safely access and modify data in a multi-threaded environment. Here is an example using ConcurrentHashMap:

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentMapExample {<!-- -->
    public static void main(String[] args) {<!-- -->
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        map.put("one", 1);
        map.put("two", 2);
        map.put("three", 3);

        //Concurrent and safe traversal
        map.forEach((key, value) -> {<!-- -->
            System.out.println(key + ": " + value);
        });
    }
}

These concurrent collection classes allow multiple threads to read data simultaneously without conflicts, but you still need to be aware of thread safety issues.

8. Communication between threads

Communication between threads is a key issue in multithreaded programming. Java provides some mechanisms to implement inter-thread communication, such as the wait(), notify() and notifyAll() methods and java The Condition interface in the .util.concurrent package. Here is an example using wait() and notify():

class Message {<!-- -->
    private String message;
    private boolean empty = true;

    public synchronized String read() {<!-- -->
        while (empty) {<!-- -->
            try {<!-- -->
                wait(); // Wait until message is available
            } catch (InterruptedException e) {<!-- -->
                e.printStackTrace();
            }
        }
        empty = true;
        notifyAll(); // Notify the producer that production can continue
        return message;
    }

    public synchronized void write(String message) {<!-- -->
        while (!empty) {<!-- -->
            try {<!-- -->
                wait(); // Wait until the message is consumed
            } catch (InterruptedException e) {<!-- -->
                e.printStackTrace();
            }
        }
        empty = false;
        this.message = message;
        notifyAll(); // Notify consumers that they can continue consuming
    }
}

public class ThreadCommunicationExample {<!-- -->
    public static void main(String[] args) {<!-- -->
        Message message = new Message();

        Thread producer = new Thread(() -> {<!-- -->
            String[] messages = {<!-- -->"Hello", "World", "Java"};
            for (String msg : messages) {<!-- -->
                message.write(msg);
            }
        });

        Thread consumer = new Thread(() -> {<!-- -->
            for (int i = 0; i < 3; i + + ) {<!-- -->
                System.out.println("Received: " + message.read());
            }
        });

        producer.start();
        consumer.start();
    }
}

This example shows how to use wait() and notify() to implement producer-consumer pattern inter-thread communication.

9. Thread safety and performance

In multi-threaded programming, thread safety is an important issue. Although thread synchronization can ensure data consistency, too many thread synchronization operations may reduce performance. Therefore, there is a trade-off between thread safety and performance when designing multi-threaded applications.

Conclusion

This part introduces advanced multithreading topics in Java, including thread pools, concurrent collections, and inter-thread communication. By learning more about these topics, you can better address the challenges of multithreaded programming and write efficient and reliable multithreaded applications.

Part 3: Advanced Multithreading Advances

10. Callable and Future

In addition to using Runnable to create threads, Java also provides the Callable interface, which allows a thread to execute a task with a return value and obtain the execution result of the task through a Future object. Here is an example using Callable and Future:

import java.util.concurrent.*;

public class CallableExample {<!-- -->
    public static void main(String[] args) throws InterruptedException, ExecutionException {<!-- -->
        Callable<Integer> task = () -> {<!-- -->
            //Perform time-consuming operations
            Thread.sleep(2000);
            return 42;
        };

        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<Integer> future = executor.submit(task);

        System.out.println("Task submitted, waiting for result...");
        Integer result = future.get(); // Block waiting for the task to complete and obtain the result
        System.out.println("Task result: " + result);

        executor.shutdown();
    }
}

Callable and Future make it more convenient to perform tasks with return values in a multi-threaded environment.

11. Volatile keyword

The Volatile keyword is used to declare a variable to be a “volatile” variable, which ensures visibility between multiple threads. When one thread modifies the value of a volatile variable, other threads can immediately see the change. Here is an example using volatile:

public class VolatileExample {<!-- -->
    private volatile boolean flag = false;

    public void toggleFlag() {<!-- -->
        flag = !flag;
    }

    public boolean isFlag() {<!-- -->
        return flag;
    }

    public static void main(String[] args) {<!-- -->
        VolatileExample example = new VolatileExample();

        Thread writerThread = new Thread(() -> {<!-- -->
            example.toggleFlag();
            System.out.println("Flag set to true");
        });

        Thread readerThread = new Thread(() -> {<!-- -->
            while (!example.isFlag()) {<!-- -->
                // Wait for flag to change to true
            }
            System.out.println("Flag is now true");
        });

        writerThread.start();
        readerThread.start();
    }
}

In this example, the volatile keyword ensures the visibility of the flag variable so that the reading thread can see the writing thread’s modifications in time.

12. Thread safety and performance optimization

Thread safety is a core issue in multi-threaded programming, but remember that too many thread synchronization operations will bring performance overhead. Therefore, when writing multi-threaded applications, thread safety and performance need to be weighed on a case-by-case basis.

Some common performance optimization strategies include:

  • Reduce lock granularity: Try to reduce the lock scope to the minimum necessary scope to reduce lock contention.
  • Use lock-free data structures: Some concurrent data structures such as ConcurrentHashMap and ConcurrentLinkedQueue use lock-free technology to provide better performance.
  • Use thread pool: Thread pool can reuse threads and reduce the overhead of thread creation and destruction.
  • Using volatile and CAS operations: In some cases, using the volatile keyword and CAS (Compare and Swap) operations can improve performance.

Conclusion

This part introduces advanced multi-threading topics in Java, including Callable and Future, Volatile keywords, and thread safety and performance optimization. By learning more about these topics, you can better address the challenges of multithreaded programming and write efficient and reliable multithreaded applications.

This article is just an introduction to multi-threaded programming. Multi-threaded programming also involves more complex topics, such as thread deadlocks, inter-thread communication patterns, etc. In-depth study and practice are the keys to mastering multi-threaded programming.