为是么需要线程
并发的发展历史
-
真空管和穿孔打卡
最早的计算机只能解决简单的数学运算问题,比如正弦、余弦等。运行方式:程序员首先把程序写到纸上,然后穿孔成卡片,再把卡片盒带入到专门的输入室。输入室会有专门的操作员将卡片的程序输入到计算机上。计算机运行完当前的任务以后,把计算结果从打印机上进行输出,操作员再把打印出来的结果送入到输出室,程序员就可以从输出室取到结果。然后,操作员再继续从已经送入到输入室的卡片盒中读入另一个任务重复上述的步骤。操作员在机房里面来回调度资源,以及计算机同一个时刻只能运行一个程序,在程序输入的过程中,计算机计算机和处理空闲状态 。而当时的计算机是非常昂贵的,人们为了减少这种资源的浪费。就采用了 批处理系统来解决
-
晶体管和批处理系统
批处理操作系统的运行方式:在输入室收集全部的作业,然后用一台比较便宜的计算机把它们读取到磁带上。然后把磁带输入到计算机,计算机通过读取磁带的指令来进行运算,最后把结果输出磁带上。批处理操作系统的好处在于,计算机会一直处于运算状态,合理的利用了计算机资源。批处理操作系统虽然能够解决计算机的空闲问题,但是当某一个作业因为等待磁盘或者其他 I/O 操作而暂停时,那CPU 就只能阻塞直到该 I/O 完成,对于 CPU 操作密集型的程序,I/O 操作相对较少,因此浪费的时间也很少。但是对于 I/O 操作较多的场景来说,CPU 的资源是属于严重浪费的。
-
集成电路和多道程序设计
多道程序设计的出现解决了这个问题,就是把内存分为几个部分,每一个部分放不同的程序。当一个程序需要等待I/O 操作完成时。那么 CPU 可以切换执行内存中的另外一个程序。如果内存中可以同时存放足够多的程序,那 CPU的利用率可以接近 100%。
在这个时候,引入了第一个概念- 进程, 进程的本质是一个正在执行的程序,程序运行时系统会创建一个进程,并且给每个进程分配独立的内存地址空间保证每个进程地址不会相互干扰。同时,在 CPU 对进程做时间片的切换时,保证进程切换过程中仍然要从进程切换之前运行的位置出开始执行。所以进程通常还会包括程序计数器、堆栈指针。有了进程以后,可以让操作系统从宏观层面实现多应用并发。而并发的实现是通过 CPU 时间片不端切换执行的。对于单核 CPU 来说,在任意一个时刻只会有一个进程在被CPU 调度
线程
- 在多核 CPU 中,利用多线程可以实现真正意义上的并行
执行 - 在一个应用进程中,会存在多个同时执行的任务,如果其中一个任务被阻塞,将会引起不依赖该任务的任务也被阻塞。通过对不同任务创建不同的线程去处理,可以提升程序处理的实时性
- 线程可以认为是轻量级的进程,所以线程的创建、销毁比进程更快
在Java中,当我们启动main函数时其实就启动了一个JVM的进程,而main函数所在的线程就是这个进程中的一个线程,也称主线程。
一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。
每个线程都有自己的栈资源,用于存储该线程的局部变量,这些局部变量是该线程私有的,其他线程是访问不了的,除此之外栈还用来存放线程的调用栈帧。
堆是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建时分配的,堆里面主要存放使用new操作创建的对象实例。
方法区则用来存放JVM加载的类、常量及静态变量等信息,也是线程共享的
线程的创建
继承Thread类创建线程
public class ThreadTest {
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("我是是子线程");
}
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
当创建完thread对象后该线程并没有被启动执行,直到调用了start方法后才真正启动了线程。其实调用start方法后线程并没有马上执行而是处于就绪状态,这个就绪状态是指该线程已经获取了除CPU资源外的其他资源,等待获取CPU资源后才会真正处于运行状态。一旦run方法执行完毕,该线程就处于终止状态。
使用继承方式的好处是,在run()方法内获取当前线程直接使用this就可以了,无须使用Thread.currentThread()方法;不好的地方是Java不支持多继承,如果继承了Thread类,那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码,而Runable则没有这个限制。
实现 Runnable 接口创建线程
public class MyThread extends OtherClass implements Runnable {
public void run {
System.out.println("MyThread run ");
}
}
实现 Callable 接口通过 FutureTask 包装器来创建 Thread 线程
public class CallableTask implements Callable<String> {
@Override
public String call() throws Exception {
return "hello";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1.创建异步任务
FutureTask<String> futureTask = new FutureTask<>(new CallableTask());
// 启动线程
new Thread(futureTask).start();
// 阻塞等待任务执行完毕,并返回结果
String result = futureTask.get();
System.out.println(result);
// 2.线程池方式
ExecutorService executorService = Executors.newFixedThreadPool(1);
Future<String> future = executorService.submit(new CallableTask());
System.out.println(future.get());
executorService.shutdown();
}
}
线程生命周期
java.lang.Thread.State
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
NEW
:初始状态,线程被构建,但是还没有调用 start 方法
RUNNABLED
:运行状态,JAVA 线程把操作系统中的就绪和运行两种状态统一称为“运行中”
BLOCKED
:阻塞状态,表示线程进入等待状态,也就是线程
因为某种原因放弃了 CPU 使用权,阻塞也分为几种情况
➢ 等待阻塞:运行的线程执行 wait 方法,jvm 会把当前线程放入到等待队列
➢ 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么 jvm 会把当前的线程放入到锁池中
➢ 其他阻塞:运行的线程执行 Thread.sleep
或者t.join
方法,或者发出了 I/O 请求时,JVM 会把当前线程设置为阻塞状态,当 sleep 结束、join 线程终止、io 处理完毕则线程恢复
TIME_WAITING
:超时等待状态,超时以后自动返回
TERMINATED
:终止状态,表示当前线程执行完毕