一、引言
在现代软件开发中,尤其是在处理复杂的任务和提高系统性能方面,多线程技术扮演着极为重要的角色。Java 作为一种广泛应用的编程语言,提供了强大的多线程支持。然而,多线程编程也带来了一系列的挑战,如线程安全、资源竞争、死锁等问题。本文将深入探讨 Java 多线程并发控制的核心技术点,包括线程的创建与启动、线程同步机制、线程间通信以及如何避免常见的并发问题,并通过实际的代码示例和案例分析来帮助读者更好地理解和应用这些知识。
二、线程的创建与启动
- 继承 Thread 类
- 定义一个类继承自
Thread
类,并重写run
方法。在run
方法中编写线程要执行的任务逻辑。例如:class MyThread extends Thread { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("MyThread: " + i); } } }
- 然后在主线程中创建并启动该线程:
public class Main { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); for (int i = 0; i < 10; i++) { System.out.println("Main Thread: " + i); } } }
- 这里需要注意的是,直接调用
thread.run()
并不会启动新的线程,而是在当前线程(主线程)中执行run
方法。只有调用thread.start()
才会真正创建并启动一个新的线程,使run
方法在新线程中异步执行。
- 定义一个类继承自
- 实现 Runnable 接口
- 定义一个类实现
Runnable
接口,实现run
方法。例如:class MyRunnable implements Runnable { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("MyRunnable: " + i); } } }
- 在主线程中创建
Runnable
实例,并将其作为参数传递给Thread
类的构造函数来创建线程并启动:public class Main { public static void main(String[] args) { MyRunnable runnable = new MyRunnable(); Thread thread = new Thread(runnable); thread.start(); for (int i = 0; i < 10; i++) { System.out.println("Main Thread: " + i); } } }
- 这种方式更加灵活,因为一个
Runnable
实例可以被多个线程共享,实现资源的复用。
- 定义一个类实现
三、线程同步机制
- synchronized 关键字
- 同步方法:在方法声明前加上
synchronized
关键字,该方法在同一时刻只能被一个线程访问。例如:class Counter { private int count = 0; public synchronized void increment() { count++; } public int getCount() { return count; } }
- 当多个线程同时调用
increment
方法时,只有一个线程能够进入该方法执行,其他线程会被阻塞,直到当前线程执行完毕。 - 同步代码块:可以对代码块进行同步,指定一个对象作为锁。例如:
class SharedResource { private Object lock = new Object(); private int value = 0; public void modifyValue() { synchronized (lock) { value++; } } }
- 这里
lock
对象作为锁,只有获得该锁的线程才能执行同步代码块中的value++
操作。
- 同步方法:在方法声明前加上
- ReentrantLock 类
ReentrantLock
是一种可重入的互斥锁,提供了比synchronized
关键字更灵活的锁控制。例如:
```java
import java.util.concurrent.locks.ReentrantLock;
class LockCounter {
private ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
- 线程在访问 `increment` 方法时,首先通过 `lock.lock()` 获取锁,如果锁已被其他线程持有,则当前线程会被阻塞。在操作完成后,必须在 `finally` 块中通过 `lock.unlock()` 释放锁,以确保锁资源的正确释放,防止死锁和资源泄漏。
## 四、线程间通信
1. **wait()、notify() 和 notifyAll() 方法**
- 这些方法用于在多线程环境下实现线程间的通信,通常与 `synchronized` 关键字配合使用。例如,有一个生产者 - 消费者模型:
```java
import java.util.LinkedList;
import java.util.Queue;
class ProducerConsumer {
private Queue<Integer> queue = new LinkedList<>();
private int capacity = 5;
public void produce() throws InterruptedException {
int value = 0;
while (true) {
synchronized (this) {
while (queue.size() == capacity) {
wait();
}
queue.add(value++);
System.out.println("Produced: " + value);
notifyAll();
Thread.sleep(1000);
}
}
}
public void consume() throws InterruptedException {
while (true) {
synchronized (this) {
while (queue.isEmpty()) {
wait();
}
int value = queue.poll();
System.out.println("Consumed: " + value);
notifyAll();
Thread.sleep(2000);
}
}
}
}
- 在生产者线程中,当队列已满时,调用
wait()
方法使线程进入等待状态,释放锁资源。当消费者从队列中取出元素后,调用notifyAll()
方法唤醒所有等待的线程(包括其他生产者和消费者线程)。消费者线程的逻辑类似,当队列为空时等待,有元素时消费并通知其他线程。- BlockingQueue 接口
- Java 提供了
BlockingQueue
接口及其实现类,如ArrayBlockingQueue
、LinkedBlockingQueue
等,用于方便地实现生产者 - 消费者模式等线程间通信场景。例如:
```java
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class BlockingQueueProducerConsumer {
private BlockingQueue queue = new LinkedBlockingQueue<>();
public void produce() throws InterruptedException {
int value = 0;
while (true) {
queue.put(value++);
System.out.println("Produced: " + value);
Thread.sleep(1000);
}
}
public void consume() throws InterruptedException {
while (true) {
int value = queue.take();
System.out.println("Consumed: " + value);
Thread.sleep(2000);
}
}
}
- `BlockingQueue` 内部已经实现了线程安全的入队和出队操作,并在队列为空或已满时自动阻塞线程,简化了线程间通信的代码实现。
## 五、避免常见的并发问题
1. **死锁**
- 死锁是多线程编程中常见的问题,当多个线程相互等待对方释放资源时就会发生死锁。例如:
```java
import java.util.concurrent.locks.ReentrantLock;
class DeadlockExample {
private ReentrantLock lock1 = new ReentrantLock();
private ReentrantLock lock2 = new ReentrantLock();
public void method1() {
lock1.lock();
try {
Thread.sleep(100);
lock2.lock();
try {
// 执行一些操作
} finally {
lock2.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock1.unlock();
}
}
public void method2() {
lock2.lock();
try {
Thread.sleep(100);
lock1.lock();
try {
// 执行一些操作
} finally {
lock1.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock2.unlock();
}
}
}
在这个例子中,如果一个线程先调用
method1
,获取了lock1
,然后另一个线程调用method2
,获取了lock2
,此时两个线程都会进入等待对方释放锁的状态,从而导致死锁。为了避免死锁,可以采用资源有序分配、避免嵌套锁等策略。例如,始终按照相同的顺序获取锁资源:class DeadlockAvoidanceExample { private ReentrantLock lock1 = new ReentrantLock(); private ReentrantLock lock2 = new ReentrantLock(); public void method1() { lock1.lock(); try { Thread.sleep(100); lock2.lock(); try { // 执行一些操作 } finally { lock2.unlock(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock1.unlock(); } } public void method2() { lock1.lock(); try { Thread.sleep(100); lock2.lock(); try { // 执行一些操作 } finally { lock2.unlock(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock1.unlock(); } } }
- 资源竞争
- 资源竞争是指多个线程同时访问共享资源时可能导致数据不一致或错误的情况。除了使用同步机制(如
synchronized
和ReentrantLock
)来保护共享资源外,还可以采用原子类来处理一些简单的原子操作。例如,AtomicInteger
类:
```java
import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
```
AtomicInteger
类通过原子操作来保证count
的自增操作是线程安全的,避免了使用显式锁带来的一些性能开销和复杂性。
六、总结
Java 多线程并发控制是一个复杂而又重要的领域。通过深入理解线程的创建与启动方式、掌握线程同步机制、熟悉线程间通信的方法以及学会避免常见的并发问题,开发者能够更好地利用多线程技术来提高程序的性能和响应性。在实际项目开发中,需要根据具体的需求和场景,合理选择合适的多线程技术和并发控制策略,同时要充分考虑代码的可读性、可维护性和性能优化。不断地实践和积累经验,才能在 Java 多线程并发编程中得心应手,构建出高效、稳定的多线程应用程序。