一、什么是线程
计算机系统当中我们将一个任务称之为进程,每个进程中又包含一个或多个执行单元,这个执行单元我们称之为线程(线程又被称作轻量级进程)。线程是操作系统能够进行运算调度的最小单位。程序员可以利用它来进行多处理器编程,比如对于计算密集型任务,我们可以通过多线程来进行提速。
一条线程指的是进程中一个单一顺序的控制流,每个线程都有自己的局部变量表、程序计数器(指向当前线程正在执行的指令指针)以及各自的生命周期。多线程下,操作系统根据CPU时钟切换到不同的线程进行执行。
二、线程的生命周期
NEW:此状态的Thread就是JVM堆上的一个对象,和其他普通Java对象没什么区别;
START:此时才会调用JNI,通过操作系统实际创建一个线程,start0()方法会立即返回,不会阻塞线程;
RUNNABLE:START后的线程处于此状态,创建后的线程并不会立即启动,而是需要等待CPU调度:
该状态的线程不会直接进入BLOCKED、TERMINALED状态,即使在线程中调用wait、sleep或者其他的block的IO操作等,必须获取到CPU调度执行权才可以。严格来说该状态的线程只能进入RUNNING或者意外终止;
RUNNING:CPU通过轮询或者其他方式从任务可执行队列中选了某个线程,该线程才会进入该状态,执行逻辑代码;
具体个状态的线程状态切换可以看上图。
三、Thread中的strat方法剖析
public synchronized void start() { if (this.threadStatus != 0) { throw new IllegalThreadStateException(); } else { this.group.add(this); boolean started= false; try { this.start0(); started= true; } finally { try { if (!started) { this.group.threadStartFailed(this); } } catch (Throwable var8) { } } } }
threadStatus是指的线程状态,由JVM对其进行修改,从源码中我们可以分析出下面几个问题:
Thread被NEW创建出来之后,内部属性threadStatus的初始值为0;
同一个线程对象不能再次启动,否则会报IllegalThreadStateException,因为第一个启动后进入RUNNING状态,此时threadStatus不等于0;
一个线程生命周期结束(也就是进入TERMINALED状态),再次调用start方法是不被允许的,也就是TERMINALED状态是无法回到RUNNING状态的;
线程启动后会被加入到ThreadGroup中。
四、Thread中的模板设计模式
JDK官方文档中对run()方法的描述是:线程开始执行时,是由JVM去调用run()方法。所以我们在Thread源码中无法看到run()的调用。线程的真正执行逻辑是在run方法中实现的,通常我们把run方法称为线程的执行单元,接下来我们分析一下run方法:
private Runnable target; public void run() { if (this.target != null) { this.target.run(); } }
从源码中我们可以看到,如果我们在创建Thread时没有使用Runnable接口对其进行构造,可以把run方法看作是一个空实现。其实Thread的start与run方法就是一个比较典型的模板设计模式,父类Thread编写算法结构代码,子类实现业务逻辑细节。这样的好处是父类控制程序结构(方法是final类型,不允许被重写,如start方法),子类只需要实现想要的业务逻辑任务,两者分离开来达到职责分明、功能单一的原则。
五、Runnable接口的引入
通过上述分析我们知道Thread实现了Runnable接口,为啥要用这个接口,直接定义run方法不香吗?我们可以编写继承Thread的子类,子类中重写run方法,多简单明了,为啥要再额外定义一个Runnable接口?
原因是Thread的run方法不能共享,也就是说线程A不能执行线程B的run方法。但是Runnable接口可以实现这个,我们可以使用同一个Runnable实例构造多个不同的Thread对象,并且可以在Runnable实现类中定义多个共享变量,实现变量的共享。
举例说明:比如我们去银行办理业务,需要去取号机取号等待办理业务,假设现在我们有一个需求是该银行网点有四台取号机,每天只服务50个人,也就是说每天四台取号机总共只能出50个号。
分析上述需求我们知道,这涉及到四个取号机有一个共享变量——票号(ticketIndex)。下面我们通过重写Thread的run方法和实现Runnable接口的run方法两种方式构造执行单元,进而分析使用Runnable的优越性。
- 第一种方式
此种方式是达不到我们需求的,根本原因是四个线程的执行单元是不一样的(执行的不是同一个执行单元),但是四个线程间没有交互,所以每个线程都执行了50遍,所以我们需要改进。
- 第二种方式
通过static关键字将成员变量ticketIndex从非静态属性变为静态属性,这样对象间共享ticketIndex变量,可以实现我们的需求。但是通过static关键字来实现会带来一定的性能问题。static变量的生命周期很长,所以会长时间占用内存,如果共享的变量很多,那会占用更多的内存。同时static变量虽然实现了共享,但是还是存在线程安全的问题(ticketIndex++操作是不安全的)。
- 第三种方式
此种方式因为多个线程之间共享一个执行单元,所以完美的解决了我们的需求,但是还是存在安全性问题(ticketIndex++操作是不安全的)。同时将线程的控制与业务逻辑彻底分离开来。
同时Runnable接口与Thread中的run方法的使用也是策略模式在Thread中的应用。
六、总结
创建线程Thread的方式只有一种,那就是构造Thread类。但是实现线程的执行单元则有两种方式:
- 重写Thread的run方法;
- 实现Runnable接口的run方法,并将Runnable实例用作构造Thread的参数。