从CPU开始
CPU并不知道线程、进程之类的概念。
CPU只知道两件事:
1)从内存中取出指令;
2)执行指令,然后回到 1)。
CPU是从寄存器中取出指令执行,寄存器中有函数编译后形成的第一条指令,既函数入口。
操作系统和进程
这里有个问题,我们将程序从磁盘在内存中找到一块合适的区域,跑起来,找到函数入口,写入PC寄存器,交给CPU执行。
这一系列过程如果让程序员自己来执行,将会是很复杂的操作,如果把以上步骤结合起来作为一个程序,可以称为操作系统。
程序从磁盘加载到内存跑起来叫什么好呢?干脆就叫进程(Process)好了
从单核到多核
如果一个程序要利用到多核怎么办? 开更多进程吗?
但是同时会存在一下问题:
- 进程需要独占空间,浪费内存
- 进程通信麻烦,需要借助操作系统,增大了系统开销。
从进程到线程
所谓进程无非就是内存中的一段区域,这段区域中保存了CPU执行的机器指令以及函数运行时的堆栈信息,要想让进程运行,就把main函数的第一条机器指令地址写入PC寄存器,这样进程就运行起来了。
进程的缺点在于只有一个入口函数,也就是main函数,因此进程中的机器指令只能被一个CPU执行,那么有没有办法让多个CPU来执行同一个进程中的机器指令呢?
那么我们在程序中设计多个可执行函数交给CPU去执行,如下:
多个CPU可以在同一个屋檐下(进程占用的内存区域)同时执行属于该进程的多个入口函数,也就是说现在一个进程内可以有多个执行流了。
这些个执行流就是线程。
操作系统为每个进程维护了一堆信息,用来记录进程所处的内存空间等,这堆信息记为数据集A。
同样的,操作系统也需要为线程维护一堆信息,用来记录线程的入口函数或者栈信息等,这堆数据记为数据集B。
显然数据集B要比数据A的量要少,同时不像进程,创建一个线程时无需去内存中找一段内存空间,因为线程是运行在所处进程的地址空间的。
注意:
由于多线程共享工作空间,出现的线程安全问题需要自己解决。
并不是多核才能使用多线程,单核也可以,方法就是将CPU的时间片在各个线程之间来回分配,这样多个线程看起来就是“同时”运行了,但实际上任意时刻还是只有一个线程在运行。
啥是多线程? 串行,并行,并发
多线程
: 指的是这个程序(一个进程)运行时产生了不止一个线程。
并发
:是指同一个时间段内多个任务同时都在执行,并且都没有执行结束。并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时在执行。
并行
:是说在单位时间内多个任务同时在执行 。
注:在多线程编程实践中,线程的个数往往多于 CPU 的个数,所以一般都称多线程并发编程而不是多线程并行编程。
同步和异步
同步往往意味着双方要相互等待、相互依赖,而异步意味着双方相互独立、各行其是。
线程的状态
初始化状态:新建一个线程对象
可运行状态:其他线程调用了该线程对象的 start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取 CPU 的使用权
运行状态:可运行状态的线程获得了 cpu 时间片(timeslice),执行程序代码
阻塞状态:线程因为某种原因放弃 CPU 使用权,暂时停止运行。直到线程再次进入可运行状态,才有机会转到运行状态。如图所示,会有三种不同类型的阻塞状态:
- 等待阻塞:运行中的线程执行 wait()方法,线程会进入等待队列中。等待 notify()、notifyAll()或 interrupt()对其唤醒或中断
- 同步阻塞:运行中的线程执行在获取同步锁(注:只有 synchronized 这种方式的锁(monitor 锁)才会让线程出现 BLOCKED 状态,等待 ReentrantLock 则不会)时,若该锁已被其他线程占用,线程则会进入锁池队列。等待获取到锁
- 其他阻塞:运行的线程执行 sleep()、join(),或触发了 I/O 请求,该该线程被置为阻塞状态。当 sleep()状态超时、join()等待线程终止或超时、I/O 处理完成,线程会重新进入可运行状态。
死亡状态:线程执行完或因异常退出 run()方法,线程生命周期结束
常见方法
Thread.sleep(long millis)
静态方法。当前线程调用此方法,使当前线程进入阻塞状态(其他阻塞),但不释放任何锁资源,一定时间后线程自动进入 runnable 状态。给其它线程执行机会的最佳方式。obj.wait()
或obj.wait(long timeout)
当前线程调用某对象的 wait()方法,当前线程释放对象锁(wait 一定在 synchronized 代码块/方法中,故一定得到了锁,才进来的此方法),进入阻塞状态(等待队列)。等待 notify 或 wait 设置的 timeout 到期,方可进入另外一个阻塞状态(锁池)。t.join()
或t.join(long millis)
非静态方法。当前线程 A 执行过程中,调用 B 线程的 join 方法,使当前线程进入阻塞状态(其他阻塞),但不释放对象锁,等待 B 线程执行完后或一定时间 millis 后,A 线程进入 runnable 状态。Thread.yield()
静态方法。当前线程调用此方法,使线程由 running 态进入 runnable 态,放弃 cpu 使用权,让 cpu 再次选择要执行的线程。 注:实际过程中,yield 仅仅是让其它具有同等优先级的 runnable 线程获取执行权,但并不能保证其它具有同等优先级的线程就一定能获得 cpu 执行权。因为做出让步的当前线程,可能会被 cpu 再次选中,进入 running 状态。yield()不会导致阻塞。
线程创建
- 继承 Thread
在这里我们思考一下为什么不直接调用 run()
方法而是去调用 start()
方法?
线程的 run()
方法是由 java 虚拟机直接调用的
如果我们没有启动线程(没有调用线程的 start()
方法)而是在应用代码中直接调用 run()
方法,那么这个线程的 run()
方法其实运行在当前线程(即 run()
方法的调用方所在的线程)之中,而不是运行在其自身的线程中。
所以就是先访问 start 创建线程 jvm 去调用 run()
方法!
这种方式的弊端是一个类只能继承一个父类,如果这个类本身已经继承了其它类,就不能使用这种方式了。
- 实现 Runnable 接口
实现 Runnable 接口,这种方式的好处是一个类可以实现多个接口,不影响其继承体系。
- 实现 Callabe 接口
1 | public class CreatingThread04 implements Callable<Long> { |
FutureTask 可用于异步获取执行结果或取消执行任务的场景。通过传入 Runnable 或者 Callable 的任务给 FutureTask,直接调用其 run 方法或者放入线程池执行,之后可以在外部通过 FutureTask 的 get 方法异步获取执行结果,因此,FutureTask 非常适合用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
- 定时器
1 | public class CreatingThread05 { |
使用定时器 java.util.Timer 可以快速地实现定时任务,TimerTask 实际上实现了 Runnable 接口。
总结
上文我们讲到了创建线程的几种方法,其中最常见的是继承 Thread 类与实现 Runable 接口,其实 Thread 类的源码也是实现 Runnbale。
Thread类的start()方法调用了native方法start0,start0方法又调用了Runable的run方法,所以Thread必须实现Runable接口,实现Runnable的接口的类要交由Thread去执行。
线程执行的基本为Runble。
相比 Thread,使用 Runnale 创建线程,由以下好处:
- 资源共享。
- java 不允许多继承,因此实现了 Runnable 接口的类可以再继承其他类。
- 松耦合,将真正处理线程的操作交给一个个 Thread 对象操作。