一:创建线程的方式
(1)继承 Thread 类
继承 Thread 类来创建一个线程类,然后重写run方法
重写父类这里的run()就相当于线程的入口方法,线程具体跑起来之后,要做啥事,都是通过这个run入口来描述
Thread这里是直接把要完成的工作放到Thread类的run()方法里
class MyThread extends Thread{ //继承Thread类
@Override
public void run() { //重写父类Thread中的run()方法,这个run方法表示线程执行后需要干什么
System.out.println("Hello World!!!"); //线程执行后打印"Hello World!!!"
}
}
public class Demo1 {
public static void main(String[] args) {
MyThread myThread = new MyThread(); //实例化;创建一个MyThread类型的对象
myThread.start(); //启动线程
}
}
(2)实现Runnable接口
通过Runnable接口的实现来创建线程,同样重写run方法
Runnable分开来,把要完成的工作放到Runnable中,再让Runnable和Thread配合
除了创建Runnable对象,还要创建Thread对象,然后都实例化,将Runnable引用变量作为构造方法的参数传给Thread然后由Thread的引用变量调用方法
//使用Runable接口实现创建线程
class MyRunnable implements Runnable{ //我创建一个类MyRunnable来实现Runnable接口
@Override
public void run() {
System.out.println("Hello World!!!");
}
}
public class Demo3 {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
//还需要搞个Thread对象,因为只有myRunnable是没有办法直接运行start()的
//将myRunnable作为构造方法的参数传到Thread
Thread thread = new Thread(myRunnable);
thread.start();
}
}
(3)匿名内部类创建Thread子类对象
继承Thread,重写run,基于匿名内部类
1.匿名内部类
public class Demo {
public static void main(String[] args) {
类 引用变量 = new 对象() {
@Override
重写方法
};
}
}
2.代码示例
public class Demo4 {
public static void main(String[] args) {
//创建一个子类,子类继承Thread,同时,这个子类没有名字,所以称为匿名内部类
//一般来说,我们的类都是独立的,但是这个属于内部类,它可以存在于一个类的内部,就像这个匿名子类创建于Demo4类之中
Thread t1 = new Thread() {
@Override
public void run() { //在匿名子类中重写run方法
System.out.println("使用匿名类创建 Thread 子类对象");
}
};
t1.start(); //启动线程
}
}
(4)匿名内部类创建 Runnable 子类对象
public class Demo5 {
public static void main(String[] args) {
//此处的new Runnable()一整段都是在创建Runnable子类,作为参数通过构造方法传给Thread
Thread t2 = new Thread(new Runnable() {
@Override
public void run() { //重写run方法
System.out.println("使用匿名类创建 Runnable 子类对象");
}
}); //表示Thread构造方法的结束
t2.start(); //启动线程
}
}
(5)lambda 表达式创建 Runnable 子类对象
1.lambada表达式
本质上是一个“匿名函数”,这样的匿名函数,主要就可以作为回调函数来使用
回调函数:不需要主动调用,而是在合适的时机自动被调用
2.lambada语法
->:可理解为“被用于”的意思
3.代码示例
★不用重写run方法!
★把要执行的逻辑写在lambda里面!
public class Demo6 {
public static void main(String[] args) {
//Thread里的内括号是可以写形参的
Thread t4 = new Thread(() -> {
System.out.println("使用匿名类创建 Thread 子类对象");
});
t4.start(); //启动线程
}
}
二:认识Thread类的start()和run()
(1)start()和run()
start():创建新线程
run():无法创建新线程
★前提:我们需要知道,每次当我们点击运行程序的时候,就会先创建出一个Java进程,这个进程就包括了至少一个线程,而这个线程也叫做主线程,也就是负责执行main方法的线程
(2)通过println打印来简单理解start()和run()
//用Thread类创建对象写一个简单的Hello World
//Thread来自于java.lang这个包,因此无需import即可默认使用
class MyThread extends Thread{ //我创建一个MyThread类然后继承java标准库中现场的Thread类
@Override
public void run() { //重写父类Thread中的run()方法
System.out.println("Hello World!!!");
}
}
public class Demo1 {
public static void main(String[] args) {
MyThread myThread = new MyThread(); //创建一个MyThread类型的对象,对象名(引用变量)叫myThread
myThread.start();
myThread.run();
}
}
①重写父类这里的run()就相当于线程的入口方法,线程具体跑起来之后,要做啥事,都是通过这个run入口来描述
(如图所示,我的线程运行起来后就是打印“Hello World!!!”)
②这里的由引用变量myThread调用的方法start()就是在创建新的线程
这个start()操作就会在底层调用操作系统提供的“创建线程”的API,同时就会在操作系统内核里创建出对应的PCB结构,并且加入到对应的链表中
(管理进程要先描述后组织,在组织的时候说过创建进程是如何创建的!!!)
此时,这个新创建出来的线程就会参与到CPU的调度中,这个线程接下来要执行的工作,就是刚刚上面重写的run方法~
这个时候就有两个线程,一个主线程,一个由start()创建的新线程!
运行后,就会打印“Hello World!!!”
③这里的由引用变量myThread调用的方法run()实际就是个入口普通方法
这个run()操作既没有在底层调用操作系统提供的“创建线程”的API,也没有创建出真正的线程来,因此,自始至终只有主线程在执行代码
此时,我们这里仍然只有一个主线程,并没有创建出新的线程!
运行后,也会打印“Hello World!!!”
(3)通过while循环来更深理解start()和run()
class MyThread1 extends Thread{ //我创建一个MyThread1类然后继承java标准库中现场的Thread类
@Override
public void run() { //重写父类Thread中的run()方法
while (true) {
System.out.println("Hello Thread!!!");
}
}
}
public class Demo2 {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1(); //创建一个MyThread1类型的对象,对象名(引用变量)叫myThread1
myThread1.start();
myThread1.run();
while (true) {
System.out.println("Hello main!!!");
}
}
}
①运行myThread1.start()时的结果:
★现象:这个时候main方法里的while循环和run方法里的while循环这两个循环是同时执行的,都在交替式的打印!
★原因:因为这里有两个线程,一个主线程执行main里的while循环,一个start创建的新线程执行重写run方法里的while循环,两个线程都在分别执行自己的循环,这两个线程都能参与到CPU的调度中!
★执行流程:从main方法开始
先执行第一条语句 MyThread1 myThread1 = new MyThread1();
再执行第二条语句 myThread1.start();(而这个start会创建出新的线程,这个新的线程就会来执行重写run方法里的while循环代码;此时原来的主线程则继续执行第三条语句)
最后执行第三条语句
while (true) {
System.out.println("Hello main!!!");
}
★这个时候,主线程打印着"Hello main",新线程打印着“Hello Thread”,两个线程是并发执行状态!!!
②运行myThread1.run()时的结果:
★现象:这个时候只有run方法里的while循环在打印!
★原因:因为此时只有一个主线程,run()并没有像start()一样创建出新线程,所以当主线程执行到myThread1.run()时,它要去执行重写run方法里的while循环代码,又因为这个while循环是无限循环,所以无法停下,自然也就执行不到main方法里的while循环!
(4)结论
①每个线程,都是一个独立执行流
②每个线程,都可以执行一段代码
③多个线程,它们之间是并发关系(并不是执行完这个才能执行那个,而是一起执行)
三:深入了解Thread属性和方法
(1)Thread类的构造方法
①Thread() //无参;创建线程对象
②Thread(Runnable target) //使用 Runnable 对象作为参数;创建线程对象
③Thread(String name) //创建线程对象,并命名
④Thread(Runnable target, String name) //使用 Runnable 对象作为参数,并命名
1.举例子
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
★用④作为例子写一段代码更好的理解:
public class Demo7 {
public static void main(String[] args) {
//使用了Thread(Runnable target, String name)这个构造方法
Thread t = new Thread(() -> {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "myThread"); //自定义线程名字叫myThread
t.start(); //启动线程
}
}
2.分析代码
①通过jconsole我们可以看到自己创建且自定义名字的线程myThread
②但是!!!main线程也就是主线程没有了!!!
原因:main线程往下执行,当执行到t.start()时,myThread线程就会去执行lambda里的逻辑,也就是去执行while(true),当然,因为是无限循环,所以线程无法停下!但此时main线程已经执行完毕,main线程就结束了,因此我们就看不到main线程
③结论:
线程是通过start()创建
主线程的入口执行与完毕靠的是main
其他线程的入口执行与完毕靠的是run/lambda
(如果while循环执行完毕,则myThread线程也就消失了)
(2)Thread的常见属性和普通方法
1.ID
①概念
ID 是由JVM给线程的唯一标识,相当于线程身份证,不同线程不会重复
②获取ID方法
引用变量.getId();
2.名称
①概念
名称是各种调试工具用到
②获取名称方法
引用变量.getName();
3.状态
①概念
状态表示线程当前所处的一个情况(在进程调度的时候说过)
②获取状态方法
引用变量.getState();
4.优先级
①概念
优先级高的线程理论上来说更容易被调度到(在进程调度的时候说过)
★一般我们会使用默认的优先级
因为设置/获取优先级的作用并不大,因为进程的调度主要是在内核上的,而且系统调度的速度是极快的,因此我们感知不到!
②获取优先级方法
引用变量.getPriority();
5.后台线程
①概念
一:后台线程也称为守护线程
二:后台线程不影响进程的结束
三:当一个进程中所有的前台线程都执行完成,退出进程;即使还存在一些后台线程没有执行完,也会跟着进程一起退出
②引入前台线程
一:前台线程就是会影响进程结束,如果前台线程没有执行完,进程是不会结束的
二:只要有任何前台还在运行,程序就不会终止。比如,执行main()的就是一个前台线程
③设置后台线程
创建的线程默认是前台线程,可以通过设置后台线程
引用变量.setDaemon(true);
④判断是否是后台线程
引用变量.isDaemon();
⑤代码理解
public class Demo8 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (true){
System.out.println("Hello Thread");
}
},"myThread"); //创建的线程名字叫myThread
//将myThread设置成后台线程,也就是说,只要main线程执行完t.start()退出,myThread也会跟着退出,不再无限循环打印
t.setDaemon(true);
t.start();
}
}
6.存活
①概念
线程是否存活
简单的理解为 run 方法运行是否结束了
②区分Thread对象与线程的生命周期
★Thread对象与线程并不是“同生共死”!
一:从创建角度来说
先有Thread对象后有线程
一般是Thread对象先创建好
直到手动调用start方法,内核才真正创建出线程
二:从消亡角度来说
可能是Thread对象先结束
可能是线程先结束,因为此时已经把run执行完了
③判断线程还是否存活
引用变量.isAlive();
四:启动一个线程-start()
🔺前面已经有很详细的介绍了,这里不过多赘述
1.start的作用
start()就是在创建并启动新的线程
通过start来执行父类重写的run方法,执行线程具体要干什么
2.深入理解
这个start()操作就会在底层调用操作系统提供的“创建线程”的API,同时就会在操作系统内核里创建出对应的PCB结构,并且加入到对应的链表中
五:获取当前线程引用
1.方法
public static Thread currentThread(); //返回当前线程对象的引用
2.代码
public class Demo12 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
//Thread.currentThread()就相当于hlizoo
System.out.println(Thread.currentThread().getName());
},"hlizoo");
t.start();
}
}
六:终止一个线程
1.概念
终止线程:想办法让这个run方法尽快或立刻执行完毕
★一般来说,不会出现run还没执行完,线程就突然没了的情况;除非你强行拔电源
2.方法一
手动设置标志位,通过标志位让run方法尽快结束
★必须将标志位设置为成员变量(类内方法外)
①代码举例:
public class Demo9 {
public static boolean isQuit = false; //设置成员变量(类内方法外)isQuit作为标志位
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!isQuit){ //取反逻辑
System.out.println("Hello Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//启动线程
t.start();
//主线程执行sleep逻辑之后,想让t线程结束
Thread.sleep(3000);
//将isQuit改为true,取反后就是false,让t结束
isQuit=true;
System.out.println("t线程终止");
}
}
②分析代码:
观察代码运行结果可以看出,执行t.start()后,t线程去执行run方法,由于主线程sleep睡眠了3秒,这个时候t线程正在执行while循环,“Hello Thread”得以打印,但是3秒之后,标志位isQuit改为true,取反后就是false,就会把while循环停下,此时run方法执行完毕,主线程继续执行,就打印了“t线程终止”!!!
③为什么必须将标志位设置为成员变量:
★当我把标志位设置在main方法里面会报错
①理论:因为lambda引入了“变量捕获”机制,lambda内部看起来是直接访问外部的变量,其实本质上是把外部的变量给复制一份到lambda里面,为了解决生命周期问题
②原因:我们都知道lambda是一个回调函数,它的执行时机并非立即执行,而是更靠后,是在后续线程被创建好了之后,在这个新线程内部被执行;这就会导致当后续真正执行lambda的时候,局部变量isQuit或许已经随着main方法的执行完毕被销毁了,但此时线程可能还在继续执行,让线程去访问一个已经被销毁的变量显然是不合适的
③变量捕获的限制:要求捕获的变量至少是final或者事实上得是final;如果这个变量想要修改,就不能进行变量捕获!!!
当加了个final,就没有报错了!!!
★虽然没有报错,但是你在接下来也无法改变isQuit的值,因为是final修饰了,这个标识符也就没法让线程终止了
3.方法二
利用Thread给我们提供现成的标志位
Thread.currentThread().isInterrupted()
引用变量.Interrupt()表示将标志位设为true
currentThread()的作用是获取当前线程对象,在哪个线程调用这个方法,就能够获取哪个线程的引用
例:Thread.currentThread()就是能够获取到线程t,也可以说Thread.currentThread就是t
isInterrupted()是Thread内部提供的一个标志位(boolean)
true表示线程应该要结束
false表示线程还不用结束
①代码举例:
public class Demo10 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
while(!Thread.currentThread().isInterrupted()){
System.out.println("Hello Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(3000);
t.interrupt(); //将标志位设为true,此时while条件取反后就是false从而终止t线程
}
}
②分析代码:
①现象:线程并没有结束,而是持续运行,并且还抛出sleep异常
②原因:t线程正在while循环里的sleep休眠时,被interrupt唤醒,并且唤醒后自动清除前面的标志位,也就导致了isInterrupted()标志位无效,从而不会使线程终止!!!
而对于手动设置标志位,是不会唤醒sleep的!!!
③结论:因此,当线程正在sleep过程中,其他线程调用interrupt方法,就会强制使sleep抛出异常,sleep就被立即唤醒,并且sleep在被唤醒的过程中,会自动清除标志位
例:你设定了sleep1000ms,虽然才过去10ms,没到1000ms,但也会被立即唤醒!
④自动清除标志位的好处:给程序员留下更多的操作空间
操作①-立即结束线程:catch中加个break
操作②-继续做点别的事,一会再结束线程:先在catch中执行别的逻辑再break
操作③-忽略唤醒请求,继续执行:不加break
七:休眠当前线程-sleep()
(1)sleep()的作用
使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性
(2)了解不同操作系统下的sleep
①在Windows上的Sleep休眠单位是毫秒(ms)
②在Linux上的sleep休眠单位是秒(s)
对于Java程序员,会用sleep就行,JVM会根据你用的操作系统来帮你!!!
(3)根据代码理解sleep
①sleep怎么用
★sleep是Thread类的静态方法,直接使用 类名.静态方法名() 即可使用
②sleep抛异常细节
★sleep需要抛出异常!
①重写父类Thread中的run()方法中,必须要使用try-catch抛出异常,否则sleep报错
②main()方法里的sleep就可以直接throws异常
③代码图
class MyThread1 extends Thread{ //我创建一个MyThread1类然后继承java标准库中现场的Thread类
@Override
public void run() { //重写父类Thread中的run()方法
while (true) {
System.out.println("Hello Thread!!!");
try { //使用try-catch抛出异常,否则sleep报错
// 注意:这里只能用try-catch,不能用throws,因为此处是方法重写,对于父类run()来说,就没有throws异常这种设定
Thread.sleep(1000); //sleep是Thread类的静态方法,直接使用类名.静态方法名()即可使用
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo2 {
public static void main(String[] args) throws InterruptedException { //main方法直接throws异常即可
MyThread1 myThread1 = new MyThread1(); //创建一个MyThread1类型的对象,对象名(引用变量)叫myThread1
myThread1.start();
while (true) {
System.out.println("Hello main!!!");
Thread.sleep(1000); //这个sleep就可以直接throws异常
}
}
}
④运行结果
1.有了sleep之后确实比较有规律和节奏的交替打印
2.但是这里并非是一个很严格的交替(*可以看到箭头部分有两个Hello Thread*)
原因:这两个线程在进行sleep之后,就会进入堵塞状态,当时间到,系统就会唤醒这俩线程,并且恢复这两个线程的调度,但是当这两个线程都唤醒之后,谁先调度,谁后调度,都可以视为“随机”
3.系统在进行多个调度的时候,并没有一个明确的顺序,而是按照这种“随机”的方式进行调度,而这种"随机"调度的过程,称为“抢占式执行"
八:等待一个线程-join()
1.概念
等待线程,其实就是规划线程结束的先后顺序
2.方法
public void join() //等待线程结束(死等)
public void join(long millis) //等待线程结束,最大等待毫秒时间(推荐使用)
★希望谁先结束,就 先结束线程.join() , 然后用另一个线程调用即可
例如:有A、B两个线程,希望B先结束A后结束,此时就用A线程调用B.join()方法
情况一:如果A执行到B.join()时,此时B线程如果没执行完(所谓的执行完就是run方法执行完毕),A线程就会进入阻塞状态(阻塞:代码停止,不继续往下执行),相当于给B留下执行时间,B执行完毕后,A再从阻塞状态恢复回来,并且往后继续执行
情况二:如果A执行到B.join()时,B已经执行完了,A就不必阻塞,继续往下执行
3.代码举例
①正常情况:
B线程循环5次,A线程循环3次,A线程比B线程先结束
public class Demo11 {
public static void main(String[] args) {
Thread B = new Thread(() ->{ //B线程
for (int i = 0; i < 5; i++) {
System.out.println("Hello B");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("B结束了");
});
Thread A = new Thread(() ->{ //A线程
for (int i = 0; i < 3; i++) {
System.out.println("Hello A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("A结束了");
});
B.start(); //启动B线程
A.start(); //启动A线程
}
}
②join方法:
我们利用在A线程里面调用B.join方法使B先结束,A后结束
★join同样需要抛出异常
public class Demo11 {
public static void main(String[] args) {
Thread B = new Thread(() ->{ //B线程
for (int i = 0; i < 5; i++) {
System.out.println("Hello B");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("B结束了");
});
Thread A = new Thread(() ->{ //A线程
for (int i = 0; i < 3; i++) {
System.out.println("Hello A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
B.join(); //如果B线程此时没有执行完毕,就会使A线程停止不动,不再往下执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("A结束了");
});
B.start(); //启动B线程
A.start(); //启动A线程
}
}
4.join与sleep共同特点
join和sleep一样,都需要抛出异常
join和sleep一样,产生阻塞后都可以被interrupt唤醒,唤醒后自动清除标志位