Thread类
在java中,我们通过Thread类来进一步操作线程;
线程的创建
比较常用的创建线程的方法和jconsole的使用可以看这个=> 创建线程的常用方法
这里我们来看一下Thread类中的两个构造方法;
1. Thread (String name) ; 在创建线程时,给线程设置一个名字 , 这样方便我们后续调试 ;
2. Thread (Runnable runnable , String name) ; 第一个参数是把一个Runnable任务交给线程, 第二个参数 是 给线程设置一个名字;
例子: 我想创建一个 名字是t2的线程;
public class Test1 {
public static void main(String[] args) {
Thread t2 = new Thread(new Runnable(){
@Override
public void run() {
while (true){
System.out.println("t2线程");
}
}
} , "t2"); // 创建一个名字是 t2 线程 , 并且t2线程里循环输出 "t2线程"
t2.start(); // 不要忘记 调用start() ;
while (true){
System.out.println("main"); // main主线程循环输出 "main"
}
}
}
(创建线程时,设置线程名字是 "t2" , 并且线程的任务是循环输出"t2线程")
此时运行代码的 和 查看 jconsole 的结果 :
(给线程设置一个名字,后面我们一看名字就知道是哪个线程)
一定要调用 start()方法, 调用start()才能真正调用系统api创建出线程 ;
注意:变量捕获问题
举一个例子: 我想创建一个 t1 线程 , 让他把1到100打印输出 ;
(用 num 来 记录 1到100的值, 但编译器确包错了)
原因: Lambda表达式本质就是匿名内部类,匿名内部类里的变量捕获, 要求匿名内部类里访问的变量要是 final修饰的 常量 或者 从创建出来就没有修改过值的变量(把变量当成常量来用) , 很明显这里 num 有修改值的 操作 , 所以编译器报错了 ;
解决方法 : 将这里的 num 从局部变量 改成 成员变量 或 静态成员变量 ;
(此时num作为类的成员属性定义,编译器就没有报错了)
此时这里就是用的 匿名内部类访问外部类成员的语法, 就不会触发 变量捕获 ;
线程的休眠
举个例子 : 我想创建一个 t1 线程 , 让他把1到100打印输出 ;
public class Test1 {
private static int num = 1 ; // num 写在方法外面
public static void main(String[] args) {
Thread t1 = new Thread(()->{ // 这里用Lambda表达式的方法创建线程
while (num <= 100){ //判断 num <= 100
System.out.println(num);
num ++ ;
}
} , "t1");
t1.start(); // 记得调用start方法`
}
}
运行一下 : 结果 刷一下就结束了
(怎么眨一下眼睛就把1到100给打印完了?)
Thread.sleep() 方法 : 是Thread类的静态方法
可以让线程休眠 xx 毫秒 , 需要抛一个 InterruptedException 异常;
休眠时线程不会执行任务,也不会被销毁,相当于摸鱼一段时间,时间到我在来工作;
我们用这个方法让线程每隔 1 秒 打印输出一次 (每打印输出一次就休眠1秒) ;
public class Test1 {
private static int num = 1 ; // num 写在方法外面
public static void main(String[] args) {
Thread t1 = new Thread(()->{ // 这里用Lambda表达式的方法创建线程
while (num <= 100){ //判断 num <= 100
System.out.println(num);
num ++ ;
try {
Thread.sleep(1000); // 休眠 1000毫秒 = 1 秒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
} , "t1");
t1.start(); // 记得调用start方法`
}
}
此时看运行结果 : 每隔1秒打印一次,就不会搜一下就结束了;
线程的终止/打断
如果想让一个线程执行到一半或者执行到某个阶段,就终止了,不在继续运行下去;
在java中唯一的方法,就是让 run()方法执行完 ; (run()方法执行完了,线程就自动销毁了);
1.设置一个"标记" 作为run结束的标记
大部分情况下,run没有执行完,是因为run里有一个 while 之类的循环没有结束,while循环不结束,导致run也迟迟不能结束,我们可以设置一个"标记",来让while循环停止,从而达到让 run 快点结束的效果;
例子: 我想让 t1 线程执行10秒后就终止;
代码思路:
1. 设置 一般while 里我们用的是 true, 但这里我们用 一个变量 isQuit 作为 while结束的标记
2.main主线程里,我们用sleep,让main线程休眠10秒后,将isQuit置为true;
3.让t1线程在执行过程中,打印一些信息方便我们查看 ;
整体流程:
运行结果
(在差不多10秒后, t1 结束)
代码
public class Test2 {
private static boolean isQuit = false ;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while(!isQuit){ // 用 isQuit代替 true
System.out.println("t1正在执行"); // 打印输出,方便我们查看
try {
Thread.sleep(2000); // 让t1每输出一次就休眠 2 秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1终止");
} , "t1"); // 用Lambda创建 t1线程 ;
t1.start();
// 在t1 运行 10 秒后 , 将 isQuit 置为 true, 让 while循环停止 ;
System.out.println("10秒后t1停止");
Thread.sleep(10000);
System.out.println("将isQuit置为true");
isQuit = true ; // 将isQuit设置为 true
}
}
2. 使用Thread自带的"标志位"来实现终止
上面手动设置"标记"的方法,有两个大问题: 1.要创建出一个变量(这个变量还要考虑Lambda的变量捕获问题); 2.当我们把isQuit设置true时,while不会立刻停止,还要等sleep休眠结束返回到while判断才会停止(不够及时) ;
通过这两个方法来用Thread自带的"标志位"来实现终止
1.isInterrupted() 方法 : 获取当前线程的标志位 ;
2. Interrupted() 方法 : 将当前线程的标志位置为 true;
例子: 我想让 t1 线程执行5秒后就终止;
代码思路:
1.用 isInterrupted() 作为 while的 判断 , 不需要手动创建 一个变量;
2.在 线程执行5秒后, 调用 Interrupted() 将 t1对象的标志位 置为 true;
细节问题: 怎么在Lambda表达式里调用isInterrupted()方法?
用 Thread.currentThread() 来, 获取 当前 t1线程的实例,就可以调用isInterrupted()方法;
代码
public class Test1 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while (!Thread.currentThread().isInterrupted()){
try{
System.out.println("t1正在运行");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
break; // 使用break让while循环停止 ;
}
}
System.out.println("t1停止");
}, "t1");
t1.start();
// 在t1运行5秒后,将t1的标志位置为true,
System.out.println("5秒后t1停止");
Thread.sleep(5000);
System.out.println("将t1的 标志位 置为true");
t1.interrupt();
}
}
运行结果
(调用Interrupted()后,while循环结束,线程也执行完了)
interrupted()的终止原理
1. sleep 让线程休眠, 线程必须到时间了才会被唤醒,就算 此时 标志位已经为 true了, 也要等我这里的sleep休眠完;
2.interrupted 将标志为改为true , 同时会触发 sleep的一个异常, 达到 唤醒线程的作用,让线程不必等sleep休眠结束 就被唤醒 ; 这里的异常就是 : InterruptedException ;
3.当sleep触发异常后, 会清除掉我们设置的 标记位 , 需要我们手动在 catch 里加一个 break , 来让while循环停止 (如果没有break的话,线程就不会停止; 我们也可以用来做了一些收尾工作,然后在break);
用自带标志位的好处
1. 不需要额外创建变量 (不用我们手动创建变量,我们也无需考虑变量捕获的问题);
2. 可以唤醒正在sleep休眠的线程 , 手动设置的标志位还要等sleep休眠完, 而自带的标志位可以直接唤醒线程(设置休眠个100秒,就可以很好的看出两者的区别)
线程的等待
让一个线程等待 另一个线程的执行完后, 自己再执行 ;
join() 方法 : 在哪个线程里调用 , 哪个线程就要等待 , 哪个对象调用的join, 哪个对象就是被等待的;
和 sleep一样 , join要抛一个 InterruptedException异常 ;
例子: 让 b线程等待a线程执行完后 ,b线程才执行 ;
代码 : a和b 线程每隔一秒输出一次,输出5次 ; b线程等待a线程执行完后,自己才开始输出 ;
public class Test2 {
public static void main(String[] args) throws InterruptedException {
Thread a = new Thread(()->{
try{
for (int i = 0; i < 5; i++) {
System.out.println("a正在运行");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("a结束运行");
}, "a");
Thread b = new Thread(()->{
try {
a.join(); // 让b线程等待a线程 : a线程执行完后,b线程才执行 ;
for (int i = 0; i < 5; i++) {
System.out.println("b正在运行");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "b");
a.start();
b.start();
}
}
运行结果
(b线程要等a线程结束了,自己才开始输出)
join()方法的等待是"死等", "等待的线程" 一定要等到 "被等待的线程"执行完,自己才开始执行 ;
(如果a线程里是一个while(true)循环, 那么b线程要等到天荒地老)
设置等到时间
可以给 join传一个参数,设置线程等待的时间, 时间单位是毫秒(ms) ;(设置一个界限,如果超时了我就不等了)
代码 : b线程只等待 a 线程 3秒 , 3秒后b线程就不等了 ;
public class Test2 {
public static void main(String[] args) throws InterruptedException {
Thread a = new Thread(()->{
try{
for (int i = 0; i < 5; i++) {
System.out.println("a正在运行");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("a结束运行");
}, "a");
Thread b = new Thread(()->{
try {
a.join(3000); // b线程只等a线程3秒, 3秒后b线程就不等了 ; 3秒=3000毫秒
for (int i = 0; i < 5; i++) {
System.out.println("b正在运行");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "b");
a.start();
b.start();
}
}
运行结果
(b在等了a 3秒后 , 就走了)
获取线程的实例
Thread.currentThread() :是Thread类的静态方法;
可以获取当前线程的引用(谁调用就是获取谁的)
例子: 获取main的主线程的引用
public static void main(String[] args) {
Thread t1 = Thread.currentThread();
System.out.println(t1.getName()); //打印输出 线程的名字
}
main方法本身就会创建一个 "main主线程" ,在 main线程里调用的 Thread.currentThread(), 自然获取的是 main 线程的引用 , 所以t1获取到的就是 main线程 ;
运行结果输出: main ;
getName():获取线程的名字, main线程,名字自然是: "main", 所以最后输出main;
线程还有很多属性,和方法例如这里的getName(), 咋们下次在学习;
总结
1. Thread.sleep( xx ms) 能让线程休眠 xx 毫秒 ;
2. isInterrupted() 获取Thread对象自带的标志位,并且可以通过interrupt() 将标志位修改成true; 同时 interrupt() 可以将休眠的线程唤醒, 唤醒线程后可以针对后续工作做出调整:(可以什么都不做,继续让线程运行 ; 可以直接break 停止循环,终止线程 ; 可以做一些收尾工作,然后break)
3.join() 线程等待,是哪个对象调用的,哪个就是"被等待" ;在哪个线程里调用的,哪个线程就是"要等待"; 一般最好加上等待时间,不然"被等待的人"迟迟不来, "等的人"岂不可怜;