In-depth understanding of JUC concurrent programming: comprehensive analysis from entry to advanced applications

As programmers, we often need to deal with the problem of concurrent programming, but the nature of concurrent programming is very complicated. Java concurrent programming can be said to be a very large and important field, and JUC (Java Util Concurrent) is the cornerstone of Java concurrent programming.

So, what is JUC concurrent programming? JUC is an extended library of concurrent programming introduced in Java 5, the purpose of which is to realize concurrent programming more conveniently, quickly and safely. It provides a series of tool classes, locks, queues, and atomic classes to coordinate operations between multiple threads. These tools can help us effectively manage shared resources and control thread execution.

Next, we will gain an in-depth understanding of JUC concurrent programming and explore how to use it to solve practical problems.

1. The basic concept of JUC

  1. thread

In Java, a thread is the smallest unit of executing a program, and multithreading can realize the effect of executing multiple tasks at the same time. In JUC, we can use the thread pool to effectively control the number and scheduling of threads.

  1. Lock

In multi-threaded operations, there are concurrency issues, and locks can be used to ensure that threads' access to shared resources is mutually exclusive. In JUC, ReentrantLock is the most commonly used lock implementation, which has the characteristics of reentrant, fair/unfair, interruptible and timeout.

  1. atomic operation

An atomic operation is an entire operation that is indivisible and cannot be interrupted by other threads, and can be used to deal with thread safety issues. JUC provides a series of atomic operation classes, such as AtomicInteger, AtomicLong, etc.

  1. asynchronous programming

Asynchronous programming is a programming mode implemented through callback, Future/Promise, etc., which can improve the response speed and resource utilization of the program. CompletableFuture class is provided in JUC, which can realize asynchronous programming conveniently.

2. JUC common components and their examples

  1. Thread Pool

The thread pool is a way to manage threads, which can avoid performance problems caused by frequent creation and destruction of threads. In JUC, we can use the ThreadPoolExecutor class to implement the thread pool.

public class ThreadPoolDemo {
    
    
    public static void main(String[] args) {
    
    
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 10; i++) {
    
    
            executorService.execute(() -> {
    
    
                System.out.println(Thread.currentThread().getName() + " running...");
            });
        }

        executorService.shutdown();
    }
}

In the above example, we created a thread pool with a fixed size of 5, submitted 10 tasks to it, and each task will print out the name of the current thread. Finally, the shutdown() method is called to close the thread pool.

  1. Lock

Lock is a commonly used means of concurrent programming, which can control the access of concurrent threads to shared resources. In JUC, we can use the ReentrantLock class to implement the lock mechanism.

public class LockDemo {
    
    
    private final Lock lock = new ReentrantLock();

    public void lockMethod() {
    
    
        lock.lock();

        try {
    
    
            System.out.println(Thread.currentThread().getName() + " acquire lock");
            // do some job...
        } finally {
    
    
            lock.unlock();
            System.out.println(Thread.currentThread().getName() + " release lock");
        }
    }
}

In the above example, we create a lock instance, and in the lockMethod() method first acquire the lock, perform some tasks, and finally release the lock.

  1. atomic class

Atomic classes are a thread-safe method that can ensure data consistency and correctness in a multi-threaded environment. In JUC, atomic classes can be implemented using AtomicInteger, AtomicLong, etc.

public class AtomicDemo {
    
    
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
    
    
        count.incrementAndGet();
    }

    public int getCount() {
    
    
        return count.get();
    }
}

In the above example, we create an AtomicInteger instance, call the incrementAndGet() method to perform the atomic self-increment operation, and call the get() method to obtain the current value.

  1. CompletableFuture

CompletableFuture is an asynchronous programming tool class in JUC, which can conveniently realize asynchronous calling and processing. It supports both serial execution and parallel execution.

public class CompletableFutureDemo {
    
    
    public static void main(String[] args) {
    
    
        CompletableFuture.supplyAsync(() -> "Hello ")
                .thenApplyAsync(value -> value + "World ")
                .thenAcceptAsync(System.out::println);
    }
}

In the above example, we use the supplyAsync() method to create an asynchronous task, call the thenApplyAsync() method to implement serial operations, and finally call the thenAcceptAsync() method to process the result.

3. In-depth understanding of the key to JUC concurrent programming

When using JUC concurrent programming, you need to pay attention to the following key points:

  1. atomicity

Atomicity is one of the most basic goals of concurrent programming. It ensures that multiple threads access shared resources atomically. In JUC, there are many atomic classes that can help us achieve atomic access.

  1. visibility

Visibility means that when multiple threads access a shared resource, they can see the modifications made by other threads to the shared resource. In JUC, visibility can be achieved by using volatile keyword and Atomic class etc.

  1. orderliness

Order means that the order in which the program is executed is consistent with the order in the program code. In JUC, we can use synchronized, ReentrantLock, etc. to ensure orderliness.

In the process of learning JUC concurrent programming, it is necessary to deeply understand the above key points and flexibly apply them to actual business in order to truly avoid various pitfalls in Java concurrent programming.

4. Case application of JUC concurrent programming

  1. producer consumer problem

The producer-consumer problem is a classic concurrent programming problem, which can be implemented with blocking queues in JUC.

public class ProducerConsumerDemo {
    
    
    private static int MAX_CAPACITY = 10;
    private static ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(MAX_CAPACITY);

    private static class Producer implements Runnable {
    
    
        @Override
        public void run() {
    
    
            while (true) {
    
    
                try {
    
    
                    queue.put(produce());
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        }

        private Integer produce() {
    
    
            int num = new Random().nextInt(100);
            System.out.println("Producing: " + num);
            return num;
        }
    }

    private static class Consumer implements Runnable {
    
    
        @Override
        public void run() {
    
    
            while (true) {
    
    
                try {
    
    
                    consume(queue.take());
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        }

        private void consume(Integer num) {
    
    
            System.out.println("Consuming: " + num);
        }
    }

    public static void main(String[] args) {
    
    
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        executorService.execute(new Producer());
        executorService.execute(new Consumer());

        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

        executorService.shutdown();
    }
}

In the above example, we use ArrayBlockingQueue to implement the producer consumer pattern. The producer continuously generates random numbers and puts them in the blocking queue, and the consumer takes elements from the queue and consumes them.

  1. Starbucks Coffee Order Simulation

Starbucks coffee order simulation is an interesting case of JUC concurrent programming, which can simulate the process of Starbucks coffee order generation, processing and completion.

public class StarbucksDemo {
    
    
    private static Queue<Integer> orderQueue = new LinkedList<>();
    private static int MAX_ORDERS = 10;
    private static AtomicInteger orderNumber = new AtomicInteger(0);

    private static class Customer implements Runnable {
    
    
        @Override
        public void run() {
    
    
            while (orderNumber.get() < MAX_ORDERS) {
    
    
                System.out.println(Thread.currentThread().getName() + " is ordering...");
                try {
    
    
                    placeOrder();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        }

        private void placeOrder() throws InterruptedException {
    
    
            synchronized (orderQueue) {
    
    
                while (orderQueue.size() == MAX_ORDERS) {
    
    
                    System.out.println("Order queue is full, " + Thread.currentThread().getName() + " is waiting...");
                    orderQueue.wait();
                }
                int order = orderNumber.incrementAndGet();
                System.out.println(Thread.currentThread().getName() + " placed order #" + order);
                orderQueue.offer(order);
                orderQueue.notifyAll();
            }
        }
    }

    private static class Barista implements Runnable {
    
    
        @Override
        public void run() {
    
    
            while (orderNumber.get() <= MAX_ORDERS || !orderQueue.isEmpty()) {
    
    
                System.out.println(Thread.currentThread().getName() + " is preparing drink...");
                try {
    
    
                    prepareDrink();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        }

        private void prepareDrink() throws InterruptedException {
    
    
            synchronized (orderQueue) {
    
    
                while (orderQueue.isEmpty()) {
    
    
                    System.out.println("Order queue is empty, " + Thread.currentThread().getName() + " is waiting...");
                    orderQueue.wait();
                }
                int order = orderQueue.poll();
                System.out.println(Thread.currentThread().getName() + " is preparing drink for order #" + order);
                orderQueue.notifyAll();
            }
            Thread.sleep(5000);
            System.out.println(Thread.currentThread().getName() + " has prepared drink for order #" + orderNumber);
        }
    }

    public static void main(String[] args) {
    
    
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 3; i++) {
    
    
            executorService.execute(new Customer());
        }
        for (int i = 0; i < 2; i++) {
    
    
            executorService.execute(new Barista());
        }

        try {
    
    
            Thread.sleep(30000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

        executorService.shutdown();
    }
}

In the above example, we used a queue to represent the order queue, the customer thread keeps placing orders into the queue, and the beverage master thread takes the order from the queue and prepares the drinks.

Since the order queue is a shared resource, the synchronized keyword needs to be used for synchronization control, and wait() and notifyAll() are used to block and wake up threads. This case also involves some details of multi-threaded concurrency issues, such as thread priority, thread state, thread safety and inter-thread cooperation, etc., which is very suitable for the practice and learning of JUC concurrent programming.

Guess you like

Origin blog.csdn.net/canshanyin/article/details/130632471