文章目录
- 一、创建和启动线程
- (1)概述
- (2)方式1:继承Thread类
- 1、使用步骤
- 2、举例
- 2.1 案例1
- 2.2 案例2
- 2.3 案例3
- 3、两个问题
- 3.1 问题1
- 3.2 问题2
- 4、代码及总结
- 二、练习
- (1)方式一
- (2)方式二
一、创建和启动线程
(1)概述
- Java语言的JVM允许程序运行多个线程,使用
java.lang.Thread
类代表线程,所有的线程对象都必须是Thread类或其子类的实例。 (创建一个线程可以理解为创建这个类的一个对象,一个对象对应一个线程) - Thread类的特性
- 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,因此把run()方法体称为
线程执行体
。 - 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
- 要想实现多线程,必须在主线程中创建新的线程对象。
- 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,因此把run()方法体称为
(2)方式1:继承Thread类
1、使用步骤
Java通过继承Thread类来创建并启动多线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
如下:
举例代码如下:
package com.atguigu.thread;
//自定义线程类
public class MyThread extends Thread {//定义指定线程名称的构造方法public MyThread(String name) {//调用父类的String参数的构造方法,指定线程的名称super(name);}/*** 重写run方法,完成该线程执行的逻辑*/@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(getName()+":正在执行!"+i);}}
}
测试类:
package com.atguigu.thread;public class TestMyThread {public static void main(String[] args) {//创建自定义线程对象1MyThread mt1 = new MyThread("子线程1");//开启子线程1mt1.start();//创建自定义线程对象2MyThread mt2 = new MyThread("子线程2");//开启子线程2mt2.start();//在主方法中执行for循环for (int i = 0; i < 10; i++) {System.out.println("main线程!"+i);}}
}
注意:
- 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
- run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
- 想要启动多线程,必须调用start方法。
- 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常“
IllegalThreadStateException
”。
2、举例
2.1 案例1
🌋描述:创建一个分线程1,用于遍历100以内的偶数。
🚗分析
<1> 创建一个继承于Thread类的子类。
public class EvenNumberTest {}//1.创建一个继承于Thread类的子类
class PrintNumber extends Thread{}
<2> 重写Thread类的run() —>将此线程要执行的操作,声明在此方法体中。
public class EvenNumberTest {}//1.创建一个继承于Thread类的子类
class PrintNumber extends Thread{//2.重写Thread类的run() --->将此线程要执行的操作,声明在此方法体中@Overridepublic void run() {for (int i = 1; i <= 100; i++) {if(i%2==0){System.out.println(i);}}}
}
<3> 创建当前Thread的子类的对象。
public class EvenNumberTest {public static void main(String[] args) {//3.创建当前Thread的子类的对象PrintNumber t1=new PrintNumber();}
}
<4>通过对象调用start()
: 1.启动线程 2.调用当前线程的run()
。
public class EvenNumberTest {public static void main(String[] args) {//3.创建当前Thread的子类的对象PrintNumber t1=new PrintNumber();//4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()t1.start();}
}//1.创建一个继承于Thread类的子类
class PrintNumber extends Thread{//2.重写Thread类的run() --->将此线程要执行的操作,声明在此方法体中@Overridepublic void run() {for (int i = 1; i <= 100; i++) {if(i%2==0){System.out.println(i);}}}
}
当前PrintNumber
类里面没有重写start方法,这就意味着调用的是父类Thread
里面的start方法。
这个start()
方法有什么作用呢?
所以start方法有两个作用:1.启动线程 2.调用当前线程的
run()
方法。
在刚才的代码中,t1
调用start()
方法,这里的start()
方法是父类中的。
此时调用了当前线程的run()
方法,这个方法在Thread
类里面定义的,并且在子类PrintNumber
里面被重写了,所以父类中的方法被覆盖了,此时调用的就是子类的run()方法。
整体来看,调用t1.start()
,子类中的run()
方法就被执行了。
🌱整体代码
package yuyi01.thread;/*** ClassName: EvenNumberTest* Package: yuyi01.thread* Description:* 创建一个分线程1,用于遍历100以内的偶数* @Author 雨翼轻尘* @Create 2024/1/19 0019 11:57*/
public class EvenNumberTest {public static void main(String[] args) {//3.创建当前Thread的子类的对象PrintNumber t1=new PrintNumber();//4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()t1.start();}
}//1.创建一个继承于Thread类的子类
class PrintNumber extends Thread{//2.重写Thread类的run() --->将此线程要执行的操作,声明在此方法体中@Overridepublic void run() {for (int i = 1; i <= 100; i++) {if(i%2==0){System.out.println(i);}}}
}
🍺输出结果(部分)
2.2 案例2
刚才并没有感觉到线程的存在啊,现在修改一下代码:
package yuyi01.thread;public class EvenNumberTest {public static void main(String[] args) {//3.创建当前Thread的子类的对象PrintNumber t1=new PrintNumber();//4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()t1.start();//main()方法所在的线程执行的操作:for (int i = 1; i <= 100; i++) {if(i%2==0){System.out.println(i+"main()做的事情");}}}
}//1.创建一个继承于Thread类的子类
class PrintNumber extends Thread{//2.重写Thread类的run() --->将此线程要执行的操作,声明在此方法体中@Overridepublic void run() {for (int i = 1; i <= 100; i++) {if(i%2==0){System.out.println(i);}}}
}
此时有两个线程:
再次执行代码:(部分)
此时它们没有交互,如果数字足够多,会有交叉的数据出现,就是交替执行。
这说明两个线程都在执行,前面的执行一下,后面的执行一下。谁先执行都有可能。这里就体现出了两个不同的线程。
2.3 案例3
其实这里还有一种方式可以看出来是两个线程在交替执行,需要用到一个方法,后面再说,这里先用一下。
Thread
有个方法叫做currentThread()
,获取当前执行的线程;然后它又有一个getName()
方法,即获取线程的名字。
即:
Thread.currentThread().getName()
🌱代码
package yuyi01.thread;/*** ClassName: EvenNumberTest* Package: yuyi01.thread* Description:* 创建一个分线程1,用于遍历100以内的偶数* @Author 雨翼轻尘* @Create 2024/1/19 0019 11:57*/
public class EvenNumberTest {public static void main(String[] args) {//3.创建当前Thread的子类的对象PrintNumber t1=new PrintNumber();//4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()t1.start();//main()方法所在的线程执行的操作:for (int i = 1; i <= 10000; i++) {if(i%2==0){System.out.println(Thread.currentThread().getName() +":" +i+"**********");}}}
}//1.创建一个继承于Thread类的子类
class PrintNumber extends Thread{//2.重写Thread类的run() --->将此线程要执行的操作,声明在此方法体中@Overridepublic void run() {for (int i = 1; i <= 10000; i++) {if(i%2==0){System.out.println(Thread.currentThread().getName() +":" +i);}}}
}
🍺输出结果(部分)
可以看到,现在的执行结果,前面都有自己线程的名字。
t1线程也有自己的线程名字,默认叫Thread-0
。因为此时new的是当前类PrintNumber的对象,默认的是调用super()
,是父类Thread里面的构造器,如下:
所以,当我们不给线程起名字的时候,默认第一个线程就是Thread-0
,第二个线程就是Thread-1
。
注意下面:
3、两个问题
3.1 问题1
☕问题1
启动线程,包括调用上面的run方法都用的是start()
。
那么能否使用t1.run()
来替换t1.start()
的调用,实现分线程的创建和调用?
🍰分析
比如现在这样写:
运行看一下:
可以看到不仅没有交替执行,而且还都是main,没有Thread-0了。它认为run()
方法是主线程main做的。
此时就是主线程造了一个t1对象,然后就调用了run()
这个普通的方法,执行完之后再执行循环输出。只有一条线程了。这就不是多线程问题了。
再举个例子,看看下面的代码是不是多线程:
public class SingleThread {public void method1(String str) {System.out.println(str);}public void method2(String str) {method1(str);}public static void main(String[] args) { //main线程SingleThread s = new SingleThread();s.method2("hello!");}}
首先main方法进去,造了一个当前类的一个对象s,这个对象s调用method2(),method2()里面调用method1()。这里是单线程。
判断单线程还是多线程,就看能不能拿一条线穿起来。
比如此时就可以拿一条线穿起来,只有一条执行路径,那就是一个线程,即单线程
。如下:
之前的那个例子,也是类似,就是一个线程。如下:(用鼠标手画的有点不堪入目哈哈)
而start()
不一样,一个作用是启动线程,然后是调用run方法。
若是仅仅调用run方法的话,线程并没有启动,相当于还是只有主线程。
所以,不能使用t1.run()
来替换t1.start()
的调用。
3.2 问题2
☕问题2
比如现在启动了一个线程,调用一下start()
,打印了1000以内的偶数。代码如下:
package yuyi01.thread;public class EvenNumberTest {public static void main(String[] args) {//3.创建当前Thread的子类的对象PrintNumber t1=new PrintNumber();//4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()t1.start();//main()方法所在的线程执行的操作:for (int i = 1; i <= 1000; i++) {if(i%2==0){System.out.println(Thread.currentThread().getName() +":" +i+"**********");}}}
}//1.创建一个继承于Thread类的子类
class PrintNumber extends Thread{//2.重写Thread类的run() --->将此线程要执行的操作,声明在此方法体中@Overridepublic void run() {for (int i = 1; i <= 1000; i++) {if(i%2==0){System.out.println(Thread.currentThread().getName() +":" +i);}}}
}
现在需要再创建一个分线程,同样要去遍历一下1000以内的偶数。
那么此时要怎样去做呢?
🍰分析
直接用t1再次调用start()
方法可以吗?如下:
运行结果:
可以看到,出现了IllegalThreadStateException
的异常。
当我们首次调用start()
方法的时候,threadStatus
的值是0 ,就没有抛异常。当我们再次调用start()
之后,threadStatus
状态就不是0了,便会抛异常。
所以,线程不能start()
多次。
不能让已经start()的线程,再次执行start()操作,否则报
IllegalThreadStateException
非法线程状态的异常。
🎲解决方案
既然上面的方法不行,那么究竟应该怎么做呢?
需要重新创建一个对象,类PrintNumber
不需要重新创建了,因为要做的事情一样,都是遍历1000以内的偶数。
所以现在只需要再建立一个对象,然后用这个新的对象去调用start()
方法即可。
如下:
🌱整体代码
package yuyi01.thread;public class EvenNumberTest {public static void main(String[] args) {//3.创建当前Thread的子类的对象PrintNumber t1=new PrintNumber();//4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()t1.start();/** 问题2:再提供一个分线程,用于100以内偶数的遍历** 注意:不能让已经start()的线程,再次执行start()操作,否则报IllegalThreadStateException非法线程状态的异常。* *///t1.start();PrintNumber t2=new PrintNumber();t2.start();//main()方法所在的线程执行的操作:for (int i = 1; i <= 1000; i++) {if(i%2==0){System.out.println(Thread.currentThread().getName() +":" +i+"**********");}}}
}//1.创建一个继承于Thread类的子类
class PrintNumber extends Thread{//2.重写Thread类的run() --->将此线程要执行的操作,声明在此方法体中@Overridepublic void run() {for (int i = 1; i <= 1000; i++) {if(i%2==0){System.out.println(Thread.currentThread().getName() +":" +i);}}}
}
🍺输出结果(部分)
输出默认的线程名也很好理解,造的第二个对象就是Thread-1
。
从上面的输出结果可以看到,三个线程交替执行。
4、代码及总结
线程的创建方式一:继承Thread类
步骤:
① 创建一个继承于Thread类的子类
② 重写Thread类的run() —>将此线程要执行的操作,声明在此方法体中
③ 创建当前Thread的子类的对象
④ 通过对象调用start()
: 1.启动线程 2.调用当前线程的run()
方法
start()是父类Thread的方法。
🌱整体代码总结
package yuyi01.thread;/*** ClassName: EvenNumberTest* Package: yuyi01.thread* Description:* 创建一个分线程1,用于遍历100以内的偶数* @Author 雨翼轻尘* @Create 2024/1/19 0019 11:57*/
public class EvenNumberTest {public static void main(String[] args) {//3.创建当前Thread的子类的对象PrintNumber t1=new PrintNumber();//4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()t1.start();/*问题1:能否使用t1.run()来替换t1.start()的调用,实现分线程的创建和调用? 不能*///t1.run();/** 问题2:再提供一个分线程,用于100以内偶数的遍历** 注意:不能让已经start()的线程,再次执行start()操作,否则报IllegalThreadStateException非法线程状态的异常。* *///t1.start();PrintNumber t2=new PrintNumber();t2.start();//main()方法所在的线程执行的操作:for (int i = 1; i <= 1000; i++) {if(i%2==0){System.out.println(Thread.currentThread().getName() +":" +i+"**********");}}}
}//1.创建一个继承于Thread类的子类
class PrintNumber extends Thread{//2.重写Thread类的run() --->将此线程要执行的操作,声明在此方法体中@Overridepublic void run() {for (int i = 1; i <= 1000; i++) {if(i%2==0){System.out.println(Thread.currentThread().getName() +":" +i);}}}
}
🍺输出结果(部分)
二、练习
🌋题目描述
练习:创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数。
(1)方式一
🍰分析
【回顾】
线程的创建方式一:继承Thread类
步骤:
① 创建一个继承于Thread类的子类
② 重写Thread类的run() —>将此线程要执行的操作,声明在此方法体中
③ 创建当前Thread的子类的对象
④ 通过对象调用start()
: 1.启动线程 2.调用当前线程的run()
方法
【分析】
现在是两个线程,做的事情不一样。run方法里面执行的方法体就不一样了。
两个分线程,这里就不要使用主线程了。
既然两个线程指定的事情不一样,那就写两个run,这就意味着要声明两个类了(要是两个线程做的事情一样,那就只用写一个类,就像上面的案例)。
🌱代码(方式一)
package yuyi01.thread.exer1;/*** ClassName: PrintNumberTest* Package: yuyi01.thread.exer1* Description:** @Author 雨翼轻尘* @Create 2024/1/19 0019 22:38*/
public class PrintNumberTest {public static void main(String[] args) {//方式一EvenNumberPrint t1=new EvenNumberPrint();OddNumberPrint t2=new OddNumberPrint();t1.start();t2.start();}
}//打印偶数
class EvenNumberPrint extends Thread {@Overridepublic void run() {for (int i = 1; i <= 100; i++) {if (i % 2 == 0) {System.out.println(Thread.currentThread().getName() + ":"+i);}}}
}//打印奇数
class OddNumberPrint extends Thread {@Overridepublic void run() {for (int i = 1; i <= 100; i++) {if (i % 2 != 0) {System.out.println(Thread.currentThread().getName() + "-->"+i);}}}
}
🍺输出结果(部分)
(2)方式二
🍰分析
上面的方式一是一种标准写法,之前还讲过匿名子类的方式,需要提供Thread
子类的对象。
所以直接new一个Thread()
,因为是匿名子类,后面直接加一对大括号,这样对象就造好了,如下:
new Thread(){}
这时可以声明为t1,然后通过t1去调start()
,如下:
Thread t1=new Thread(){};
t1.start();
当然,我们可以不声明它为t1,直接在后面.start()
,如下:
new Thread(){}.start();
在大括号里面,可以将run()
方法重写一下。如下:
//方式二
new Thread(){//打印偶数public void run() {for (int i = 1; i <= 100; i++) {if (i % 2 == 0) {System.out.println(Thread.currentThread().getName() + ":"+i);}}}
}.start();new Thread(){//打印奇数public void run() {for (int i = 1; i <= 100; i++) {if (i % 2 != 0) {System.out.println(Thread.currentThread().getName() + "-->"+i);}}}
}.start();
方法二可以叫做,创建Thread类的匿名子类的匿名对象,造完对象之后还顺便将start()方法给调用了。
🌱代码(方式二)
package yuyi01.thread.exer1;/*** ClassName: PrintNumberTest* Package: yuyi01.thread.exer1* Description:** @Author 雨翼轻尘* @Create 2024/1/19 0019 22:38*/
public class PrintNumberTest {public static void main(String[] args) {//方式二new Thread(){//打印偶数public void run() {for (int i = 1; i <= 100; i++) {if (i % 2 == 0) {System.out.println(Thread.currentThread().getName() + ":"+i);}}}}.start();new Thread(){//打印奇数public void run() {for (int i = 1; i <= 100; i++) {if (i % 2 != 0) {System.out.println(Thread.currentThread().getName() + "-->"+i);}}}}.start();}
}
🍺输出结果(部分)
可以看到,方法一与方法二的效果一样。
若是以后,有一个临时需求,造一个线程做个事情,可能都不会按照方式一那么正规地写,都是采用匿名的方式做一下。