【多线程】线程的状态和线程安全问题

线程的状态

1.NEW

NEW:表示我们现在已经把线程类创建出来的,但是现在还没有使用start()方法进行线程启动

代码示例:
请添加图片描述

2.TERMINATED

TERMINATED:表示线程在执行结束该线程中的代码之后,那么系统中的这个线程就销毁了,但是我们以前创建出的Thread实例依然还在

代码示例
请添加图片描述

3.小结一下

在上述中的两个状态 NEW和TERMINATED都是java自己搞出来的,和咱们操作系统中的PCB没有任何关系。

4.RUNNABLE

RUNNABLE:表示此时我们创建出来的线程,在就绪队列中。此时在就绪队列中的线程,随时都可以加载到CUP上去执行。此时在就绪队列中的线程要么是 正在执行,要么就是还没有被执行,但将要被进行调度。总的来说,在线程中如果没有出现sleep()操作,也没有导致线程阻塞的操作,那么此时的线程状态大概率都是处于RUNNABLE状态的。

代码演示:
请添加图片描述

5.TIMED_WAITING

TIMED_WAITING:**表示当前线程中存在阻塞。**在代码中存在sleep()方法,或者join(超时时间)这样的能够导致线程进入阻塞状态的方法。

代码示例:
请添加图片描述

6.BLOCKED

BLOCKED也表示当前线程存在阻塞状态,一般我们在对线程进行加锁的时候,就会触发该状态。当时我们现在还没有仔细介绍加锁操作,所以在这里不做详细介绍。

7.WAITING

WAITING:也表示当前线程存在阻塞状态,一般我们在等待线程唤醒的时候,也就是使用wait()方法使线程处于阻塞状态,之后通过notify()方法把线程唤醒。在后期的博客中我们就会介绍到wait()和noitfy()两个方法。

8.总结

我们学习了上述的线程状态,那么为什么java要对线程进行细分呢

因为通过这样对线程状态的细分,这样可以在程序员工作的时候减轻负担。也就是简单的说我们在日常的程序开发过程中,可能会遇到"线程卡死"的时候,第一步就可以先看看当前程序里的各种关键的相爱难成所处的状态。在这里我们也知道了线程阻塞的3个不同的状态。

那么如果我们在开发的过程中,对线程的状态进行检查。

如果此时的阻塞状态为 WAITING 那么就是线程在等待唤醒

如果当前的阻塞状态为 BLOCKED 那么就可以推断当前别的线程在进行加锁操作,加锁结束后才能执行这个线程。

如果当前的阻塞状态为 TIME_WAITING 那么就可以推断当前线程处于阻塞状态,过一段时间之后,阻塞就会被解除。

线程状态转换图:
请添加图片描述
在上述转换图中 NEW—>RUNNABLE—>TERMINTED是主线任务,其他都是支线任务。在我们执行线程代码的时候,最主要执行的是RUNNABLE一定是主线任务。在RUNNABLE中执行一些特殊的操作,使线程进入阻塞状态。

线程安全

线程安全问题这一块的知识是非常重要的,未来在面试的时候,只要面试官问到了相关多线程的问题,可定逃不过线程安全。

同时在我们日常开发的时候,也会经常会遇到线程安全的相关问题。

1.什么是线程安全,什么又是线程不安全?

我们可以这样理解,操作系统调度线程的时候是随机调度的(抢占式执行),正因为这样的随机性,就可能导致程序在执行的时候出现一些bug!!!

如果因为这样的随机性调度,引入了bug,那么就认为当前线程是线程不安全的。

如果因为杨洋的随机性调度,没有引入bug,那么就认为当前线程是线程安全的。

这里的线程安全指的是在代码执行的时候没有bug产生。我们平时所说的安全都是关于黑客是不是侵入了你的计算机,破坏你的系统。

2.一个线程不安全的案例

使用两个线程对同一个整形变量实现自增效果。

在两个线程中,每个线程都对这个整形变量自增5000次,看看最后这个整形变量自增的结果是多少。

代码如下:

请添加图片描述
在上述的代码中,两个线程对一个整形变量进行自增,我们可以从运行结果中看出不是我们预期得到的结果,那么此时就产生了bug!!!线程就会不安全

那么为什么会线程不安全呢?

其实原因只有一个,那就是两个线程在对一个整形变量进行自增的时候,同时自增的时候,原本是在原来count的基础上对其进行增加2,但是由于多线程的随机性,就在原来的基础上就只增加了1

我们要从count++入手,那么count++到底干了什么呢?

我们此时就要站到CPU的角度来看待,count实际上增加了两个

其实在CPU上count++分为了3个指令。

  1. 把内存中的count的值加载到CUP寄存器中。

  2. 把寄存器中的值,给+1

  3. 把寄存器中的值写回到寄存器中。

    图示:

    请添加图片描述

那么我们在看看两个线程在对一个整形变量进行自增的时候,count++在CUP和内存中指的变化。

请添加图片描述那么有些同学就会问,为什么能自增到8000,为什么就不能够自增到4000,3000呢?

其实累加的结果是5000----10000之间。

程序在并发相加的时候,有的时候可能是串行执行的(+2),有的时候是交错执行的(+1),具体串行的有多少,交错的有多少,咱们不知道,都是随机执行的。

在极端情况下:

如果所有的操作都是串行执行的,此时的结果就是10000(可能出现,但是是下概率事件)

如果所有的操作都是交错执行的,此时的结果就是5000(可能出现,但是也是小概率事件)

3.如何解决线程不安全问题?

加锁!!!

请添加图片描述

但是这样变成串行的,那么就和单向成没撒区别了。

加了锁之后,并发的程度就降低了,此时数据就更可靠了,但是速度就慢了。此时肯定有些童鞋会想有没有一种方案,即可以计算正确,执行线程代码的速度又不会变慢的方法呢?

很可惜没有,要线程安全总是要付出一点代价的嘛。

还有童鞋想,既然加锁之后,线程就变成串行执行了,那么在以后的日常开发中,多线程都是串行执行了吗,串行执行和单线程执行没有区别呀?

其实我们在日常开发的时候,一个线程中的任务有很多。

例如:有四个步骤,步骤一步骤二步骤三步骤四其实很可能只有在步骤四中会涉及到线程安全问题。只针对步骤4加锁即可,此时上面的步骤1,2,3都可以并发执行

加锁之后的执行代码:

请添加图片描述

总结:

  1. 在java中加锁操作有很多种,最常见的就是适应synchronized这样的关键字给方法直接加上,此时进入方法,就会自动加锁,离开方法就会自动解锁。
  2. 当一个线程加锁成功之后,要想对其他的线程进行加锁,就会产生阻塞,此时对应的线程就处于BLOCKED状态。
  3. 阻塞会一直持续到,占用锁的线程把所释放为止。
  4. 不是所有的线程都需要加锁,如果所有的线程都进行加锁,那么多线程就会形同虚设。

4.造成线程不安全的4大主要原因,和解决办法

  1. 线程是抢占式执行的,线程间的调度充满了随机性(线程不安全的万恶之源) 虽然是根本原因但是我们还是无可奈何

  2. 在多个线程中,对一个变量进行修改(如果多个线程对一个变量没事,如果多个线程对不同的变量修改也没事) 解决办法:可以通过调整代码的结构,使不同的线程操作不同的变量。

  3. 针对变量的操作不是原子的

    一个操作就相当于一个原子,如果在一个程序中有多个指令操作,将几个操作打包成一个整体,要么成一个整体统一执行,要么操作都不执行。

    我们学了加锁操作,就可以利用加锁操作,让原本针对变量操作不是原子性的,加上synchronized之后,把针对变量操作的指令打吧成一个整体,统一执行打包后的指令。

    在上述两个线程针对同一个变量自增时,针对count++中的3个指令操作,进行打包,把它整成原子性的,这样就线程安全了。

    **我们现在就可以解锁synchronized的一个功能:**synchronized关键字可以让原本针对变量的操作不是原子的变成原子的。

  4. 内存可见性问题

    内存可见性问题,也会影响到线程安全。

    针对同一个变量,一个线程进行读操作(循环进行很多次),一个线程进行修改操作(何时的时候执行一次)

    如图:

    在该图中,t1线程在不停的在内存中读取数据,t2线程在合适的时候对一个变量进行修改。

    请添加图片描述

    相关代码:

    请添加图片描述

    我们此时可以看到在运行结果中,没有出现 t1线程执行结束,那么证明t1线程还在执行中,整个程序还没有执行结束。

    当我们把isQuit的值进行改变之后,本应该在t1线程中的while循环,应该会跳出循环,执行sout语句,但是它没有,这就会带来bug,从而就会产生线程不安全问题。

    其实就是如果main线程中的isQuit迟迟没有进行修改,在t1线程中的while循环中的 isQuit == 0,每次都要在内存中读取isQuit的数据,并进行判断,他会发现isQuit的值始终没有得到修改。那么为了代码的优化,直接在寄存器中读取isQuit的数据,但是如果main线程中的isQuit的值发生改变,那么t1线程就不会感知到isQuit的值发生改变,t1线程就会一直在寄存器中读取isQuit的数据。

PS:其实产生内存可见性问题的原因就是 java编译器进行代码优化产生的效果,编译器就会对程序员写出的代码做出一些调整,保证原有逻辑不变的前提下,程序的执行效率能够大大提升。但是在多线程中是可能翻车的,多线程代码在执行的时候的一个不确定性,编译器编译阶段,很难预知执行行为。进行优化很可能会产生误判。

那么解决内存可见性问题的方法是什么呢?

  1. 我们可以使用synchronized关键字

    synchronized不更能保证指令的原子性,同时也能保证内存可见性,被synchronized修饰过得代码,编译器就不会轻易的做出上述假设,相当于手动禁用了编译器的优化。

  2. 使用volatile关键字

  3. volatile和原子性无关,但是能够保证内存可见性,禁用编译器做出的优化,编译器每次判定相等都会重新从内存中读取isQuit的值。

  4. 使用volatile 关键字
    在这里插入图片描述
    PS:内存可见性是属于编译器优化范围内的一个典型案例。编译器优化本身就是一个玄学问题,对于普通的程序员来说,啥时候优化,啥时候不优化,很难说。

5.指令重排序,也会影响到线程安全问题

指令重排序也是编译器优化的一种操作。
请添加图片描述
这样的重排序,在多线程中也屡见不鲜,如果代码时单线程的,编译器一般都很准。但是如果是多线程程序的话,编译器就可能会产生误判。

那么如何解决指令重排序问题呢?

我们还要使用synchronized,现在我们已经解锁了synchronized使用的三个场景,它不光能保证原子性,还可以保证能存可见性,同时还能禁止指令重排序。

  • 10
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小周学编程~~~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值