1.创建线程
1.1继承Thread类
线程创建需要Thread类但是不需要import导入是为什么?
因为java.lang默认import不需要导入,java.lang中包含Thread
为什么在MyThread类中只能使用try catch 无法使用throws?
因为父类Thread run中没有throws
class Demo {
public static class MyThread extends Thread{
public void run(){
while(true){
System.out.println("hello world");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
//创建线程
t.start();
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
结果:一直循环下去hello main 和 hello world谁先谁后都有可能,这是两个线程调度顺序是随机的,无法预测谁先执行谁后执行。而且是由操作系统内核调度器去控制的咋们没法在应用程序中编写代码去控制(调度器没有提供api),唯一能做的就是给线程设置优先级,但是优先级对于操作系统来说也是仅供参考,不会严格的定量遵守。
此时我们可以去JDK的bin中的jconsole.exe查看调度
点击不安全连接
此时我们就可以看到main的状态
1.2 实现Runnable接口
MyRunnable runnable = new MyRunnable()就是一个任务要执行的逻辑,最终还是要通过Tread真正创建线程,线程通过Runnale来表示而不是直接重写Thread run来表示,使用Runnable主要是为了解耦合使代码之间的关联关系降低方便以后进行更改,我们以后写代码就是要写成低耦合(模版之间的依赖尽量少)高内聚(一个模版之内有关联得东西放在一起)。
第一个线程:
第二个线程:
main方法对应的线程就是进程至少包含的那个线程即主线程,run方法相当于回调函数,与java中的compareTo 和 compare一样也是属于回调函数(该函数不用自己去调用而是交给别人调用)
1.3使用匿名内部类
本质上是方法一为啥叫匿名内部类
这样可以少定义一些类了,如果代码是一次性的就可以使用匿名内部类
1.4使用Runable匿名内部类
使用Runnab 任务和线程是分离的,降低耦合度
1.5 引入Lambda表达式
本质上是一个匿名函数,最主要的用途就是作为回调函数
这种方法不需要重写run方法
2.Thread类
2.1Thread类及其常见方法
关于命名的应用:
2.2Tread 的几个常见属性
Daemon“是否后台线程”意思“是否守护线程”
后台线程:JVM自带的线程他们的存在不影响进程结束,如果线程要结束了他们也随之结束
前台线程:前台线程的存在能够影响到进程继续存在,这样的线程就是前台线程,相当于t1 main线程虽然结束了但是t1还在,所以进程仍然存在。
进程之间也有父子关系:IDEA本身也是一个java进程,又创建出一个新的java进程这两个进程是父子关系,线程之间不存在父子关系。
如果把线程设为后台线程无力阻止后台线程结束,比如下面的例子:
关于isAlive(),java代码中创建的Thread对象和系统的线程是一一对应的关系,但是Thead对象的生命周期和系统的线程生命周期是不同的(可能存在Thread对象还存活,但是系统中的线程已经销毁的情况)
虽然是存在3S但是打印了4个true,此处打印出来的结果可能是三个true也可能是4个true,因为不同的线程是随机调度的我们无法决定,同时主线程的四次打印和t线程的结束谁先谁后也不一定
同时补充ThreadGroup 线程组,这个是把多个线程放在一个组里面统一针对这个线程组里面所有的线程进行一些属性设置
运用:
2.3 启动一个线程 start()
java标准库/jvm提供的方法本质上是调用操作系统的API
ctrl点击start会在源代码里面发现只能看到以下这样的代码,这样的方法没有实现只是一个声明,native代表的是本地方法,实现是在JVM内部通过c++来实现的
同时每个Thread对象只能start一次,每次创建一个新的线程都给创建一个新的Thread对象(不能重复利用) 。
2.4终止一个线程
中断后面可能还会恢复此处使用终止更为合适,终止线程不会再恢复。那如何终止一个线程呢?终止就是让线程结束,我们使用的方法是让线程的入口方法执行完毕(即让run方法尽快return),线程就随之结束了。
示例:
public class Demo {
private static boolean isFinished = false;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while (!isFinished){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("thread线程结束");
});
t.start();
Thread.sleep(3000);
isFinished = true;
}
}
结果:
那我们如果把定义的变量放在里面会如何?显然是不会成功启动线程,那为什么不会成功启动呢?
此处鼠标停留会显示错误由上面一句我们可以知道,它提示我们在lambda里面的变量应该是fianl或者实际上是final,那这句话是什么意思呢?fianl修饰的变量是不可修改的,但是我们没加fianl也没真的修改,此处涉及的问题其实就是关于变量捕获的问题
lambda里面希望使用外面的变量,触发变量捕获这样的语法,lambda是回调函数执行时机是很久之后(操作系统真正创建出线程之后才会执行)很可能后续线程创建好了,当前main这里的方法都执行完了,对应的isFinished就销毁了为了解决这个问题java的做法是
把被捕获的变量给拷贝一份如下所示,拷贝给lambda里面,外面的变量是否销毁就不影响lambda里面的执行了(相比之下,c++的做法更加野蛮,c++要程序员自习保证,你lambda里访问的变量生命周期有效)c++还能让你选择,是不是要拷贝)
拷贝就意味着这样的变量就不适合进行修改,修改一方,另一方不会随之变化(本质上是两个变量)这种一边变一边不变,可能会给程序员带来更多的困惑,java大佬们就想了一个方法,压根不允许你这里进行修改,所以上述代码会一直循环下去
如果是引用类型引用类型本身不能修改(不能修改引用类型指向其他的对象),但是引用指向的对象本体是可以修改的。对象本体的生命周期是jvm垃圾回收管理的(GC),不会随着main方法执行完毕就销毁这样的说法。
示例如下
为什么把代码改成成员变量就可以修改呢?
把上述代码改成成员变量,此时不再是变量捕获语法,而是切换成“内部类访问外部类的成员”语法,lanbda本质上是函数式接口相当于一个内部类,isFinished变量本身就是外部类的成员,内部类本来就可以访问外部类的成员,成员变量生命周期也是让GC来管理,在lambda里面不担心变量生命周期失效的问题,也就不必拷贝,也就不比限制fianl之类的
lanbda是定义在new Thread之前,也是在Thread t声明之前
关于线程终止java的Thread对象提供现成的变量直接进行判定,不需要自己创建了,但是while循环里面不能使用t.isInterrupted,由上述lambda的定义可知t无法引入成员,关于java自带的终止如下所述,但是运行代码会发现代码会报错。
代码报错是因为t.interrupt()不只是修改boolean变量的值,它同时还会唤醒sleep,线程在执行的时候绝大多数的时间是休眠的当sleep被唤醒就会被上面的catch捕获到触发RuntimeException(e)抛出InterruptedException的异常
针对这个异常我们的处理方法是,使用break结束循环
如果不加上述break空着呢会怎样? t线程会无法终止,针对上述代码其实是sleep在搞鬼,正常来说调用Interrupt方法会被修改isInterruptted方法内部的标志位设为true,由于上述代码中是把sleep给唤醒了,唤醒后isInterruptted标志位给设置回false,因此在这样的情况下,如果继续执行到循环的条件判定,就会发现能够继续执行。java中的线程终止不是一个强制性的措施,不是mian让t终止,t就一定终止选择权在t自己手上(看t线程自己的代码咋写)
2.5等待一个线程--join
多个线程之间并发执行随机调度,join能够要求多个线程之间结束的先后顺序,比如在主线程中调用t.join就是让主线程等待t线程先结束。虽然可以通过sleep休眠的时间来控制线程结束的顺序,但是有的情况下这样设定并不科学。有的时候就是希望t先结束,mian就可以紧跟着结束了,此时通过设置时间的方式不一定靠谱。
在mian线程调用t.join效果,让mian线程等待t先结束,当执行到t.join此时main线程就会阻塞等待,一直等到,t 线程执行完毕,join才能继续执行,只要t 线程不结束,主线程的join就会一直一直等待下去,但是join也提供了带参数的版本,指定“超时时间”等待的最大时间
带有超时时间的等待,才是更科学的做法,计算机中尤其是和网络通信相关的逻辑,一般都是需要超时时间
2.6 休眠当前线程
休眠一个线程使用sleep,有一点要记住因为线程的调度是不可控,所以这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间。写了sleep(1000)实际上可能比1000多一点,代码调用sleep,相等于让当前线程让出CPU的资源,后续时间到的时候,需要操作系统内核把这个线程重新调到CPU上才能继续执行,时间到了意味着允许调度了,而不是立即执行了,对于操作系统是毫秒级别的时间,同时还有sleep(0)这样的用法,写了sleep(0)意味着让当前的线程立即放弃CPU资源,等待操作系统重新调度,把cpu让出来给别人更多执行的机会
2.7线程状态