一、线程的创建及使用
使用线程时一般分为两步,即,第一步创建线程,第二步启动线程。以下是几种不同的线程创建方法:
方法一:直接使用Thread
直接使用Thread,采用匿名内部类的方式实现。
public class App
{
public static void main( String[] args ) throws IOException {
//创建线程
Thread t = new Thread("t1"){
@Override
public void run(){
System.out.println(this.getName()+" Running");
}
};
//启动线程
t.start();
//main继续方法执行
System.out.println("main running");
}
}
方法二:
将线程和任务(要执行的代码)分开,推荐使用
- Thread 代表线程
- Runnable 代表可运行的任务(线程要执行的代码)
public class App
{
public static void main( String[] args ) throws IOException {
//创建任务对象
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Running");
}
};
//lambda方式创建Runnable
//Runnable r = () -> System.out.println("Running");
//创建线程对象
Thread t = new Thread(r,"t");
t.start();
}
}
方法一和方法二对比:
- 方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
- 用 Runnable 更容易与线程池等高级 API 配合
- 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
方法三:FutureTask配合Thread
FutureTask是对Runnable的一个扩展(实现了Runnable接口),FutureTask能够接收Callable类型参数;用来处理有返回结果的情况。
public class App
{
public static void main( String[] args ) throws IOException, ExecutionException, InterruptedException {
FutureTask<Integer> task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
System.out.println("running...");
Thread.sleep(1000);
return 100; //返回结果
}
});
Thread t = new Thread(task,"t0");
t.start();
//task.get();阻塞等待Task任务返回结果
System.out.println(task.get());
}
}
二、线程运行的原理
2.1 栈与栈帧
我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能由一个活动栈帧,对应着当前正在执行的那个方法
栈帧示例:
public class App
{
public static void main( String[] args ) {
method1(10);
}
public static void method1(int x){
int y=x+1;
Object m = method2();
System.out.println(m);
}
public static Object method2(){
Object n = new Object();
return n;
}
}
栈帧图解:
栈内存,方法执行完之后,占内存会自动释放掉。
2.2 线程上下文切换(Thread Context Switch)
因为以下一些原因导致CPU不在执行当前的线程,转而执行另外一个线程的代码。
- 线程的CPU时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的:
- 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch 频繁发生会影响性能
2.3 常见方法
方法名 | 功能说明 | 注意 |
start() | 启动一个新线程,在新的线程运行 run 方法中的代码 | start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现 IllegalThreadStateException |
run() | 新线程启动后会调用的方法 | 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为 |
join() | 等待线程运行结束 | 等待调用join的线程结束 |
join(long n) | 等待线程运行结束,最多等待n毫秒 | |
getState() | 获取线程状态 | Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED |
isAlive() | 线程是否存货(还没有运行完毕) | |
sleep(long n) static方法 | 让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程 | 写在哪个线程,就让哪个线程睡眠。 sleep也可以避免while(true)空转,导致CPU占用率过高 |
interrupt() | 打断线程 | 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除 打断标记 ;如果打断的正在运行的线程,则会设置 打断标记 ;park 的线程被打断,也会设置 打断标记 |
isInterrupted() | 判断当前线程是否被打断 | 不会清除打断标记 |
interrupted() | 判断当前线程是否被打断 | 会清除打断标记 |
2.3.1 join方法:
等待调用join()方法的线程运行结束,然后才向下继续执行。
public class App
{
static int r = 0;
public static void main( String[] args ) throws InterruptedException {
Thread t1 = new Thread(){
@Override
public void run(){
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r=10;
}
};
t1.setName("t1");
t1.start();
//主线程等待t1线程运行结束之后,在继续运行。
t1.join();
System.out.println(r);
}
}
以上代码运行结果为10,如果没有t1.join()语句,运行结果为0。
2.3.2 interrupt方法:
- 打断阻塞状态的线程;打断 sleep 的线程, 会清空打断状态,以 sleep 为例,以下程序运行结果打断标记为false;
public class App
{
static int r = 0;
public static void main( String[] args ) throws InterruptedException {
Thread t1 = new Thread(()->{
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1");
t1.start();
Thread.sleep(500) ;
t1.interrupt();
System.out.println("打断状态:"+t1.isInterrupted());
}
}
- 打断正常运行的线程, 不会清空打断状态
public class App
{
static int r = 0;
public static void main( String[] args ) throws InterruptedException {
Thread t1 = new Thread(()->{
while(true){
if(Thread.currentThread().isInterrupted()){
System.out.println("t1线程被打断,退出循环");
break;
}
}
},"t1");
t1.start();
Thread.sleep(500) ;
t1.interrupt();
System.out.println("打断状态:"+t1.isInterrupted());
}
}
interrupt打断正在运行的线程时,并不会影响线程的执行(线程会继续执行)。只是该线程知道自己被打断了,至于线程在被打断了具体怎么做,由线程内部自己决定。如上述代码,线程一旦被打断,会打印 "t1线程被打断,退出循环",然后退出循环。这种方式可以作为一种很优雅的线程退出方式,可以让线程退出时,从容的进行资源的释放等(两阶段终止模式)。
三、主线程和守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
public class App
{
public static void main( String[] args ) throws InterruptedException {
Thread t1 = new Thread(()->{
while(true){
if(Thread.currentThread().isInterrupted())
break;
}
System.out.println("结束");
},"t1");
//设置t1线程为守护线程
t1.setDaemon(true);
t1.start();
//主线程运行
Thread.sleep(1000);
System.out.println("结束");
}
}
程序中,如果设置t1线程为守护线程,则主线程结束之后,整个进程就会结束,但是如果注释掉t1.setDaemon(true); 也就是t1线程不是主线程,那么即使主线程结束,进程也不会结束,因为t1线程还在继续。这就是主线程和守护线程的区别。
四、线程状态
4.1 五种状态:
从操作系统层面描述,线程具有5种状态,如下图所示:
- 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
- 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
- 【运行状态】指获取了 CPU 时间片运行中的状态
当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
- 【阻塞状态】
如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】
等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
- 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
4.2 六种状态
从Java API层面描述,根据Thread State枚举,线程具有6种状态,如下图所示:
- NEW 线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
- BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分
- TERMINATED 当线程代码运行结束