1.终止线程
- 通过改变循环的条件结束线程
package classTest6;
public class Test {
private static boolean isQuit = false;
public static void main(String[] args) {
Thread thread = new Thread(() ->{
while (!isQuit) {
System.out.println("gggg");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程结束");
isQuit = true;
//通过该方法结束线程1
}
}
-
通过调用interrupt方法
-
注意
- 如果代码中有sleep,会存在一定得问题
- 在执行sleep得过程中,调用interrupt大概率sleep得休眠时间还没到,会被提前唤醒
- 唤醒得同时抛出interruptedException,紧接着就会被catch获取到,就会清除Thread对象得isinterrupted标志位
- 就会把标志位又返回false 代码就会报错
- 此时循环就会一直执行
package classTest7;
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hhhhh");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();//打印异常
//抛出异常,结束线程(tiao'ch
break;
}
}
System.out.println("gggggg");
});
t.start();
Thread.sleep(3000);
System.out.println("game over");
t.interrupt();
//调用interrupt方法,来修改刚才标志位得值
}
}
2.等待线程
-
有时,我们需要等待⼀个线程完成它的⼯作后,才能进⾏⾃⼰的下⼀步⼯作。例如,排队的时候只有等前面的人离开了我们才能再上去(当然你也可以非常粗暴的插队,但是不建议)。
-
多个线程的执行顺序是不确定的(随机调度,抢占式执行)
-
可以通过一些api来影响线程执行的顺序 -> join
-
ps:看不懂线程的创建方式可以看我的上一篇文章,有详细的介绍
package classTest9; public class Test { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { for (int i = 0; i < 5; i++) { System.out.println("hhhhh工作中"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("执行完毕"); }); t.start(); t.join(); /* * 让main线程等待t线程结束(谁调用谁等待) * 如果t运行中,main线程就会阻塞,直到t运行结束*/ System.out.println("主线程 ,t执行过后开始打印"); } } - 结果:t线程调用join,join就会等待t线程执行结束
- 死等(一直等待,不干其他的事情)
- 带有超时时间的等待,过了超时时间就不等待
- 带有超时时间的等待,有着更高的精度
3.线程的状态
- NEW: Thread创建好了,还没调用start方法创建线程
- RUNNABLE: 就绪状态
- BLOCKED: 由于锁竞争引起的阻塞
- WAITING: 死等
- TIMED_WAITING: 时间的阻塞,到达一定时间过后自动解除阻塞,sleep和带有超时时间的join也会进入这个状态
- TERMINATED: 线程执行完毕,对象仍然存在
4.线程的安全
- 某个代码,单线程下执行没有问题,多线程执行下出现bug(线程调度是随机的)
package classTest10;
public class Test {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 500000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 500000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + count);
//count 的结果是不确定的,存在线程的安全
/*
* count++ 本质上是由三个cpu指令构成的
* load:把内存数据加载到寄存器中
* add 进行+1运算
* save 把寄存器数据写到内存中
*/
}
}
原因
- 根据时间线,t1线程执行load操作后就被t2线程抢过去,t2执行过后结果自增为1,count的结果为1
- 然后又回到t1线程,load的操作再t2之前已经执行了,此时内存中的count的值为0,所以t1执行过后count的值还是为1
- 正确的执行方法
t1执行完过后内存中count的值为1,t2继续读取内存中的值进行自增操作,此时count的值为2
- 根本原因:操作系统上的线程是抢占式执行,随机调度,线程之间执行的顺序带来了很多的变数
- 代码结构:代码中多个线程,同时修改同一变量
- 直接原因:上述多线程修改操作,本身不是原子的(每个cpu指令都是原子的),一个线程执行这些指令,执行到一半,可能就会被调走(加锁进行解决)
- 利用 synchronized进行加锁操作,此时t1就不会像之前那样进行插队,就不会有线程的安全问题(之前说过不建议插队,这下信了吧,可是有大问题的哦)
package classTest10;
public class Test {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
/*
* java中任意对象都可以当作锁对象
* 通过锁对象进行加锁
* 当t1被加锁过后,t2会阻塞,直到t1结束过后才可以对其加锁
*/
Object locker = new Object();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 500000; i++) {
synchronized (locker) {
count++;
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 500000; i++) {
synchronized (locker) {
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + count);
}
}
- 内存可见性问题
package classTest14;
import java.util.Scanner;
public class Test {
private static int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
/*
* 此时进行的核心指令有两条
* 1.load读取内存中flag的值到cpu寄存器中
* 2.拿到寄存器的值和0进行比较(条件跳转指令)
* 此时频繁的执行load和条件跳转
* load的开销大,并且load没有变化
* 此时jvm就可能进行代码优化,对load进行优化,
* jvm会只智能的对代码进行优化 */
while (flag == 0) {
/*
* 相当于t2修改了内存 但是t1没有看到这个内存的变化
* 当循环里面有代码的时候,这个问题就会得到解决
* 可见内存可见性高度依赖编译器优化 什么时候触发是不确定的
* */
}
System.out.println("t1循环结束");
});
Thread t2 = new Thread(() -> {
System.out.println("输入flag的值 :");
Scanner sc = new Scanner(System.in);
flag = sc.nextInt();
});
/*
* t2要等待用户输入 此时t1已经开始循环了很多次
* 结果:输入flag的值过后 循环并没有结束*/
t1.start();
t2.start();
}
}
-
上述过程在jvm中的情况
- 编译器发现,每次循环都要读取主内存(内存)
- 就会把数据从主内存复制到工作内存(cpu寄存器)中,后续读取的都是工作内存
-
解决办法:java提供了volatile 可以将上面的优化强制关闭,确保每次循环条件都会从内存中读取数据
- 指令重排序问题
5.死锁
package classTest12;
public class Test {
public static void main(String[] args) {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
//sleep一下,给t2时间,让t2也能拿到B
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//尝试获取B 没有释放A
synchronized (B) {
/*
* 彼此都在等待对方的锁
* 但是双放的锁都没有释放
* 出现死锁*/
System.out.println("t1拿到了两把锁");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (B) {
//sleep一下,给t1时间,让t1也能拿到A
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (A) {
System.out.println("t2拿到了两把锁");
}
}
});
t1.start();
t2.start();
}
}
-
死锁产生的四个必要条件
- 互斥使用:一个线程拿到锁过后,另外一个锁想要获得,只能阻塞等待
- 不可抢占:一个线程拿到锁之后,只能主动解锁,不能让别的线程强行把锁抢走
- 请求保持:一个线程拿到锁A之后吗,在持有A的前提下,尝试获取B
- 循环等待(环路等待)
-
解锁:破坏其中一个条件即可 (最好的方式,避免循环等待)
6.wait 和 notify
- wait() / wait(long timeout): 让当前线程进⼊等待状态.
wait 做的事情:
• 使当前执⾏代码的线程进⾏等待. (把线程放到等待队列中)
• 释放当前的锁
• 满⾜⼀定条件时被唤醒, 重新尝试获取这个锁.
wait 要搭配 synchronized 来使⽤. 脱离 synchronized 使⽤ wait 会直接抛出异常.
wait 结束等待的条件: •
其他线程调⽤该对象的 notify ⽅法.
• wait 等待时间超时 (wait ⽅法提供⼀个带有 timeout 参数的版本, 来指定等待时间).
• 其他线程调⽤该等待线程的 interrupted ⽅法, 导致 wait 抛出 InterruptedException 异常.
package classTest15;
public class TEst {
public static void main(String[] args) throws InterruptedException {
Object ob = new Object();
synchronized (ob) {
System.out.println("wait 之前");
ob.wait();//释放ob对象的锁 此时线程进入等待状态
//同时wait被唤醒后,还是可以参与枪锁
System.out.println("wait 之后");
}
}
}
结果(当前线程进入了等待状态,并没有被唤醒,所以不会执行后面的代码)
- wait的几个版本
0. 死等
- 带有时间的等待 最多等待timeoutmillis 过了这个时间就不在等待(这个时间是兜底的)
- sleep 提前唤醒 通过异常的方式,正常的业务逻辑不应该依赖于此
- notify() 只唤醒其中一个线程
- notifyAll(): 唤醒在当前对象上等待的所有线程
package classTest16;
public class Test {
public static void main(String[] args) {
Object ob = new Object();
Thread t1 = new Thread(() -> {
synchronized (ob) {
System.out.println("t1 wati 之前 ");
try {
ob.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 wait 之后");
}
});
Thread t2 = new Thread(() -> {
/*
* wait放到 synchronized里面是因为要释放锁,首先需要加锁
* notify可以不放到 synchronized,
* 因为不需要加锁,但是java中约定要放入其中
* 同时两个对象应该一致 */
try {
Thread.sleep(5000);//写到 synchronized外面
synchronized (ob) {
System.out.println("t2 notify 之前");
ob.notify();
System.out.println("t2 notify 之后");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
}
}
/*
* 结果
* t1 wati 之前
t2 notify 之前
t2 notify 之后
t1 wait 之后
notify唤醒了等待的线程,线程会重新加锁,继续执行
注意:此时t1被唤醒过后,t2线程还没有释放锁,所以t1要进行等待,直到t2释放锁,t1线程才会继续。这就是为什么t2 notify 之后 在 t1 wait 之后 先执行的原因。
*/