【多线程】Thread类

这篇具有很好参考价值的文章主要介绍了【多线程】Thread类。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1. Java中如何进行多线程编程?

线程是操作系统中的概念,操作系统内核实现了线程这样的机制,并且对用户层提供了一些 API 供用户使用(如 Linux 中的 pthread 库)。

所以本身关于线程的操作,是依赖操作系统提供的的 API,而 Java 的 JVM 已经把很多操作系统提供的功能封装好了,我们就不需要学习系统原生的 API,只需要学习 Java 提供的 API 就好了。

在 Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进一步的抽象和封装!

可以认为,Java 操作多线程最核心的类就是 Thread 类!


2. 简单使用多线程

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("hello world");
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
    }
}

上述就是我们第一个多线程代码,使用多线程打印 "hello world"

这里是第一个创建线程的方式,继承 Thread 类,重写 run 方法!

上述代码中的 t.start(); 这里的工作就是创建了一个新的线程,而这个线程负责执行 t 对象中的 run 方法.

start 方法创建一个新的线程,本质上就是调用操作系统的API,通过操作系统内核创建新线程的 PCB,并且把要执行的指令交给这个 PCB,当 PCB 调度到 CPU 上执行的时候,也就执行到了线程的 run 方法中的代码了!

注意:这里可能有个让人误解的地方,start 方法里是没有调用 run 方法的,start 只是创建了一个线程,由新创建的线程去调用 run 方法!

上述我们代码的执行流程就是:主线程(main线程) 中调用 t.start(); 创建了一个新线程,这个新线程调用 t.run(); 如果 run 方法执行完结束了,这个新的线程也会随之销毁。


3. start 和 run 的区别

start 方法是真正创建了一个线程(从系统这里创建的),线程是一个独立的执行流.

run 方法只是描述了线程要干什么样的活,如果直接在 main 方法调用 run,此时是不会创建新线程的,这个 run 方法会在 main 线程中执行:

public static void main(String[] args) {
        MyThread t = new MyThread();
        t.run();
}

上述这种情况,只是单纯在 main 线程中执行 t 对象里的 run 方法罢了!

提问:new 一个 Thead 对象是在干嘛呢?

其实也就是创建一个对象罢了,只不过这个对象能够通过 start 方法创建一个线程罢了!


4. jconsole 工具

我们也可以通过 jdk 自带的工具 jconsole 查看当前的 java 进程中的所有线程(bin 目录下):

【多线程】Thread类

因为进程和线程之间是包含关系,当要查看线程的时候,需要先连接上指定的进程,才能看指定进程中所拥有的线程。

【多线程】Thread类

此处可以看到一个是 main 线程,也就是主线程,还有一个是我们创建的线程这个默认起了个名字 Thread-0,除了这两个线程之外,其他的线程都是 JVM 自带的,这里我们不用过多关心,后续还会使用这个工具进行查看线程的阻塞状态等


5. Java 中创建线程的写法

● 继承 Thread 重写 run

这里上述我们简单使用多线程的时候已经见过了,这里就不过多讲述了。

● 实现 Runnable 接口 重写 run

class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println("hello world");
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        // 描述一个任务
        Runnable runnable = new MyThread();
        // 把任务交给线程通过 start 方法来执行
        Thread t = new Thread(runnable);
        t.start();
    }
}

上述的 runnable 对象,只是描述了一个任务,这里的写法最主要就是解耦合,目的让线程和线程要干的活之间分离开。

● 使用匿内部类 继承 Thread

public class ThreadDemo {
    public static void main(String[] args) {
        Thread t = new Thread() {
            @Override
            public void run() {
                System.out.println("hello world");
            }
        };
        t.start();
    }
}

这里创建了一个 Thread 的子类,但是是没有名字(匿名)的,Thrad() 后面大括号中表示子类重写父类 Thread 的 run 方法,最后让 t 引用指向该实例。

● 使用匿名内部类 实现 Runnable

public class ThreadDemo {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello world");
            }
        });
        t.start();
    }
}

这样的写法本质上和上一个写法相同,此处只是创建了一个匿名内部类,实现了 Runnable 接口重写了 run 方法,同时创建了类的实例,把这个匿名的 Runnable 对象作为参数传递给了 Thread 的构造方法。

● 使用 Lambda 表达式

public class ThreadDemo {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("hello world");
        });
        // Thread t = new Thread(() -> System.out.println("hello world")); 等价上面
        t.start();
    }
}

此处是通过 Lambda 表达式来描述任务,直接把 Lambda 传给 Thread 构造方法,这里跟上种方法没有啥区别,只是语法的不同而已,因为 Runnable 这个接口就是一个函数式接口,才能使用这种语法,具体内容见 Lambda 章节。

上述介绍的几种写法,离不开 Thread 类,只不过是使用了不同的方法来描述 Thread 里的任务是啥,只是语法规则的不同,本质上都是一样的方法,这些方法创建出来的线程都是一样的,随着后面学习的深入,会见识到其他创建线程的方法但大体都是大同小异。

6. Thread 类方法介绍

6.1 构造方法

方法

说明

Thread()

创建线程对象

Thread(Runnable target)

使用 Runnable 对象创建线程对象

Thread(String name)

创建线程对象,并命名

Thread(Runnable target, String name)

使用 Runnable 对象创建线程对象,并命名

如果使用中直接 Thread t = new Thread(); t.start(); 这样的话相当于执行了一个空的 run 方法:

【多线程】Thread类

这里是 Thread 源码中的 run,此处的 target 就是一个 Runnable 类型的,所以要想创建的线程能正常的执行 run方法,要不继承 Thread 类重写 run,要不实现 Runnable 接口重写 run。

上述介绍的构造方法中,最后一个方法,是可以给线程起个名字,取名是为了方便调试,线程默认的名字叫做 Thread-0,Thread-1....

public static void main(String[] args) {
    Thread t = new Thread(() -> {
        System.out.println("hell world");
        try {
            Thread.sleep(10_0000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, "myThread");
    t.start();
}

此处加上 Thread.slepp(),让创建的线程进行休眠,为了是让我们通过 jconsole 工具更好的观察:

【多线程】Thread类

这里通过查看,确实发现给我们创建的线程取名为 myThread

提问:这里main线程为什么没了呢?

注意看上述代码,main 线程执行完 t.start() 之后,后面就没有任何需要执行的代码了,对于主线程来说,main 方法执行完了也就被销毁了,而且每个线程是一个独立的执行流,main线程的销毁,不影响 myThread 线程的继续执行!

6.2 Thread 类常见属性

属性

对应获取方法

ID

getId()

名称

getName()

状态

getState()

优先级

getPriority()

是否后台线程

isDaemon()

是否存活

isAlive()

是否被中断

isInterrupted()

● ID 是线程的唯一标识,不同的线程 ID 都不同

● 名称 是线程的名字,创建线程对象通过构造方法指定的名称,如果没指定就是默认的名字

● 状态 是线程所处的状态,有很多种,具体我们后续讲解

● 优先级 理论上优先级越高的线程越容易被调度到

● 是否是后台线程(是否是守护线程) 后面会讲解

● 是否被中断,可以通过一些手段中断线程,我们后续讲解

上述的方法获取线程对应的属性,大家可以下来自行尝试一下,这里就不做过多演示了!

6.3 什么是守护线程?

这里守护线程就是后台线程,为什么叫做守护呢?这个是历史遗留翻译的问题,守护这个词语从字面意思确实不好理解,这里更习惯把守护线程叫作后台线程

后台线程(守护线程),不会阻止进程的结束,即使后台线程的工作没有做完,进程也是可以结束的!
前台线程(非守护线程),会阻止进程的结束,如果前台线程的工作没有做完,进程是不能结束的!

注意:我们默认创建的线程都是前台线程!包括 main 方法也是一个前台线程(非守护线程)!

像我们上面通过 jconsole 工具看到的线程,除了自己创建的线程和 main 线程,剩下的都是 JVM 自带的线程,而 JVM 自带的线程都是后台线程(守护线程)

这里也可以使用 setDaemon() 这个方法来将创建的线程设置成后台线程(守护线程):

public static void main(String[] args) {
    Thread t = new Thread(() -> {
        System.out.println("hello world");
    });
    t.setDaemon(true); // 将创建的线程对象对应的线程设置为后台线程
    t.start();
}

这里将线程 t 设置成守护线程后,此时进程的结束与 t 就无关了!

public static void main(String[] args) {
    Thread t = new Thread(() -> {
        while (true) {
            System.out.println("hello");
        }
    });
    t.setDaemon(true); // 将创建的线程对象对应的线程设置为后台线程
    t.start();
    System.out.println("main 方法执行结束!");
}

这段代码,已经将 t 线程设置成后台线程了,此时这个 t 线程要执行的任务是一个死循环,但是真的会一直执行吗?

【多线程】Thread类

通过打印不难发现,t 线程并不会永无止境的循环下去,因为将线程设置成守护线程后,我们启动的进程是否结束,就与 t 线程没有关系了,而进程的结束,进程里面对应的线程也会直接结束!

如果没有将 t 设置成守护线程呢?此时就会永无止境的打印 hello !大家可以自行下去测试!

6.5 什么情况线程才是存活的?

上述介绍 Thread 类常见属性的时候,有一个属性是通过调用 isAlive 方法判断线程是否存活,那么线程存活到底是什么意思呢?

简单来说,在线程执行 run 方法的时候,就是存活的,执行 run 方法之前,或者执行完 run 方法之后,线程就不是存活的了!

那么这里我们就要弄清楚,线程是什么时候去执行 run 方法的?

其实在之前就讲到过,只有当线程对象,调用 start 方法后,才会真正的创建一个线程,然后线程去执行对应的 run 方法!

至于线程是否存活,那么就从三个点进行分析,start 之前,是肯定没有存活的,start 之后线程就会执行 run 方法,所以此时线程肯定是存活的,run 方法结束后,线程肯定是没有存活的!

下面就通过一段代码来验证下上述的结论:

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(() -> {
        for (int i = 0; i < 100_0000; i++) {

        }
    });
    System.out.println("start之前: " + t.isAlive());
    t.start();
    System.out.println("start之后: " + t.isAlive());
    Thread.sleep(100);
    System.out.println("run 方法执行完毕后 : " + t.isAlive());
}
【多线程】Thread类

上述代码就是让 t 线程干一件事,执行一百万次空循环,启动线程之后,令 main 线程等待 100 毫秒,此处的 100 毫秒足够执行完 run 方法中的内容了!

通过打印结果能发现,只有在执行 run 方法的时候,isAlive() 结果才是 true。

此处需要注意,线程把 run 方法执行完了,此时线程销毁,对应的 PCB 随之释放,但是 t 这个对象还不一定被释放,此时 isAlive() 也是 false,所以线程存在与否,与线程对象无关!

6.6 什么是线程中断?

中断的意思是,不是让线程立即就结束,而是通知线程应该要结束了,是否真的结束还取决于线程这里代码的具体写法,这里我们简单来举一个例子:

6.6.1 自定义一个标志位

public class ThreadDemo01 {
    public static Boolean flag = false; // false表示不终止
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (!flag) {
                System.out.println("hello world");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        Thread.sleep(3000);
        flag = true;
    }
}

上述代码,执行完 t.start() 之后,main 线程休眠 30 秒后,将 flag 变量修改为 ture,表示告诉线程 t 应该要终止了,线程 t 下次循环发现 !flag 为假,就会退出这个循环!

上述这种自定义的方式,有一个缺点,如果上述代码线程中的 sleep 休眠时间太久,就可能不能及时感知到外面的 flag 已经发生改变了!

这里只是通过修改 flag 的方式,告诉线程,应该要结束了,但是这个线程会立马结束吗?其实还是取决于这个线程内部执行的代码,比如上述 t 线程执行的代码中,在 while 循环外再加上其他代码,此时也就不会立马就结束了!

6.6.2 Thread 自带标志位

可以自定义标志位的同时,也可也使用当前线程自带的标志位:

Thread.currentThread().isInterrupted();

前面的是 Thread 类的静态方法,获取线程对象的引用,在哪个线程中调用的,就获取对应的线程的实例,后面的 isInterrupted 则相当于是获取标志位的值,如果为 true 则表示线程该终止了,如果为 false 则表示不用终止,线程继续执行!

同时可以通过 interrupt() 这个方法,就可以通知对应线程该终止了!

下面就把上述的代码改成自带的标志位的模式:

public class ThreadDemo01 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("hello world");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        Thread.sleep(3000);
        t.interrupt(); //通知线程该终止了
    }
}
【多线程】Thread类

此时发现了一个奇怪现象,调用 interrupt 方法后,居然触发了 t 线程的 sleep 方法的异常?而且 t 线程并没有终止,这是怎么一回事呢?要想了解清楚,首先要弄清楚 interrupt 方法背后做了哪些事情!

interrupt 会做两件事:

  1. 把线程内部的标志位给设置成 true,告诉线程该终止了!

  1. 如果线程在 sleep,则会触发 sleep 的异常,把 sleep 提前唤醒!

但是 sleep 在被唤醒的时候,还会把标志位设置成 false!

扩展:像 wait,join 等类似造成线程 "阻塞挂起" 的方法,都有类似清除标志位的设定。

这样一来,就 interrupt 就白忙活了,如果没有没有 sleep,则是会正常终止上述线程的。

那么这样有什么好处呢?

就举个简单的例子,假设张三在打游戏,张三的女朋友让张三放下游戏陪她去逛街,那么张三就有三种选择:

  • 立刻放下游戏,陪女朋友

  • 忽略女朋友,不管她,当作没听到

  • 等过一会游戏打完,再去陪女朋友

此时我们就可以修改上述的代码了:

立刻放下手机陪女朋友版本:

Thread t = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        System.out.println("打游戏!!!");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            break; //马上放下手机陪女朋友
        }
    }
});

忽略女朋友版本:

Thread t = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        System.out.println("打游戏!!!");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println("接着打!");
        }
    }
});

过一会再陪女朋友版本:

Thread t = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        System.out.println("打游戏!!!");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // 此处可以写任意代码
            System.out.println("等游戏打完!");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException interruptedException) {
                interruptedException.printStackTrace();
            }
        }
    }
});

为什么 interrupt 不设定成立刻终止线程呢?而是让线程自己做选择呢?

因为 CPU 是随机调度线程的,所以当 interrupt 方法执行后,并不确定对应线程执行到哪里了,如果对应线程活还没干完,直接啪一下终止了,这样是很危险的行为!把是否真的终止线程的选择权交给程序猿,这才是一个很好的选择!

同时 Thread 类中还有一个 Thread.interrupted() 方法,手动清除标志位,这个了解即可。


下期预告:【多线程】认识线程的状态文章来源地址https://www.toymoban.com/news/detail-429400.html

到了这里,关于【多线程】Thread类的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 基于stm32单片机和rt-thread操作系统的智能灯

    目    录 一、 总体概况 二、 各部分介绍 2.1  STM32F4开发板 2.2  光敏模块 2.3  麦克风模块 2.4  超声波模块 三、 RT-Thread介绍 四、 开发过程 五、 未来设想 六、 开发心得 总体概况 本次测试技术与信号处理课程作业,我利用了stm32单片机和rt-thread实时操作系统进行实践。

    2023年04月16日
    浏览(82)
  • 【操作系统】线程简介

    线程概念 在许多经典的操作系统教科书中,总是把进程定义为程序的执行实例,它并不执行什么, 只是维护应用程序所需的各种资源,而线程则是真正的执行实体。 所以,线程是轻量级的进程(LWP:light weight process),在Linux环境下线程的本质仍是进程。 为了让进程完成一定

    2024年02月06日
    浏览(42)
  • 操作系统-线程复用

    操作系统执行线程复用的过程涉及到线程调度和管理。 线程复用是指操作系统能够有效地重用现有的线程来执行新的任务,而不必每次都创建新线程。 这有助于减少线程创建和销毁的开销,提高系统性能。下面是操作系统如何执行线程复用的关键步骤: 线程池管理 :操作系

    2024年02月09日
    浏览(34)
  • 【操作系统——进程与线程(一)】

    2.1.1 进程的概念和特征 进程是指正在执行中的程序的实例。它是计算机系统进行资源分配和调度的基本单位。每个进程都有自己的地址空间、堆栈和数据区域,以及与其他进程通信和同步所需要的操作系统资源。 进程具有以下特点: 独立性:进程是独立的执行实体,拥有自

    2024年02月11日
    浏览(56)
  • 操作系统原理 —— 线程的概念、实现方式、多线程模型(十)

    有的进程可能需要 “同时” 做很多事情,而传统的进程只能串行的执行一系列的程序,为此,引入了 “线程” ,来增加并发度。 可以把线程理解为 轻量级进程 ,线程是可以基本的 CPU 执行单位,也是程序执行流的最小单位,引入线程之后,不仅是进程之间可以并发,进程

    2024年02月03日
    浏览(35)
  • 操作系统-进程和线程-同步、互斥、死锁

    目录 一、同步互斥  二、互斥的实现方法 2.1软件实现 2.1.1单标志法 2.1.2双标志先检查 2.1.3双标志后检查 2.1.4Petersons算法 2.2硬件实现 2.2.1 TestAndSet指令 2.2.2 Swap指令   三、信号量机制 3.1整形变量  3.2 记录型变量  3.3用信号量实现进程互斥、同步、前驱关系 3.3.1互斥  3.3.2同步

    2024年02月08日
    浏览(52)
  • 操作系统进程线程(一)—进程线程协程区别、多进程多线程、进程调度算法、进程线程通信

    定义上 进程: 资源分配和拥有 的基本单位,是调度的基本单位。 运行一个可执行程序会创建一个或者多个进程;进程就是运行起来的程序 线程:程序 执行 基本单位,轻量级进程。 每个进程中都有唯一的主线程 ,主线程和进程是相互依赖的关系。 协程: 用户态 的轻量级

    2024年02月01日
    浏览(57)
  • 操作系统进程线程(三)—进程状态、同步互斥、锁、死锁

    原子操作的概念 原子操作就是不可中断的一个或者一系列操作。 原子操作如何实现 总线锁定 使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号的时候,其他处理器的请求将被阻塞住,那么该处理器可以独占内存。 缓存锁 总线锁开销比较大,因为把CPU和内

    2024年02月04日
    浏览(52)
  • 【操作系统和计网从入门到深入】(八)线程

    只要满足,比进程轻量化,cpu内所有线程资源共享,创建维护成 本更低等要求,就能叫线程。 不同的OS实现方式不同,下面这个是Linux特有的方案。Linux没有给线程重新设计数据结构! 什么叫做进程? pcb + 地址空间 + 页表 CPU调度的基本单位:线程! 性能损失 一个很少被外部

    2024年02月19日
    浏览(32)
  • 深入理解操作系统中进程与线程的区别及切换机制(上)

    所谓进程,大家可以理解为我们打开的应用程序,如微信、QQ、游戏等,但也有系统应用是我们看不见的,可以打开任务管理器一探究竟,我们写的代码程序在服务器上在不运行的情况下,它就是一个二进制文件,并不是进程! 一个进程可以包含一个或者多个线程,但对于

    2024年02月11日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包