线程基础知识
创建线程
- 直接使用thread
Thread t1 = new Thread("t1"){
@Override
public void run() {
log.debug("t1");
}
};
- 配合 runnable
Runnable task = new Runnable() {
@Override
public void run() {
//要执行的任务
}
};
Thread thread = new Thread(task);
- lamdba简化
java8开始对于函数接口,可以使用lambda表达式进行简化
Thread thread = new Thread(() -> {
log.debug("task1");
},"name");
- FutureTask配合Thread
FutureTask<Integer> futureTask = new FutureTask<>(() -> {
//执行的方法体 ,并返回值
return 100;
});
new Thread(futureTask, "线程名").start();
try {
Integer result = futureTask.get();
} catch (ExecutionException e) {
e.printStackTrace();
}
查看进程、线程的方法
- windows
- tasklist 查看进程
- taskkill 杀死进程
- linux
- ps -fe 查看所有进程
- ps -fT -p 查看某个进程的所有线程
- kill 杀死进程 -top 按大写 H 切换是否显示线程
- top -H -p 查看某个进程的所有线程
- java
- jps 命令查看所有Java进程
- jstack 查看某个Java进程的所有线程状态
- jconsole 来查看某个Java进程中线程的运行情况(图形界面)
java命令对应的可执行程序都在 jdk的bin 文件夹下面,如果在cmd里直接执行有问题,可以检查下是否把bin路径添加到系统路径中
线程运行原理
- 栈与栈帧
JVM中由堆、栈、方法区组成,其中栈内存是给谁用的呢? 其实就是线程 ,每个线程启动后,虚拟机就会为其分配一块栈内存- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
- 线程上下文切换
因为以下一些原因导致cpu不再执行当前的线程,转而执行另一个线程的代码- 线程的cpu时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep yield wait join park synchronized lock等方法
线程常见方法
-
start
启动一个新线程,在新的线程中运行 run 方法 。
注意: start 方法只是让线程进入就绪,里面代码不一定立即运行(CPU时间片还没分给它)。每个线程对象的start只能调用一次,如果调用了多次会出现 IllegalThreadStateException 。有个threadStatus 属性,默认为 0 ,对应的线程状态就是NEW
线程只能start一次,那么线程池里的线程为什么可以重复运行?
因为线程池里的需要重复运行的线程根本就没有运行完成过,这些线程的run方法其实是一个死循环。如果有任务过来,就运行任务的run 方法,如果没有就是一个 while(ture) 的死循环 -
run
新线程启动后会调用的方法; 如果在构造Thread 对象时传递了Runnable 参数,则线程启动后会调用Runnable对象的 run 方法,否则默认不执行任何操作。但可以创建Thread 的子类对象,来覆盖默认行为
-
join 等待线程结束
-
join(long n) 等待线程运行结束,最多等待 n 毫秒
-
getId() 获取线程长整型的id id 唯一
-
getName() 获取线程名
-
setName(String) 设置线程名
-
getPriority() 获取线程优先级
-
setPriotity(int ) 设置线程优先级 java中规定线程优先级是 1~10 ,默认优先级是 5 ,较大的优先级能提高该线程被CPU调度的几率
-
getState()
获取线程状态。
JAVA中线程状态是用 6个enum表示:
NEW RUNNABLE BLOCKING WAITING TIMED_WAITING TERMINATED
-
isInterrupted() 判断是否被打断 不会清除打断标记
-
isAlive() 线程是否存活(还没有运行完毕)
-
interrpt()
打断线程
如果被打断的线程正在 sleep wait join 会导致被打断的线程抛出 InterruptedException ,并清除打断标记;
如果打断的正在运行的线程,则会设置打断标记 ,具体要不要退出,需要线程自己决定;
park 的线程被打断,也会设置 打断标记
-
intertupted() static 判断当前线程是否被打断 会清除打断标记
-
currentThread() static 获取当前正在执行的线程
-
sleep(long) static 让当前执行的线程休眠 n 毫秒,休眠时让出 CPU的时间片给其他线程
-
yield() static 提示线程调度器让出当前线程对CPU的使用 主要是为了测试和调试
线程常见方法详解
-
sleep
- 调用 sleep 会让当前线程从Runnable 进入 timed_waiting 状态(阻塞)
- 其他线程可以使用 interrupt 方法 打断正在睡眠的线程,这是 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立即得到执行
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好地可读性
-
yield
- 调用yield 会让当前线程从 running 进入 Runnable 就绪状态(还是有机会重新获得时间片),然后调度执行其他线程
- 具体的实现依赖于操作系统的任务调度器
-
线程优先级
- 线程优先级会提示 调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果CPU比较忙,那么优先级高的线程会获得更多的时间片,但CPU闲时,优先级几乎没用
-
防止 CPU 占用 100% - Sleep 实现
在没有利用cpu来计算时,不要让 while(true) 空转浪费CPU (在单核的时候非常明显,基本上会全部占用CPU),这时可以使用 yield或 sleep 来让出cpu的使用权给其他程序
while(true) {
try {
Thread.sleep(50);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
可以使用wait 或者 条件变量 达到类似的效果
不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景
sleeep 适用于无需锁同步的场景
- wait 实现
synchrozied(obj){
//锁对象.wait()
}
-
join 方法详解
等待线程结束,如果送入参数 ,则表示最多等待多久
Thread t1 = new Thread(()->{
log.info("进入 sleep...");
try {
TimeUnit.SECONDS.sleep(2);
count = 10;
} catch (InterruptedException e) {
log.info("wake up...");
e.printStackTrace();
}
});
t1.start();
t1.join(); //等待 t1 执行完成, 如果没有这个 count 的值还是默认值 0
log.info("count的值为{}", count);
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
应用之同步
以调用方角度来讲,如果:
- 需要等待结果返回, 才能继续运行就是
- 不需要等待结果,就能继续运行就是异步
-
打断标记
使用 interrupte 打断线程时,如果线程处于 sleep wait join 时,即使被打断,也会将打断标记清除;
如果是正常运行的线程被打断,则会添加打断标记;
Thread t1 = new Thread(()->{
log.info("进入线程...");
while(true) {
if (Thread.currentThread().isInterrupted()) {
log.info("收到打断请求,按照约定将结束该线程...");
break;
}
}
});
t1.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("准备打断,但如何处理打断标记还是需要线程本身来决定...");
t1.interrupt();
两阶段终止模式
在一个线程T1 中如何 “优雅” 终止 线程 T2 ? 这里的 【优雅】 指的是给T2 一个料理后事的机会
-
错误思路
-
使用对象的 stop 方法停止线程 (该方法已经废弃,不建议大家使用)
stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其他线程永远无法获取锁
-
使用 System.exit() 方法停止线程
目的仅是停止一个线程, 但这种做法会让整个程序都停止
-
private Thread monitor;
//启动监控线程
public void start() {
monitor = new Thread(()-> {
while(true) {
//检查打断标记
if (Thread.currentThread().isInterrupted()) {
log.info("监控线程监测到打断标记,即将料理后事,之后将关闭线程...");
break;
}
log.info("正常监控...");
try {
//休眠2秒
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
//如果在休眠期间被打断 ,这时候的打断标记已经被清除了 所以手动再设置回去
log.info("休眠期间收到打断标记..,检查此时的打断标记:{}",
Thread.currentThread().isInterrupted());
//Thread.interrupted()
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
}, "monitor");
monitor.start();
}
public void stop() {
monitor.interrupt();
}
- park 线程
LockSupport 中提供两个方法:
LockSupport.park () // 暂停当前线程
LockSupport.unpark(暂停线程对象) // 恢复某个线程
Thread t1 = new Thread(() -> {
log.info("park");
LockSupport.park(); //线程被暂停后,下面的代码不会被执行了
//log.info("unpark...");
log.info("打断状态: {}", Thread.currentThread().isInterrupted()); //此时打断标记为 true
//LockSupport.park();//在打断标记为 true 的情况下,park 方法将不起作用
log.info("unpark...");
}, "t1");
t1.start();
//主线程将主动打断 park线程 ,不然就一直暂停住了
TimeUnit.SECONDS.sleep(1);
//t1.interrupt();
log.info("即将恢复被暂停的线程...,此时该线程的状态为{}", t1.getState());
LockSupport.unpark(t1);
线程被暂停之后,如果使用 t1.interrupt ,可以恢复该线程 ,并且打断标记为 true;
如果使用 LockSupport.unpark 恢复线程,那么打断标记依旧为 false