目录
引言
多线程是指在同一时间,操作系统能够执行多个线程,这些线程可以共享进程的资源,如内存空间、打开的文件等。线程(thread)是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
在Java中,线程是Java虚拟机(JVM)调度的最小单位,它负责执行Java代码。多线程编程能够充分利用CPU资源,提高程序的执行效率。
一、多线程的实现方式
Java多线程的实现方式主要有以下几种:
1.1 继承Thread类
自定义线程类继承Thread类,并重写run()方法,编写线程执行体。然后创建线程对象,调用start()方法启动线程。
// 自定义线程类继承Thread类
class MyThread extends Thread {
@Override
public void run() {
// 线程执行体
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i);
try {
// 让线程休眠一段时间
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
// 创建线程对象
MyThread myThread = new MyThread();
// 启动线程
myThread.start();
}
}
1.2 实现Runnable接口
定义类实现Runnable接口,并重写run()方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target。然后创建线程对象,调用start()方法启动线程。
// 定义类实现Runnable接口
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行体
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i);
try {
// 让线程休眠一段时间
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
// 创建Runnable实现类的实例对象
MyRunnable myRunnable = new MyRunnable();
// 将Runnable对象作为Thread构造函数的target
Thread thread = new Thread(myRunnable);
// 启动线程
thread.start();
}
}
1.3 通过Callable和FutureTask创建线程
创建Callable接口的实现类,并实现call方法。然后创建Callable实现类的实例,使用FutureTask类包装Callable对象,该FutureTask对象封装了Callable对象的call方法的返回值。最后使用FutureTask对象作为Thread对象的target创建并启动线程,调用FutureTask对象的get()来获取子线程执行结束的返回值。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
// 创建Callable接口的实现类
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i);
sum += i;
Thread.sleep(1000); // 模拟耗时操作
}
return sum;
}
}
public class Main {
public static void main(String[] args) {
// 创建Callable实现类的实例
MyCallable myCallable = new MyCallable();
// 使用FutureTask类包装Callable对象
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
// 使用FutureTask对象作为Thread对象的target创建线程
Thread thread = new Thread(futureTask);
// 启动线程
thread.start();
try {
// 获取子线程执行结束的返回值
Integer result = futureTask.get(); // 这会阻塞直到Callable的call方法完成
System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
二、常见的成员方法
Java多线程常见的成员方法包括:
2.1 设置和获取线程名称
可以通过setName()方法设置线程名称,通过getName()方法获取线程名称。如果没有给线程设置名字,线程会有默认名字,如Thread-X(X为序号,从0开始)。
public class ThreadNameExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
// 获取线程名称(未设置前为默认名称)
System.out.println("Default thread name: " + Thread.currentThread().getName());
// 设置线程名称
Thread.currentThread().setName("MyCustomThread");
// 再次获取线程名称(已设置后的名称)
System.out.println("Set thread name: " + Thread.currentThread().getName());
});
thread.start();
}
}
2.2 currentThread()方法
返回对当前执行线程的引用。
public class CurrentThreadExample {
public static void main(String[] args) {
// 获取当前执行线程(main线程)的引用
Thread mainThread = Thread.currentThread();
System.out.println("Current thread (main): " + mainThread.getName());
Thread thread = new Thread(() -> {
// 获取当前执行线程(子线程)的引用
Thread currentThread = Thread.currentThread();
System.out.println("Current thread (inside new thread): " + currentThread.getName());
});
thread.start();
}
}
2.3 设置和获取线程优先级
线程的优先级表示线程被调度器选中的优先级,优先级范围从1到10,默认是5。可以通过setPriority(int newPriority)方法设置线程优先级,通过getPriority()方法获取线程优先级。
public class ThreadPriorityExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
// 获取线程优先级(默认优先级为5)
int priority = Thread.currentThread().getPriority();
System.out.println("Default thread priority: " + priority);
// 设置线程优先级
Thread.currentThread().setPriority(10);
// 再次获取线程优先级(已设置后的优先级)
priority = Thread.currentThread().getPriority();
System.out.println("Set thread priority: " + priority);
});
// 设置线程启动时的优先级(虽然这里设置了,但线程已经启动后设置的优先级会覆盖这个值)
thread.setPriority(1); // 这个设置实际上不会生效,因为线程已经在后面启动了
thread.start(); // 线程启动后会使用默认优先级或后面设置的优先级
// 注意:上面的设置方式只是为了演示如何设置优先级,正确的做法是在线程启动前或线程内部设置
}
}
2.4 设置守护线程
守护线程是运行在后台的一种特殊线程,它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。可以通过setDaemon(boolean on)方法将线程设置为守护线程。当非守护线程结束之后,守护线程会陆续结束。
public class DaemonThreadExample {
public static void main(String[] args) {
Thread daemonThread = new Thread(() -> {
while (true) {
System.out.println("Daemon thread is running: " + Thread.currentThread().getName());
try {
Thread.sleep(1000); // 模拟一些后台工作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 将线程设置为守护线程
daemonThread.setDaemon(true);
daemonThread.start();
try {
// 让主线程休眠一段时间以观察守护线程的行为
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 当主线程(非守护线程)结束时,守护线程也会结束
System.out.println("Main thread is exiting. Daemon thread should exit soon.");
}
}
2.5 出让/礼让线程
出让线程,也称为线程礼让(Thread Yield),是Java多线程编程中的一种机制,允许一个线程在其运行到某个点时,主动出让CPU资源,使得其他具有相同优先级的线程有机会运行。这通常用于调试或测试,以及在某些情况下提高程序的响应性。
public class YieldExample {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i);
// 在某些情况下出让CPU资源
if (i % 2 == 0) {
Thread.yield(); // 提示调度器出让CPU
}
try {
Thread.sleep(50); // 模拟一些工作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i);
try {
Thread.sleep(50); // 模拟一些工作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread1.start();
thread2.start();
}
}
2.6 插入/插队线程
在Java多线程编程中,join方法提供了一种机制,允许一个线程等待另一个线程完成其执行。它可以用来确保某个线程在另一个线程完成之前不会继续执行。这可以模拟一种“优先级”或“顺序”执行的效果,使得一个线程看起来像是在另一个线程之前“插队”。
join方法是非阻塞的,但调用它的线程会暂停执行,直到被join的线程完成。这可以用于确保线程之间的依赖关系得到维护,或者用于在继续执行之前等待某个特定任务完成。
public class JoinExample {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i);
try {
Thread.sleep(1000); // 模拟一些工作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(() -> {
System.out.println("Waiting for thread1 to finish...");
try {
thread1.join(); // 等待thread1完成
} catch (InterruptedException e) {
e.printStackTrace();
}
// thread1完成后,thread2开始执行
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i);
try {
Thread.sleep(1000); // 模拟一些工作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread1.start();
// 注意:如果先启动thread2,那么thread2会立即等待thread1,无论thread1是否已经启动
// 在这个例子中,我们先启动thread1,然后再启动thread2
// 这模拟了thread2“插队”等待thread1完成的效果(尽管实际上并没有真正的插队发生)
try {
// 这里可以添加一些延迟来模拟thread1已经启动但尚未完成的情况
// 但在这个例子中,我们直接启动thread2,因为它会立即等待thread1
Thread.sleep(500); // 可选的延迟,仅用于演示目的
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
}
}
三、死锁
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法继续执行。产生死锁的必要条件包括互斥条件、请求和保持条件、不剥夺条件和循环等待条件。
解决死锁的方法包括:
- 破坏互斥条件:允许线程同时访问某些资源。
- 破坏请求和保持条件:要么一次性申请所有的资源,要么在申请不到资源时释放所有已占有的资源。
- 破坏不剥夺条件:允许一个线程强行从另一个线程那里夺取资源。
- 破坏循环等待条件:将系统中的所有资源统一编号,所有线程申请资源时必须按照编号的递增顺序进行。
四、线程安全的问题
线程安全是指在多线程环境下,无论操作系统如何调度线程,程序都能够正确运行,不会发生如脏读、丢失更新等问题。常见的线程安全问题包括数据不一致、死锁和性能下降等。
解决线程安全问题的方法包括:
- 加强同步:通过添加synchronized关键字或者使用java.util.concurrent包中的高级同步机制来加强同步。
- 减少锁粒度:将一个大锁分解为几个小锁,可以减少线程间的竞争,提高程序的并发性。
- 使用原子类:java.util.concurrent.atomic包中的原子类提供了无锁的线程安全操作,适用于简单的递增、读取等操作。
- 避免死锁:设计系统时要避免嵌套锁和循环等待条件,或者使用超时尝试获取锁来避免死锁。
五、生产者与消费者
生产者与消费者问题是多线程编程中的一个经典问题。生产者线程负责生产数据,并将其放入缓冲区中;消费者线程负责从缓冲区中取出数据,并进行处理。为了保证生产者和消费者之间的协调,通常需要使用同步机制来确保数据的一致性和完整性。
解决生产者与消费者问题的方法包括:
- 使用synchronized关键字:在缓冲区的访问方法上添加synchronized关键字,以确保同一时间只有一个线程能够访问缓冲区。
- 使用wait()和notify()方法:在生产者线程中,当缓冲区满时调用wait()方法使线程进入等待状态;在消费者线程中,当缓冲区为空时调用wait()方法使线程进入等待状态。当生产者线程生产了新的数据或消费者线程消费了数据后,调用notify()方法唤醒等待的线程。
- 使用java.util.concurrent包中的阻塞队列:Java提供了多种阻塞队列实现,如ArrayBlockingQueue、LinkedBlockingQueue等。这些阻塞队列提供了put()和take()方法,当缓冲区满时put()方法会阻塞生产者线程,当缓冲区为空时take()方法会阻塞消费者线程。这样可以方便地实现生产者与消费者之间的协调。
下面以阻塞队列为例来实现生产者与消费者:
5.1 生产者
import java.util.concurrent.ArrayBlockingQueue;
public class Producer implements Runnable {
private ArrayBlockingQueue<Integer> queue;
public Producer(ArrayBlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
int itemNo = 0;
try {
while (true) {
// 模拟生产一个项目
Integer item = produceItem(itemNo++);
// 将项目放入队列中
queue.put(item);
System.out.println("Produced: " + item);
// 为了模拟有限的生产能力,让生产者线程休眠一段时间
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private Integer produceItem(int itemNo) {
// 这里只是简单地返回一个整数作为生产的项目
// 在实际应用中,这里可以是任何生产逻辑
return itemNo;
}
}
5.2 消费者
import java.util.concurrent.ArrayBlockingQueue;
public class Consumer implements Runnable {
private ArrayBlockingQueue<Integer> queue;
public Consumer(ArrayBlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
while (true) {
// 从队列中获取一个项目(如果队列为空,则线程会阻塞直到有项目可用)
Integer item = queue.take();
System.out.println("Consumed: " + item);
// 为了模拟有限的消费能力,让消费者线程休眠一段时间
Thread.sleep(150);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
5.1 测试类
import java.util.concurrent.ArrayBlockingQueue;
public class ProducerConsumerExample {
public static void main(String[] args) {
ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
Producer producer = new Producer(queue);
Consumer consumer = new Consumer(queue);
Thread producerThread = new Thread(producer);
Thread consumerThread = new Thread(consumer);
producerThread.start();
consumerThread.start();
}
}
结语
Java多线程编程是一个复杂而强大的工具,它允许开发者同时执行多个任务,提高程序的执行效率和响应速度。然而,多线程编程也带来了线程安全、死锁等问题,需要开发者谨慎处理。通过理解多线程的基本概念、实现方式、常见的成员方法以及线程安全和死锁等问题,并掌握相应的解决策略和方法,开发者可以更好地利用多线程编程来提高程序的性能和可靠性。