JAVA多线程
多线程,说白了就是多条执行路径,原来是一条路径,就主路径(main),现在是多条路径。
一,常用概念
1.1 程序
Java源程序和字节码文件被称为“程序” ( Program ),是一个静态的概念。
1.2 进程
执行中的程序叫做进程(Process),是一个动态的概念。为了使计算机程序得以运行,计算机需要加载代 码,同时也要加载数据。
- 进程是程序的一次动态执行过程, 占用特定的地址空间。
- 每个进程由3部分组成:cpu,data,code。每个进程都是独立的,保有自己的cpu时间,代码和数 据,即便用同一份程序产生好几个进程,它们之间还是拥有自己的这3样东西。
- 多任务(Multitasking)操作系统将CPU时间动态地划分给每个进程,操作系统同时执行多个进程,每 个进程独立运行。以进程的观点来看,它会以为自己独占Cpu的使用权
1.3 线程
线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程 并行执行不同的任务。
1.4 进程与线程之间的区别
区别 | 进程 | 线程 |
---|---|---|
根本区别 | 作为资源分配的单位 | 调度和执行的单位 |
开销 | 每个进程都有独立的代码和数据空间,进程间的切换有较大开销 | 线程可以看做轻量级的进程,同一类线程共享 代码和数据空间,每个线程有独立运行栈和程 序计数器(PC),线程切换的开销小 |
所处环境 | 在操作系统中能同时运行多个任务(程序) | 在同一个应用程序中有多个顺序流同时进行 |
分配内存 | 系统在运行的时候会为每个进程分配不 同的内存区域 | 线程间共享进程的所有资源,每个线程只有有 自己的堆栈和局部变量。线程由CPU独立调度 执行,在多CPU环境下就允许多个线程同时运 行 |
包含关系 | 没有线程的进程可以看作单线程,如果 一个进程拥有多个线程,则执行过程不 是一条线的,而是多条线(线程)共同 完成的 | 线程是进程的一部分,所以线程有的时候会被 称为是轻量级进程或轻权进程 |
注意:有的多线程是模拟出来的,真正的多线程是指有多个 cpu,即多核,如服务器。如果是模拟出来的 多线程,即一个 cpu 的情况下,在同一个时间点,cpu 只能执行一个代码, 因为切换的很快,所以就有同时执行的错觉。
1.5 多线程的优缺点
优点
资源利用率更好;程序设计在某些情况下更简单;程序响应更快
缺点
- 设计更复杂,虽然有一些多线程应用程序比单线程的应用程序要简单,但其他的一般都更复杂。在 多线程访问共享数据的时候,这部分代码需要特别的注意。线程之间的交互往 往非常复杂。不正确 的线程同步产 生的错误非常难以被发现,并且重现以修复。
- 上下文切换的开销 当 CPU 从执行一个线程切换到执行另外一个线程的时候,它需要 先存储当前线 程的本地的数据,程序 指针等,然后载入另一个线程的本地数据,程序指针 等,最后才开始执 行。这种切换称为“上下文切 换”(“context switch”)。CPU 会在一 个上下文中执行一个线程,然后 切换到另外一个上下文中执 行另外一个线程。上下文切换 并不廉价。如果没有必要,应该减少上 下文切换的发生。
二,创建线程
2.1 继承Thread类实现
- 创建线程类: 继承 Thread类 +重写 run() 方法
- 构造线程类对象: 创建 子类的对象
- 启动线程: 通过子类对象调用 start() 方法
//继承多线程父类
public class ThreadDemo extends Thread {
//重写run()方法
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("吃饭");
//只能抓取异常,父类没有抛出异常
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
Sleep s = new Sleep();
//开始多线程
td.start();
s.start();
for (int i = 0; i < 20; i++) {
System.out.println("打豆豆");
try {
Thread.sleep(2);//延迟执行,放大问题
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
这种方式的特点:那就是如果我们的类已经从一个类继承,则无法再 继承 Thread 类,异常只能捕获。
2.2 实现Runnable接口实现
- 创建实现 Runnable 接口的实现类 + 重写 run() 方法
- 创建一个实现类对象
- 利用实现类对象创建Thread类对象
- 启动线程
public class ThreadInterfaceDemo implements Runnable {
public static void main(String[] args) {
//真实角色
ThreadInterfaceDemo tid = new ThreadInterfaceDemo();
//创建代理线程
Thread td = new Thread(tid);
//开始线程
td.start();
for (int i = 0; i < 100; i++) {
System.out.println("Thread");
}
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("Runnable");
}
}
}
优点:接口可以多实现,类只能单继承,实现资源共享
2.3 实现Callable接口实现
- 创建实现 Callable 接口的实现类 + 重写 call() 方法
- 创建一个实现类对象
- 由 Callable 创建一个 FutureTask 对象
- 由 FutureTask 创建一个 Thread 对象
- 启动线程
public class CallAbleTest {
public static void main(String[] args) throws Exception{
MyCallable callable = new MyCallable();
// 将Callable包装成FutureTask,FutureTask也是一种Runnable
FutureTask<Integer> futureTask = new FutureTask<>(callable);
// 将FutureTask包装成Thread
new Thread(futureTask).start();
System.out.println(futureTask.isDone());
System.out.println(futureTask.get());
}
}
class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100000; i++) {
sum += i;
}
return sum;
}
}
- Thread: 继承方式, 不建议使用, 因为Java是单继承的,继承了Thread就没办法继承其它类了,不够 灵活
- Runnable: 实现接口,比Thread类更加灵活,没有单继承的限制 Callable: Thread和Runnable都是重写的run()方法并且没有返回值,
- Callable是重写的call()方法并 且有返回值并可以借助FutureTask类来判断线程是否已经执行完毕或者取消线程执行
- 当线程不需要返回值时使用Runnable,需要返回值时就使用Callable,一般情况下不直接把线程体 代码放到Thread类中,一般通过Thread类来启动线程
- Thread类是实现Runnable,Callable封装成FutureTask,FutureTask实现RunnableFuture, RunnableFuture继承Runnable,所以Callable也算是一种Runnable,所以三种实现方式本质上都 是Runnable实现
优点:
call方法可以抛出异常可以定义返回值
缺点:
使用复杂麻烦
2.4 使用线程池创建
线程池
就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系 统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任 务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。
线程池的工作机制
在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任 务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。 一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。
使用
- 使用 Executors 类中的 newFixedThreadPool(int num) 方法创建一个线程数量为num的线程池
- 调用线程池中的 execute() 方法执行由实现 Runnable 接口创建的线程;调用 submit() 方法执行 由实现 Callable 接口创建的线程
- 调用线程池中的 shutdown() 方法关闭线程池
public class ThreadPoolTest {
public static void main(String[] args)throws Exception {
Thread.currentThread().setName("主线程");
System.out.println(Thread.currentThread().getName() + ": 输出的结果" );
// 通过线程池工厂创建线程数量为2的线程池
ExecutorService service = Executors.newFixedThreadPool(2);
//执行线程,execute()适用于实现Runnable接口创建的线程
service.execute(new MyThread());
//submit()适用于实现Callable接口创建的线程
Future<Integer> task = service.submit(new MyCallable());
System.out.println(task.get());
// 关闭线程池
service.shutdown();
}
}
三,线程的5种状态
3.1 新建状态
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持 这个状态直到程序 start() 这个线程。
3.2 就绪状态
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要 等待JVM里线程调度器的调度。
3.3 运行状态
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态 的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
3.4 阻塞状态
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就 从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
- 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
- 同步阻塞:线程在获取 synchronized同步锁失败(因为同步锁被其他线程占用)。
- 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O请求时,线程就会进入到阻塞状态。 当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状 态。
3.5 终止状态
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。