1. 线程的创建
主要来简单学习一下以下几种方法:
1.1 继承 Thread 类
具体代码见前面的一章节,主体的步骤有以下几部分;
1、继承 Thread 来创建一个自定义线程类MyThread
class MyThread2 extends Thread{ //重写run方法 @Override public void run() { //run 方法就是该线程的入口方法 while (true){ System.out.println("hello thread,委婉待续"); try { Thread.sleep(1000);//休眠1000ms } catch (InterruptedException e) { throw new RuntimeException(e); } } } }
2、创建 MyThread 类的实例
3、调用 start 方法启动线程
1.2 实现 Runnable 接口
1、实现 Runnable 接口
class MyThread3 implements Runnable { @Override public void run() { while (true) { System.out.println("hello runnable"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
runnable 可以理解成“可执行的”,通过该接口,就可以抽象的表示出一端可以被其他实体来执行的代码;同时也可以简单的理解成runnable指的是重写的run方法的代码块;
2、创建 Thread 类实例, 且调用 Thread 的构造方法时将 Runnable 对象作为目标传递参数
代码讲解如下图所示:
3、调用 start 方法
总体代码如下:继承Thread,重写run,但是使用匿名内部类
package thread; class MyThread3 implements Runnable { @Override public void run() { while (true) { System.out.println("hello runnable"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class ThreadDemo3 { public static void main(String[] args) { Thread t = new Thread(new MyThread3()); t.start(); while (true) { System.out.println("hello main"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
1.3 使用匿名内部类
内部类,在一个类里面定义的类~~匿名内部类最大的用途,没有名字意味着只能使用一次就无法生效了
1.3.1 匿名内部类创建 Thread 子类对象
继承Thread,重写run
代码如下:
// 使用匿名类创建 Thread 子类对象 Thread t1 = new Thread() { @Override public void run() { System.out.println("使用匿名类创建 Thread 子类对象"); } };
1.3.2 匿名内部类实现runnable,重写run
代码如下:
public static void main(String[] args) { Thread t = new Thread(new Runnable() { @Override public void run() { while (true) { System.out.println("使用匿名类创建 Runnable 子类对象"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }); t.start();
总体来说,()里面的内容是thread父类构造方法的参数,代码部分其实是填写了runnable的匿名内部类的实例
1.3.3 lambda 表达式创建 Runnable 子类对象
(函数式接口属于lambda背后的实现),相当于java在没被破坏原有的规则(方法不能脱离类,而单独存在)基础上,给了lambda一个合法的解释
代码如下:
public static void main(String[] args) { Thread t = new Thread(() -> { while (true) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } });
2. Thread 类及常见方法
Thread 类是 JVM 用来管理线程的一个类,即每个线程都有一个唯一的 Thread 对象与之关联。
每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象 就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。
2.1 Thread 的常见构造方法
大体使用如下:
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是当前线程的名字");
Thread t4 = new Thread(new MyRunnable(), "这是当前线程的名字");
命名操作:
可以通过使用 jconsole 命令观察线程时的看到自己的重命名线程名子
代码如下:
package thread;
public class ThreadDemo7 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("hello thread,it's smallye");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "委婉待续001");
// 在 start 之前, 设置线程为 后台线程 (不能在 start 之后设置)
// t.setDaemon(true);
t.start();
}
}
结果如下:
2.2 Thread 的几个常见属性
常见属性说明:
1、ID 是线程的唯一标识,不同线程不会重复; Getid()是jvm自动分配的身份标识,会保证线程的唯一性
2、名称是各种调试工具用到
3、状态表示线程当前所处的一个情况;Getstate():进程有状态(就绪,阻塞),线程也有状态,java中对线程的状态,有进行了进一步的区分;
4、优先级高的线程理论上来说更容易被调度到
5、关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行;Isdaemon(demon:守护)判断是否是守护线程,也叫做“是否是后台线程”; 前台线程的运行会阻止进程结束,后台进程的运行不会阻止进程结束;
代码举例:
public class ThreadDemo7 { public static void main(String[] args) { Thread thread = new Thread(() -> { for (int i = 0; i < 10; i++) { try { System.out.println(Thread.currentThread().getName() + ": 我还活着"); Thread.sleep(1 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + ": 我即将死去"); },"委婉待续001"); System.out.println(Thread.currentThread().getName() + ": ID: " + thread.getId()); System.out.println(Thread.currentThread().getName() + ": 名称: " + thread.getName()); System.out.println(Thread.currentThread().getName() + ": 状态: " + thread.getState()); System.out.println(Thread.currentThread().getName() + ": 优先级: " + thread.getPriority()); System.out.println(Thread.currentThread().getName() + ": 后台线程: " + thread.isDaemon()); System.out.println(Thread.currentThread().getName() + ": 活着: " + thread.isAlive()); System.out.println(Thread.currentThread().getName() + ": 被中断: " + thread.isInterrupted()); thread.start(); while (thread.isAlive()) {} System.out.println(Thread.currentThread().getName() + ": 状态: " + thread.getState()); } }
结果展示:
package thread; public class ThreadDemo7 { public static void main(String[] args) { Thread t = new Thread(new Runnable() { @Override public void run() { while (true) { System.out.println("hello thread,it's smallye"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }, "委婉待续001"); // 在 start 之前, 设置线程为 后台线程 (不能在 start 之后设置) // t.setDaemon(true); t.start(); } }
如上代码执行的时候,t持续执行,但是main已经结束了,结果如下:
观察线程的状态:
在idea中。没有出现下图情况时,我们的进程不算结束;
咱们代码创建的t线程,默认就是前台线程,会阻止进程结束,只要前台线程没执行完,进程就不会结束,即时main已经执行完毕
如下面的代码,我们将我们的线程设置为后台线程,他不会阻止我们的线程结束,故此有如下结果;
6、是否存活,即简单的理解,为 run 方法是否运行结束了;IsAliva:表示了内核中的线程pcb是否存在,Java代码中定义的线程对象(thread)实例虽然表示一个线程,但是对象本身的生命周期和内核中的pcb的生命周期是不一样的;
2.3 启动一个线程-start()
Thread类,使用一个start方法,来启动一个线程,对于同一个thread对象来说,只能调用一次。下图中的一个thread类对象被start启动两次,会有异常;
运行结果如下所示:
此时可以看到,另外一个线程依旧在运行 ;IlllegalThreadStateException,该异常是指非法的线程状态异常;即想要启动更多的线程,就是得创建新的对象。
调用strart创建出新的线程(本质上是start会调用系统的api,来完成创建线程的操作)
Q:下面的代码中关于start和run的区别,以及两者之间的关系?
package thread;
class MyThread4 extends Thread {
@Override
public void run() {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo11 {
public static void main(String[] args) {
Thread t = new MyThread4();
t.start();
// t.run();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
A: 1、在上述代码中,两个词语没有一点关系;
2、详细分析如下图图解所示:
2.4 中断一个线程
中断一个线程(终止一个线程)-->让线程run方法(入口方法)执行完毕---->核心就是让run方法能够提前结束----->非常取决于具体代码实现方式了
2.4.1 引入标志位
代码如下:
测试结果:
通过上述代码引入标志位isquit,就可以让t线程结束掉,具体线程结束的时候却取决于在另外一个线程中何时修改isquit的值。
Main线程要想让t线程结束,其前提是t线程的代码,对于这样的逻辑有所支持,而不是t里的代码随便咋写都能够提前结束的。
2.4.2 标志位优化
上述方法是手动设置变量,来进行结束线程。我们的优化是尝试让一个thread类,进行内置变量;代码解说图解如下:
我们的线程中,如果没有sleep,interrupt可以让线程如我们所料的那样顺利结束,但是sleep引起了变数。刚才在执行sleep的过程中,调用interrupt,会大概率出现sleep休眠时间还没到,就被提前唤醒了。我们的线程被提前唤醒,会做以下两件事:
- 抛出interruptException(紧接着就会被catch获取到)
- 清除thread对象的isInterrupted标志位(通过interrupt方法,已经把标志位设置为true了,但是sleep提前唤醒,就把标志位又设回到false,所以此时循环还是会继续执行),如下情况:
所以如果依旧想让线程按照原先的计划结束,我们需要在catch语句中添加break语句;
代码如下所示:
package thread;
public class ThreadDemo13 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("我是一个线程, 正在工作中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// e.printStackTrace();
// 加上 break , 此时抛出异常之后, 线程也会结束
break;
}
}
System.out.println("线程执行完毕!");
});
t.start();
Thread.sleep(3000);
// 使用一个 interrupt 方法, 来修改刚才标志位的值
System.out.println("让 t 线程结束");
t.interrupt();
}
}
2.5 join()-等待一个线程
多个线程的执行顺序是不确定的,虽然线程的底层的调度是无序的,但是我们可以在应用程序中。通过一些api来影响线程执行的顺序;所以我们引入了join方法,该方法能够应该线程执行的先后顺序;
比如t1线程等待t0线程,则最后的结果是t0线程结束之后t1线程才会被执行;join会导致等待的那个线程处于阻塞的状态(开车如果1车被2车加塞,则2车需要等待,所以2车被阻塞)
接下来我们要知道使用join方法之后,关于谁等待谁的问题?具体图解如下图所示:
深入分析:
执行join的时候,接下来主要是看t线程是否在运行:
1、如果t在运行,main线程就会处于阻塞状态(t线程加入了,所以main线程就不去参加cpu的执行了)
2、如果t运行结束,main线程就会从阻塞中恢复过来,继续向下执行(加塞的前面的那个车走了,我的车就需要继续执行我的任务了,总之导致我的任务结束的比前面的车晚)
综上所述,是由于阻塞,是这两个线程的结束时间产生了先后关系;
ps:本次的内容就到这里了,如果大家感兴趣的话就请一键三连哦!!!
我的idea的背景图是刘姝贤,如果感兴趣的话关注b站up主刘景灵000!!!