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


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


相关文章
|
30天前
|
存储 安全 Java
【Java集合类面试二十五】、有哪些线程安全的List?
线程安全的List包括Vector、Collections.SynchronizedList和CopyOnWriteArrayList,其中CopyOnWriteArrayList通过复制底层数组实现写操作,提供了最优的线程安全性能。
|
16天前
|
存储 Java 程序员
优化Java多线程应用:是创建Thread对象直接调用start()方法?还是用个变量调用?
这篇文章探讨了Java中两种创建和启动线程的方法,并分析了它们的区别。作者建议直接调用 `Thread` 对象的 `start()` 方法,而非保持强引用,以避免内存泄漏、简化线程生命周期管理,并减少不必要的线程控制。文章详细解释了这种方法在使用 `ThreadLocal` 时的优势,并提供了代码示例。作者洛小豆,文章来源于稀土掘金。
|
28天前
|
安全 Java 调度
|
28天前
|
安全 Java 程序员
线程安全与 Vector 类的分析
【8月更文挑战第22天】
20 4
|
30天前
|
安全 Java
【Java集合类面试十三】、HashMap如何实现线程安全?
实现HashMap线程安全的方法包括使用Hashtable类、ConcurrentHashMap,或通过Collections工具类将HashMap包装成线程安全的Map。
|
30天前
|
Java
【Java集合类面试十二】、HashMap为什么线程不安全?
HashMap在并发环境下执行put操作可能导致循环链表的形成,进而引起死循环,因而它是线程不安全的。
|
30天前
|
安全 算法 Java
【Java集合类面试二】、 Java中的容器,线程安全和线程不安全的分别有哪些?
这篇文章讨论了Java集合类的线程安全性,列举了线程不安全的集合类(如HashSet、ArrayList、HashMap)和线程安全的集合类(如Vector、Hashtable),同时介绍了Java 5之后提供的java.util.concurrent包中的高效并发集合类,如ConcurrentHashMap和CopyOnWriteArrayList。
【Java集合类面试二】、 Java中的容器,线程安全和线程不安全的分别有哪些?
|
24天前
|
安全 Java API
Java多线程编程:使用Atomic类实现原子操作
在Java多线程环境中,共享资源的并发访问可能导致数据不一致。传统的同步机制如`synchronized`关键字或显式锁虽能保障数据一致性,但在高并发场景下可能导致线程阻塞和性能下降。为此,Java提供了`java.util.concurrent.atomic`包下的原子类,利用底层硬件的原子操作确保变量更新的原子性,实现无锁线程安全。
13 0
【多线程面试题 二】、 说说Thread类的常用方法
Thread类的常用方法包括构造方法(如Thread()、Thread(Runnable target)等)、静态方法(如currentThread()、sleep(long millis)、yield()等)和实例方法(如getId()、getName()、interrupt()、join()等),用于线程的创建、控制和管理。
|
30天前
|
安全 Java
【Java集合类面试五】、 如何得到一个线程安全的Map?
如何得到一个线程安全的Map的方法包括:使用Collections工具类将Map包装为线程安全,使用java.util.concurrent包下的ConcurrentHashMap,以及不推荐使用性能较差的Hashtable。