一、引言
在现代软件开发中,多线程与并发编程是至关重要的技术。随着计算机硬件的发展,多核处理器已经成为主流,为了充分利用多核处理器的性能,提高程序的执行效率和响应速度,多线程与并发编程变得越来越重要。Java 作为一种广泛使用的编程语言,提供了丰富的多线程与并发编程支持,本文将详细介绍 Java 中的多线程与并发技术。
二、多线程基础
2.1 线程的概念
线程是程序执行的最小单位,一个进程可以包含多个线程。每个线程都有自己的执行路径和栈空间,多个线程可以并发执行,从而提高程序的执行效率。
2.2 创建线程的方式
在 Java 中,有两种主要的方式来创建线程:继承
Thread
类和实现Runnable
接口。
2.2.1 继承 Thread
类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running.");
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
在上述代码中,我们创建了一个继承自 Thread
类的 MyThread
类,并重写了 run
方法。在 main
方法中,我们创建了 MyThread
类的实例,并调用 start
方法启动线程。
2.2.2 实现 Runnable
接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable is running.");
}
}
public class RunnableExample {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
在上述代码中,我们创建了一个实现 Runnable
接口的 MyRunnable
类,并重写了 run
方法。在 main
方法中,我们创建了 MyRunnable
类的实例,并将其作为参数传递给 Thread
类的构造函数,然后调用 start
方法启动线程。
2.3 线程的生命周期
线程的生命周期包括以下几个状态:
- 新建(New):当创建一个线程对象时,线程处于新建状态。
- 就绪(Runnable):当调用线程的
start
方法后,线程进入就绪状态,等待 CPU 调度。 - 运行(Running):当 CPU 调度该线程时,线程进入运行状态,执行
run
方法中的代码。 - 阻塞(Blocked):线程在某些情况下会进入阻塞状态,例如等待 I/O 操作、等待锁等。
- 死亡(Terminated):当线程的
run
方法执行完毕或者发生异常时,线程进入死亡状态。
2.4 线程的常用方法
start()
:启动线程,使线程进入就绪状态。run()
:线程的执行体,包含线程要执行的代码。join()
:等待该线程终止。sleep(long millis)
:使当前线程休眠指定的毫秒数。yield()
:暂停当前正在执行的线程对象,并执行其他线程。interrupt()
:中断线程。
三、线程同步
3.1 线程安全问题
在多线程环境中,如果多个线程同时访问共享资源,可能会导致数据不一致的问题,这就是线程安全问题。例如,多个线程同时对一个计数器进行自增操作,可能会导致计数器的值不正确。
3.2 同步机制
为了解决线程安全问题,Java 提供了多种同步机制,主要包括 synchronized
关键字和 Lock
接口。
3.2.1 synchronized
关键字
ynchronized
关键字可以用来修饰方法或代码块,确保同一时间只有一个线程可以访问被修饰的方法或代码块。
修饰方法
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class SynchronizedMethodExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Count: " + counter.getCount());
}
}
在上述代码中,increment
方法被 synchronized
关键字修饰,确保同一时间只有一个线程可以调用该方法,从而避免了线程安全问题。
修饰代码块
class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
public int getCount() {
return count;
}
}
public class SynchronizedBlockExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Count: " + counter.getCount());
}
}
在上述代码中,increment
方法中的代码块被 synchronized
关键字修饰,使用一个 Object
类型的锁对象来确保同一时间只有一个线程可以进入该代码块。
3.2.2 Lock
接口
Lock
接口是 Java 5 引入的一种更灵活的同步机制,它提供了比 synchronized
关键字更多的功能。ReentrantLock
是 Lock
接口的一个实现类。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
public class LockExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Count: " + counter.getCount());
}
}
在上述代码中,我们使用 ReentrantLock
来实现线程同步。在 increment
方法中,我们首先调用 lock
方法获取锁,然后执行自增操作,最后在 finally
块中调用 unlock
方法释放锁,确保锁一定会被释放。
3.3 死锁
死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
死锁的四个必要条件
- 互斥条件:进程对所分配到的资源进行排他性使用,即在一段时间内某资源只由一个进程占用。
- 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
- 环路等待条件:在发生死锁时,必然存在一个进程 —— 资源的环形链,即进程集合 {P0,P1,P2,・・・,Pn} 中的 P0 正在等待一个 P1 占用的资源;P1 正在等待 P2 占用的资源,……,Pn 正在等待已被 P0 占用的资源。
避免死锁的方法
- 破坏互斥条件:有些资源本身就不支持共享,所以这种方法可行性不高。
- 破坏请求和保持条件:可以采用预先分配资源的策略,即进程在运行前一次性申请它所需要的全部资源,如果资源不能满足,则不分配任何资源,进程暂时不运行。
- 破坏不剥夺条件:当一个已经保持了某些资源的进程,再提出新的资源请求而不能立即得到满足时,它必须释放已经保持的所有资源,待以后需要时再重新申请。
- 破坏环路等待条件:可以采用资源有序分配法,即把系统中所有资源编号,进程在申请资源时必须按照资源编号的顺序进行,这样就不会形成环路。
四、线程通信
4.1 wait()
、notify()
和 notifyAll()
wait()
、notify()
和 notifyAll()
是 Object
类的方法,用于实现线程之间的通信。
wait()
:使当前线程进入等待状态,直到其他线程调用该对象的notify()
或notifyAll()
方法。notify()
:唤醒在此对象监视器上等待的单个线程。notifyAll()
:唤醒在此对象监视器上等待的所有线程。
class Message {
private String content;
private boolean empty = true;
public synchronized String read() {
while (empty) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
empty = true;
notifyAll();
return content;
}
public synchronized void write(String content) {
while (!empty) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
empty = false;
this.content = content;
notifyAll();
}
}
public class ThreadCommunicationExample {
public static void main(String[] args) {
Message message = new Message();
Thread writer = new Thread(() -> {
message.write("Hello, World!");
});
Thread reader = new Thread(() -> {
System.out.println("Read: " + message.read());
});
writer.start();
reader.start();
}
}
在上述代码中,Message
类包含一个 read
方法和一个 write
方法,分别用于读取和写入消息。在 read
方法中,如果消息为空,则调用 wait
方法使当前线程进入等待状态;在 write
方法中,如果消息不为空,则调用 wait
方法使当前线程进入等待状态。当消息写入完成后,调用 notifyAll
方法唤醒所有等待的线程。
4.2 Condition
接口
Condition
接口是 Java 5 引入的一种更灵活的线程通信机制,它与 Lock
接口配合使用。Condition
接口提供了 await()
、signal()
和 signalAll()
方法,分别对应 wait()
、notify()
和 notifyAll()
方法。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Message {
private String content;
private boolean empty = true;
private final Lock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition emptyCondition = lock.newCondition();
public String read() {
lock.lock();
try {
while (empty) {
notEmpty.await();
}
empty = true;
emptyCondition.signalAll();
return content;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
} finally {
lock.unlock();
}
}
public void write(String content) {
lock.lock();
try {
while (!empty) {
emptyCondition.await();
}
empty = false;
this.content = content;
notEmpty.signalAll();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
}
public class ConditionExample {
public static void main(String[] args) {
Message message = new Message();
Thread writer = new Thread(() -> {
message.write("Hello, World!");
});
Thread reader = new Thread(() -> {
System.out.println("Read: " + message.read());
});
writer.start();
reader.start();
}
}
在上述代码中,我们使用 ReentrantLock
和 Condition
接口来实现线程通信。在 read
方法中,如果消息为空,则调用 notEmpty.await()
方法使当前线程进入等待状态;在 write
方法中,如果消息不为空,则调用 emptyCondition.await()
方法使当前线程进入等待状态。当消息写入完成后,调用 notEmpty.signalAll()
方法唤醒所有等待的线程。
五、线程池
5.1 线程池的概念
线程池是一种管理线程的机制,它可以预先创建一定数量的线程,当有任务提交时,从线程池中获取一个空闲线程来执行任务,任务执行完成后,线程不会销毁,而是返回线程池等待下一个任务。使用线程池可以减少线程创建和销毁的开销,提高程序的性能。
5.2 Java 中的线程池
Java 提供了 ExecutorService
接口和 ThreadPoolExecutor
类来实现线程池。
5.2.1 ExecutorService
接口
ExecutorService
接口是线程池的核心接口,它定义了线程池的基本操作,如提交任务、关闭线程池等。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorServiceExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 提交任务
executorService.submit(() -> {
System.out.println("Task 1 is running.");
});
executorService.submit(() -> {
System.out.println("Task 2 is running.");
});
// 关闭线程池
executorService.shutdown();
}
}
在上述代码中,我们使用 Executors.newFixedThreadPool(2)
方法创建了一个固定大小为 2 的线程池,然后使用 submit
方法提交了两个任务,最后调用 shutdown
方法关闭线程池。
5.2.2 ThreadPoolExecutor
类
ThreadPoolExecutor
类是 ExecutorService
接口的一个具体实现类,它提供了更多的配置选项,可以根据需要自定义线程池。
import java.util.concurrent.*;
public class ThreadPoolExecutorExample {
public static void main(String[] args) {
// 创建一个自定义的线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
60, // 线程空闲时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10) // 任务队列
);
// 提交任务
executor.submit(() -> {
System.out.println("Task 1 is running.");
});
executor.submit(() -> {
System.out.println("Task 2 is running.");
});
// 关闭线程池
executor.shutdown();
}
}
在上述代码中,我们使用 ThreadPoolExecutor
类创建了一个自定义的线程池,设置了核心线程数、最大线程数、线程空闲时间和任务队列。然后使用 submit
方法提交了两个任务,最后调用 shutdown
方法关闭线程池。
5.3 线程池的状态
线程池有以下几种状态:
- RUNNING:线程池正常运行,可以接受新任务并处理队列中的任务。
- SHUTDOWN:线程池不再接受新任务,但会继续处理队列中的任务。
- STOP:线程池不再接受新任务,也不再处理队列中的任务,并且会中断正在执行的任务。
- TIDYING:所有任务都已终止,工作线程数为 0,线程池进入整理状态。
- TERMINATED:线程池完全终止。
5.4 线程池的配置参数
- corePoolSize:核心线程数,线程池启动时会创建的线程数量。
- maximumPoolSize:最大线程数,线程池允许的最大线程数量。
- keepAliveTime:线程空闲时间,当线程空闲时间超过该值时,线程会被销毁。
- unit:线程空闲时间的单位。
- workQueue:任务队列,用于存储等待执行的任务。
- threadFactory:线程工厂,用于创建线程。
- handler:任务拒绝策略,当任务队列已满且线程池中的线程数量达到最大线程数时,新提交的任务将由该策略处理。
六、并发集合
6.1 ConcurrentHashMap
ConcurrentHashMap
是 Java 提供的一种线程安全的哈希表,它可以在多线程环境下高效地进行读写操作。与 Hashtable
相比,ConcurrentHashMap
采用了分段锁的机制,允许多个线程同时访问不同的段,从而提高了并发性能。
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 插入数据
map.put("apple", 1);
map.put("banana", 2);
// 获取数据
Integer value = map.get("apple");
System.out.println("Value: " + value);
}
}
在上述代码中,我们创建了一个 ConcurrentHashMap
对象,并插入了两个键值对,然后通过键获取了对应的值。
6.2 CopyOnWriteArrayList
CopyOnWriteArrayList
是 Java 提供的一种线程安全的列表,它采用了写时复制的机制。当进行写操作时,会创建一个新的数组副本,将修改后的内容写入新的数组副本,然后将原数组引用指向新的数组副本。这种机制保证了在进行读操作时不需要加锁,从而提高了读操作的性能。
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListExample {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// 添加元素
list.add("apple");
list.add("banana");
// 遍历元素
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
在上述代码中,我们创建了一个 CopyOnWriteArrayList
对象,并添加了两个元素,然后使用迭代器遍历了列表中的元素。
6.3 BlockingQueue
BlockingQueue
是 Java 提供的一种阻塞队列,它可以在队列为空时阻塞获取元素的线程,在队列已满时阻塞插入元素的线程。BlockingQueue
有多种实现类,如 ArrayBlockingQueue
、LinkedBlockingQueue
等。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueExample {
public static void main(String[] args) {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
// 生产者线程
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
queue.put("Item " + i);
System.out.println("Produced: Item " + i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 消费者线程
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
String item = queue.take();
System.out.println("Consumed: " + item);
Thread.sleep(200);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}
在上述代码中,我们创建了一个 ArrayBlockingQueue
对象,并使用两个线程分别作为生产者和消费者。生产者线程向队列中插入元素,消费者线程从队列中获取元素。当队列为空时,消费者线程会阻塞;当队列已满时,生产者线程会阻塞。
七、并发工具类
7.1 CountDownLatch
CountDownLatch
是一个同步辅助类,它允许一个或多个线程等待其他线程完成操作。CountDownLatch
初始化时需要指定一个计数器的值,当计数器的值减为 0 时,等待的线程将被唤醒。
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
int threadCount = 3;
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
final int index = i;
new Thread(() -> {
try {
System.out.println("Thread " + index + " is working.");
Thread.sleep(1000);
System.out.println("Thread " + index + " has finished.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown();
}
}).start();
}
// 等待所有线程完成
latch.await();
System.out.println("All threads have finished.");
}
}
在上述代码中,我们创建了一个 CountDownLatch
对象,计数器的值为 3。然后启动了 3 个线程,每个线程执行完任务后调用 latch.countDown()
方法将计数器的值减 1。主线程调用 latch.await()
方法等待计数器的值减为 0,当计数器的值减为 0 时,主线程继续执行。
7.2 CyclicBarrier
CyclicBarrier
是一个同步辅助类,它允许一组线程相互等待,直到所有线程都到达一个屏障点,然后所有线程才可以继续执行。CyclicBarrier
可以重复使用。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
int threadCount = 3;
CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
System.out.println("All threads have reached the barrier.");
});
for (int i = 0; i < threadCount; i++) {
final int index = i;
new Thread(() -> {
try {
System.out.println("Thread " + index + " is working.");
Thread.sleep(1000);
System.out.println("Thread " + index + " has reached the barrier.");
barrier.await();
System.out.println("Thread " + index + " continues to work.");
} catch (InterruptedException | BrokenBarrierException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
}
在上述代码中,我们创建了一个 CyclicBarrier
对象,指定了等待的线程数量为 3,并设置了一个屏障动作。然后启动了 3 个线程,每个线程执行完任务后调用 barrier.await()
方法等待其他线程到达屏障点。当所有线程都到达屏障点时,屏障动作会被执行,然后所有线程继续执行。
7.3 Semaphore
Semaphore
是一个计数信号量,它可以用来控制同时访问某个资源的线程数量。Semaphore
初始化时需要指定一个许可数量,线程在访问资源前需要先获取许可,如果许可数量为 0,则线程会阻塞;线程访问完资源后需要释放许可。
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
int permitCount = 2;
Semaphore semaphore = new Semaphore(permitCount);
for (int i = 0; i < 5; i++) {
final int index = i;
new Thread(() -> {
try {
// 获取许可
semaphore.acquire();
System.out.println("Thread " + index + " has acquired a permit.");
Thread.sleep(1000);
System.out.println("Thread " + index + " is releasing a permit.");
// 释放许可
semaphore.release();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
}
在上述代码中,我们创建了一个 Semaphore
对象,许可数量为 2。然后启动了 5 个线程,每个线程在访问资源前调用 semaphore.acquire()
方法获取许可,访问完资源后调用 semaphore.release()
方法释放许可。由于许可数量为 2,所以最多只能有 2 个线程同时访问资源。
7.4 Exchanger
Exchanger
是一个同步辅助类,它允许两个线程在某个同步点交换数据。当两个线程都到达同步点时,它们会交换各自的数据。
import java.util.concurrent.Exchanger;
public class ExchangerExample {
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<>();
Thread thread1 = new Thread(() -> {
try {
String data1 = "Data from Thread 1";
System.out.println("Thread 1 is sending: " + data1);
String receivedData = exchanger.exchange(data1);
System.out.println("Thread 1 received: " + receivedData);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread thread2 = new Thread(() -> {
try {
String data2 = "Data from Thread 2";
System.out.println("Thread 2 is sending: " + data2);
String receivedData = exchanger.exchange(data2);
System.out.println("Thread 2 received: " + receivedData);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
thread1.start();
thread2.start();
}
}
在上述代码中,我们创建了一个 Exchanger
对象,然后启动了两个线程。每个线程在到达同步点时调用 exchanger.exchange()
方法交换数据。当两个线程都调用了 exchanger.exchange()
方法时,它们会交换各自的数据。
八、总结
Java 提供了丰富的多线程与并发编程支持,通过使用线程、线程同步、线程通信、线程池、并发集合和并发工具类等技术,可以有效地提高程序的性能和响应速度。在实际开发中,需要根据具体的需求选择合适的技术,并注意避免线程安全问题和死锁等问题。同时,合理配置线程池和使用并发工具类可以提高程序的并发性能和可维护性。