文章目录
- 一、Thread 类
- 1.1 跨平台性
- 二、Thread 类里的常用方法
- 三、创建线程的方法
- 1、自定义一个类,继承Thread类,重写run方法
- 1.1、调用 start() 方法与调用 run() 方法来创建线程,有什么区别?
- 1.2、sleep()方法
- 2、自定义一个类,实现Runnable接口,重写run方法
- 3、继承 Thread 类,重写 run 方法,基于匿名内部类
- 4、实现 Runnable 接口,重写 run 方法,基于匿名内部类
- 5、匿名内部类
- 5.1、回调函数的使用场景
一、Thread 类
线程本身就是OS(操作系统简称OS,以下统一使用OS表示操作系统)提供的概念,OS也提供了一些 API 供程序员使用,譬如说:Linux 提供 pthread ,而在Java中,就把OS提供的 API 进行了封装,统一使用 Thread 类,供程序员在Java代码中调用来创建/操作线程。
1.1 跨平台性
那可能有同学疑惑了,为啥Java要封装OS提供的操作线程的API,自己提供一个Thread类供Java程序员调用来操作线程呢??
这是因为Java语言的特性:跨平台。
只要是学习过Java语言的同学,肯定听说过:一次编译,终身运行 这句话吧。其实就是在描述Java语言的跨平台性。
那么Java语言如何实现其跨平台性呢??我简单描述一下吧,以便大家更深刻理解Java为啥要封装一个 Thread 类 供程序员调用,而不直接使用OS提供的API。
JVM 通过把不同OS提供的不同的API统一封装成相同风格的API给Java程序员使用,因此JVM就能够屏蔽不同的OS的差异。
此时Java程序员写程序代码,就不需要考虑当前写的这个程序是在哪个OS上运行,运行时是否适配此OS,因为这些问题已经由JVM解决了。
二、Thread 类里的常用方法
我们通过 Thread类 创建线程时,需要先了解 Thread 类中有哪些常用方法。
Java官方文档对Thread类里的方法介绍
三、创建线程的方法
1、自定义一个类,继承Thread类,重写run方法
第一种创建线程的方式就是:自定义一个类,并且使该类继承自Java标准库 Thread 类,此时自定义的类需要重写 run() 方法。
注意:重写的 run() 方法,要处理异常时,只能 try {} catch (),并不能 throws,这是因为 Thread 类中的 run() 方法并没有throws xxx这样的设定。
重写的 run() 方法里书写的逻辑代码就是我们创建出来的新线程,所要执行的任务。
自定义一个继承自 Thread 类的自定义类,并且在该类中重写 Thread 类里的 run()方法后,并没有真正创建出一个新线程,还需要调用 start() 方法,让它真正被创建出来并执行起来。
代码展示如下:
class MyThread extends Thread {
// 重写 run() 方法
@Override
public void run() {
while (true) {
System.out.println("hello world!");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class testThread1 {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
// 真正创建一个新的线程
thread.start();
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
上述代码中,main() 方法中有一个 while循环,新线程的 run() 方法中也有一个 while 循环,这两个循环都是死循环。主线程(main)和新创建出的线程都在分别执行自己的循环。这两个线程都能参与到cpu的调度中,这两个线程是在并发执行,那么此时的运行结果就比较复杂不确定了,每台机子的性能都不一样,多个线程并发执行时,到底执行哪个线程,不知道,要看操作系统的调度。
我电脑的运行结果:
再来看看以下代码片段含有什么问题:
class MyThread extends Thread {
// 重写 run() 方法
@Override
public void run() {
while (true) {
System.out.println("hello world!");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class testThread1 {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
// 没有创建一个新的线程
thread.run();
}
}
运行结果:
上述代码片段作了一些改动:可以发现当前代码并没有调用 start() 方法去真正创建一个新的线程出来,而是调用了 run() ,但是其运行结果居然与调用 start() 方法一致,这是为什么呢??此处就涉及到一个问题:调用 start() 方法与调用 run() 方法来创建线程,有什么区别?
1.1、调用 start() 方法与调用 run() 方法来创建线程,有什么区别?
首先我们要明确:什么叫做创建出了新的线程,什么叫做没有创建出新的线程?
创建出了新的线程就是:每个线程都能够独立的调度执行。
当我们调用 start() 方法创建线程时,是真正的创建出了新的线程。此时的OS就会在底层调用创建线程的API,同时会在系统内核中创建出对应的PCB结构,并且将此PCB加入到对应的链表中。此时这个新创建出来的线程就会参与到cpu的调度中,执行任务。
run() 方法只是上面的入口方法,并没有去调动系统的API,在系统内核中创建出一个对应的PCB结构,因此并没有创建出新的线程。
以往我们只有一个线程,那就是main主线程,代码都是从前往后、从上到下执行的,遇到函数调用就先进入函数内部执行代码,然后再退出函数回到原来的代码段继续往后执行。但是现在我们接触了多线程的并发编程,虽然从宏观上来看,线程是同时在执行的,但其实多线程的执行顺序是不确定的,操作系统调度哪个线程到cpu上执行,就轮到哪个线程执行。每个线程,都是一个独立的执行流,每个线程都可以执行一段代码,多个线程之间是并发的关系。
1.2、sleep()方法
我们可以看到在代码中出现了sleep()方法,来了解以下这个常用方法。
sleep() 方法是 Thread 类的静态方法,线程调用该方法表示进入休眠/阻塞状态,其参数是 休眠的时间,单位是ms。
线程调用sleep()方法后,进入阻塞状态,当阻塞时间到,系统就会唤醒线程,并且恢复对线程的调度。如果是多个线程阻塞后都被唤醒了,那么此时谁先被调度到,谁后被调度,可以视为是”随机“的(随机在日常生活中,我们一般理解为是“概率均等”的情况,但是在这里只是看起来随机,因为我们也不知道操作系统是怎么调度的,我们只能在代码的设定上表示为随机),这样 “随机” 调度的过程,称为 “抢占式执行”。
2、自定义一个类,实现Runnable接口,重写run方法
class MyRunnable implements Runnable{
@Override
public void run() {
while (true){
System.out.println("hello world!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class TestThread2{
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
t.start();
while (true) {
System.out.println("hello main!");
Thread.sleep(1000);
}
}
}
这类创建线程的方法,把线程本身与线程要执行的任务(在 Runnable中)分开了,进一步的解耦合了。当我们还想要并发编程,但是不想使用线程的方式实现并发编程,想使用其他方式时:譬如说线程池、协程…就可以使用Runnable搭配他们来使用,进而实现并发编程。
第一种方法创建线程,是将线程本身与线程所要执行的任务放在了一起,耦合度较高。
3、继承 Thread 类,重写 run 方法,基于匿名内部类
/**
* 继承自Thread,重写run()方法,基于匿名内部类
*/
public class testThread3 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(){
@Override
public void run() {
while (true){
System.out.println("hello world!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t.start();
while (true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
4、实现 Runnable 接口,重写 run 方法,基于匿名内部类
/**
* 实现 runnable ,重写 run(),基于匿名内部类
*/
public class testThread4 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Runnable(){
public void run(){
while (true){
System.out.println("hello world!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t.start();
while (true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
5、匿名内部类
上述的几种创建线程的方法,多多少少有些啰嗦、复杂。因此就有了第5种方法:使用匿名内部类。
匿名内部类是一个 lambda 表达式,这个 lambda 表达式里就表示了run()方法里的内容。
lambda 表达式,本质上是一个 匿名函数,这样的匿名函数,主要可以用来作为回调函数来使用。
回调函数:先写好,但不需要程序员手动调用,在合适的时机自动被调用。
5.1、回调函数的使用场景
1、服务器开发:服务器收到一个请求,就会触发一个对应的回调函数,使用回调函数对该请求做出具体的处理。
2、图形界面开发:针对用户的某个操作,触发一个对应的回调。
/**
* 使用 匿名函数 创建线程
*/
public class testThread5 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while (true){
System.out.println("hello world!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
while (true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
那么在我们上述的代码中,此回调函数什么时候执行呢??即当线程被真正创建出来时会自动执行。
创建线程的方法还有许多许多,主要是介绍常用的写法,还有的写法后续会继续补充!