杰瑞科技汇

Java并发编程核心难点与实战技巧?

Of course! "Java Concurrent" refers to the rich set of classes and utilities in the Java Development Kit (JDK) designed for writing concurrent, multi-threaded applications. This is a vast and critical topic for building high-performance, scalable, and responsive applications in Java.

Java并发编程核心难点与实战技巧?-图1
(图片来源网络,侵删)

Let's break it down into a comprehensive guide, from the basics to advanced topics.


The "Why": Why is Concurrency Important?

Before diving into the "how," let's understand the motivation:

  • Performance: Modern computers have multiple CPU cores. Concurrency allows your application to perform multiple tasks in parallel, utilizing these cores and significantly speeding up computation-intensive operations.
  • Responsiveness: In applications with a graphical user interface (GUI) or backend services, concurrency prevents long-running tasks from blocking the main thread. This keeps the application responsive to user input.
  • Scalability: For server-side applications, concurrency allows you to handle many client requests simultaneously without creating a new, heavy OS process for each one.

The Foundation: Core Concepts

You can't use the tools effectively without understanding the underlying concepts.

a. Threads vs. Processes

  • Process: An independent program with its own memory space and resources. It's heavy to create and manage.
  • Thread: A lightweight subprocess, the smallest unit of execution within a process. Threads share the same memory space, making communication between them faster but also more error-prone.

b. The java.lang.Thread Class

This is the most basic way to create a thread in Java.

Java并发编程核心难点与实战技巧?-图2
(图片来源网络,侵删)

Example:

// 1. Extend the Thread class
class MyThread extends Thread {
    @Override
    public void run() {
        // Code that this thread will execute
        System.out.println("Thread running: " + Thread.currentThread().getName());
    }
}
// 2. Implement the Runnable interface (more flexible)
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable running: " + Thread.currentThread().getName());
    }
}
public class BasicThreadExample {
    public static void main(String[] args) {
        // Using the Thread class
        Thread thread1 = new MyThread();
        thread1.start(); // Don't call run() directly!
        // Using the Runnable interface
        Thread thread2 = new Thread(new MyRunnable());
        thread2.start();
        // Using a lambda expression (modern Java)
        Thread thread3 = new Thread(() -> {
            System.out.println("Lambda running: " + Thread.currentThread().getName());
        });
        thread3.start();
    }
}

Key Point: Always call thread.start(). This tells the JVM to create a new OS-level thread and execute its run() method. Calling run() directly just executes the code on the current thread.


The Modern Approach: The java.util.concurrent Package

The java.lang.Thread class is low-level and error-prone. The java.util.concurrent package (introduced in Java 5) provides high-level, safer, and more powerful abstractions. This is the recommended way to write concurrent code in modern Java.

a. The Executor Framework

Instead of manually creating and managing threads, you use an Executor service to manage a pool of threads for you. This is much more efficient.

Java并发编程核心难点与实战技巧?-图3
(图片来源网络,侵删)

Key Components:

  • Executor: An interface with a single method execute(Runnable).
  • ExecutorService: A sub-interface that adds lifecycle management (shutdown(), shutdownNow()).
  • Executors: A utility class for creating pre-configured ExecutorService instances.

Example: Using a Thread Pool

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorExample {
    public static void main(String[] args) {
        // Create a thread pool with 2 threads
        ExecutorService executor = Executors.newFixedThreadPool(2);
        // Submit 5 tasks to the executor
        for (int i = 0; i < 5; i++) {
            final int taskId = i;
            executor.execute(() -> {
                System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName());
                try {
                    // Simulate work
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }
        // Shut down the executor. It will no longer accept new tasks.
        // It will wait for running tasks to complete before shutting down.
        executor.shutdown();
    }
}

Synchronization and Shared Data: The Biggest Challenge

When multiple threads access shared data, you can get race conditions, leading to inconsistent and incorrect results. You need mechanisms to control access.

a. The synchronized Keyword

This is the most basic form of locking in Java.

  1. Synchronized Method: The entire method is locked. Only one thread can execute any synchronized method of an object at a time.
  2. Synchronized Block: A more granular way to lock. You specify the object to lock on. This is preferred as it minimizes the amount of code that is locked.
class Counter {
    private int count = 0;
    // Method 1: Synchronized method
    public synchronized void increment() {
        count++;
    }
    // Method 2: Synchronized block (often better)
    public void incrementWithBlock() {
        synchronized(this) { // 'this' is the lock object
            count++;
        }
    }
    public int getCount() {
        return count;
    }
}

b. Low-Level Locks: ReentrantLock

The java.util.concurrent.locks.ReentrantLock class provides more advanced features than synchronized:

  • Fairness: You can create a "fair" lock that grants access to the longest-waiting thread.
  • Timeout: tryLock() can attempt to acquire the lock and return false if it can't after a certain time, avoiding indefinite blocking.
  • Lock and Unlock: Explicit control over the lock's lifecycle.
import java.util.concurrent.locks.ReentrantLock;
class CounterWithLock {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock();
    public void increment() {
        lock.lock(); // Acquire the lock
        try {
            count++;
        } finally {
            lock.unlock(); // Always release the lock in a finally block!
        }
    }
    // ...
}

High-Level Concurrency Utilities

These are the workhorses of the java.util.concurrent package.

a. Atomic Variables (java.util.concurrent.atomic)

Classes like AtomicInteger, AtomicLong, and AtomicReference provide lock-free, thread-safe operations on single variables. They use highly efficient low-level CPU instructions (like compare-and-swap) for better performance than using synchronized.

import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter {
    private final AtomicInteger count = new AtomicInteger(0);
    public void increment() {
        count.incrementAndGet(); // Atomic operation
    }
    public int getCount() {
        return count.get();
    }
}

b. Concurrent Collections

Standard Java collections (like HashMap, ArrayList) are not thread-safe. The java.util.concurrent package provides high-performance, thread-safe alternatives.

Non-Thread-Safe Concurrent Alternative Key Feature
HashMap ConcurrentHashMap High performance for read/write operations.
ArrayList CopyOnWriteArrayList Excellent for scenarios with many reads and few writes.
HashSet ConcurrentHashMap (using newKeySet()) or CopyOnWriteArraySet Thread-safe sets.
Hashtable ConcurrentHashMap ConcurrentHashMap is generally faster as it allows concurrent reads.

c. Synchronizers

These are utilities for coordinating the progress of threads.

  • CountDownLatch: Allows one or more threads to wait for a set of operations to complete.

    • Use Case: Waiting for several initialization tasks to finish before starting the main application logic.
  • CyclicBarrier: Allows a set of threads to wait for each other to reach a common barrier point.

    • Use Case: Parallelizing a large task into smaller pieces. When all pieces are done, they can proceed to the next stage.
  • Semaphore: Controls the number of threads that can access a resource simultaneously.

    • Use Case: Limiting the number of connections to a database or the number of users that can access a critical section.
  • Phaser: A more flexible and reusable version of CyclicBarrier.

    • Use Case: Complex multi-stage tasks where the number of participating threads can change over time.

The Memory Model: volatile and final

This is a more advanced but crucial topic.

a. The volatile Keyword

A volatile variable has two key properties:

  1. Visibility: When one thread writes to a volatile variable, all other threads immediately see the new value. It prevents a thread from seeing a "stale" value cached in its own CPU cache.
  2. Happens-Before: The write to a volatile variable happens-before any subsequent read of that same variable. This establishes a memory ordering guarantee.

Use Case: For flags or simple status variables where you don't need compound atomic operations (like i++).

class Worker implements Runnable {
    private volatile boolean isRunning = true;
    public void stop() {
        isRunning = false;
    }
    @Override
    public void run() {
        while (isRunning) {
            // do work
        }
        System.out.println("Worker stopped.");
    }
}

b. The final Keyword and Safe Publication

In Java, an object is "safe published" if other threads can see it in a consistent state without needing synchronization. Making an object's fields final helps achieve this.

For an object to be safely published via a reference:

  1. The reference to the object must be either volatile or final.
  2. Or, the reference must be safely published through a synchronized block.

Best Practices and Pitfalls

  • Prefer java.util.concurrent over synchronized: Use high-level abstractions (ExecutorService, ConcurrentHashMap, AtomicInteger) whenever possible. They are easier to use and often perform better.
  • Minimize Synchronized Blocks: Keep the critical section (the code inside synchronized or lock()) as small as possible.
  • Avoid Deadlocks: A deadlock occurs when two or more threads are blocked forever, each waiting for the other to release a lock.
    • Prevention: Always acquire locks in a consistent, global order. Use tryLock() with timeouts.
  • Beware of Hidden Iterators: Modifying a collection (like ArrayList or HashMap) while another thread is iterating over it will cause a ConcurrentModificationException. Use CopyOnWriteArrayList or synchronize on the collection's monitor.
  • Don't new a Thread for every task: Use a thread pool (ExecutorService) to manage thread lifecycle and reuse threads.
  • Use volatile for simple flags: It's cheaper than a full lock for visibility-only purposes.

Summary of Key APIs

Category Key Classes/Interfaces Purpose
Execution ExecutorService, Executors Manage a pool of threads.
Locking ReentrantLock, synchronized Control access to shared resources.
Atomicity AtomicInteger, AtomicReference Lock-free thread-safe variables.
Collections ConcurrentHashMap, CopyOnWriteArrayList Thread-safe data structures.
Coordination CountDownLatch, CyclicBarrier, Semaphore Coordinate thread execution.
Memory volatile, final Ensure visibility and safe publication.

Mastering Java concurrency is a journey. Start with the basics of threads, then move to the java.util.concurrent package, and finally dive into the memory model. Practice and careful design are key to writing robust and high-performance concurrent applications.

分享:
扫描分享到社交APP
上一篇
下一篇