目录
一、Thread类的常见构造方法
更详细的参考Java标准库文档Java Platform SE 8
方法 | 说明 |
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用Runnable对象创建线程对象,并命名 |
注:第三第四个方法的name只是给线程起了个名字,目的是为了方便调试。线程默认的名字为thread-0之类的……
我们执行如下代码:
public class ThreadDemo6 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println("hello");
}
}
},"mythread");
t.start();
}
}
我们运行之后使用jconsole进行查看,我们可以看到,在线程中就有了一个mythread的线程:
二、Thread的几个常见属性
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
解释一下其中几个属性:
名称getName()
构造方法里起的名字;
状态getState()
线程状态(Java里线程的状态操作比操作系统原生的状态要更丰富一些);
优先级getPriority()
这个可以获取,也可以设置,但是实际上设置了也没啥用,多线程同时进行很难做出先后顺序;
是否后台线程isDaemon()
与是否是“守护线程”一个意思;
线程有分前台后台:前台线程,会阻止线程结束。前台线程的工作没做完,进程是结束不了的;后台线程,不会阻止进程结束。后台线程工作没做完,进程也是可以结束的。
注:代码里手动创建的线程,默认都是前台的,包括main默认也是前台的。其他的jvm自带的线程都是后台的。我们也可以手动的使用setDaemon设置成后台线程。注意线程是前台还是后台,取决于调用的setDaemon方法做了什么。
我们使用上述代码,添加调用setDaemon方法并设置为true,可以看到运行结果中什么都没有打印就结束了:
很明显这个t线程是没有执行完的,但是我们的进程仍然结束了,因为当我们把这个线程设置为一个守护线程的时候,那我们的前台线程就剩下main了,main什么时候执行完,我们整个进程就什么时候执行完,所以我们的t线程执行与否就和t无关了。
是否存活isAlive()
判定的是系统里面的一个线程是是否存在。
如图所示,比如说我们有一个应用程序,应用程序里面创建了一个Thread对象t,当我们去调用t.start()方法的时候,就会在我们内核里创建出一个PCB,此时这个PCB才表示一个真正的线程。然后这个线程才开始工作了,在系统里进行参与调度。
所以在真正调用start之前,调用t.isAlive就是false;调用start之后,isAlive就是true。
另外,如果内核里的线程把run干完了,此时线程销毁,PCB随之释放,但是Thread t 这个对象还不一定被释放,此时isAlive也是false。
注:“不一定被释放”:
public class ThreadDemo6 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
//run执行完PCB释放,操作系统里的线程就没了
@Override
public void run() {
System.out.println("hello");
}
}, "mythread");
//同时,t对象仍然存在
t.start();
while (true) {
try {
Thread.sleep(1000);
System.out.println(t.isAlive());//false
//t什么时候不指向当前这个对象,被GC回收时就不存在了
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
t.isAlive为true,使用for循环延长线程执行时间:
总结isAlive:如果t的run还没跑,isAlive就是false;如果t的run正在,isAlive就是true;如果t的run跑完了,isAlive就是false。
三、线程终止
中断的意思是,不是让线程立即就停止,而是通知线程,你应该要停止了。是否真的停止取决于线程这里具体的代码写法。
使用方式:
(1)使用标志位来控制线程是否要停止;
public class ThreadDemo8 {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException{
Thread t = new Thread(()->{
while (flag){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(3000);
//在主线程里就可以随时通过 flag 变量的取值,来操作 t 线程是否结束
flag = false;
}
}
注:这个代码之所以设置flag能够让线程结束,完全取决于t线程内部的代码写法,代码里通过flag控制循环。因此,flag = false这里只是告诉让这个线程结束。这个线程是否要结束,啥时候结束,都是线程内部自己的代码来决定的。
(2)使用Thread自带的标志位,来进行判定。
上一个自定义变量这种方式,是不能够及时相应的,尤其是在sleep休眠时间比较久的时候。所以我们就可以借助Thread自带的标志位来进行判定,这个东西是可以唤醒上面这个sleep这样的方法的。
我们使用currentThread()这个方法,这是Thread类的静态方法。通过这个方法可以获取到当前线程。换句话说就是哪个线程调用的这个方法,就是得到哪个线程的对象引用。(很类似于this)
public class ThreadDemo9 {
public static void main(String[] args) throws InterruptedException{
Thread t = new Thread(()->{
while (!Thread.currentThread().isInterrupted()){
//Thread.currentThread()是在t.run中被调用的,此处获取的线程就是t线程。
//isInterrupted()为true表示被终止,为false表示未被终止(应该要继续走);
// 这个方法背后相当于在判定应该boolean变量
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(3000);
t.interrupt();//使用该方法终止线程,这里就是终止t线程;
//设置上面isInterrupted()的boolean变量
}
}
执行以上代码,我们会看到执行结果中前3行表示t正常执行,然后t调用interrupt出发了异常,之后t还在继续执行。
这里interrupt会做两件事:1)把线程内部的标志位(boolean)给设置成true;2)如果线程在进行sleep,就会触发异常,把sleep唤醒。但是sleep唤醒的时候,还会做一件事,把刚才设置的这个标志位,再设置会false(清空了标志位)。这就导致,当sleep的异常被catch完了之后,如果什么也不做,循环还要继续执行。(线程t忽略了我们的终止请求)
当我们加了break之后,就可以让线程立即结束了。(线程t立即相应我们的终止请求)
加个稍后进行终止。
为什么sleep要清除标志位?可以让程序员自由控制在 唤醒之后,线程是否要终止以及是否立即终止或者稍后,选择权就在于程序员怎么写代码。
四、等待一个线程 - join()
线程是一个随机调度的过程,所以完全无法判定两个线程谁先先执行什么代码谁后执行什么代码,这个顺序是不可预期的,但是实际上我们写代码的时候不希望有这种不可预期的情况,我们更希望我们的结果是可预期的,所以很多时候我们就要对这里的情况进行调整。那么等待线程做的事情就是在控制两个线程的结束顺序,这里涉及的方法就是join()。
public class ThreadDemo10 {
public static void main(String[] args) {
Thread t = new Thread(() ->{
for (int i = 0; i < 3; i++) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
System.out.println("join之前");
//此处的join就是让当前的main线程等待t线程的结束(等待t的run方法执行完)
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("join之后");
}
}
如上述代码,本身执行完start之后,t线程和main线程就并发执行,分头行动。main继续往下执行,t也会继续往下执行。但是main遇到join之后,就会发生阻塞,让main“暂停”执行,一直阻塞到,t线程执行结束,main线程才会从join中恢复回来,才会继续往下执行。(t线程肯定是比main线程先结束的)
TestDemo10运行结果:
接下来我们假设开始执行join的时候,t已经结束了,此时join会怎么样?
我们让t线程等待5秒,可以知道5秒之后t线程已经结束了,结束了之后主线程再去join就可以看到这次的join没有等待立即就返回了。
所以对于我们的join来说是不是一定要等待也得看情况。如果执行join的时候t已经结束了,join就不会阻塞,就会立即返回。
附录:
方法 | 说明 |
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等millis毫秒 |
public void join(long millis, int nanos) | 同理,但可以更高精度 |
注:
public void join():无参版本,“死等”不见不散;
public void join(long millis):指定一个超时时间(最大等待时间,这种操作方式更常见,死等很容易有问题)
五、获取当前线程引用
方法 | 说明 |
public static Thread currentThread(); | 返回当前线程对象的引用 |
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
注:理解:这个方法在哪个线程中调用,就能获取到哪个线程的实例
六、休眠当前线程
方法 | 说明 |
public static void sleep(long millis) throws InterruptedException
|
休眠当前线程
millis 毫秒
|
public static void sleep(long millis, int nanos) throws
InterruptedException
|
可以更高精度的休眠
|
这个方法可以让线程休眠,本质上就是让这个线程不参与调度了(也就是不去CPU上执行了)
举个例子解释:
操作系统内核中有这样一些PCB,这些PCB构成这样一个链表。当前线程A调用sleep,此时A就会进入休眠的状态。这个时候就相当于把A从上述链表中拎出来,放进另一个链表中,这个链表的PCB都是“阻塞状态”,暂时不参与CPU的调度执行。
注:PCB是使用链表来组织的 这种说法并不具体,实际的情况并不是一个简单的链表,其实这是一系列以链表为核心的数据结构。那么从我们理解角度来讲,操作系统每次只需要调度一个线程去执行,就直接从就绪队列中选一个就好了。一旦线程进入阻塞状态,对应的PCB就进入阻塞队列了,此时就暂时无法参与调度了。
而且此时需要注意:当线程阻塞之后(处于阻塞队列),回到就绪队列时,考虑调度的开销,实际上线程是无法在唤醒之后立即执行的,实际上的时间间隔大概率会比理论间隔要大。