1.线程简介
线程:
操作系统有多任务在执行,对计算机来说每一个任务就是一个进程(Process),每一个进程内部至少有一个线程(Thread)在运行。线程是程序执行的一个路径,每一个线程都有自己的局部变量表,程序计数器及各自的生命周期。
线程的生命周期:
线程生命周期分以下5个阶段
NEW:new方法创建一个Thread对象,可以通过start方法进入RUNNABLE状态,此时线程尚不存在,Thread对象只是一个普通的java对象。
RUNNABLE:线程对象调用start方法进入此状态,此时才真正的在JVM进程中创建一个线程,线程具备执行的资格,当CPU调度到它就可以执行。
RUNNING:线程正则执行的状态。该状态下可以进行如下切换:
直接进入TERMINATED状态,如调用stop方法; 进入BLOCKED状态,如调用sleep、wait方法、如调用阻塞的IO操作,如为获取某个锁进入该锁的阻塞队列;进入RUNNABLE,如CPU将执行切换到其他线程,调用yield方法放弃执行权
BLOCKED:此状态可以进行如下状态切换:
直接进入TERMINATED状态,如调用stop方法或意外死亡(JVM Crash);进入RUANNABLE状态,如io阻塞结束,wait结束,获取到锁,如线程阻塞被打断进入RUNNABLE状态
TERMINATED:线程生命周期结束。有以下情况进入此状态:
正常结束
线程运行错误意外结束
JVM Crash
2.线程的Start方法
Thread 类 start方法源码:
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){
}
}
}
其核心是对JNI方法start0()的调用
jdk文档对start()方法的描述是:使这个线程开始运行,jvm调用这个线程的run()方法。
由此可以看出run()方法是被JNI方法start0()调用.
3.Runnable 接口的引入及策略模式在Thread类中的使用
Runnable接口的职责声明线程的执行单元
创建线程有两种方式(构造Thread类与实现Runnable接口)的说法是错误的、不严谨的,jdk中代表线程的只有Thread类,这两种方式实际上是实现线程执行单元的两种方式。
Thread的行为在run()方法中进行定义,run()方法是被JNI方法start0()调用,Thread类通过使用策略模式来改变线程的行为,如下所示:
//Thread的run方法:
@Override
public void run() {
if (target != null) {
target.run();
}
}
//Thread的target声明:
private Runnable target;
//Thread的target赋值:
public Thread(Runnable target) {
this(null, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(ThreadGroup group, Runnable target, String name,
long stackSize) {
this(group, target, name, stackSize, null, true);
}
private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
。。。
this.target = target;
。。。
}
4.Thread构造函数
4.1线程命名
线程默认以Thread-X命名,X为jvm内维护的一个自增长整数。
public Thread(Runnable target) { this(null, target, "Thread-" + nextThreadNum(), 0); } private static int threadInitNumber; private static synchronized int nextThreadNum() { return threadInitNumber++; }
建议给线程指定名字,以便维护。
4.2线程父子关系
一个线程肯定被另外一个线程所创建
被创建的线程的父线程是创建它的线程
4.3Thread和ThreadGroup
在Thread构造函数中,可以显示指定ThreadGroup,如果不指定,默认使用父线程的,Thread的初始化方法中相关代码如下:
Thread parent = currentThread(); SecurityManager security = System.getSecurityManager(); if (g == null) { if (security != null) { g = security.getThreadGroup(); } if (g == null) { g = parent.getThreadGroup(); } } this.group = g;
4.4jvm内存结构
jvm结构图如下
1)程序计数器
程序计数器为线程私有,用于存放当前线程接下来要执行的字节码指令、分支、循环、跳转、异常处理等信息,以便能够在CPU时间片轮转切换上下文后顺利回到正确执行位置。
2)java虚拟机栈
java虚拟机栈为线程私有,生命周期同线程相同,jvm运行时创建。
线程中,方法执行时会创建一个名为栈帧的数据结构,用于存放局部变量、操作栈、动态链接、方法出口等信息。
方法的调用则对应着栈帧在虚拟机栈中的压栈和弹栈过程。
虚拟机栈可以通过-xss来配置
3)本地方法栈
java提供了调用本地方法的接口(Java Native Interface),也就是c/c++程序,在线程执行过程中,经常会有调用JNI方法的情况、如网络通信、问卷操作的底层,深圳String的intern。JVM为本地方法划分出来的内存区域为本地方法栈,为线程私有内存区域。
4)堆内存
堆内存为jvm最大的一块内存,所有线程共享,java运行时几乎所有对象都存在该内存区域。
5)方法区
方法区被多个线程共享的内存区域,用于存放已被虚拟机加载的类信息、常量、静态变量、即时编译器(JIT)编译后的数据。
2.5守护线程
在正常情况下,若jvm中没有一个非守护线程,则jvm进程会退出。
守护线程一般用于处理一些后台工作,如jdk的来讲回收线程。
如下代码:
public class DaemonThreadTest { public static void main(String[] args) throws Exception { //1)线程开始 Thread t = new Thread(() -> { while(true) { try { Thread.sleep(1); } catch (Exception e) { e.printStackTrace(); } } }); //t.setDaemon(true);//2)调用setDaemon()方法将线程设置为守护线程 t.start();//3)启动线程 Thread.sleep(10000); System.out.println("main线程结束"); //4)主线程结束 } }
代码中有两个线程,一个是jvm启动的main线程,一个自己创建的线程。
运行上面的线程会发现main线程结束,进程仍未结束,原因是有我们自己创建的一个非守护线程还在运行。
打开2)处的注释,会发现main线程结束,进程也就结束,因为我们创建的线程被设置为守护线程
线程是否为守护线程默认继承其父线程。
守护线程具有自动结束生命周期的特性,而非守护线程不具备这个特点。
5 Thread API
5.1 线程sleep
sleep方法使当前线程进入指定时间长度的休眠,暂停执行,虽然给定类一个休眠的时间,但是最终要以系统的定时器和调度器的精度为准。
jdk引入TimeUnit枚举类,可以在休眠时免去时间换算,如休眠1天2小时3分4秒可以如下写:
TimeUnit.DAYS.sleep(1);
TimeUnit.HOURS.sleep(2);
TimeUnit.MINUTES.sleep(3);
TimeUnit.SECONDS.sleep(4);
5.2线程yield
yield方法属于一种启发式的方法,会提醒调度器我愿意放弃当前CPU资源,如果CPU资源不紧张,则会忽略这种提醒
sleep和yield:
1)sleep会导致当前线程暂停指定时间,没有CPU时间片的消耗
2)yield只是对CPU调度器的一个提示,如果CPU调度器没有忽略这个提示,它会导致线程上下文的切换
3)sleep会使线程短暂block,会在给定的时间内释放CPU资源
4)yield会使RUNNING状态的Thread进入RUNABLE状态(如果CPU调度器没有忽略这个提示)
5)sleep几乎百分百地完成给定时间的休眠,而yield的提示不能一定担保
6)一个线程sleep另一个线程调用interrupt会捕捉中断信号,而yield不会
5.3线程优先级
线程可以设置优先级,不过是一种暗示性操作
线程设置优先级方法源码如下:
public final void setPriority(int newPriority) { ThreadGroup g; checkAccess(); if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) { throw new IllegalArgumentException(); } if((g = getThreadGroup()) != null) { if (newPriority > g.getMaxPriority()) { newPriority = g.getMaxPriority(); } setPriority0(priority = newPriority); } }
由代码可以看出,优先级是是有范围的,取决于线程所在的ThreadGroup
一般不设置线程优先级,考虑到业务需求,往往借助优先级设置线程谁先执行不可取的。
5.4获取线程ID
public long getId(),获取线程唯一ID,该ID在jvm进程中是惟一的
5.5线程interrupt
1)public void interrupt()
线程进入阻塞状态,调用interrupt方法可以打断阻塞,打断一个线程并不等于该线程的生命周期结束,紧急是打断类这个线程的阻塞状态。
线程阻塞被打断会报错InterruptedException异常。
interrupt方法做了什么?在一个线程中存在着名为interrupt flag的表示,如果一个线程被interrupt,那么它的flag将被设置,但是如果当前线程正则执行可中断方法被阻塞时,调用interrupt方法将其中断,反而会导致flag被清除。对一个已经死亡的线程调用interrupt会直接被忽略。这段话比较拗口,看了2)后再看下面代码讲解:
代码1:
public static void main(String[] args) throws Exception { Thread t = new Thread(()-> { double d = Double.MIN_VALUE; while(d < Double.MAX_VALUE-1) { d += 0.1; } System.out.println("HA Ha"); }); t.start(); Thread.sleep(1000);//main线程休眠一会,保证线程t获得CPU执行权真正开始执行 System.out.println(t.isInterrupted());//打断前 t.interrupt();//打断 System.out.println(t.isInterrupted());//查看线程t的flag Thread.sleep(1000);//main线程休眠一会,保证线程t获得CPU执行权继续执行 System.out.println(t.isInterrupted());//再次查看线程t的flag }
代码1输出如下:
false
true
true
代码1中,线程t内的while循环调用的方法是不会让线程t出现阻塞的,即非阻塞方法,即非可中断方法,main线程对其调用interrupt方法,为它设置类flag,flag一种存在
代码2:
public static void main(String[] args) throws Exception { Thread t = new Thread(()-> { try { TimeUnit.SECONDS.sleep(10); } catch (Exception e) { System.out.println("I'm interrepted"); } double d = Double.MIN_VALUE; while(d < Double.MAX_VALUE-1) { d += 0.1; } System.out.println("HA Ha"); }); t.start(); Thread.sleep(1000);//main线程休眠一会,保证线程t获得CPU执行权真正开始执行 System.out.println(t.isInterrupted());//打断前 t.interrupt();//打断 System.out.println(t.isInterrupted());//查看线程t的flag Thread.sleep(1000);//main线程休眠一会,保证线程t获得CPU执行权继续执行 System.out.println(t.isInterrupted());//再次查看线程t的flag }
代码2输出如下:
false
true
I'm interrepted
false
代码2中,线程t先是休眠10秒,休眠为阻塞方法,为可中断方法,在这段时间内,main线程调用它的interrupt方法,调用前查看标志,flag为false,调用后立即查看标志,flag为ture,等一会,待t线程继续执行抛出异常后,再次查看线程t的flag,flag为false,说明线程flag被清除类,且应是抛出异常时清除的。
2)isInterrupted
isInterrupted是Thread类的一个成员方法,主要是判断指定线程是否被中断,该方法仅仅是对interrupt标识的一个判断,并不会影响标识发生任何改变。
3)interrupted
interrupted是一个静态方法,用于判断当前线程是否被中断,与isInterrupted方法有很大区别,一是interrupted方法判断的是当前线程,而isInterrepted判断的是调用的那个线程;二是interrupted方法会清除interrupt标识,而isInterrupted不会。
如果当前线程被打断类,那么第一次调用interrupt方法会返回true,并且立即擦除interrupt标识,第二次包括以后永远都会返回false,除非再次打断。
验证代码如下:
public static void main(String[] args) throws Exception { Thread t = new Thread(()-> { while(true) { //因为判断的是当前线程,只能将判断方法写在线程t的run方法范围内 System.out.println(Thread.interrupted()); } }); t.setDaemon(true); t.start(); Thread.sleep(10); t.interrupt(); Thread.sleep(1); t.interrupt(); Thread.sleep(1); }
为了避免抛出异常时清除标识的影响,t中没有用sleep,因此会有很多输出
输出如下:
false
false
...
true
false
false
...
true
false
false
...
输出中只有两个true,有很多false,因为t线程全程被打断类两次。
4)注意事项
Thread类中,isInterrupted方法和interrupted方法调用的是同一个JNI方法,如下:
public boolean isInterrupted() { return isInterrupted(false); } public static boolean interrupted() { return currentThread().isInterrupted(true); } private native boolean isInterrupted(boolean ClearInterrupted);
JNI方法isInterrupted的ClearInterrupted参数用来控制是否擦除interrupt标识,两个调用分别传入不同参数
一个线程如果设置了interrupt标识,那么接下来执行可打断方法时会被立即打断,如sleep方法,验证代码如下:
public static void main(String[] args) throws Exception { SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SS"); System.out.println("测试开始:"+sdf.format(new Date())); Thread t = new Thread(()-> { try { System.out.println("初次打断:"+sdf.format(new Date())); Thread.currentThread().interrupt(); TimeUnit.MINUTES.sleep(3); } catch (InterruptedException e) { System.out.println("休眠打断:"+sdf.format(new Date())); } }); System.out.println("线程启动:"+sdf.format(new Date())); t.start(); TimeUnit.MINUTES.sleep(1); System.out.println("再次打断:"+sdf.format(new Date())); t.interrupt(); }
上面代码初始设置打断只能在t的run方法内,因为start前线程t并不真正存在
输出结果如下:
测试开始:14:45:58.536
线程启动:14:45:58.539
初次打断:14:45:58.540
休眠打断:14:45:58.540
再次打断:14:46:58.541
5.6线程的join
join某个线程A,会使当前线程B进入等待,直到线程A结束生命周期或者达到给定的时间,在此期间线程B将一直处在BLOCKED状态。
import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.IntStream; public class ThreadJoin { public static void main(String[] args) { //1 定义两个线程,放到threads中 List<Thread> threads = IntStream.range(1, 3).mapToObj(ThreadJoin::create).collect(Collectors.toList()); //2启动两个线程 threads.forEach(Thread :: start); //3执行两个线程的join threads.forEach(t -> { try { System.out.println(t.getName() + ",join"); t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); //4main线程自己的输出 for(int i = 0;i < 5;i ++) { System.out.println(Thread.currentThread().getName() + ",#" + i); shortSleep(); } } //构造一个简单线程,每一个线程只是简单的输出和休眠 private static Thread create(int seq) { return new Thread(()->{ for(int i = 0;i < 5;i ++) { System.out.println(Thread.currentThread().getName() + ",#" + i); shortSleep(); } },"Thread=="+seq); } private static void shortSleep() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } }
结果:
Thread==1,join
Thread==2,#0
Thread==1,#0
Thread==1,#1
Thread==2,#1
Thread==1,#2
Thread==2,#2
Thread==1,#3
Thread==2,#3
Thread==2,#4
Thread==1,#4
Thread==2,join
main,#0
main,#1
main,#2
main,#3
main,#4
由上面的输出可以看出:1)join方法可以使当前线程永远等待下去,直到期间被另外的线程中断或者join的线程执行结束或者join指定的时间结束。2)一个线程同一段时间只可以被一个线程join,因为Thread.join()这段代码要在被join的线程中执行,而执行完这一句被join的线程就阻塞类,后面的join语句必须等前面的join结束后才能被执行到
6关闭线程
6.1正常关闭
1)线程结束生命周期
2)捕获中断信号关闭:循环执行任务,循环中调用isInterrupted方法检查中断标志,若中断结束循环
3)使用volatile开个控制:循环执行任务,循环中检查volatile变量,volatile变量发生合适改变结束循环
6.2异常突出
线程的执行单元中适当地方抛出运行时异常结束当前线程
7.ThreadGroup
ThreadGroup也类似线程,存在父子ThreadGroup,若创建ThreadGroup时不指定父ThreadGroup,那么父ThreadGroup默认为当前线程的ThreadGroup。
ThreadGroup也有interrupt操作,interrupt一个ThreadGroup将导致该group中所有的active线程都被interrupt,也就是该group中每一个线程的interrupt标识都被设置类。
ThreadGroup也可以设置守护ThreadGroup,但设置daemon并不影响Group里面线程的daemon属性。