线程概述
1. 进程 和 线程
- 进程:在一个操作系统中,每个独立执行的程序都可以称之为一个进程,也就是 " 正在运行的程序 "
- 线程:一个进程中可以有多个执行单元同时运行,来同时完成一个或多个程序任务,这些执行单元可以看作程序执行的一条条线索,被称为线程。操作系统中的每一个进程都至少存在一个线程
- 多线程:一个进程中可以并发多个线程,每条线程并行执行不同的任务。多线程是多任务的一种特别的形式
2. 进程 和 线程 的区别
- 进程是包含线程的,每个进程至少有一个线程存在,即主线程
- 进程和进程之间不共享内存空间, 同一个进程的线程之间共享同一个内存空间
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位
3. 并行 和 并发
- 并发:微观上一个 CPU 核心,先执行一会任务A,再执行一会任务B,任务A和任务B交替进行,只要切换的够快,宏观上看来就像多个任务同时执行
- 并行:微观上两个 CPU 核心,同时执行任务A和任务B
4. 为什么使用多线程
- 为了更好的利用cpu的资源,若只有一个线程,则第二个任务必须等到第一个任务结束后才能进行,如果使用多线程则在主线程执行任务的同时可以执行其他任务,而不需要等待
- 进程之间不能共享数据,线程可以
- 线程比进程更轻量:创建线程比创建进程更快;销毁线程比销毁进程更快 ;调度线程比调度进程更快
5. Java 的线程 和 操作系统线程 的关系
- 线程是操作系统中的概念,操作系统内核实现了线程这样的机制,并且对用户层提供了一些 API 供用户使用
- Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装
线程的生命周期
- 新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态,此时不能运行,仅仅由 JVM 为其分配了内存 - 就绪状态:
当线程对象调用了 start() 方法之后,该线程就进入状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度 - 运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。 - 阻塞状态:
处于运行状态的线程可能会因为某些原因失去 CPU 的执行权,暂时停止运行进入阻塞状态,此时, JVM 不会给线程分配 CPU ,直到线程重新进入就绪状态。阻塞状态的线程只能先进入就绪状态,不能直接进入运行状态
一般在两种情况下进入阻塞状态:
- 当线程 A 运行过程中试图获取同步锁时,却被线程 B 获取,此时 JVM 会把当前线程 A 存到对象的锁池中,线程 A 进入阻塞状态
- 当线程运行过程中,发出 I/O 请求时,该线程也会进入阻塞状态
- 等待状态:
当处于运行状态的线程调用了无时间参数限制的方法后,例如:wait()、jion() 等方法,就会将当前运行中的线程转变为等待状态
处于等待状态中的线程不能立即争夺 CPU 使用权,必须等待其他线程执行特定的操作后,才有机会将等待状态转换为运行状态
例如:等待其他线程调用 notify() 或者 notifyAll() 方法唤醒当前等待中的线程;调用 jion() 方法而处于等待状态中的线程,必须等待其他加入的线程终止
- 定时等待状态:
和等待状态类似,只是运行线程调用了有时间参数限制的方法,例如:sleep(long millis)、wait(long timeout)、jion(long millis)等方法
处于定时等待状态中的线程也不能立即争夺 CPU 使用权,必须等待其他相关线程执行完特定的操作或者限制的时间后,才有机会将等待状态转换为运行状态
例如:通过其他线程调用 notify() 或者 notifyAll() 方法唤醒当前等待中的线程,或者等待限时时间结束后也可以进行状态转换
- 终止状态:
线程的 run() 方法、call() 方法正常执行完毕或者线程抛出一个未捕获的异常、错误,线程就会进入终止状态,一旦进入终止状态,线程将不再拥有运行的资格,也不能再转换为其他状态,生命周期结束
线程的创建
1. 继承 Thread 类,重写 run() 方法
步骤:
- 创建一个 Thread 线程类的子类(子线程),同时重写 Thread 类的 run() 方法
class MyThread extends Thread {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
- 创建该子类的实例对象,并通过调用 start() 方法启动线程
MyThread t = new MyThread();
t.start(); // 线程开始运行
代码实现:
class myThread extends Thread {
public void run() {
for (int x = 0; x < 10; x++) {
//currentThread(): Thread 类的静态方法,用来获取当前线程对象
//getName(): 用来获取线程名称
System.out.println(Thread.currentThread().getName()+ "运行");
}
}
}
public class duoxiancheng {
public static void main(String[] args) {
myThread t1 = new myThread();
t1.start();
myThread t2 = new myThread();
t2.start();
for(int x = 0; x < 10; x++){
System.out.println("main:"+x);
}
}
}
main() 方法中有一条主线程在运行
2. 实现 Runnable 接口,重写 run() 方法
Java 只支持类的单继承,如果某个类已经继承了其他父类,就无法再继承 Thread 类来实现多继承,此时,可以通过实现 Runnable 接口来实现多线程
步骤:
- 创建一个 Runnable 接口的实现类,同时重写接口中的 run() 方法
class myThread implements Runnable{
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
}
- 创建 Runnable 接口的实现类对象,使用 Thread 有参构造方法创建线程实例,并将 Runnable 接口的实现类的实例对象作为参数传入
//创建 Runnable 接口的实现类对象
myThread1 t1 = new myThread1();
//使用 Thread(Runnable target,String name) 构造方法创建线程对象
Thread t2 = new Thread(t1,"t2");
//调用线程对象的 start() 方法启动线程
t2.start();
Thread 的常见构造方法:
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
代码实现:
class myThread1 implements Runnable{
@Override
public void run() {
for (int x = 0; x < 10; x++) {
//currentThread(): Thread 类的静态方法,用来获取当前线程对象
//getName(): 用来获取线程名称
System.out.println(Thread.currentThread().getName()+ " 运行");
}
}
}
public class duoxiancheng {
public static void main(String[] args) {
myThread1 t = new myThread1();
Thread t2 = new Thread(t,"顺顺");
t2.start();
//创建并启动另一个线程
Thread t3 = new Thread(t,"利利");
t3.start();
}
}
其他实现:
//1:将 Runnable 实例传给 Thread
public static void main(String[] args) {
Thread t = new Thread(new myThread1());
t.start();
}
//2:创建匿名内部类
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
for (int x = 0; x < 10; x++) {
System.out.println(Thread.currentThread().getName()+ "运行");
}
}
};
t.start();
}
//3:使用匿名类创建 Runnable 子类对象
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int x = 0; x < 10; x++) {
System.out.println(Thread.currentThread().getName()+ "运行");
}
}
});
t.start();
}
//4:lambda 表达式
public static void main(String[] args) {
Thread t = new Thread(() -> {
for (int x = 0; x < 10; x++) {
System.out.println(Thread.currentThread().getName()+ "运行");
}
});
t.start();
}
3. 实现 Callable 接口,重写 call() 方法,并使用 Future 来获取 call() 方法的返回结果
Thread 类和 Runnable 接口实现多线程时需要重写 run() 方法,但 run() 方法没有返回值,因此无法从多个线程中获取返回结果,此时,可以使用 Callable 接口来满足既创建多线程又有返回值的需求
步骤:
- 创建一个 Callable 接口的实现类,同时重写 call() 方法
- 创建 Callable 接口的实现类对象
- 通过 FutureTask 线程结果处理类的有参构造方法封装 Callable 接口实现类对象
- 使用参数为 FutureTask 类对象的 Thread 有参构造方法创建 Thread 线程实例
- 调用线程实例的 Start() 方法启动线程
代码实现:
class myThread2 implements Callable<Object>{
//重写 call() 方法
@Override
public Object call() throws Exception {
int x = 0;
for (; x < 10; x++) {
System.out.println(Thread.currentThread().getName()+ "运行");
}
return x;
}
}
public class duoxiancheng {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建 Callable 接口的实现类对象
myThread2 t1 = new myThread2();
//通过 FutureTask 封装 Callable 接口实现类对象
FutureTask<Object> f1 = new FutureTask<>(t1);
//使用 Thread 有参构造方法创建线程对象
Thread t11 = new Thread(f1,"ABC");
t11.start();
FutureTask<Object> f2 = new FutureTask<>(t1);
Thread t22 = new Thread(f2,"DEF");
t22.start();
System.out.println("t11 返回结果:" + f1.get());
System.out.println("t22 返回结果:" + f2.get());
}
}
- Callable 和 Runnable 相对, 都是描述一个 “任务”。 Callable 描述的是带有返回值的任务,Runnable 描述的是不带返回值的任务
- Callable 通常需要搭配 FutureTask 来使用,FutureTask 用来保存 Callable 的返回结果。因为Callable 往往是在另一个线程中执行的, 什么时候执行完并不确定,FutureTask 就可以负责这个等待结果出来的工作
4. 三种方式对比
三种方式中,Runnable 接口和 Callable 接口 实现多线程的方式基本相同,主要区别是 Callable 接口方法中有返回值,并且可以声明抛异常
Thread 和 Runnable 的区别:
- Runnable 接口(或 Callable 接口)比起 Thread 类 来说,更适合多个线程处理同一个共享资源的情况
- Runnable 接口(或 Callable 接口)可以避免 Java 单继承带来的局限性
class Ticket extends Thread{
private int tickets = 20;
@Override
public void run() {
while(true)