Java并发编程
一、并发编程的基本概念
- 线程与进程的区别
- 并发与并行的区别
- Java中的线程模型
线程与进程的区别
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件描述符等。
进程是计算机中已运行程序的实体,它拥有独立的地址空间、内存、数据栈以及其他辅助运行的数据。进程是资源分配的基本单位,每个进程都有自己独立的内存空间,因此进程间的通信需要通过特定的机制如管道、信号、套接字等。
并发与并行的区别
并发是指多个任务在同一时间段内交替执行,但实际上这些任务在任意时刻只有一个在运行。并发的核心在于任务的处理顺序和时间的分配,它使得多个任务看起来像是同时进行的。
并行是指多个任务在同一时刻真正同时执行,这通常需要多核处理器或多台计算机的支持。并行的核心在于任务的同时执行,它能够显著提高处理效率。
Java中的线程模型
Java中的线程模型基于java.lang.Thread
类和java.lang.Runnable
接口。每个线程都是Thread
类的一个实例,而Runnable
接口则定义了线程执行的任务。
创建线程的方式有两种:一种是继承Thread
类并重写run
方法,另一种是实现Runnable
接口并将其实例传递给Thread
构造器。
// 继承Thread类
class MyThread extends Thread {
public void run() {
// 线程执行的任务
}
}
// 实现Runnable接口
class MyRunnable implements Runnable {
public void run() {
// 线程执行的任务
}
}
// 创建线程
MyThread thread1 = new MyThread();
Thread thread2 = new Thread(new MyRunnable());
// 启动线程
thread1.start();
thread2.start();
Java还提供了java.util.concurrent
包,其中包含了许多高级并发工具,如线程池、锁、原子变量等,这些工具能够帮助开发者更高效地管理线程和并发任务。
二、线程安全问题
- 竞态条件
- 数据竞争
- 死锁、活锁与饥饿
线程安全问题是多线程编程中常见的挑战,主要包括竞态条件、数据竞争、死锁、活锁与饥饿。以下是这些问题的详细解释及应对方法。
竞态条件
竞态条件发生在多个线程对共享资源进行操作时,由于执行顺序的不确定性,导致程序的行为依赖于线程的执行时序。竞态条件通常会导致不可预测的结果。
解决方法:
- 使用同步机制(如锁、信号量)来确保对共享资源的互斥访问。
- 使用原子操作来避免竞态条件。
synchronized (lock) {
// 临界区代码
}
数据竞争
数据竞争发生在多个线程同时访问共享数据,并且至少有一个线程在写入数据时。数据竞争可能导致数据不一致或程序崩溃。
解决方法:
- 使用锁或其他同步机制来保护共享数据。
- 使用线程安全的数据结构,如
ConcurrentHashMap
。
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
死锁
死锁发生在多个线程相互等待对方释放资源,导致所有线程都无法继续执行。死锁通常涉及多个锁和资源的循环依赖。
解决方法:
- 避免嵌套锁,尽量按固定顺序获取锁。
- 使用超时机制,避免无限期等待。
if (lock1.tryLock(timeout, TimeUnit.MILLISECONDS)) {
try {
if (lock2.tryLock(timeout, TimeUnit.MILLISECONDS)) {
try {
// 临界区代码
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
活锁
活锁发生在多个线程不断改变状态以响应对方,但无法取得进展。活锁通常是由于线程过于频繁地响应其他线程的行为。
解决方法:
- 引入随机性,避免线程过于频繁地响应。
- 重新设计线程的交互逻辑,减少不必要的状态变化。
Thread.sleep(random.nextInt(100)); // 引入随机延迟
饥饿
饥饿发生在某些线程由于优先级低或资源分配不均,长时间无法获得所需的资源,导致无法执行。
解决方法:
- 使用公平锁,确保所有线程都有机会获得资源。
- 调整线程优先级,避免某些线程长时间得不到执行。
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
fairLock.lock();
try {
// 临界区代码
} finally {
fairLock.unlock();
}
通过理解这些线程安全问题及其解决方法,可以编写出更加健壮和可靠的多线程程序。
三、Java并发工具类
synchronized
关键字volatile
关键字ReentrantLock
与Condition
ReadWriteLock
Semaphore
与CountDownLatch
synchronized
关键字
synchronized
是Java中最基本的同步机制,用于控制多个线程对共享资源的访问。它可以修饰方法或代码块,确保同一时刻只有一个线程执行被synchronized
修饰的代码。
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
}
volatile
关键字
volatile
关键字用于确保变量的可见性。当一个变量被声明为volatile
时,线程在读取该变量时会直接从主内存中获取,而不是从线程的本地缓存中获取。这可以防止线程之间的数据不一致问题。
public class VolatileExample {
private volatile boolean flag = false;
public void toggleFlag() {
flag = !flag;
}
}
ReentrantLock
与Condition
ReentrantLock
是java.util.concurrent.locks
包中的一个类,它提供了比synchronized
更灵活的锁机制。Condition
则是与ReentrantLock
配合使用的工具,用于实现线程间的等待/通知机制。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private boolean ready = false;
public void waitForReady() throws InterruptedException {
lock.lock();
try {
while (!ready) {
condition.await();
}
}