多线程(三):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 核心上执行。


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


相关文章
|
1月前
|
Java 开发者
Java面试题:请解释内存泄漏的原因,并说明如何使用Thread类和ExecutorService实现多线程编程,请解释CountDownLatch和CyclicBarrier在并发编程中的用途和区别
Java面试题:请解释内存泄漏的原因,并说明如何使用Thread类和ExecutorService实现多线程编程,请解释CountDownLatch和CyclicBarrier在并发编程中的用途和区别
26 0
|
7天前
|
Java 开发者
奇迹时刻!探索 Java 多线程的奇幻之旅:Thread 类和 Runnable 接口的惊人对决
【8月更文挑战第13天】Java的多线程特性能显著提升程序性能与响应性。本文通过示例代码详细解析了两种核心实现方式:Thread类与Runnable接口。Thread类适用于简单场景,直接定义线程行为;Runnable接口则更适合复杂的项目结构,尤其在需要继承其他类时,能保持代码的清晰与模块化。理解两者差异有助于开发者在实际应用中做出合理选择,构建高效稳定的多线程程序。
28 7
|
26天前
|
缓存 安全 Java
多线程线程池问题之为什么手动创建的线程池比使用Executors类提供的线程池更安全
多线程线程池问题之为什么手动创建的线程池比使用Executors类提供的线程池更安全
|
6天前
|
SQL 机器学习/深度学习 算法
【python】python指南(一):线程Thread
【python】python指南(一):线程Thread
20 0
|
1月前
|
Java C# Python
线程等待(Thread Sleep)
线程等待(Thread Sleep)
|
1月前
|
设计模式 存储 安全
Java面试题:设计一个线程安全的单例类并解释其内存占用情况?使用Java多线程工具类实现一个高效的线程池,并解释其背后的原理。结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
Java面试题:设计一个线程安全的单例类并解释其内存占用情况?使用Java多线程工具类实现一个高效的线程池,并解释其背后的原理。结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
35 1
|
2月前
|
存储 设计模式 安全
C++一分钟之-并发编程基础:线程与std::thread
【6月更文挑战第26天】C++11的`std::thread`简化了多线程编程,允许并发执行任务以提升效率。文中介绍了创建线程的基本方法,包括使用函数和lambda表达式,并强调了数据竞争、线程生命周期管理及异常安全等关键问题。通过示例展示了如何用互斥锁避免数据竞争,还提及了线程属性定制、线程局部存储和同步工具。理解并发编程的挑战与解决方案是提升程序性能的关键。
53 3
|
1月前
|
设计模式 安全 NoSQL
Java面试题:结合单例模式与Java内存管理,设计一个线程安全的单例类?分析Java多线程工具类ExecutorService与Java并发工具包中的工具类,设计一个Java并发框架的分布式锁实现
Java面试题:结合单例模式与Java内存管理,设计一个线程安全的单例类?分析Java多线程工具类ExecutorService与Java并发工具包中的工具类,设计一个Java并发框架的分布式锁实现
30 0
|
1月前
|
设计模式 存储 缓存
Java面试题:结合单例模式与Java内存模型,设计一个线程安全的单例类?使用内存屏障与Java并发工具类,实现一个高效的并发缓存系统?结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
Java面试题:结合单例模式与Java内存模型,设计一个线程安全的单例类?使用内存屏障与Java并发工具类,实现一个高效的并发缓存系统?结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
20 0
|
1月前
|
安全 算法 Java
实现Java中的线程安全集合类
实现Java中的线程安全集合类