Java 多线程学习笔记
一、什么是线程?
- **进程:**是操作系统进行资源分配和调度的基本单位,是程序的一次执行过程。
**例如:**当你打开一个应用程序,如浏览器、音乐播放器等,操作系统就会为其创建一个进程。 - **线程:**是进程的一个执行单元,是 CPU 调度和分派的基本单位,一个进程可以包含多个线程。
**例如:**在浏览器进程中,可以有多个线程同时运行,例如一个线程负责页面渲染,另一个线程负责网络请求。
多线程的优点:
- 提高 CPU 利用率,充分利用多核处理器
**例如:**下载软件使用多线程可以同时下载多个文件片段,充分利用带宽,提高下载速度。 - 提高程序响应速度,避免单线程阻塞导致整个程序卡顿
**例如:**GUI 程序使用多线程可以将耗时操作放在后台线程执行,避免界面卡顿。 - 简化程序结构,将复杂任务分解成多个简单任务并发执行
**例如:**网络爬虫可以使用多线程同时爬取多个网页,提高效率。
多线程的缺点:
- 增加了程序设计的复杂度
**例如:**需要考虑线程同步、数据共享等问题。 - 线程同步和数据共享问题
**例如:**多个线程同时修改共享数据可能导致数据不一致。 - 线程上下文切换开销
**案例:**CPU 在多个线程之间切换需要保存和恢复线程的上下文信息,会带来一定的开销。
二、创建和启动线程
Java 中创建线程主要有两种方式:
1. 继承 Thread 类
- 定义一个继承 Thread 类的子类,重写 run() 方法。
- 创建子类对象,调用 start() 方法启动线程。
public class ThreadExample1 extends Thread {
@Override
public void run() {
// 线程执行的代码
System.out.println("Thread " + Thread.currentThread().getId() + " is running.");
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
ThreadExample1 thread = new ThreadExample1();
thread.start();
}
}
}
案例解释:
ThreadExample1
类继承了Thread
类,并重写了run()
方法,定义了线程要执行的代码。- 在
main()
方法中,创建了 5 个ThreadExample1
对象,并分别调用start()
方法启动线程。
2. 实现 Runnable 接口
- 定义一个实现 Runnable 接口的类,实现 run() 方法。
- 创建 Runnable 对象,将其作为参数传递给 Thread 构造方法创建 Thread 对象。
- 调用 Thread 对象的 start() 方法启动线程。
public class ThreadExample2 implements Runnable {
@Override
public void run() {
// 线程执行的代码
System.out.println("Thread " + Thread.currentThread().getId() + " is running.");
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
ThreadExample2 runnable = new ThreadExample2();
Thread thread = new Thread(runnable);
thread.start();
}
}
}
案例解释:
ThreadExample2
类实现了Runnable
接口,并实现了run()
方法,定义了线程要执行的代码。- 在
main()
方法中,创建了 5 个ThreadExample2
对象,并将它们分别作为参数传递给Thread
构造方法创建线程对象,然后调用start()
方法启动线程。
推荐使用实现 Runnable 接口的方式创建线程,因为它更灵活,避免了单继承的限制。
三、线程的生命周期
Java 线程的生命周期包含以下几种状态:
- **新建(New):**线程对象被创建,但还未调用 start() 方法。
例如:Thread thread = new Thread();
- **就绪(Runnable):**线程对象调用 start() 方法后,进入就绪状态,等待 CPU 调度。
例如:thread.start();
- **运行(Running):**CPU 开始执行线程的 run() 方法。
例如: 当线程获取到 CPU 资源时,就会执行run()
方法中的代码。 - **阻塞(Blocked):**线程因为某些原因暂停执行,例如等待 I/O 操作完成、获取锁失败等。
**例如:**线程调用sleep()
方法、等待某个对象的锁等都会导致线程进入阻塞状态。 - **死亡(Dead):**线程的 run() 方法执行完毕或者发生异常终止。
例如:run()
方法执行完毕后,线程就会进入死亡状态。
四、线程同步
当多个线程访问共享资源时,需要进行同步控制,避免数据出现不一致的情况。Java 提供了以下几种同步机制:
1. synchronized 关键字
- 修饰代码块:
synchronized(object) {}
,object 为锁对象。 - 修饰方法:
synchronized void method() {}
,锁对象为当前对象 this。
案例:
public class Counter {
private int count;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
案例解释:
Counter
类表示一个计数器,count
变量表示计数器的值。increment()
方法使用synchronized
关键字修饰,保证了多个线程同时调用该方法时,只有一个线程能够进入方法体,从而保证了计数器的线程安全。getCount()
方法也使用了synchronized
关键字修饰,保证了在读取计数器的值时,不会出现数据不一致的情况。
2. Lock 接口
- ReentrantLock 类是 Lock 接口的常用实现类。
- 使用 lock() 方法获取锁,unlock() 方法释放锁。
案例:
public class Counter {
private int count;
private ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
案例解释:
Counter
类表示一个计数器,count
变量表示计数器的值,lock
是一个ReentrantLock
对象,用于控制对计数器的访问。increment()
方法在修改count
变量之前,先调用lock.lock()
方法获取锁,修改完成后调用lock.unlock()
方法释放锁。getCount()
方法在读取count
变量之前,也需要先获取锁,读取完成后释放锁。
Lock
接口相比 synchronized
关键字更加灵活,可以实现更细粒度的锁控制,例如可以实现公平锁、可中断锁等。
五、线程间通信
Java 提供了以下机制实现线程间通信:
1. wait()、notify()、notifyAll() 方法
- 这些方法都是
Object
类的方法,必须在synchronized
代码块中使用。 wait()
方法:使当前线程进入等待状态,释放锁。notify()
方法:唤醒一个等待该锁的线程。notifyAll()
方法:唤醒所有等待该锁的线程。
案例:生产者消费者模型
public class ProducerConsumer {
private static final int QUEUE_SIZE = 10;
private static final Object lock = new Object();
private static int[] queue = new int[QUEUE_SIZE];
private static int head = 0;
private static int tail = 0;
public static void main(String[] args) {
Thread producer = new Thread(() -> {
while (true) {
synchronized (lock) {
while ((tail + 1) % QUEUE_SIZE == head) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue[tail] = (int) (Math.random() * 100);
tail = (tail + 1) % QUEUE_SIZE;
System.out.println("生产者生产:" + queue[tail]);
lock.notifyAll();
}
}
});
Thread consumer = new Thread(() -> {
while (true) {
synchronized (lock) {
while (head == tail) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int data = queue[head];
head = (head + 1) % QUEUE_SIZE;
System.out.println("消费者消费:" + data);
lock.notifyAll();
}
}
});
producer.start();
consumer.start();
}
}
案例解释:
- 使用一个数组
queue
模拟队列,head
指向队头,tail
指向队尾。 - 生产者线程不断生成数据,放入队列中,如果队列满了就等待。
- 消费者线程不断从队列中取出数据,如果队列为空就等待。
- 使用
lock
对象作为锁,保证对队列的访问是线程安全的。 - 使用
wait()
、notifyAll()
方法实现线程间的通信。
2. Condition 接口
Condition
接口提供了更灵活的线程间通信机制。- 通过
Lock
对象的newCondition()
方法获取Condition
对象。 await()
方法类似wait()
方法,signal()
方法类似notify()
方法,signalAll()
方法类似notifyAll()
方法。
案例:使用 Condition 实现生产者消费者模型
public class ProducerConsumerCondition {
private static final int QUEUE_SIZE = 10;
private static final Lock lock = new ReentrantLock();
private static final Condition notFull = lock.newCondition();
private static final Condition notEmpty = lock.newCondition();
private static int[] queue = new int[QUEUE_SIZE];
private static int head = 0;
private static int tail = 0;
public static void main(String[] args) {
Thread producer = new Thread(() -> {
while (true) {
lock.lock();
try {
while ((tail + 1) % QUEUE_SIZE == head) {
try {
notFull.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue[tail] = (int) (Math.random() * 100);
tail = (tail + 1) % QUEUE_SIZE;
System.out.println("生产者生产:" + queue[tail]);
notEmpty.signal();
} finally {
lock.unlock();
}
}
});
Thread consumer = new Thread(() -> {
while (true) {
lock.lock();
try {
while (head == tail) {
try {
notEmpty.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int data = queue[head];
head = (head + 1) % QUEUE_SIZE;
System.out.println("消费者消费:" + data);
notFull.signal();
} finally {
lock.unlock();
}
}
});
producer.start();
consumer.start();
}
}
案例解释:
- 使用
ReentrantLock
对象作为锁,并通过该对象创建了两个Condition
对象:notFull
和notEmpty
。 notFull
用于生产者线程等待队列不满,notEmpty
用于消费者线程等待队列不空。- 生产者线程在生产数据后,调用
notEmpty.signal()
方法唤醒等待notEmpty
条件的消费者线程。 - 消费者线程在消费数据后,调用
notFull.signal()
方法唤醒等待notFull
条件的生产者线程。
Condition
接口可以实现更精确的线程控制,例如可以实现多个生产者线程和多个消费者线程之间的同步。
六、线程池
-
**概念:**预先创建多个线程,放入线程池中,需要时从线程池获取线程执行任务,任务结束后将线程归还线程池。
-
优点:
- 降低线程创建和销毁的开销
- 提高线程利用率
- 方便管理线程
-
常用线程池:
- **newFixedThreadPool:**固定大小线程池
- **newCachedThreadPool:**缓存线程池,可动态调整线程数量
- **newScheduledThreadPool:**定时任务线程池
- **newSingleThreadExecutor:**单线程线程池
案例:使用线程池执行任务
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建固定大小线程池,线程池大小为 5
ExecutorService threadPool = Executors.newFixedThreadPool(5);
// 提交 10 个任务到线程池
for (int i = 0; i < 10; i++) {
final int taskIndex = i;
threadPool.execute(() -> {
System.out.println("Task " + taskIndex + " is running in thread " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
threadPool.shutdown();
}
}
案例解释:
- 使用
Executors.newFixedThreadPool(5)
创建了一个固定大小为 5 的线程池。 - 使用
threadPool.execute()
方法提交了 10 个任务到线程池。 - 线程池会从线程池中获取空闲线程执行任务,如果所有线程都在忙,则任务会被放入队列中等待。
- 调用
threadPool.shutdown()
方法关闭线程池,不再接受新的任务,已提交的任务会继续执行完毕。
七、volatile 关键字
- **作用:**保证变量的可见性和有序性。
- **可见性:**当一个线程修改了
volatile
变量的值,其他线程能立即看到修改后的值。 - **有序性:**禁止指令重排序,保证代码执行顺序符合预期。
案例:使用 volatile 关键字保证变量可见性
public class VolatileExample {
private static volatile boolean flag = false;
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
while (!flag) {
// do something
}
System.out.println("Thread 1 exits.");
});
Thread thread2 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("Thread 2 sets flag to true.");
});
thread1.start();
thread2.start();
}
}
案例解释:
flag
变量使用volatile
关键字修饰,保证了thread2
修改flag
变量的值后,thread1
能立即看到修改后的值。- 如果没有使用
volatile
关键字,thread1
可能看不到thread2
对flag
变量的修改,导致线程无法退出。
八、ThreadLocal 类
- **作用:**为每个线程提供一个独立的变量副本,避免线程安全问题。
- **使用场景:**存储线程私有的数据,例如数据库连接、用户 session 等。
案例:使用 ThreadLocal 存储用户 session 信息
public class ThreadLocalExample {
private static final ThreadLocal<String> session = new ThreadLocal<>();
public static void main(String[] args) {
// 模拟多个用户请求
for (int i = 0; i < 5; i++) {
final int userId = i;
new Thread(() -> {
// 设置用户 session 信息
session.set("user" + userId);
// 获取用户 session 信息
System.out.println("Thread " + Thread.currentThread().getId() + " session: " + session.get());
}).start();
}
}
}
案例解释:
- 使用
ThreadLocal<String>
创建了一个线程局部变量session
,用于存储用户 session 信息。 - 每个线程都可以通过
session.set()
方法设置自己的 session 信息,并通过session.get()
方法获取自己的 session 信息。 - 不同线程之间不会互相影响,保证了线程安全。
九、学习建议
- 掌握线程的基本概念、创建、启动、生命周期等基础知识。
- 深入理解线程同步机制,掌握
synchronized
关键字和Lock
接口的使用。 - 学习线程间通信机制,了解
wait()
、notify()
、notifyAll()
方法和Condition
接口的使用。 - 掌握线程池的使用,了解不同类型线程池的特点和应用场景。
- 阅读相关书籍和博客,进行实战练习,加深对 Java 多线程的理解。
十、参考资料
- Oracle 官方文档:https://docs.oracle.com/javase/tutorial/essential/concurrency/