一、线程的创建
Java中主要有继承Thread类、实现Runnable接口、实现Callable接口、线程池ThreadPoolExecutor来进行创建
1.继承Thread类创建线程
package panw.base;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ThreadTest extends Thread {
private int a;
@Override
public void run() {
while (true) {
sleep(1000);
log.debug("runTimes: " + a);
}
}
public static void sleep(long time){
try {
Thread.sleep(time);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
ThreadTest threadTest = new ThreadTest();
threadTest.start();
}
}
2.实现Runnable接口
package panw.base;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class RunnableTest implements Runnable {
private int id;
@Override
public void run() {
while (true) {
if (id<=10){
log.debug("runTimes: {}", id);
id++;
}
}
}
public static void main(String[] args) {
new Thread(new RunnableTest()).start();
}
}
3.实现Callable接口
在某些情况下,我们需要线程运行结束提供给主线程一个返回值,主线程拿到值之后进行后续的处理,那么我们就需要用到Callable接口,通常Callable接口配合线程池来进行使用,Java中提供了如下的实现方式:
package panw.base;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
@Slf4j
public class CallableTest implements Callable<String> {
@Override
public String call() throws Exception {
return "123";
}
public static void main(String[] args) {
FutureTask<String> future = new FutureTask<>(new CallableTest());
try {
new Thread(future).start();
while (!future.isDone()) {
}
log.debug(future.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
总结:
使用继承方式的好处是方便传参,你可以在子类里面添加成员变量,通过set方法设置参数或者通过构造函数进行传递。不好的地方是Java不支持多继承,如果继承了Thread类,那么子类不能再继承其他类,而Runable则没有这个限制。前两种方式都没办法拿到任务的返回结果,但是Futuretask方式可以
二、线程的生命周期
此处在网上有两种说法,五种生命周期是在操作系统层面,六种生命周期则是Java定义的。
1.五种生命周期
包括 新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。
- 新建状态(New):
创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,与其他Java对象一样,仅仅由Java虚拟机为其分配了内存。
- 就绪状态(Runnable)
当线程对象调用了start()
方法后,该线程就进入就绪状态。处于就绪状态的线程位于线程队列中,此时它只是具备了运行的条件,能否获得CPU的使用权并开始运行,还需要等待系统的调度。
- 运行状态(Running)
如果处于就绪状态的线程获得了CPU的使用权,并开始执行run()
方法中的线程执行体,则该线程处于运行状态。一个线程启动后,它可能不会一直处于运行状态,当运行状态的线程使用完系统分配的时间后,系统就会让出该线程占用的CPU资源,让其他线程获得执行的机会。需要注意的是,只有处于就绪状态的线程才可能转换到运行状态。
- 阻塞状态(Blocked)
一个正在执行的线程在某些特殊情况下,如被人为挂起或执行耗时的输入/输出操作时,会让出CPU的使用权并暂时中止自己的执行,进人阻塞状态。线程进人阻塞状态后,就不能进入排队队列。只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。
- 死亡状态(Terminated)
如果线程调用stop()
方法或nun()
方法正常执行完毕,或者线程抛出一个未捕获的异常(Exception)错误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他状态。
2.六种生命周期
- NEW 线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)
- BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,如sleep就位TIMED_WAITING, join为WAITING状态。后面会在状态转换一节详述。
-
三、线程常用方法
1.线程启动
启动一个线程为啥是
start()
,而不是run()
?
调用start()
实际上是调用了一个native方法start0()
来启动一个线程,而run()
方法只是单纯调用了Runnable中的run()
方法;public class Thread implements Runnable {
/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
...
static {
registerNatives();
}
@Override
public void run() {
if (target != null) {
target.run();
}
}
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();
}
2.sleep()与yield()
sleep (使线程阻塞)
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞),可通过state()方法查看
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性 。如:
//休眠一秒
TimeUnit.SECONDS.sleep(1);
//休眠一分钟
TimeUnit.MINUTES.sleep(1);
yield (让出当前线程)
调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态(仍然有可能被执行),然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器
线程优先级
- 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
- 设置方法:
thread1.setPriority(Thread.MAX_PRIORITY); //设置为优先级最高
3.join()方法
用于等待某个线程结束。哪个线程内调用join()方法,就等待哪个线程结束,然后再去执行其他线程。
如在主线程中调用ti.join(),则是主线程等待t1线程结束 ```java package panw.base;
import lombok.extern.slf4j.Slf4j;
@Slf4j public class JoinTest { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 100; i++) { log.debug(“t1: “ + i); } }); t1.start(); Thread.sleep(10); t1.join(); log.debug(“main run”);
}
}
<a name="eYGxj"></a>
## 4.线程终止
线程终止不是简单的调用`stop()`方法,和其他控制线程方法如`suspend()、resume()`,这些方法已经过时,就拿stop来说,在结束一个线程时并不会保证线程的资源正常释放,因此可能导致程序出现一些不确定的状态。<br />因此要中断一个线程,在线程中提供了一个`interrupt()`方法:
<a name="rhb5e"></a>
### 1)interrupt()
其他线程通过调用当前线程的`interrupt()`方法,表示向当前线程打了招呼,告诉该线程可以中断线程的执行了,至于什么时候中断,取决于自己。<br />线程也可以通过检查自身 通过 `isInterrupted()`方法来判断是否被中断。
```java
package panw.base;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class InterruptTest {
private static volatile int count;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
count++;
log.debug("count: {}", count);
}
}, "interrupt");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
t1.interrupt();
}
}
2)中断标识复位
上面的interrupt()
方法,通过设置一个中断标识告诉线程可以停止,线程中还提供了一个静态方法Thread.interrupted()
来对中断标识线程复位;
package panw.base;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class InterruptedTest {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
log.debug("-----before:interrupted is " + Thread.currentThread().isInterrupted());
Thread.interrupted();
log.debug("-----after:interrupted is " + Thread.currentThread().isInterrupted());
}
}
}, "Interrupted");
t1.start();
try {
Thread.sleep(50);
} catch (InterruptedException e) {
}
t1.interrupt();
}
}
3)异常复位
除了上面的Thread.interrupted()
方法对线程中断标识进行复位以外,还有一个被动的复位方式,就是当抛出InterruptedException
之前,JVM会先把线程的中断标识为清除,然后才抛出InterruptedException
,这时候调用isInterrupted()
,将会返回false。
package panw.base;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j
public class InterruptedTest {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
log.debug("未被阻塞");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
log.debug("isInterrupted:"+Thread.currentThread().isInterrupted());
e.printStackTrace();
}
}
}, "Interrupt");
t1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
t1.interrupt();
}
}
Object.wait
、Thread.sleep
和 Thread.join
在被打断时 都 会 抛 出InterruptedException
,打断park线程则直接会直接unpark,并且再次unpark不会起效。
4)两阶段终止模式-interrupt
两阶段终止模式不是23种传统设计模式中的,它是由 黄文海在《Java多线程编程实战指南 设计模式》中所提到的模式,具体思路如下图:
这里使用interrupt来进行简单实现:
package panw.model;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class TwoStageTermination {
private Thread monitor;
public void start(){
this.monitor = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
log.debug("after interrupted--------------do something");
break;
}
try {
Thread.sleep(2000);
log.debug("TwoStageTermination: Thread sleeping");
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
}
});
monitor.start();
}
public void stop(){
monitor.interrupt();
}
public static void main(String[] args) {
TwoStageTermination twoStageTermination = new TwoStageTermination();
twoStageTermination.start();
try {
Thread.sleep(3500);
} catch (InterruptedException e) {
e.printStackTrace();
}
twoStageTermination.stop();
}
}
5.守护线程
当JAVA进程中有多个线程在执行时,只有当所有非守护线程都执行完毕后,JAVA进程才会结束。但当非守护线程全部执行完毕后,守护线程无论是否执行完毕,也会一同结束。
//将线程设置为守护线程, 默认为false
monitor.setDaemon(true);
守护线程的应用
- 垃圾回收器线程就是一种守护线程
Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等 待它们处理完当前请求
四、线程执行原理
JVM中由堆、栈、方法区所组成,其中栈内存就是分配给线程使用的,每个线程启动后,虚拟机都会为其分配一块栈内存。
每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的方法
这里就不深入研究,等到JVM章再进行讲解。