引言:当我们了解进程和线程之后,那就让我们来认识一下什么是多线程吧。
一、创造多线程
1.用Tread类创造线程
方式如下:
public class Mythread extends Thread{
public void run(){
while(true){
System.out.println("hello thread");
}
}
}
在类中继承我们的Tread类,重写我们的run方法就能再次创建一个线程了。
⭐️重点解析:
①创造了一个类之后继承Tread类之后,重写run方法是多态的体现
②然而这个run方法也是这个线程入口标志。
🔆这个入口的标志就类似于主方法main()的入口标志。要运行一个java程序,必须有一个进程,进程中最少有一个线程吧。所以在java程序中,main函数也就是一个(主)线程,它是jvm自动自己创造出来的线程。所以主函数线程的入口就是main()方法,run()方法就也是线程的入口啦~
2.调用/启动线程
(1)run
当我们创造出Tread类的时候就有了一个线程,当我们new出这一对象这有了这一线程。那我们应该如何启动这个线程呢?
有人会说直接调用run方法就好了啊。
很好,如果你只调用run方法,只能证明你学过javase。但这并不是真正启动线程的方式!上面已经提及到run方法只是一个入口(描述的是一个任务)当你调用run方法时只能按代码顺序依次执行,当你在run方法加入一个死循环时就再也执行不到main线程里的指令了。
(2)start
所以我们该怎么样才能同时执行到这两个线程或者多个线程呢?
这时候就要用到这个start()方法了。当我们调用到t.start()这个方法时,它就能启动t的这个线程,同时执行下面的主线程。
public static void main(String[] args) {
Mythread t1 = new Mythread();
t1.start();
while(true){
System.out.println("hello main");
}
}
public class Mythread extends Thread{
public void run(){
while(true){
System.out.println("hello thread");
}
}
}
⚠️这里打印的顺序都是随机的,并不是固定的。这是因为多个线程之间的调度顺序是随机的,所以不一定这次执行这个线程下次执行另一个线程。每次每个线程都是随机执行的。
(3)run和start区别
run:描述的只是一个任务,一个入口
start:调用的系统的API,系统内核创造线程。
二、Runnable
1.用类实现Runnable接口
上面介绍了继承Tread类创建线程,那我们就来学第二种创建线程的方式实现Runnable接口。
class MyRunnable implements Runnable {
public void run() {
while (true) {
System.out.println("Hello from MyRunnable");
}
}
可以看到和Tread类实现方式大差不差。主要区别是通过实现接口的方式来继承类。这样可以更便于我们向上转型的使用。
三、Tread和Runnable
同:
要非说相同点那就都是同在java.lang包下面了。都不需要导包。
区别:
①Tread是继承的关系,它只能单一的继承,不能多个继承。
②Runnable是实现接口的关系,多态是最大的体现。
⭐️⭐️⭐️最大区别再于:解耦合。因为Runnable是一个函数式接口,在Runnable中run方法可以表示为一个可执行程序,可以设置线程和开启线程,任务只是一个任务,启动时可以启动。各司其职,肯定选择第二种方式创建线程更好!由于第一种方式run方法也不是函数式接口最多也就只能用匿名内部类的方式创建,不能用lamdba表示了。
四、匿名内部类和lambda
当然我们用匿名内部类和lamdba表达式创建是最简单不过的了
public static void main(String[] args) {
Thread t1 = new Thread(){
@Override
public void run() {
while(true){
System.out.println("匿名类内部类创建多线程");
}
}
};
t1.start();
while (true){
System.out.println("主线程");
}
}
public static void main(String[] args) {
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("Runnable匿名内部类创建线程");
}
}
});
t2.start();
while (true){
System.out.println("主线程");
}
ps:注意两者细小的区别
1.Runnable匿名内部类后面有一个小反括号}
2.构造完任务需要一个分号;
public static void main(String[] args) {
Thread t3 = new Thread(()->{
while(true){
System.out.println("lamdba表达式创建");
}
});
t3.start();
}
lamdba表达式后面只需要跟要执行的任务内容就可以了,不需要再重写run方法表示方法的入口。因为lamdba表达式本来就是一种函数式接口,{ }内部就表示一种逻辑。
所以只有Runnable能实现lamdba表达式,因为它实现的Runnable函数接口的模式;而继承Thread的类就不行了。
五、Tread类中的方法
以下介绍几个重要一点的方法
下面几个构造方法中
1.id
public static void main(String[] args) {
Thread t3 = new Thread(()->{
});
System.out.println(": ID: " + t3.getId());
t3.start();
}
id往往是java提供的id
2.name
public static void main(String[] args) {
Thread t3 = new Thread(()->{
});
System.out.println(": name: " + t3.getName());
t3.start();
}
线程中的名字呢往往都是用jconsole调试观察线程
3.后台线程
public static void main(String[] args) {
Thread t3 = new Thread(()->{
});
System.out.println(t3.isDeamon());
t3.setDeamon();//也可以设置为后台线程
t3.start();
}
与之对立的就是前台线程。两者最主要的区别呢就是后台线程不会影响程序的结束,是一个比较独立的线程;而前台线程呢就会影响进程的结束,也就是前台线程结束了,进程也就结束了。
4.存活
public static void main(String[] args) {
Thread t3 = new Thread(()->{
});
System.out.println(t3.isAlive());
t3.start();
}
一般来说线程的周期是大于我们内核线程的周期的。
5、sleep
public static void main3(String[] args) {
Thread t3 = new Thread(() -> {
});
while (true) {
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
sleep是使线程处于休眠状态,是一个类方法,通过输入的数字决定线程休息多少秒。
sleep()一般单位为ms。1000ms=1s。也可用于更好的观察线程的运行。
一般使用sleep都会有报错,我们只需要将这个错误抛出,或者抓住就好了。
六、线程终止interrupt
1.定义
interrupt是终止线程、停止线程的意思。
2.引入interrupt
怎么让一个线程停止呢?一个线程靠run来执行,run执行结束了线程就结束了。所以现在我们只需要让循环提前结束就好了。
第一种方法我们可以手动设置一个标志物,设置一个成员变量,然后在第五秒修改为fales结束循环。
public class demo3 {
public static boolean isQuite = false;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
while (!isQuite) {
System.out.println("线程工作ing");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("线程工作结束");
});
t.start();
System.out.println("主线程启动");
Thread.sleep(5000);
isQuite = true;
}
}
由此可见这种方法是可以的 ,但也有缺陷:需要自己手动设置太麻烦;不能在进入sleep的时候结束循环。
3.interrupt
🌰
public static void main(String[] args) {
Thread t = new Thread(()->{
while ((!Thread.currentThread().isInterrupted())) {
System.out.println("正在工作ing~");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();;
}
}
});
t.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.interrupt();//停止线程
}
我们可以用Tread.currentTread()获取到t这个对象 然后.IsInterrupt()调用方法。当你想终止的时候就可以t.interrupt终止了。
这里终止是终止了,但是另外一个问题又出现了。当我在sleep的过程的抛出了一个异常没错,但是为什么又继续循环了呢?这是因为当你在sleep抛出异常的时候,它会清除你设置的标志位又变成true进入循环开始打印了。所以解决方法就是:①你可以继续执行②你可以在后面break结束循环③或者主动抛出异常处理其他业务。所以这种情况就给我们带来了很大的便利性。而这也仅仅是在唤醒sleep报异常的时候做的处理。
ps:如果把isquite设成局部变量是否能行呢?这就涉及到lamdba表达式里面的内容了。
答案是肯定不行的,因为变量捕获的原因,内部的那一份变量属于复制的变量,两者是完全不一样的。当要修改常量时当然三是不行的啦~
七、线程等待join
1.定义:
线程等待顾名思义就是让一个线程等待另一个线程结束。
栗子
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 0; i<5; i++) {
System.out.println("t线程正在执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
System.out.println("主线程开始执行");
t.join();
System.out.println("主线程结束");
}
在上面栗子中就是让main线程等待t线程结束再结束
这里敲黑板注意了:谁被调用谁被等待。在main方法里调用t.join;就是main线程等待t线程执行结束。
2.作用:
线程等待呢最主要的作用是:控制线程结束时的顺序。
八、线程状态
线程一般有下面几种状new,runnable,waiting,timed-wait,terminated,blocked,具体请看大荧幕
看懂这张图你也就大成不就了。虽然挺复杂的,但是挺简单的。
1.就绪
Runable
在cpu上的执行、运行状态或者是准备运行、等待(就绪)的状态。
2.堵塞
一般分为三种:
(1)sleep
在线程上休眠,不占用cpu内存空间。
(2)wait
(3)锁
后续线程安全讲~
3.终端
trminated
意味线程运行终止运行结束。