MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java实现多线程的实用途径

2023-07-254.3k 阅读

一、Java 多线程基础概念

在深入探讨 Java 实现多线程的实用途径之前,我们先来回顾一些基础概念。线程是程序执行流的最小单元,一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件描述符等。在 Java 中,多线程编程使得程序能够同时执行多个任务,提高了程序的效率和响应性。

(一)线程的生命周期

Java 线程具有以下几种状态,这些状态构成了线程的生命周期:

  1. 新建(New):当创建一个 Thread 实例时,线程处于新建状态。例如:
Thread thread = new Thread();

此时线程还未开始执行。 2. 就绪(Runnable):调用 start() 方法后,线程进入就绪状态。在这个状态下,线程等待 CPU 分配时间片,一旦获得 CPU 资源,就可以执行。

thread.start();
  1. 运行(Running):线程获取到 CPU 时间片后开始执行 run() 方法中的代码,此时线程处于运行状态。
  2. 阻塞(Blocked):线程由于某些原因暂时无法继续执行,进入阻塞状态。例如,线程在等待 I/O 操作完成、等待获取锁等情况时会进入阻塞状态。
  3. 等待(Waiting):线程通过调用 Object 类的 wait() 方法、Thread 类的 join() 方法等进入等待状态。处于等待状态的线程需要其他线程唤醒才能继续执行。
  4. 超时等待(Timed Waiting):与等待状态类似,但有一个超时时间。例如,调用 Thread.sleep(long millis) 方法,线程会进入超时等待状态,在指定的时间过去后,线程会自动唤醒。
  5. 终止(Terminated):线程执行完 run() 方法或者因异常退出,就进入终止状态,此时线程生命结束。

(二)线程的优先级

Java 线程有优先级的概念,优先级范围从 1(Thread.MIN_PRIORITY)到 10(Thread.MAX_PRIORITY),默认优先级为 5(Thread.NORM_PRIORITY)。优先级较高的线程在竞争 CPU 资源时会有更大的优势,但这并不意味着优先级高的线程一定会先执行完,因为 CPU 调度算法是复杂的,而且不同操作系统的实现也有所不同。可以通过 setPriority(int newPriority) 方法来设置线程的优先级,通过 getPriority() 方法获取线程的优先级。例如:

Thread highPriorityThread = new Thread(() -> {
    // 线程执行代码
});
highPriorityThread.setPriority(Thread.MAX_PRIORITY);

二、Java 实现多线程的经典途径

(一)继承 Thread 类

这是最直接的实现多线程的方式。通过继承 Thread 类,并重写其 run() 方法来定义线程的执行逻辑。

  1. 示例代码
class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " 正在执行,i = " + i);
        }
    }
}

public class ThreadInheritanceExample {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " 正在执行,i = " + i);
        }
    }
}

在上述代码中,MyThread 类继承自 Thread 类,并重写了 run() 方法。在 main 方法中,创建了 MyThread 的实例并调用 start() 方法启动线程。主线程和新创建的线程会并发执行,输出相应的信息。 2. 优缺点分析 - 优点:实现简单,代码结构清晰,对于简单的多线程任务易于理解和实现。 - 缺点:Java 不支持多重继承,如果一个类已经继承了其他类,就无法再继承 Thread 类。这在一定程度上限制了其使用场景。

(二)实现 Runnable 接口

实现 Runnable 接口也是常用的实现多线程的方式。这种方式将线程执行逻辑与线程对象分离,更符合面向对象的设计原则。

  1. 示例代码
class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " 正在执行,i = " + i);
        }
    }
}

public class RunnableImplementationExample {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " 正在执行,i = " + i);
        }
    }
}

在这个例子中,MyRunnable 类实现了 Runnable 接口,并重写了 run() 方法。然后在 main 方法中,创建 MyRunnable 的实例,并将其作为参数传递给 Thread 类的构造函数来创建线程对象,最后调用 start() 方法启动线程。 2. 优缺点分析 - 优点:一个类可以实现多个接口,因此避免了单继承的限制,更灵活。同时,将线程执行逻辑与线程对象分离,使得代码的可维护性和扩展性更好。 - 缺点:相比继承 Thread 类,代码稍微复杂一些,需要创建 Thread 对象并传入 Runnable 实例。

(三)实现 Callable 接口

Callable 接口与 Runnable 接口类似,但 Callable 接口的 call() 方法可以有返回值,并且可以抛出异常。这使得在多线程编程中可以获取线程执行的结果。

  1. 示例代码
import java.util.concurrent.*;

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}

public class CallableImplementationExample {
    public static void main(String[] args) {
        MyCallable myCallable = new MyCallable();
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<Integer> future = executorService.submit(myCallable);
        try {
            Integer result = future.get();
            System.out.println("线程执行结果:" + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        } finally {
            executorService.shutdown();
        }
    }
}

在上述代码中,MyCallable 类实现了 Callable 接口,call() 方法计算 1 到 100 的累加和并返回。在 main 方法中,使用 ExecutorService 来提交 Callable 任务,并通过 Future 获取任务的执行结果。 2. 优缺点分析 - 优点:能够获取线程执行的返回值,适用于需要得到线程执行结果的场景。同时,异常处理更加灵活,可以在调用处捕获 call() 方法抛出的异常。 - 缺点:使用起来相对复杂,需要结合 ExecutorServiceFuture 等类来管理线程和获取结果,对于简单的多线程任务可能显得过于繁琐。

三、使用线程池实现多线程

(一)线程池的概念

线程池是一种多线程处理形式,它维护着一个线程队列,线程池中的线程可以被重复使用来执行任务。当有新任务提交时,线程池会从线程队列中取出一个空闲线程来执行任务,如果线程队列中没有空闲线程,新任务会被放入任务队列等待执行。使用线程池可以避免频繁创建和销毁线程带来的开销,提高系统性能和资源利用率。

(二)Java 中的线程池实现

  1. 使用 Executors 工厂类创建线程池
    • 固定大小线程池(FixedThreadPool)
      • 示例代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FixedThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 5; i++) {
            int taskNumber = i;
            executorService.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " 正在执行任务 " + taskNumber);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        executorService.shutdown();
    }
}
  - **原理**:`Executors.newFixedThreadPool(int nThreads)` 方法创建一个固定大小的线程池,池中线程数量固定为 `nThreads`。当提交的任务数量超过线程池的大小,多余的任务会被放入任务队列等待执行。
- **缓存线程池(CachedThreadPool)**:
  - **示例代码**:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CachedThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            int taskNumber = i;
            executorService.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " 正在执行任务 " + taskNumber);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        executorService.shutdown();
    }
}
  - **原理**:`Executors.newCachedThreadPool()` 方法创建一个缓存线程池。该线程池没有固定大小,当有新任务提交时,如果线程池中有空闲线程则复用,否则创建新线程。线程池中的线程如果 60 秒内没有被使用,会被回收。
- **单线程化线程池(SingleThreadExecutor)**:
  - **示例代码**:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SingleThreadExecutorExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 5; i++) {
            int taskNumber = i;
            executorService.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " 正在执行任务 " + taskNumber);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        executorService.shutdown();
    }
}
  - **原理**:`Executors.newSingleThreadExecutor()` 方法创建一个单线程的线程池。所有任务会按照提交顺序依次在这个单线程中执行,保证了任务的顺序性。

2. 使用 ThreadPoolExecutor 类自定义线程池 ThreadPoolExecutor 类提供了更灵活的方式来创建线程池,可以自定义线程池的各种参数。

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

public class CustomThreadPoolExample {
    public static void main(String[] args) {
        BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>(10);
        ThreadPoolExecutor executorService = new ThreadPoolExecutor(
                2,
                4,
                10,
                TimeUnit.SECONDS,
                taskQueue
        );
        for (int i = 0; i < 15; i++) {
            int taskNumber = i;
            executorService.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " 正在执行任务 " + taskNumber);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        executorService.shutdown();
    }
}

在上述代码中,通过 ThreadPoolExecutor 的构造函数创建了一个自定义线程池。构造函数参数解释如下: - corePoolSize:核心线程数,线程池会一直维护这么多线程,即使它们处于空闲状态。 - maximumPoolSize:最大线程数,线程池允许创建的最大线程数。 - keepAliveTime:当线程数大于核心线程数时,多余的空闲线程的存活时间。 - unit:存活时间的单位。 - workQueue:任务队列,用于存放提交但未执行的任务。

(三)线程池的优点和适用场景

  1. 优点
    • 提高性能:避免了频繁创建和销毁线程的开销,提高了线程的复用性,从而提高系统性能。
    • 资源控制:可以控制线程池的大小,避免线程过多导致系统资源耗尽。同时,通过任务队列可以控制任务的数量,避免任务过多积压。
    • 便于管理:线程池提供了统一的管理方式,如线程的生命周期管理、任务的提交和执行等,使得多线程编程更加规范和易于维护。
  2. 适用场景
    • 高并发场景:如 Web 服务器处理大量用户请求,使用线程池可以有效处理并发请求,提高系统的吞吐量。
    • 重复性任务:对于一些需要重复执行的任务,如定时任务、批量数据处理等,使用线程池可以提高效率。

四、Java 多线程中的同步与通信

(一)线程同步的必要性

在多线程编程中,当多个线程同时访问共享资源时,可能会出现数据不一致的问题。例如,多个线程同时对一个共享变量进行读写操作,可能导致数据的错误更新。为了保证数据的一致性和完整性,需要进行线程同步。

(二)使用 synchronized 关键字实现同步

  1. 同步方法:在方法声明中加上 synchronized 关键字,使得该方法在同一时间只能被一个线程访问。
class SynchronizedExample {
    private int count = 0;

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

    public synchronized int getCount() {
        return count;
    }
}

在上述代码中,increment()getCount() 方法都被声明为同步方法。当一个线程调用 increment() 方法时,其他线程无法同时调用这两个同步方法,从而保证了 count 变量的操作是线程安全的。 2. 同步块:使用 synchronized 关键字修饰代码块,对特定的代码区域进行同步。

class SynchronizedBlockExample {
    private int count = 0;

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

    public int getCount() {
        synchronized (this) {
            return count;
        }
    }
}

在这个例子中,increment()getCount() 方法通过同步块对 count 变量的操作进行同步。同步块的锁对象为 this,即当前实例对象。

(三)使用 Lock 接口实现同步

Lock 接口提供了比 synchronized 关键字更灵活的同步控制。Lock 接口的实现类如 ReentrantLock 提供了可中断的锁获取、公平锁等功能。

  1. 示例代码
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class LockExample {
    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();
        }
    }
}

在上述代码中,ReentrantLock 用于同步对 count 变量的操作。lock() 方法用于获取锁,unlock() 方法用于释放锁,为了确保锁一定被释放,通常将 unlock() 方法放在 finally 块中。

(四)线程通信

  1. 使用 wait()、notify() 和 notifyAll() 方法:这三个方法是 Object 类的方法,用于线程间的通信。wait() 方法使当前线程等待,直到其他线程调用 notify()notifyAll() 方法唤醒它。
class ThreadCommunicationExample {
    private static final Object lock = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("线程1获取到锁,开始等待");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程1被唤醒");
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("线程2获取到锁,开始工作");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock.notify();
                System.out.println("线程2唤醒其他线程");
            }
        });

        thread1.start();
        thread2.start();
    }
}

在上述代码中,thread1 调用 lock.wait() 方法进入等待状态,释放锁。thread2 执行一段时间后调用 lock.notify() 方法唤醒 thread1。 2. 使用 Condition 接口Condition 接口是 Lock 接口的配套接口,提供了更灵活的线程通信方式。Condition 可以创建多个等待队列,而 Objectwait()notify() 方法只能使用一个等待队列。

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

class ConditionExample {
    private static final Lock lock = new ReentrantLock();
    private static final Condition condition = lock.newCondition();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("线程1获取到锁,开始等待");
                condition.await();
                System.out.println("线程1被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });

        Thread thread2 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("线程2获取到锁,开始工作");
                Thread.sleep(2000);
                condition.signal();
                System.out.println("线程2唤醒其他线程");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });

        thread1.start();
        thread2.start();
    }
}

在这个例子中,thread1 通过 condition.await() 方法进入等待状态,thread2 执行一段时间后通过 condition.signal() 方法唤醒 thread1

五、Java 多线程中的并发工具类

(一)CountDownLatch

CountDownLatch 是一个同步工具类,它允许一个或多个线程等待其他线程完成操作。CountDownLatch 初始化一个计数器,线程可以调用 await() 方法等待计数器归零,其他线程可以调用 countDown() 方法将计数器减一。

  1. 示例代码
import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) {
        int threadCount = 3;
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    Thread.sleep((long) (Math.random() * 2000));
                    System.out.println(Thread.currentThread().getName() + " 完成任务");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            }).start();
        }

        try {
            System.out.println("主线程等待所有子线程完成任务");
            countDownLatch.await();
            System.out.println("所有子线程任务完成,主线程继续执行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,CountDownLatch 初始化计数器为 3。三个子线程分别执行任务,完成后调用 countDown() 方法将计数器减一。主线程调用 await() 方法等待计数器归零,即所有子线程任务完成后才继续执行。

(二)CyclicBarrier

CyclicBarrier 也是一个同步工具类,它允许一组线程相互等待,直到所有线程都到达某个屏障点。与 CountDownLatch 不同的是,CyclicBarrier 的计数器可以重置,即可以重复使用。

  1. 示例代码
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        int threadCount = 3;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(threadCount, () -> {
            System.out.println("所有线程都到达屏障点,开始执行后续任务");
        });

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    Thread.sleep((long) (Math.random() * 2000));
                    System.out.println(Thread.currentThread().getName() + " 到达屏障点");
                    cyclicBarrier.await();
                    System.out.println(Thread.currentThread().getName() + " 继续执行");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

在这个例子中,CyclicBarrier 初始化时设置参与的线程数为 3,并传入一个屏障动作。当所有线程调用 await() 方法到达屏障点时,会执行屏障动作,然后所有线程继续执行。

(三)Semaphore

Semaphore 是一个计数信号量,它通过一个计数器来控制同时访问某个资源的线程数量。当一个线程获取信号量时,如果计数器大于 0,则计数器减一,线程可以继续执行;如果计数器为 0,则线程会被阻塞,直到其他线程释放信号量(计数器加一)。

  1. 示例代码
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    public static void main(String[] args) {
        int permits = 2;
        Semaphore semaphore = new Semaphore(permits);

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + " 获取到信号量,开始执行任务");
                    Thread.sleep((long) (Math.random() * 2000));
                    System.out.println(Thread.currentThread().getName() + " 任务执行完毕,释放信号量");
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

在上述代码中,Semaphore 初始化允许的信号量数量为 2,即最多允许两个线程同时获取信号量并执行任务。其他线程需要等待有线程释放信号量后才能获取并执行任务。

通过以上多种方式,在 Java 编程中能够有效地实现多线程,并处理多线程编程中遇到的同步、通信以及资源管理等问题,从而编写出高效、稳定的多线程应用程序。不同的实现途径和工具类适用于不同的场景,开发者需要根据具体需求进行选择和组合使用。