- 对于计算机来说一个任务就是一个进程,进程是资源分配的最小单位,一个进程内存可能跑着多个线程,有时也成线程为轻量级进程,线程共享进程的资源,线程是资源调度的最小单位。
- 线程是程序执行的一个路径,每一个线程都有自己的局部变量表、程序计数器及其各自的生命周期,当启动一个java程序时候,操作系统就会创建一个java进程(jvm进程),jvm进程中将会派生或者创建很多的线程。
一、线程生命周期
-
线程的生命周期大概可以分为5个主要阶段:new、runnable、running、blocked、terminated
线程生命周期状态图
1、线程new状态
- 当new了一个thread对象但是没有执行start方法启动线程,此时这个线程的状态就是new状态,此时的thread对象和普通对象没有区别。
new状态可以转化为runnable状态
2、runnable状态
- runnable也称为可执行状态,当new状态下执行start方法后就进入runnable状态,此时jvm真正的创建了一个线程。此时线程能否立即执行还需要等待cpu的调度。
严格来说,runnable的线程只能意外终止或者进入running状态
3、running状态
- 一旦获取到cpu的调度,此时的线程就是running状态,才可以真正的执行自己的代码逻辑,在running状态可以发生的转换:
1、直接进入terminated状态:使用stop(jdk不推荐)或者使用标志位或者意外死亡
2、进入blocked状态:使用sleep、wait等jdk方法;进行网络操作时候的读写阻塞等;为了获取到锁从而加入到了阻塞队列中
3、进入runnable状态:cpu的时间片用完,主动调yield方法放弃时间片。
4、blocked状态
- 阻塞状态又细分为sleep造成的阻塞、网络阻塞、wait等待阻塞、为了获取锁等待阻塞等
1、直接进入terminated状态:使用stop(jdk不推荐)或者使用标志位或者意外死亡
2、进入runnable状态:比如网络操作阻塞结束、sleep休眠结束、被notify唤醒、获取到了某一个锁资源、线程阻塞期间被打断(比如interrupt方法)
5、terminated状态
- 是一种最终状态,该状态下的线程不会切换到任何其他状态下。导致这个状态发生的原因有:线程正常结束、线程运行时出现错误、jvm crash导致的所有线程死亡
二、创建及执行线程
1、start方法源码解析
//执行这个方法时候,jvm会调用该线程的run方法,而start0方法是本地方法,
//换言之就是说run方法是被本地方法start0调用的。
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
//是一个本地方法
private native void start0();
1、thread被new之后,实际上此时的状态为0(threadState=0)
2、不能两次启动Thread,否则抛throw new IllegalThreadStateException();
3、线程启动后会被加入到ThreadGroup中
4、一个线程生命周期结束之后,也就是terminated状态,再次调用start方法是不被允许的,会抛异常。
2、模板方法在Thread中的使用(继承方式实现Thread)
- Thread实现Runnable接口,在Thread中run方法源码如下。可以看出如果我们不传入一个Runnable实现,那么Thread中的run方法实际上就是一个空方法,也就是说我们创建线程时候(如果不实现Runnable接口)就要自己实现run方法,这就是一个典型的模板方法(父类编写算法结构,子类自己实现)
private Runnable target;
@Override
public void run() {
//如果实现了Runnable接口,就会执行实现Runnable接口的方法
if (target != null) {
target.run();
}
//否则就重写run中的方法
}
- 案例说明(先不考虑线程安全问题):
public class TicketWindow extends Thread {
/**
* 柜台业务
*/
private final String name;
/**
* 最大受理50单
*/
private static final int MAX = 50;
/**
* 从编号为1的开始(注意是静态的,是类所有的,唯一的)
*/
private static int index = 1;
public TicketWindow(String name) {
this.name = name;
}
@Override
public void run() {
//模板方法,子类实现
while (index <= MAX) {
System.out.println("柜台:" + name + ",当前的编号为:" + index++);
}
}
public static void main(String[] args) {
TicketWindow t1=new TicketWindow("t1");
t1.start();
TicketWindow t2=new TicketWindow("t2");
t2.start();
TicketWindow t3=new TicketWindow("t3");
t3.start();
}
}
3、策略模式在Thread中使用(Runnable接口实现)
- 创建线程只有一种方式,就是实现Thread类,而实现线程的执行单元(run方法)有两个方式。一种是前面说过的重写run方法,还有一种就是实现Runnable接口,然后将Runnable实例构造Thread的参数。
- 策略模式:其思想是针对一组算法,将每一种算法都封装到具有共同接口的独立的类中,从而是它们可以相互替换。
public class TicketWindowRunnable implements Runnable {
/**
* 注意可以不需要static修饰
*/
private int index = 1;
private static final int MAX = 10;
@Override
public void run() {
//模板方法,子类实现
while (index <= MAX) {
System.out.println("柜台:" + Thread.currentThread().getName()
+ ",当前的编号为:" + index++);
}
}
public static void main(String[] args) {
TicketWindowRunnable task = new TicketWindowRunnable();
Thread t1=new Thread(task,"t1");
Thread t2=new Thread(task,"t2");
Thread t3=new Thread(task,"t3");
t1.start();
t2.start();
t3.start();
}
}
4、Thread构造和Runnable构造线程的区别
(1)从面相对象角度看,Thread构造是通过继承的方式、而Runnable构造是通过组合的方式,继承的方式相对于组合的方式耦合更加紧密,因此优先推荐使用组合的方式
(2)从共享对象的角度看,Runnable构造方式意味着多个线程实例可以共享一个Runnable实例,而Thread构造不能共享run方法,如果需要实现共享,需要把相关的属性设置为static修饰的类变量。
5、线程与普通对象的区别
- 线程就是一个对象,但是创建一个线程与创建其他对象类型不同,jvm会为每一个线程分配调用栈需要的内存空间,调用栈用于跟踪java代码间的调用关系以及java代码对本地代码的调用。另外,每一个线程可能还有一个内核线程与之对应,因此创建一个线程比创建其他对象类型的成本要高一些。
三、线程构造
1、线程名称
(1)默认命名
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
/* For autonumbering anonymous threads. */
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
(2)线程命名
- 可以在Thread构造函数中传入命名,比如
- public Thread(String name)
- public Thread(ThreadGroup group, String name)
- public Thread(Runnable target, String name)
(3)修改线程命名
- 在线程调用start方法之前还能进行线程名字的修改,一旦启动了线程,那么就不能修改线程名称了。
public final synchronized void setName(String name) {
checkAccess();
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
if (threadStatus != 0) {
setNativeName(name);
}
}
2、线程的父子关系
- 创建任何一个线程都会有一个父线程,通过下面的源码可以的出结论(一个线程的创建肯定由另一个县城完成;被创建线程的父线程是创建它的线程)
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
//获取当前线程作为父线程
Thread parent = currentThread();
...
3、Thread与虚拟机栈
- 每一个线程在创建的时候,jvm都会为其创建对象的虚拟机栈和jvm堆内存,虚拟机栈通过-xss配置,堆内存使用-XMS和-Xmx配置。与线程创建、运行、销毁等关系比较大的是虚拟机栈,而且虚拟机栈内存的划分大小将直接决定一个Jvm中可以创建多少个线程。
-
线程的创建数量是随着虚拟机内存的增多而减少的,是一种反比关系。近似公式:java进程内存=堆内存+线程数量*栈内存;而堆内存作为进程内存的基数,它的增大对线程数量的影响也是成反比的,但是没有虚拟机栈影响更深刻。
image.png
4、守护线程
public class DeamonThread {
public static void main(String[] args) throws InterruptedException {
//1、开始main线程
Thread thread=new Thread(()->{
while (true){
System.out.println("hahahha");
}
});
//2、设置thread线程为守护线程
thread.setDaemon(true);
//3、开始thread线程
thread.start();
Thread.sleep(2000);
//4、main线程结束
System.out.println("main finished");
}
}
- 上面是一个关于守护线程的案例,如果注释掉2,jvm永远不会退出,即时是main线程结束了生命周期,原因是jvm进程中还存在一个非守护线程在执行
- 守护线程:是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因 此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。
- 将线程转换为守护线程可以通过调用Thread对象的setDaemon(true)方法来实现。在使用守护线程时需要注意一下几点:
(1) thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
(2) 在Daemon线程中产生的新线程也是Daemon的。
(3) 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
- 守护线程常用作后台线程,当需要关闭某些线程时候或者退出jvm时候,一些线程能够自动退出,此时就可以考虑使用守护线程了
网友评论