作者
:学Java的冬瓜
博客主页
:☀冬瓜的主页
专栏
:【JavaEE】
分享
:愉快骑行的一天!
主要内容
:Thread方法的使用,终止一个线程interrupt,等待一个线程join、休眠一个线程sleep、控制线程执行顺序wait和notify。
线程的六大状态:NEW RUNNABLE TERMINATED TIMEWAITING WAITING BLOCKED
前台线程
:会组织进程结束。只要前台线程还在运行,那么进程就不会结束,main以及我们手动创建的线程都是前台线程。可以用t.setDeamon()将前台线程改为后台线程。后台线程
:也叫做守护线程,不会组织进程结束。即使守护线程还没运行完,进程也可以结束。isAlive 为true的条件是在PCB生命周期内
。Thread对象比在内核中的PCB的生命周期长
。public static Thread currentThread();
使用标志位来控制线程是否停止
// 线程中断 法一:利用标志位来控制线程是否停止
public class ThreadTest {
private static boolean flag = true; // flag为标志位
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while (flag) {
System.out.println("Hello thread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"myThread");
t.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = false; // 标志位取false,意味着告诉t线程要结束了
}
}
使用当前线程自带的标志位控制线程中断(终止)
Thread.currentThread().isInterrupted()
代替自定义标志位;使用t.interrupt()
终止线程// 使用Thread自带的表示位和方法,控制线程终止
public class ThreadTest {
public static void main(String[] args) {
Thread t = new Thread(()->
{
while (!Thread.currentThread().isInterrupted()){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello thread!");
}
});
t.start();
try {
Thread.sleep(2300);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 终止t线程
t.interrupt();
}
}
// 结果
hello thread!
hello thread!
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at ThreadTest.lambda$main$0(ThreadTest.java:8)
at java.lang.Thread.run(Thread.java:748)
hello thread!
hello thread!
hello thread!
...
分析:可以在运行时发现t线程被提前唤醒抛出异常,还有在抛出异常后"hello thread"会一直打印下去,请看以下分析:
当执行到第三秒时,打印了两个"hello world!",
然后t线程休眠1s(其实只休眠了0.3),main线程再休眠0.3s,然后执行t.interrupt(),这时很明显的看到异常很快(0.3s)抛出,也就是说,t线程本来要休眠一秒,但是t.interrupt()让它提前抛出异常了。
interrupt()会做两件事:
1> 把线程内部的标志位设置成true
2> 如果线程在sleep,就会触发异常,把sleep唤醒。
sleep被唤醒时,会做一件事:
sleep被唤醒时,sleep会把标志位再设置回false(清空标志位)(wait,join等也有相应操作)。因此,抛出异常后,还会循环打印下去,所以上面的代码其实是t线程忽略了终止请求。接下来是三种类型:
程序员可以根据自己的需求
忽略终止请求
立刻执行终止线程
先执行其它任务再终止线程
join可以控制两个线程的结束顺序:让调用join()的线程先执行完,其它线程阻塞
。public class ThreadTest {
public static void main(String[] args) {
Thread t = new Thread(()->{
for (int i = 0; i < 3; i++) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
System.out.println("t——join之前");
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t——join之后");
}
}
结果:
t——join之前
hello thread
hello thread
hello thread
t——join之后
public static void sleep(long millis);
来举个例子:比如 t1 t2两个线程要执行任务,要求
t1先执行的差不多了,再执行 t2。那此时,我们让 t2先wait(阻塞),等 t1执行的差不多了,再在 t1中通过 notify来通知
t2,把t2从阻塞中唤醒,继续执行 t2任务,这样就可以控制线程执行次序。
当然,这个例子也可以用join完成,即在
t2线程run的第一行使用 t1.join(),就可以控制 t1在 t2前完成。但是当需求是
t1先执行50%,然后t2执行,那join就不能用了。而sleep不知道
t1执行50%具体消耗的时间,所以不能精确确定。然而,wait和notify就可以根据程序员自己决定。
wait操作其实共要执行三个操作:
a> 释放锁
b> 进行阻塞等待
c> 收到notify的通知后,重新尝试获取锁,并且在获取锁后,继续往后执行。
在上面的操作中,还没获取锁,就想要阻塞等待,那就出问题了,所以报错:非法锁状态异常。
要求:t1先执行一半,再让 t2执行完,最后执行 t1的剩下的一半。
代码如下:
// wait 和 notify
public class Main {
public static void main(String[] args) {
Object object = new Object();
Thread t1 = new Thread(()->{
synchronized (object){
try {
System.out.println("t1 wait之前");
object.wait();
System.out.println("t1 wait之后");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(()->{
synchronized (object){
System.out.println("t2 notify之前");
object.notify();
System.out.println("t2 notify之后");
}
});
t1.start();t2.start();
}
}
结果:
t1 wait之前
t2 notify之前
t2 notify之后
t1 wait之后
需要注意的点:
b> 在上面代码中,结果也可能为
t2 notify之前
t2 notify之后
t1 wait之前
然后就死等了
因为在上面代码中t1和t2是抢占式执行的,我们无法确定谁先执行,如果是t2先执行,那t2先执行完前一半,然后notify就是空打了一炮,没有任何作用。然后就继续执行 t2剩下的一半,然后再调度到t1,执行t1的前一半,但是执行完t1的前一半后,t1就wait陷入死等了,没人将它唤醒。
c> 为了满足需求,让notify再wait之后执行,我们可以在t2最前面加上一个sleep,或者t1.start()和t2.start()中间加上sleep,那就可以大概率确保最开始是t1执行的。
d> 在上述代码中,wait是死等t2完成,t1才能完成剩下的一半,那我们可以给它传参,设置最大等待时间,如果超过这个等待时间,我们t1就会被唤醒。提前唤醒是正常操作,不会抛异常。但是interrupt唤醒sleep则是抛出异常。
e> 如果多个线程在阻塞等待object对象,此时有一个线程object.notify,那会唤醒任意一个在阻塞的线程(notifyAll则是唤醒全部正在阻塞的线程)。为了解决这样的问题,此时需要用到多个不同对象。
要求:三个线程,使用notify,分别只能打印A,B,C,要保证是固定按照A、B、C的顺序打印的。
public class Main {
public static void main(String[] args) throws InterruptedException {
Object o1 = new Object();
Object o2 = new Object();
Thread t1 = new Thread(()->{
System.out.println("A");
synchronized (o1){
o1.notify();
}
});
Thread t2 = new Thread(()->{
synchronized (o1){
try {
o1.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("B");
synchronized (o2){
o2.notify();
}
});
Thread t3 = new Thread(()->{
synchronized (o2){
try {
o2.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("C");
}
});
t2.start();
t3.start();
Thread.sleep(100); // 保证t2和t3的wait先执行,t1的notify后执行
t1.start();
}
}
NEW
:创建了Thread对象,但还没有start(操作系统内核中还没有对应的PCB)
TERMINATED
:内核中PCB已经执行完,但是对应的Thread对象还在。
RUNNABLE
:可运行的。正在CPU上执行的PCB或者在就绪队列中的PCB(线程)。
以下都是阻塞下的状态:,此时PCB在阻塞队列中。
WAITING
:runnable中,sleep时对应的状态
TIME_WAITING
:runnable中,wait对应的状态
BLOCKED
:runnable中,加锁后对应的状态。
线程的状态:
NEW RUNNABLE TERMINATED状态演示
:
// NEW RUNNABLE TERMINATED状态演示
public class ThreadTest {
public static void main(String[] args) {
Thread t = new Thread(()->{
// 这个循环是确保start后,t获得RUNNABLE状态
for (int i = 0; i < 1000000000; i++) {
}
});
// start前,getState得到NEW状态
System.out.println("start之前:"+t.getState());
t.start();
// 在start后,getState得到RUNNABLE状态
System.out.println("start之后,t调度执行时:" + t.getState());
// 加sleep让t线程执行完,再getState,就可得到TERMINATED状态
// (不用sleep,使用join也行)
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t结束后:" + t.getState());
}
}
start之前:NEW
start之后,t调度执行时:RUNNABLE
t结束后:TERMINATED
CPU密集
:包含了大量的 加减乘除 等运算。IO密集
:包含读写文件,读写控制台,读写网络。比如启动eclipse时,需要加载数据,就涉及到大量的读写硬盘,阻塞了界面的响应,用多线程可以缓解。业务:当前有两个变量,需要把两个变量各自自增100亿次(属于CPU密集)。代码如下:
// 演示单线程和多线程效率差别
public class ThreadTest {
public static void main(String[] args) {
serial();
concurrency();
}
// 串行化
public static void serial(){
long begin = System.currentTimeMillis();
long a = 0;
for (long i = 0; i < 100_0000_0000L; i++) { // int表示数据范围:-21亿~21亿,超过范围用long,且数值后加上L
a++;
}
long b = 0;
for (long i = 0; i < 100_0000_0000L; i++) {
b++;
}
long end = System.currentTimeMillis();
System.out.println("一个线程执行:"+(end-begin)+"ms");
}
// 并发执行
public static void concurrency(){ //concurrency:并发的
Thread t1 = new Thread(()->{
long a = 0;
for (long i = 0; i < 100_0000_0000L; i++) {
a++;
}
});
Thread t2 = new Thread(()->{
long b = 0;
for (long i = 0; i < 100_0000_0000L; i++) { // int表示数据范围:-21亿~21亿,超过范围用long,且数值后加上L
b++;
}
});
long begin = System.currentTimeMillis();
t1.start();
t2.start();
// 确保t1和t2已经执行完
try{
t1.join();
t2.join();
}catch (Exception e){
e.printStackTrace();
}
// 这时main从阻塞中恢复,继续执行下面代码:
long end = System.currentTimeMillis();
System.out.println("两个线程并发执行:"+(end-begin)+"ms");
}
}
运行结果:
从运行结果中可以发现,多线程效率确实比单线程快很多。
// 第一次
一个线程执行:8429ms
两个线程并发执行:3856ms
// 第二次
一个线程执行:8657ms
两个线程并发执行:4009ms
// 第三次
一个线程执行:9168ms
两个线程并发执行:3591ms