多线程(三):Thread 类的基本属性

简介: 多线程(三):Thread 类的基本属性

上一个篇章浅浅了解了一下 线程的概念,进程与线程的区别,如何实现多线程编程。


而且上一章提到一个重要的面试点: start 方法和 run 方法的区别。


start 方法是从系统那里创建一个新的线程,这个线程会自动调用内部的run 方法;

run 方法是一个线程的执行入口,所有的实现逻辑都写在该方法里。


从概念上来说,这两个方法是属于线程的属性。我们接着这个线程属性继续来认识线程。


线程属性


就像我们之前写的代码:


b6373bcc4e7a4dfe877a52c589488dda.png

我们如何让某个线程中断呢?


这里的中断意味着:让线程停下来,线程的终止。


本质上来说让线程终止的办法只有一种,让线程的入口方法 执行完毕。无论是是 return 、抛出异常还是别的方法都是执行完毕。


这里就需要提到 给线程设定一个结束标志。


给线程设定一个结束标志


来看下面的这段代码:  


355eafda34334d9ebb70880c117854a4.png


我们可以选择这种方式来控制循环,不让他写的那么死。


f9c3ec92d65a46c4aefbdfdafa756b05.png


但这仍是我们自己设置的变量,而不属于线程的属性,线程中也有个方法:interru方法


interru方法


我们来看看这一段代码:

4c21d88291de46068acd3cb50fa780ce.png


我们这里就用到了几个线程的属性,  

Thread.currentThread()、isInterrupted()、t.interrupt()

上述的e.printStackTrace() 就是打印出异常信息。  

我们来看看结果:


7655cbc204974aac91f711682e0590c7.png


我们发现,在main线程睡眠了 3s 之后并没有打印完异常就结束,而是继续线程 t 的运行。

这是为什么呢?

这就需要来聊聊 interrupt 方法的作用了:

  1. 将标识符设置为 true
  2. 如果该线程正在阻塞状态中(例如:sleep,后面还有其他方法也会使线程阻塞),会直接被interrupt 方法唤醒,此时通过抛出异常就会让 sleep 立即结束。


按照 interrupt 方法的作用,此时应该结束了啊,t 线程这么还会继续呢?

这里有个非常重要的点;


 当 sleep 被唤醒以后,会自动将 isInterrupted 设置为 false,也就是说将异常标志位清空了(true -> false);

sleep 为什么要清空异常标志位呢?


目的是为了让这个线程能够对于线程什么时候结束有一个更精准的控制。

当前的 interrpt 方法是告诉线程你该结束了,至于什么时候结束,都是由代码灵活控制的。

interrpt 方法 是个通知而不是命令。


等待线程 -- join 方法


线程与线程之间的的并发执行的,线程间的调度是无序的,我们无法判断两个线程之间谁先结束谁后结束。

例如:

60e988939c5d4f549c9d70f626ac65b9.png


但是我们可以通过 join 方法进行控制,例如:

adca9700aebf4897a5fac21493722796.png


在t 执行 join 方法时,如果 t 线程尚未结束,那么main线程就会进入 Blocking (阻塞状态),因为t.join 在 main线程中执行。

main 线程代码走到这一行 就不参与 cpu 的执行调度了。

如果这个有多个线程并发执行, 那么 只是main 线程不参与线程的调度,其余的线程任然是抢占执行的。

我们使用的 join 是无参的版本,我们还存在另一个带参的版本。


这个带参的版本,参数作为等待的最大时间,超过了这个最大时间,t 线程还没有结束,那么main 线程 就会被唤醒,不再是Blocking 状态 就会继续执行。

而上面不带参的版本,就是死等,等不到 t 线程结束,mian 线程就不会开始。

既然上面都提到了线程状态,那么就简单理解一下线程状态。


线程状态

Java中线程的状态分为6种:


1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。

2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。

线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。

3. 阻塞(BLOCKED):表示线程阻塞于锁。

4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。

6. 终止(TERMINATED):表示该线程已经执行完毕。


feffac504b6643c1972ee85fc7245d54.png



我们简单理解就是一条主线,三条支线。

我们就简单了解一下就好,后面还会再聊到。


简单了解线程安全


对于多线程编程雀氏提供了很多便利,我们可以再同一时间完成多个任务,相比于多进程编程多线程还降低了 cpu 资源的占用。


这几章都提到了,多线程之间的线程调度是抢占式执行的,所以在此基础之上会产生很多的线程安全问题。


本章就稍稍介绍认识一下线程安全的问题就行。


本章以一个不安全的例子来认识


例如:我们设置一个 count ,两个线程各执行 5 w 次自增运算。


代码如下:

class Count {
    private int count = 0;
    public void add() {
        count++;
    }
    public int get() {
        return count;
    }
}
public class demo10 {
    public static void main(String[] args) throws InterruptedException {
        Count count = new Count();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count.get());
    }
}

运行一次看看结果:


28e395594fd34599bc39cd7be0bc3b7a.png


再来试试看:

a0ca83d4d7584516a931c21b89af5874.png


再来一次:

d00fa91e8c804183a4d97b092625eb44.png


从结果上来看它是一个随机数。

至于为什么会产生一个随机数,我们需要了解在这执行的内部发生了什么事情:

对于这个 count++ 这个操作,本质上是由 cpu 上三个指令构成的:

  1. load : 从内存中将数据读取到 cpu 寄存器中
  2. add :在 cpu 中的值进行 + 1 运算
  3. save : 把寄存器中的值写入到内存中

由于是抢占式执行的,那么就会产生如下等问题:

7bfca9a1bce848d6b9b01844fdc6a4dd.png


这个 3 的执行顺序就变成不确定的了,产生多种排列组合结果。


只有 25 亿分之 1 的可能得到 10w 这个数字,也有可能会产生结果小于 5w 具体的可以自己排列组合看看。


对于 t1 和 t2 这两个线程可能式运行在同一个 cpu 核心上执行,也有可能在不同的 cpu 核心上执行。


归根结底,线程安全问题,全是因为线程之间的无序调度,导致了执行顺序不确定,结果不确定。


相关文章
|
18天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
2月前
lua面向对象(类)和lua协同线程与协同函数、Lua文件I/O
Lua的面向对象编程、协同线程与协同函数的概念和使用,以及Lua文件I/O操作的基本方法。
32 4
lua面向对象(类)和lua协同线程与协同函数、Lua文件I/O
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
20 3
|
2月前
|
Java
在Java多线程编程中,实现Runnable接口通常优于继承Thread类
【10月更文挑战第20天】在Java多线程编程中,实现Runnable接口通常优于继承Thread类。原因包括:1) Java只支持单继承,实现接口不受此限制;2) Runnable接口便于代码复用和线程池管理;3) 分离任务与线程,提高灵活性。因此,实现Runnable接口是更佳选择。
37 2
|
2月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
31 2
|
2月前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
34 1
|
2月前
|
Java C++
【多线程】JUC的常见类,Callable接口,ReentranLock,Semaphore,CountDownLatch
【多线程】JUC的常见类,Callable接口,ReentranLock,Semaphore,CountDownLatch
34 0
|
2月前
|
Java 调度
【多线程-从零开始-贰】线程的构造方法和常见属性
【多线程-从零开始-贰】线程的构造方法和常见属性
18 0
|
3月前
|
存储 Java 程序员
优化Java多线程应用:是创建Thread对象直接调用start()方法?还是用个变量调用?
这篇文章探讨了Java中两种创建和启动线程的方法,并分析了它们的区别。作者建议直接调用 `Thread` 对象的 `start()` 方法,而非保持强引用,以避免内存泄漏、简化线程生命周期管理,并减少不必要的线程控制。文章详细解释了这种方法在使用 `ThreadLocal` 时的优势,并提供了代码示例。作者洛小豆,文章来源于稀土掘金。
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
48 1
C++ 多线程之初识多线程