【线程】Java线程操作
【线程】Java线程操作
- 一、启动线程
- 1.1 run()和start()的区别
- 二、终止线程
- 三、等待线程
- 四、线程的状态
一、启动线程
Java中通过start()方法来启动一个线程,其次我们要着重理解start()和run()的区别。
1.1 run()和start()的区别
我们通过一份代码来进行观察:
public class ThreadDemo1 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()-> {
while(true){
System.out.println("hello 000");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
//第一次运行start()
t.start();
//第二次运行run()
//t.run()
while (true){
System.out.println("hello 111");
Thread.sleep(1000);
}
}
}
从结果上来看,用start() 方法,两个线程都正常运行了(main线程和t线程);而使用run()方法,似乎只有一个线程在正常运行。
我们可以用Jconsole来查看一下:
可以发现,使用start()方法真正创建出了线程,而使用run()方法只是在main线程里执行t线程对象的逻辑,并没有真正创建线程。那么我们可以这样理解:** start()方法用于创建线程,系统在合适的时机调用run方法,run()方法用于执行线程内部的逻辑。**
二、终止线程
想要终止一个线程,其实是需要里外配合的。具体来说,比如我们想要在main线程内控制t线程,我们就需要在t线程中引入一个标志位,用来控制线程。同时,我们要能在main线程中能够控制这个标志位。接下来看示例:
- 手动设置标志位
public class ThreadDemo1 {
private static boolean isQuit =false;
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()-> {
while(!isQuit){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
//这个时间需要大于t线程中的休眠时间
//避免因线程的随机调度使得t线程还没启动,标志位就被更改
Thread.sleep(2000);
isQuit=true;
}
}
运行结果:
这里还有一个小问题:
如果我们把isQuit(自定义的标志位)写到main方法内部作为一个局部变量,此时就会编译报错。
这是因为λ表达式在进行变量捕获时,对于外部定义的局部变量,要求是final或者是effectively final类型的。
那为什么写成成员变量就可以了呢,此时是走了另一条语法规则,λ表达式/内部类可以访问外部类的成员变量,这件事情不受λ表达式变量捕获的限制。
但是我们认为,这种手动设置标志位的方式不够优雅,Thread类呢,提供了一种更加优雅的方法:interrupt()
。
- 使用Thread类提供的
interrupt
方法
public class ThreadDemo1 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()-> {
while(!Thread.currentThread().isInterrupted()){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
//这个时间需要大于t线程中的休眠时间
//避免因线程的随机调度使得t线程还没启动,标志位就被更改
Thread.sleep(2000);
t.interrupt();
}
}
- Thread.currentThread()用于获取当前线程的引用,如果是继承Thread,那么可以用
this
引用,但如果是实现Runnable或者是lambda表达式,就只能使用该方法来获取当前线程的引用。
此处出现了一个比较奇怪的错误 : 明明已经报错了,怎么还没停下来。
这里其实是sleep搞的鬼。
此处线程处于这种休眠状态,调用interrupt()就会提前唤醒这个线程。线程被提前唤醒,会做两件事:
(1)抛出 InterruptedException异常,被catch捕获到;
(2)清除Thread对象的标志位,把标志位继续设为false。
所以此时线程没有真正停止。而想要使这个线程停止,只需要做出一点小小的改进:在catch中写入break:
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
此时思考一个问题,那么我手动设置标志位,为什么不会有这样的问题?
这是因为,手动设置的标志位并不是Thread对象的属性,只是当前类的一个成员变量。即使线程被提前唤醒,也是不能去改变这里手动设置的标志位的。
Java这里对于线程的终止采用的是一种“软性”操作,即需要线程配合才能终止。操作系统的API提供了强行终止线程的操作,但这件事往往弊大于利,Java中并没有引入这样的强制性方法。
三、等待线程
由于多个线程的执行顺序是不确定的(随机调度,抢占式执行),所以我们的有些目的就难以实现。但是我们可以通过某些api,来影响线程的执行顺序,比如可以让一个线程来等待另一个线程。
public class ThreadDemo1 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()-> {
for(int i=0;i<10;i++){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException();
}
}
});
t.start();
t.join();
System.out.println("这是主线程,期望在t线程结束后打印");
}
}
这里的语法是这样,在main线程中写入t.join(),就是让main线程等待t线程执行完毕再继续执行。
从系统调度的角度来说,这里就是让main线程主动放弃被系统调度的机会,直到 t 线程执行完,再恢复。
明确来讲,这里不是确定的“执行顺序”,而是确定的“结束顺序”。
join方法也可以设置一个时间,避免要等的线程内部设计出了死循环,而出现的“死等”。较为常用的就是这种join(long millis)
。
四、线程的状态
之前谈到进程有就绪和阻塞两种状态。使用Jconsole也可以观察到线程的状态
Java中,对线程的状态进行了进一步的细分:
状态 | 对应的含义 |
---|---|
NEW | Thread对象创建好了,但是还没有调用start方法在系统中创建线程。 |
TERMINATED | Thread对象仍然存在,但是系统内部的线程已经执行完了 |
RUNNABLE | 就绪状态,表示这个线程正在CPU上执行,或者随时准备去CPU上执行 |
TIMED_WAITING | 指定时间的阻塞,到达一定时间后自动解除阻塞。例如:sleep()、join(long millis) |
WAITING | 不带时间的阻塞(死等),必须满足一定的条件才解除阻塞 |
BLOCKED | 由于锁竞争,引起的阻塞 |
讲到这里,大部分的状态我们都见识过了。对于BLOCKED的状态,涉及到我们所要讲的线程安全问题,后面在详细论述。