Java多线程学习(吐血超详细总结)

398 阅读10分钟

第一部分:Java线程的创建和启动

如何创建线程

在Java中,创建线程有两种常见的方法:

  1. 实现Runnable接口:创建一个实现了Runnable接口的类,并重写run方法。然后,将这个类的实例传递给Thread类的构造函数,并启动新线程。

  2. 继承Thread:创建一个继承自Thread的子类,并重写run方法。直接在子类中启动新线程。

如何启动线程

无论使用哪种方法创建线程,都需要调用start()方法来启动线程。start()方法会调用线程的run方法,从而开始线程的执行。

线程的生命周期

线程在Java中有一个明确的生命周期,包括以下几个状态:

  • 新建(New):线程对象被创建,但start()尚未被调用。
  • 可运行(Runnable):线程可以在JVM的调度下执行。这包括了就绪(Ready)和运行(Running)两种状态。
  • 阻塞(Blocked):线程因为某种原因无法执行,通常是等待获取锁。
  • 等待(Waiting):线程进入等待状态,直到另一个线程执行某个动作。
  • 超时等待(Timed Waiting):线程在一定时间后返回到可运行状态。
  • 终止(Terminated):线程执行完毕或者被中断。

案例源码:

// 实现Runnable接口创建线程
class MyRunnable implements Runnable {
    public void run() {
        System.out.println("Hello from a runnable!");
    }
}

public class ThreadCreationDemo {
    public static void main(String[] args) {
        // 使用Runnable接口创建线程
        Thread thread1 = new Thread(new MyRunnable());
        thread1.start(); // 启动线程

        // 继承Thread类创建线程
        Thread thread2 = new MyThread();
        thread2.start(); // 启动线程
    }
}

// 继承Thread类创建线程
class MyThread extends Thread {
    public void run() {
        System.out.println("Hello from a thread!");
    }
}

在实际开发中,通常推荐使用实现Runnable接口的方式来创建线程,因为这种方式更加灵活,也更符合面向对象的设计原则。继承Thread类会使得类的功能和线程的生命周期耦合在一起,这限制了类继承其他类的能力。

线程的生命周期对于理解线程调度和资源管理非常重要。了解线程在何时以及如何从一种状态转换到另一种状态,有助于编写出更加健壮和高效的多线程程序。

启动线程时,需要注意线程的优先级、守护线程(daemon thread)的使用,以及线程的中断(interruption)处理。这些特性对于控制线程的行为和资源的合理使用至关重要。

第二部分:线程同步和锁

线程安全的概念

线程安全是指在多线程环境中,代码能够正确地工作,不会出现数据不一致或者状态不可预测的问题。

同步方法和同步代码块

Java提供了两种同步机制来确保线程安全:

  1. 同步方法:使用synchronized关键字修饰方法,使得每次只有一个线程可以执行该方法。

  2. 同步代码块:使用synchronized关键字和锁对象,只同步代码的一部分,而不是整个方法。

synchronized关键字的使用

synchronized关键字可以用来修饰方法或者代码块,以确保线程同步。

案例源码:同步方法

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
}

案例源码:同步代码块

public class Counter {
    private int count = 0;
    private final Object lock = new Object();

    public void increment() {
        synchronized (lock) {
            count++;
        }
    }
}

显式锁:ReentrantLock

Java并发API提供了java.util.concurrent.locks.ReentrantLock,这是一个显式锁,它比synchronized关键字提供了更多的灵活性。

案例源码:

import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock();

    public void increment() {
        lock.lock(); // 显式获取锁
        try {
            count++;
        } finally {
            lock.unlock(); // 显式释放锁
        }
    }
}

线程同步是多线程编程中的核心问题之一。正确的同步策略可以避免竞态条件和数据不一致的问题。synchronized关键字是实现线程同步的一种简单而强大的方式,但它的使用也有一些限制,例如不能设置超时时间,也不能中断等待锁的线程。

相比之下,ReentrantLock提供了更高的灵活性。它允许开发者设置锁的超时时间,也支持在等待锁的过程中中断等待。此外,ReentrantLock可以被用作条件变量,这是synchronized关键字不支持的。

然而,显式锁的使用也带来了额外的复杂性,尤其是在异常处理和资源释放方面。不正确的使用可能导致锁无法释放,造成死锁。因此,在使用ReentrantLock时,需要特别小心,确保在所有可能的执行路径上都能够释放锁。

第三部分:线程间通信

线程间的共享资源

在多线程编程中,线程间通信通常涉及到对共享资源的访问。共享资源可以是内存、文件或者任何可以被多个线程访问的变量。

等待/通知机制:wait(), notify(), notifyAll()

Java提供了等待/通知机制来实现线程间的协调。这些机制通过Object类中的wait()notify()notifyAll()方法实现。

  • wait():使当前线程等待,直到另一个线程调用相同对象的notify()notifyAll()
  • notify():唤醒在此对象监视器上等待的单个线程。
  • notifyAll():唤醒在此对象监视器上等待的所有线程。

案例源码:

public class ThreadCommunicationDemo {
    private final Object lock = new Object();
    private boolean ready = false;

    public void thread1() {
        synchronized (lock) {
            while (!ready) {
                try {
                    lock.wait(); // 等待,直到被通知
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 执行后续操作
        }
    }

    public void thread2() {
        synchronized (lock) {
            ready = true;
            lock.notify(); // 通知一个等待的线程
        }
    }

    public static void main(String[] args) {
        ThreadCommunicationDemo demo = new ThreadCommunicationDemo();

        Thread t1 = new Thread(() -> demo.thread1());
        Thread t2 = new Thread(() -> demo.thread2());

        t1.start();
        t2.start();
    }
}

volatile关键字的使用

volatile关键字可以确保变量的读写操作对所有线程都是可见的,即当一个线程修改了一个volatile变量的值,其他线程能够立即看到这个改变。

案例源码:

public class VolatileDemo {
    private volatile boolean running = true;

    public void start() {
        new Thread(() -> {
            while (running) {
                // 执行任务
            }
        }).start();
    }

    public void stop() {
        running = false; // 改变状态,将被所有线程立即看到
    }
}

ThreadLocal的使用

ThreadLocal提供了线程内的局部变量,这些变量对于每个线程都是独立的,不会与其他线程共享。

案例源码:

public class ThreadLocalDemo {
    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        new Thread(() -> {
            threadLocal.set(10); // 每个线程设置自己的值
            System.out.println("Value in this thread: " + threadLocal.get());
        }).start();

        new Thread(() -> {
            threadLocal.set(20);
            System.out.println("Value in this thread: " + threadLocal.get());
        }).start();
    }
}

线程间通信是多线程编程中的一个关键概念,它允许线程协调它们的工作和共享数据。等待/通知机制是线程间通信的基础,它允许线程在特定条件下挂起,并在条件满足时被唤醒。

volatile关键字是一种轻量级同步机制,它适用于不需要原子操作的共享变量,如状态标志。它确保变量的读写操作对所有线程都是即时可见的,但并不保证复合操作的原子性。

ThreadLocal提供了一种线程安全的共享资源方式,每个线程可以访问到自己的副本,而不需要担心与其他线程发生冲突。这在处理线程特有数据时非常有用,如事务ID、用户会话等。

第四部分:Java并发API

java.util.concurrent包的介绍

java.util.concurrent包是Java并发编程的核心,它提供了一系列用于同步、线程安全集合、并发执行任务的工具类。

线程安全的集合

这个包提供了多种线程安全的集合类,可以在多线程环境中安全使用而不需要额外的同步措施。

  • ConcurrentHashMap:线程安全的HashMap实现。
  • ConcurrentLinkedQueue:线程安全的非阻塞队列。
  • BlockingQueue:支持阻塞操作的队列接口。

案例源码:

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;

public class ConcurrentCollectionDemo {
    public static void main(String[] args) {
        ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
        map.put("key1", "value1");
        map.put("key2", "value2");

        ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
        queue.offer("item1");
        queue.offer("item2");
    }
}

并发工具类

java.util.concurrent包还包含了一些并发工具类,用于简化并发任务的执行和管理。

  • ExecutorService:线程池接口,用于管理线程池和任务的执行。
  • Callable:与Runnable类似,但可以返回结果和抛出异常。
  • Future:表示异步计算的结果,可用于跟踪Callable任务的状态和结果。

案例源码:

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

public class ExecutorDemo {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        Future<String> future = executor.submit(() -> {
            Thread.sleep(1000);
            return "World";
        });

        System.out.println("Hello " + future.get());
        executor.shutdown();
    }
}
  • CountDownLatch:允许一个或多个线程等待一组事件。
  • CyclicBarrier:类似于CountDownLatch,但所有线程必须到达屏障点后才能继续执行。
  • Semaphore:用于控制同时访问特定资源的线程数量。

案例源码:使用CountDownLatch

import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {
    public static void main(String[] args) throws Exception {
        int threadCount = 5;
        CountDownLatch latch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " is running");
                try {
                    Thread.sleep((int) (Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown(); // 计数器减一
                }
            }).start();
        }

        latch.await(); // 等待所有线程完成
        System.out.println("All tasks completed");
    }
}

java.util.concurrent包极大地简化了Java中的并发编程。线程安全集合类减少了在多线程环境中手动同步集合操作的需要。并发工具类如ExecutorServiceCallableFuture提供了强大的异步任务执行和管理机制。

CountDownLatchCyclicBarrierSemaphore等同步辅助工具,为线程间的协调提供了灵活的控制手段,它们可以在复杂的多线程场景中实现精确的同步控制。

第五部分:线程池的使用和管理

线程池的基本概念和组件

线程池是一种执行器(Executor),用于在一个后台线程中执行任务。线程池的主要目的是减少在创建和销毁线程时所产生的性能开销。线程池的核心组件包括:

  • 线程工厂(ThreadFactory):用于创建新线程。
  • 拒绝策略(RejectedExecutionHandler):当任务太多,无法被线程池及时处理时,采取的策略。
  • 任务队列:用于存放待执行任务的阻塞队列。

如何创建线程池

Java提供了Executors类来创建预定义配置的线程池,或者使用ThreadPoolExecutor类来自定义线程池参数。

案例源码:使用Executors创建线程池

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadPoolExecutor;

public class ExecutorsDemo {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池
        ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(3);

        // 创建一个单线程的线程池,用于定时任务
        ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
    }
}

案例源码:使用ThreadPoolExecutor构造器创建线程池

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorDemo {
    public static void main(String[] args) {
        int corePoolSize = 2;
        int maximumPoolSize = 4;
        long keepAliveTime = 1L;
        TimeUnit unit = TimeUnit.MINUTES;
        LinkedBlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>();

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                workQueue
        );
    }
}

线程池的工作原理

线程池按照以下步骤工作:

  1. 线程池维护一定数量的线程(核心线程池)。
  2. 当提交一个任务时,线程池会尝试使用空闲的核心线程来执行任务。
  3. 如果核心线程都忙,任务会被放入任务队列等待。
  4. 如果任务队列满了,线程池会创建新的非核心线程来处理任务,直到达到最大线程数。
  5. 如果线程池满了且任务队列也满了,任务会被拒绝,线程池会调用拒绝策略。

线程池的调优和关闭

线程池的调优是生产环境中的重要环节,合理的参数设置可以提高程序的响应速度和资源利用率。

案例源码:线程池的关闭

import java.util.concurrent.ThreadPoolExecutor;

public class ThreadPoolShutdown {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(3);
        // 提交任务到线程池

        // 程序结束前,关闭线程池
        executor.shutdown(); // 非强制性关闭
        // executor.shutdownNow(); // 强制性关闭,尝试立即停止所有正在执行的任务
    }
}

线程池是多线程编程中的核心概念,它可以有效地管理和复用线程,提高程序的并发性能。通过合理地配置线程池参数,可以避免资源浪费,如线程创建和销毁的开销,以及过多的线程竞争导致的上下文切换开销。

线程池的关闭也是一个重要的环节。在应用程序关闭时,应该优雅地关闭线程池,以确保所有提交的任务都能够完成。shutdown()方法允许线程池在处理完队列中的任务后关闭,而shutdownNow()方法则尝试立即停止所有正在执行的任务,并返回等待执行的任务列表,以便开发者可以处理这些任务。