Java并发 - 线程基础
1. 程序 & 线程
-
定义:
-
程序(Process): 一个程序是一个独立的执行单元,它包含了代码、数据、和系统资源的集合。每个程序都运行在自己独立的内存空间中,互相之间不直接共享内存。
-
线程(Thread): 线程是程序的执行流程,是操作系统调度的最小单元。一个程序可以包含多个线程,它们共享程序的内存空间和资源。
-
-
资源分配:
-
程序(Process): 每个程序有独立的内存空间和系统资源,包括文件句柄、网络连接等。程序之间通常是相互隔离的,彼此不直接影响。
-
线程(Thread): 线程共享所属程序的内存空间和资源。多个线程之间可以通过共享内存来进行通信,但也因此需要注意线程安全性。
-
-
切换开销:
-
程序(Process): 进程切换的开销相对较大,因为需要切换整个内存空间。
-
线程(Thread): 线程切换的开销相对较小,因为线程共享相同的内存空间,切换时只需要保存和恢复少量的上下文信息。
-
-
并发性:
-
程序(Process): 进程之间是相互独立的,通信需要使用进程间通信(IPC)机制,如管道、消息队列等。
-
线程(Thread): 线程之间可以直接共享内存,因此通信相对容易,但也需要注意同步和线程安全问题。
-
-
创建和销毁:
-
程序(Process): 创建和销毁一个进程相对较慢,需要分配和释放大量的资源。
-
线程(Thread): 创建和销毁一个线程相对较快,开销较小。
-
-
故障隔离:
-
程序(Process): 进程之间是相互独立的,一个进程的崩溃不会直接影响其他进程。
-
线程(Thread): 线程之间共享相同的内存空间,一个线程的错误可能导致整个程序崩溃。
-
2. JAVA中线程的生命周期
- 新建状态(New): 线程对象被创建但尚未启动。
- 就绪状态(Runnable): 线程已经被启动,等待获取CPU时间片。
- 运行状态(Running): 线程获取了CPU时间片,正在执行任务。
- 阻塞状态(Blocked): 线程被阻塞,等待某个条件的解除。
- 等待状态(Waiting): 线程在等待另一个线程的通知或中断。
- 超时等待状态(Timed Waiting): 线程在等待一个具有超时时间的条件。
- 终止状态(Terminated): 线程执行完毕,结束生命周期。
3. 线程的创建方式
在Java中,线程的创建和使用有多种方式,主要包括继承Thread类、实现Runnable接口、使用Callable和Future接口、使用线程池等。下面分别介绍这几种方式,并对比它们的优缺点和特性。
3.1 继承Thread类(无返回值)
class MyThread extends Thread {
public void run() {
// 线程执行的代码
}
}
// 创建并启动线程
MyThread myThread = new MyThread();
myThread.start();
优点:
- 简单,继承Thread类即可。
缺点:
- 由于Java是单继承的,继承Thread类后无法再继承其他类。
- 不利于共享资源,因为多个线程共享Thread的实例变量。
3.2 实现Runnable接口(无返回值)
class MyRunnable implements Runnable {
public void run() {
// 线程执行的代码
}
}
// 创建线程并启动
Thread myThread = new Thread(new MyRunnable());
myThread.start();
优点:
- 支持多继承,因为实现Runnable接口后还可以继承其他类。
- 适合多个线程共享同一个资源的情况。
缺点:
- 相对于继承Thread,使用稍微复杂。
3.3 使用Callable和Future接口(有返回值)
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable<String> {
public String call() {
// 线程执行的代码
return "Task completed";
}
}
// 创建并启动线程
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
Thread myThread = new Thread(futureTask);
myThread.start();
// 获取线程执行结果
String result = futureTask.get();
优点:
- 具有返回值,可以通过FutureTask获取线程执行的结果。
- 可以抛出异常,更灵活。
缺点:
- 相对于Runnable,使用稍微复杂。
- 不支持直接通过Thread的start方法启动,需要借助FutureTask。
3.4 使用线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyTask implements Runnable {
public void run() {
// 线程执行的代码
}
}
// 创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
// 提交任务
executorService.submit(new MyTask());
优点:
- 提高线程的复用性和效率,避免频繁创建和销毁线程。
- 控制并发线程数量,防止线程过多导致资源浪费。
缺点:
- 需要了解线程池的相关概念和API,相对于简单的线程创建方式更复杂。
4. 线程的种类
4.1 用户线程(User Thread)
用户线程
是程序中创建的一般线程,当所有的用户线程执行完毕后,虚拟机就会停止,不会等待守护线程执行完毕。大部分情况下,我们创建的线程都是用户线程。
Thread userThread = new Thread(() -> {
// 线程执行的代码
});
userThread.start();
4.2 守护线程(Daemon Thread)
守护线程
是为其他线程提供服务的线程。当所有用户线程执行完毕后,守护线程会被强制停止,而不管守护线程是否执行完毕。典型的守护线程包括垃圾回收线程。
Thread daemonThread = new Thread(() -> {
// 守护线程执行的代码
});
daemonThread.setDaemon(true);
daemonThread.start();
5. 线程的基础方法
方法 | 作用 |
---|---|
start() | 启动线程,使线程进入就绪状态,等待被调度执行。 |
run() | 线程的执行体,包含线程需要执行的代码。 |
join() | 等待线程执行完毕,使当前线程进入阻塞状态,直到被等待的线程执行完毕或等待超时。 |
sleep() | 让当前线程休眠一段时间,以毫秒为单位。 |
yield() | 让出CPU的使用权,使当前线程从运行状态进入就绪状态,让其他具有相同优先级的线程有机会执行 |
interrupt() | 中断线程,使线程进入中断状态。 |
isAlive() | 判断线程是否还活着(是否启动且未终止)。 |
以下列出基本用法。
Thread myThread = new Thread(() -> {
// 线程执行的代码
});
// 启动线程
myThread.start();
// 不会启动新线程,直接在当前线程中执行run()方法
myThread.run();
// ====== join start ======
myThread.start();
try {
// 等待myThread执行完毕
myThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// ====== join end ======
// ====== sleep start ======
try {
Thread.sleep(1000); // 休眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
// ====== sleep end ======
// 让出cpu 进入就绪状态
Thread.yield();
// ====== interrupt start ======
Thread myThread = new Thread(() -> {
while (!Thread.interrupted()) {
// 线程执行的代码
}
});
myThread.interrupt(); // 中断myThread
// ====== interrupt end ======
boolean alive = myThread.isAlive(); // 判断线程是否还活着
面试题:
sleep()
和yield()
两个方法的对照
- 影响状态
sleep()
会让线程从运行状态进入阻塞状态,等待指定时间后重新进入就绪状态。yield()
会让线程从运行状态进入就绪状态,但并不保证立即被调度执行。- 时间控制
- sleep()` 是精确的时间控制,线程会在指定时间后被唤醒。
yield()
并不涉及具体的时间控制,只是告诉调度器当前线程可以让出 CPU。- 适用场景
sleep()
适用于需要固定时间的休眠,如实现定时任务。yield()
适用于协调多个线程的执行,避免某个线程长时间占用 CPU。- 异常处理
sleep()
方法会抛出InterruptedException
异常,因为线程在休眠期间可能被中断。yield()
方法不会抛出异常。
6. 线程通信
方法 | 作用 |
---|---|
wait() | wait() 方法使当前线程进入等待状态,并释放对象的锁。 |
notify() | notify() 方法用于唤醒一个等待中的线程。它会选择其中一个等待线程,通知它可以继续执行。 |
notifyAll() | notifyAll() 方法用于唤醒所有等待中的线程。它会通知所有因调用 wait() 方法而进入等待状态的线程,让它们都有机会竞争对象的锁。 |
await() | 当前线程等待,释放锁,并进入等待状态,直到被其他线程调用 signal() 或 signalAll() 方法唤醒,或被中断。 |
signal() | 唤醒一个等待中的线程,使其从 await() 方法中返回。 |
signalAll() | 唤醒所有等待中的线程,使它们从 await() 方法中返回。 |
以下是wait() 和 notify()
public class SharedResource {
private boolean flag = false;
public synchronized void produce() throws InterruptedException {
while (flag) {
wait();
}
// 生产操作
flag = true;
System.out.println("生产!!!!");
notify();
}
public synchronized void consume() throws InterruptedException {
while (!flag) {
wait();
}
// 消费操作
flag = false;
System.out.println("消费!!!!");
notify();
}
public static void main(String[] args) {
SharedResource sharedResource = new SharedResource();
// Producer
Thread producerThread = new Thread(() -> {
try {
while (true) {
sharedResource.produce();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// Consumer
Thread consumerThread = new Thread(() -> {
try {
while (true) {
sharedResource.consume();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 启动线程
producerThread.start();
consumerThread.start();
}
}
运行结果
Connected to the target VM, address: '127.0.0.1:59789', transport: 'socket'
生产!!!!
消费!!!!
生产!!!!
消费!!!!
生产!!!!
消费!!!!
生产!!!!
消费!!!!
生产!!!!
消费!!!!
......
Condition
接口是Java中用于线程协作的一部分,它通常与 Lock
接口一起使用,提供了更灵活的线程同步和通信机制。Condition
接口定义了线程等待和唤醒的操作,允许线程在特定的条件下等待或唤醒。
public class SharedResource {
private boolean flag = false;
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void produce() throws InterruptedException {
lock.lock();
try {
while (flag) {
condition.await();
}
// 生产操作
flag = true;
System.out.println("生产操作");
condition.signal();
} finally {
lock.unlock();
}
}
public void consume() throws InterruptedException {
lock.lock();
try {
while (!flag) {
condition.await();
}
// 消费操作
flag = false;
System.out.println("消费操作");
condition.signal();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
SharedResource sharedResource = new SharedResource();
// Producer thread
Thread producerThread = new Thread(() -> {
try {
while (true) {
sharedResource.produce();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// Consumer thread
Thread consumerThread = new Thread(() -> {
try {
while (true) {
sharedResource.consume();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// Start the threads
producerThread.start();
consumerThread.start();
}
}
运行结果
Connected to the target VM, address: '127.0.0.1:49736', transport: 'socket'
生产操作
消费操作
生产操作
消费操作
生产操作
消费操作
生产操作
消费操作
生产操作
消费操作
......
面试题:
wait()
和sleep()
- 使用的类和位置
wait()
方法是Object
类的方法,用于等待其他线程发出通知。它必须在同步块中(使用synchronized
关键字)被调用。sleep()
方法是Thread
类的静态方法,用于使当前线程休眠指定的时间,不需要在同步块中调用。- 同步锁的释放
- 在调用
wait()
方法时,当前线程会释放持有的锁,使其他线程有机会获取锁并执行。等待期间,线程处于阻塞状态,直到被其他线程调用notify()
、notifyAll()
或超时唤醒。- 在调用
sleep()
方法时,线程保持锁定状态,不会释放锁。线程在指定的时间内休眠,然后自动唤醒,可以通过中断来提前唤醒。- 被唤醒的方式
- 调用
wait()
方法的线程需要被其他线程调用相同对象上的notify()
或notifyAll()
方法来唤醒,或者等待超时。sleep()
方法则会在指定的时间过后自动唤醒,或者可以通过其他线程调用该线程的interrupt()
方法来提前唤醒。- 抛出的异常
- 调用
wait()
方法的线程可能抛出InterruptedException
异常,需要处理中断异常。- 调用
sleep()
方法的线程也可能抛出InterruptedException
异常,同样需要处理中断异常。- 使用场景
wait()
方法通常用于线程间的协作,等待某个条件的满足。sleep()
方法通常用于线程的暂停,实现一定的时间延迟。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!