1.Java线程
- 创建和运行线程
- 线程运行的原理
- Thread的常见方法
- 主线程与守护线程
- 线程状态之五种状态
- 线程状态之六种状态
1.1 创建和运行线程
方法一,直接使用Thread
// 构造方法的参数是给线程指定名字,推荐
Thread t1 = new Thread("t1") {
@Override
// run 方法内实现了要执行的任务
public void run() {
log.debug("hello");
}
};
t1.start();
方法二,使用Runnable配合Thread
把【线程】和【任务】(要执行的代码)分开.Thread代表线程,Runnable代表可运行的任务(线程要执行的代码)
// 创建任务对象
Runnable task2 = new Runnable() {
@Override
public void run() {
log.debug("hello");
}
};
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();
Java 8以后可以使用lambda精简代码
// 创建任务对象
Runnable task2 = () -> log.debug("hello");
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();
小结
- 方法1是把线程和任务合并在了一起,方法2是把线程和任务分开了
- 用Runnable更容易与线程池等高级API配合
- 用Runnable让任务类脱离了Thread继承体系,更灵活
方法三,FutureTask配合Thread
FutureTask能够接收Callable类型的参数,用来处理有返回结果的情况
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 实现多线程的第三种方法可以返回数据
FutureTask futureTask = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("多线程任务");
Thread.sleep(100);
return 100;
}
});
// 主线程阻塞,同步等待 task 执行完毕的结果
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(futureTask,"我的名字").start();
log.debug("主线程");
log.debug("{}",futureTask.get());
}
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
Future提供了三种功能:
- 判断任务是否完成;
- 能够中断任务;
- 能够获取任务执行结果。
查看进程的方法
windows
- 任务管理器可以查看进程和线程数,也可以用来杀死进程
- tasklist 查看进程
- taskkill 杀死进程
linux
- ps -fe 查看所有进程
- ps -fT -p 查看某个进程(PID)的所有线程
- kill 杀死进程
- top 按大写H切换是否显示线程
- top -H -p 查看某个进程(PID)的所有线程
Java
- ps 命令查看所有Java进程
- jstack查看某个Java进程(PID)的所有线程状态
- jconsole来查看某个Java进程中线程的运行情况(图形界面)
1.2 线程运行的原理
栈与栈帧
Java Virtual Machine Stacks(Java虚拟机栈)
JVM中由堆、栈、方法区所组成,其中栈内存是给线程用的,每个线程启动后,虚拟机就会为其分配一块栈内存。每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存;每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法.
线程上下文切换(Thread Context Switch)
因为以下一些原因导致cpu不再执行当前的线程,转而执行另一个线程的代码
- 线程的cpu时间片用完(每个线程轮流执行,看前面并行的概念)
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了sleep、yield、wait、join、park、synchronized、lock等方法
当Context Switch发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条jvm指令的执行地址,是线程私有的.保存的线程状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等;Context Switch频繁发生会影响性能.
1.3 Thread的常见方法
方法名 | static | 功能说明 | 注意 |
---|---|---|---|
start() | 启动一个新线程,在新的线程运行run方法中的代码 | start方法只是让线程进入就绪,里面的代码不一定立刻运行(CUP的时间片还没有分给他)。每个线程对象的start方法只能调用一次,如果调用多次会出现IllegalThreadStateException | |
run() | 新线程启用后会调用的方法 | 如果在构造Thread对象时传递了Runnable参数,则线程启动后调用Runnable中的run方法,否则默认不执行任何操作。但可以穿件Thread的子类对象,来覆盖默认行为 | |
join() | 等待线程运行结束 | ||
join(long n) | 等待线程运行结束,最多等待n毫秒 | ||
getId() | 获取线程长整型的id | id唯一 | |
getName() | 获取线程名 | ||
setName(String) | 修改线程名 | ||
getPriority() | 获取线程优先级 | ||
getPriority(int) | 修改线程优先级 | java中规定优先级是1~10的整数,比较大优先级能提高该线程被CPU调用的几率 | |
getState() | 获取线程状态 | Java 中线程状态是用 6 个 enum 表示,分别为: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED | |
isInterrupted() | 判断是否被打断 | 不会清除 “打断标记” | |
isAlive() | 线程是否存活(还没有运行完毕) | ||
interrupt() | 打断线程 | 如果被打断线程正在sleep,wait,join会导致被打断的线程抛出InterruptedException,并清除打断标记;如果打断的正在运行的线程,则会设置打断标记;park的线程被打断,也会设置打断标记 | |
interrupted() | static | 判断当前线程是否被打断 | 会清除打断标记 |
currentThread() | static | 获取当前正在执行的线程 | |
sleep(long n) | static | 让当前执行的线程休眠n毫秒,休眠时让出cpu的时间片给其它 线程 | |
yield() | static | 提示线程调度器让出当前线程对CPU的使用 | 主要是为了测试和调试 |
1.3.1 start与run
调用run
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug(Thread.currentThread().getName());
FileReader.read(Constants.MP4_FULL_PATH);
}
};
t1.run();
log.debug("do other things ...");
}
输出:程序仍在main线程运行,FileReader.read()
方法调用还是同步的.
19:39:14 [main] c.TestStart - main
19:39:14 [main] c.FileReader - read [1.mp4] start ...
19:39:18 [main] c.FileReader - read [1.mp4] end ... cost: 4227 ms
19:39:18 [main] c.TestStart - do other things ...
调用start(能不能运行任务调度器说了算)
将上面代码的t1.run();
改为t1.start();
输出结果如下:程序在t1线程运行,FileReader.read()
方法调用是异步的.
19:41:30 [main] c.TestStart - do other things ...
19:41:30 [t1] c.TestStart - t1
19:41:30 [t1] c.FileReader - read [1.mp4] start ...
19:41:35 [t1] c.FileReader - read [1.mp4] end ... cost: 4542 ms
小结
直接调用run()
是在主线程中执行了run()
,没有启动新的线程;使用start()
是启动新的线程,通过新的线程间接执行 run()
方法中的代码.
当调用start方法后,线程状态会由“NEW”变为“RUNABLE”,此时再次调用start方法会报错IllegalThreadStateException(非法的状态异常)
1.3.2 sleep与yield
sleep
- 调用sleep会让当前线程从Running进入Timed Waiting状态(阻塞)
- 其它线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException【注意:这里打断的是正在休眠的线程,而不是其它状态的线程】
- 睡眠结束后的线程未必会立刻得到执行(需要分配到cpu时间片)
- 建议用TimeUnit的sleep代替Thread的sleep来获得更好的可读性
yield
- 调用yield会让当前线程从Running进入Runnable就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器(就是可能没有其它的线程正在执行,虽然调用了yield方法,但是也没有用)
小结
yield使cpu调用其它线程,但是cpu可能会再分配时间片给该线程;而sleep需要等过了休眠时间之后才有可能被分配cpu时间片
1.3.3 线程优先级
线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它;如果cpu比较忙,那么优先级高的线程会获得更多的时间片,但cpu闲时,优先级几乎没作用.
1.3.4 join
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
r = 10;
});
t1.start();
log.debug("结果为:{}", r);
log.debug("结束");
}
因为主线程和线程t1是并行执行的,t1线程需要1秒之后才能算出r=10,而主线程一开始就要打印r的结果,所以只能打印出r=0,以上代码表现出了join的重要性.
需要等待结果返回,才能继续运行就是同步;不需要等待结果返回,就能继续运行就是异步
等待多个结果
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test2();
}
private static void test2() throws InterruptedException {
Thread t1 = new Thread(() -> {
sleep(1);
r1 = 10;
});
Thread t2 = new Thread(() -> {
sleep(2);
r2 = 20;
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
t1.join();
t2.join();
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
结果是等待了两秒:等待t1时,t2并没有停止,而在运行;1s后,执行到此,t2也运行了1s,因此也只需再等待1s.
如果颠倒两个join,运行结果如下图右所示
如果是有时效的join,如果超时在限制时间内线程执行结束会导致join结束
1.3.5 interrupt方法详解
打断sleep,wait,join的线程
这几个方法都会让线程进入阻塞状态,打断sleep的线程, 会清空打断状态,以sleep为例
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
log.debug("线程任务执行");
try {
Thread.sleep(10000); // wait, join
} catch (InterruptedException e) {
//e.printStackTrace();
log.debug("被打断");
}
}
};
t1.start();
Thread.sleep(500);
log.debug("111是否被打断?{}",t1.isInterrupted());
t1.interrupt();
log.debug("222是否被打断?{}",t1.isInterrupted());
Thread.sleep(500);
log.debug("222是否被打断?{}",t1.isInterrupted());
log.debug("主线程");
}
输出结果:可以看到,打断sleep的线程, 会清空打断状态,刚被打断完之后t1.isInterrupted()
的值为true
,后来变为false
,即打断状态会被清除。那么线程是否被打断过可以通过异常来判断。【同时要注意如果打断被join()
,wait()
blocked的线程也是一样会被清除,被清除(interrupt status will be cleared)的意思即打断状态设置为false
,被设置(interrupt status will be set)的意思就是状态设置为true
】
17:06:11.890 [Thread-0] DEBUG com.concurrent.test.Test7 - 线程任务执行
17:06:12.387 [main] DEBUG com.concurrent.test.Test7 - 111是否被打断?false
17:06:12.390 [Thread-0] DEBUG com.concurrent.test.Test7 - 被打断
17:06:12.390 [main] DEBUG com.concurrent.test.Test7 - 222是否被打断?true
17:06:12.890 [main] DEBUG com.concurrent.test.Test7 - 222是否被打断?false
17:06:12.890 [main] DEBUG com.concurrent.test.Test7 - 主线程
打断正常运行的线程
打断正常运行的线程, 线程并不会暂停,只是调用方法Thread.currentThread().isInterrupted();
的返回值为true,可以判断Thread.currentThread().isInterrupted();
的值来手动停止线程
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted) {
log.debug("被打断了, 退出循环");
break;
}
}
}, "t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
}
1.3.6 sleep,yield,wait,join对比
关于join的原理和这几个方法的对比:看这里
补充:
- sleep,join,yield,interrupted是Thread类中的方法
- wait/notify是object中的方法
sleep不释放锁、释放cpu;join释放锁、抢占cpu;yield不释放锁、释放cpu;wait释放锁、释放cpu
1.3.7 不推荐使用的方法
还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁
方法名 | static | 功能说明 |
---|---|---|
stop() | 停止线程运行 | |
suspend() | 挂起(暂停)线程运行 | |
resume() | 恢复线程运行 |
1.4 主线程与守护线程
默认情况下,java进程需要等待所有的线程结束后才会停止,但是有一种特殊的线程,叫做守护线程,在其他线程全部结束的时候即使守护线程还未结束代码未执行完java进程也会停止。普通线程t1可以调用t1.setDeamon(true);
方法变成守护线程.
log.debug("开始运行...");
Thread t1 = new Thread(() -> {
log.debug("开始运行...");
sleep(2);
log.debug("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
sleep(1);
log.debug("运行结束...");
注意
- 垃圾回收器线程就是一种守护线程
- Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收到shutdown命令后,不会等待它们处理完当前请求
1.5 线程状态之五种状态
这是从操作系统层面来描述的
- 初始状态,仅仅是在语言层面上创建了线程对象,即
Thead thread = new Thead();
,还未与操作系统线程关联 - 可运行状态,也称就绪状态,指该线程已经被创建,与操作系统相关联,等待cpu给它分配时间片就可运行
- 运行状态,指线程获取了CPU时间片,正在运行
- 当CPU时间片用完,线程会转换至【可运行状态】,等待CPU再次分配时间片,会导致我们前面讲到的上下文切换
- 阻塞状态
- 如果调用了阻塞API,如BIO读写文件,那么线程实际上不会用到CPU,不会分配CPU时间片,会导致上下文切换,进入【阻塞状态】
- 等待BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
- 与【可运行状态】的区别是,只要操作系统一直不唤醒线程,调度器就一直不会考虑调度它们,CPU就一直不会分配时间片
- 终止状态,表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
1.6 线程状态之六种状态
这是从Java API层面来描述的
- NEW跟五种状态里的初始状态是一个意思
- RUNNABLE是当调用了
start()
方法之后的状态,注意,Java API层面的RUNNABLE
状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【io阻塞状态】(由于BIO导致的线程阻塞,在Java里无法区分,仍然认为是可运行) BLOCKED
,WAITING
,TIMED_WAITING
都是Java API层面对【阻塞状态】的细分TERMINATED
当线程代码运行结束