【Java EE初阶】多线程(二)

1.在图中代码,我们调用了start方法,真正让系统调用api创建了一个新线程,而在这个线程跑起来之后,就会自动执行到run。调用start方法动作本身速度非常快,一旦执行,代码就会立即往下走,不会产生任何的阻塞等待。因此我们看到的输出为:

然而,看到的是hello main在前的情况并不是唯一的,可能会有例外。因为当我们调用start之后,main线程和t5线程,他们两个执行,是一个"并发执行"的关系。而对于线程调度,操作系统具有随机性,所以结果顺序并不是唯一的。

如果执行完start恰好被调度出cpu(概率性时间),此时,cpu下次限制性main还是先执行t5就不确定了。

2.一个线程对象只能start一次。

如上图所示,我调用了两次t5.start();然后就出现了图中所示的错误,错误显示为"非法的线程状态"。原因是设计java的人约定了Thread对象只能start一次,一个Thread对象,只能对应到操作系统中的一个线程。如果需要多个线程,可以多创建几个Thread对象。

在创建线程的时候,start方法对线程状态做了判定,线程,在执行了start方法之后,就是就绪状态/阻塞状态了,对于就绪状态(阻塞状态下)的线程,我们不能够再次start。

3.中断一个线程。

中断:中断,也是操作系统中的一个专用术语。即"打断"、"终止"。意思是正常情况下,一个线程需要把入口方法执行完才能够使线程结束。(如果希望线程在没有执行完的情况下就结束,那么就需要通过"打断线程"的操作,也需要线程本身,代码做出配合)。这种情况一般发生在线程在sleep的过程中。

中断线程的几种方法:

(1)通过变量。

(2)直接就使用线程内置的标志位is interruptted()

Thread对象中,包含了一个boolean变量。如果该变量为false,说明没有人去尝试终止这个线程,如果为true,说明有人尝试终止。

此处会报错的原因:此处针对lambda表达式的定义,其实是在new Thread之前的。因此t1还没有被初始化完成,是没办法进行方法的调用的。

currentTread也是Thread类提供的一个静态方法,哪个线程调用这个方法,就返回哪个线程对象的引用。

通过t.interrupt();我们可以来终止t线程。

线程终止这里,有一个奇怪的设定。如果t线程正在sleep,此时main中调用Interrupt方法,就会把sleep提前唤醒。

这个异常支持sleep提前唤醒,通过异常,区分sleep是睡足了还是提前醒了。

sleep提前唤醒,触发异常之后,sleep就会把isinterrupted标志位重置为false。

于是输出继续打印Hello Thread。

上述奇怪的设定,主要是为了给程序员更多的操作空间。还没睡饱,就唤醒了,可能会存在一些"还没做完"的工作。于是java希望让程序员自行决定线程t是要继续执行,还是立即结束,还是稍等一会儿再结束。

如何结束这种现象?------>在抛出异常之后添加一个break;我们也就跳出循环了。

几种情况的对比:

相当于完全忽视了请求

让线程立即结束。

在break之前去完成其他的事情,添加其他的善后逻辑,就相当于"稍后再结束"。

小结:

上述几种方式,本质上,都是t线程自己决定自己是否要终止,相当于main只是给t提供了一个"提醒"建议,而不是强制执行的。

如果采取强制终止的手段,很可能t线程的某个逻辑没有执行完,可能就会造成一些"脏数据"的输出,e.g.t执行过程中针对数据库的数据进行多次增删改查操作,结果由于上述强制中断,导致对数据库的数据修改操作只进行了一半,留下了脏数据。

使用Interrupt方法的时候,

1.t线程没有进行sleep等阻塞操作,t的isInterrupted()方法返回true,通过循环条件结束t线程。

2.t线程中进行了sleep等阻塞操作,t的isInterrupted()方法还是会返回true,但是sleep如果是被提前唤醒,抛出InterruptException,同时也会把isInterrupted()的返回结果设为false。此时就需要手动决定是否要结束线程了。

二、线程等待

一般情况下,系统是随机调度的(抢占式执行),如我们之前所说的hello main和hello thread哪一个先开始打印问题。当启动两个线程之后,这两个线程的执行顺序不确定。而对于程序员来说,程序员并不喜欢"随机"的东西。有些时候,程序员是希望顺序能够固定下来。

那么就引出了线程等待的概念------线程等待:约定了两个线程结束的先后顺序,让"后结束"的线程阻塞,等待"先结束"的线程执行完

join()方法:t.join()。哪个线程中调用的join,这个线程就是"等的一方"(此处就是main线程)。join前面是哪个引用,对应的线程就是"被等的一方"(此处就是t)。main线程等待t线程结束。从字面上去理解,t要加入到"main"中,加入到main中,前提是main要存在,所以main的存在时间就要比t长。

*Java多线程当中,只要这个方法会产生"阻塞"(就可能被Interrupt提前唤醒),就都会抛出InterruptedException异常。

join等待是"死等""不见不散",只要被等待的线程t没有结束,join都会始终阻塞。

main可以等待t,t同样也可以等待main.

是否可以同时让main也等待t,让t也等待main?

代码完全可以这么写,但是这么些是没有意义的。这种情况下就会出现两个线程都无法结束,都无法完成对方的等待操作。

*使用join的前提是,我们需要明确知道当前这里的线程结束顺序。

join在一定情况下不会触发阻塞,例如:main等待t,如果main的join之前,t就已经结束了,此时join就不会阻塞。

join默认的情况是死等,但是join还有一个重载的版本,可以指定"等待的最大时间"(超时时间)。

这次执行大概有12ms的误差

*网络编程中,超时时间非常重要,网络通信中数据传输时丢包是很常见的情况。

计算机在衡量时间的时候,是可能存在误差的,误差范围就是在ms级别。

*实际开发中,优先使用带有超时时间的版本。

哪个线程调用currentThread,就能获取到哪个线程对象的引用。

通过Thread.sleep()控制线程休眠。

Thread.sleep本质就是让线程的状态变成了"阻塞"状态,此过程就不参与cpu的调度了。直到时间到,这个线程的状态再次恢复成就绪状态,才能参与cpu调度(*此处只是恢复"就绪",而不是立即执行)

三、线程的状态

1.NEW状态

NEW:安排了工作,但还未开始行动。(Thread对象创建了,但是还没开始start。)

2.TERMINATED状态

TERMINATED:工作完成了。(线程执行完了(入口方法结束了),但是Thread对象还在)

3.RUNNABLE:可工作的,又可以分成正在工作中的和即将开始工作的。

就绪状态,随时可拿去cpu上执行。

代码中不触发阻塞类操作,都是RUNNABLE状态。

*操作系统中的线程,生命周期和Thread对象不完全一致。什么是生命周期:什么时候创建,什么时候销毁之间的一段周期。

阻塞状态:

1.BLOCKED:这几个都表示排队等着其他事情 (由于"加锁"产生的阻塞)

2.WAITING:这几个都表示排队等着其他事情(无超时时间的阻塞) join无参数版本

3.TIMED_WAITING:这几个都表示排队等着其他事情(有超时时间的阻塞)join有参数版本,或者是sleep

四、线程安全

某一段代码,在单线程环境下执行是正确的,但是放到多线程环境下去执行,就会产生bug。这就是线程安全问题:

e.g.

java 复制代码
package Thread;

public class Demo15 {
    public static int count = 0; // 共享变量,多个线程共同修改的变量,称为共享变量
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> { // 线程t1
            for (int i = 0; i < 5000; i++) { // 循环5000次
                count++; // 自增操作,相当于count = count + 1
            }
        });
        Thread t2 = new Thread(() -> { // 线程t2
            for (int i = 0; i < 5000; i++) { // 循环5000次
                count++; // 自增操作,相当于count = count + 1
            }
        });
        t1.start(); // 启动线程t1
        t2.start(); // 启动线程t2
        try { // 等待线程t1和线程t2执行完毕
            t1.join(); // 等待线程t1执行完毕
            t2.join(); // 等待线程t2执行完毕
        } catch (InterruptedException e) { // 捕获异常
            e.printStackTrace(); // 打印异常信息
        }
        System.out.println(count); // 打印count的值,应该是10000,因为每个线程都自增了5000次
        

    }

}

输出并不是10000(输出与预期不符合,这就是Bug)

t1和t2两个线程,在同时修改count这个变量,并且修改操作不是"原子的"*,这就会产生bug。

count++这样的操作,如果站在cpu指令的角度来说,其实是三个指令(指令就是机器语言,cpu执行的任务的具体细节,cpu会一条一条的读取指令,解析指令,执行指令)对于cpu来说,每个指令都是执行的最基本的单位。由于操作系统调度线程是"随机的",某个线程执行到任意一个指令的时候,都可能会触发cpu的调度。

count++本质上对应三个指令:

1.load:把内存中的数值,加载到cpu寄存器中

2.add:把寄存器中的数据进行加1操作,结果还是放到寄存器里面

3.save:把寄存器中的值写回到内存中

上述过程在多线程中进行执行的时候,会出现以下几种情况:

最后输出值为2的正确程序:

而其他的情况,最后结果并不正确!!

在整个循环5w次的过程中,也不知道有多少次是"正确"的情况,多少是"错误"的情况(线程调度顺序是随机的)。因此在宏观上,我们看到的最终结果肯定比10w次少,或者等于10w次。

出现错误的结果一定是<10w次,但是整个错误的结果,是否一定是>=5w次呢?------答案是可能的。

相关推荐
兔子蟹子3 分钟前
JAVA中Spring全局异常处理@ControllerAdvice解析
java·spring
prinrf('千寻)6 分钟前
项目右键没有add as maven project选项
java·maven
工业互联网专业10 分钟前
基于springboot+vue的健康健身追踪系统
java·vue.js·spring boot·毕业设计·源码·课程设计·健康健身追踪系统
杰仔正在努力14 分钟前
Java + Seleium4.X + TestNG自动化技术
java·开发语言·自动化
lynn-661 小时前
JAVA-使用Apache POI导出数据到Excel,并把每条数据的图片打包成zip附件项
java·apache·excel
振鹏Dong1 小时前
JVM | CMS垃圾收集器详解
java·jvm
情报员0071 小时前
Java练习6
java·算法·排序算法
andrew_12192 小时前
JVM的内存管理、垃圾回收、类加载和参数调优
java·jvm
百锦再2 小时前
Python深度挖掘:openpyxl和pandas的使用详细
java·开发语言·python·框架·pandas·压力测试·idea
microhex2 小时前
Glide 如何加载远程 Base64 图片
java·开发语言·glide