NEW:表示我们现在已经把线程类创建出来的,但是现在还没有使用start()方法进行线程启动。
TERMINATED:表示线程在执行结束该线程中的代码之后,那么系统中的这个线程就销毁了,但是我们以前创建出的Thread实例依然还在。
在上述中的两个状态 NEW和TERMINATED都是java自己搞出来的,和咱们操作系统中的PCB没有任何关系。
RUNNABLE:表示此时我们创建出来的线程,在就绪队列中。此时在就绪队列中的线程,随时都可以加载到CUP上去执行。此时在就绪队列中的线程要么是 正在执行,要么就是还没有被执行,但将要被进行调度。总的来说,在线程中如果没有出现sleep()操作,也没有导致线程阻塞的操作,那么此时的线程状态大概率都是处于RUNNABLE状态的。
TIMED_WAITING:**表示当前线程中存在阻塞。**在代码中存在sleep()方法,或者join(超时时间)这样的能够导致线程进入阻塞状态的方法。
BLOCKED也表示当前线程存在阻塞状态,一般我们在对线程进行加锁的时候,就会触发该状态。当时我们现在还没有仔细介绍加锁操作,所以在这里不做详细介绍。
WAITING:也表示当前线程存在阻塞状态,一般我们在等待线程唤醒的时候,也就是使用wait()方法使线程处于阻塞状态,之后通过notify()方法把线程唤醒。在后期的博客中我们就会介绍到wait()和noitfy()两个方法。
我们学习了上述的线程状态,那么为什么java要对线程进行细分呢?
因为通过这样对线程状态的细分,这样可以在程序员工作的时候减轻负担。也就是简单的说我们在日常的程序开发过程中,可能会遇到"线程卡死"的时候,第一步就可以先看看当前程序里的各种关键的相爱难成所处的状态。在这里我们也知道了线程阻塞的3个不同的状态。
那么如果我们在开发的过程中,对线程的状态进行检查。
如果此时的阻塞状态为 WAITING 那么就是线程在等待唤醒
如果当前的阻塞状态为 BLOCKED 那么就可以推断当前别的线程在进行加锁操作,加锁结束后才能执行这个线程。
如果当前的阻塞状态为 TIME_WAITING 那么就可以推断当前线程处于阻塞状态,过一段时间之后,阻塞就会被解除。
线程状态转换图:
在上述转换图中 NEW—>RUNNABLE—>TERMINTED是主线任务,其他都是支线任务。在我们执行线程代码的时候,最主要执行的是RUNNABLE一定是主线任务。在RUNNABLE中执行一些特殊的操作,使线程进入阻塞状态。
线程安全问题这一块的知识是非常重要的,未来在面试的时候,只要面试官问到了相关多线程的问题,可定逃不过线程安全。
同时在我们日常开发的时候,也会经常会遇到线程安全的相关问题。
我们可以这样理解,操作系统调度线程的时候是随机调度的(抢占式执行),正因为这样的随机性,就可能导致程序在执行的时候出现一些bug!!!
如果因为这样的随机性调度,引入了bug,那么就认为当前线程是线程不安全的。
如果因为杨洋的随机性调度,没有引入bug,那么就认为当前线程是线程安全的。
这里的线程安全指的是在代码执行的时候没有bug产生。我们平时所说的安全都是关于黑客是不是侵入了你的计算机,破坏你的系统。
使用两个线程对同一个整形变量实现自增效果。
在两个线程中,每个线程都对这个整形变量自增5000次,看看最后这个整形变量自增的结果是多少。
代码如下:
在上述的代码中,两个线程对一个整形变量进行自增,我们可以从运行结果中看出不是我们预期得到的结果,那么此时就产生了bug!!!线程就会不安全
那么为什么会线程不安全呢?
其实原因只有一个,那就是两个线程在对一个整形变量进行自增的时候,同时自增的时候,原本是在原来count的基础上对其进行增加2,但是由于多线程的随机性,就在原来的基础上就只增加了1
我们要从count++入手,那么count++到底干了什么呢?
我们此时就要站到CPU的角度来看待,count实际上增加了两个
其实在CPU上count++分为了3个指令。
那么我们在看看两个线程在对一个整形变量进行自增的时候,count++在CUP和内存中指的变化。
其实累加的结果是5000----10000之间。
程序在并发相加的时候,有的时候可能是串行执行的(+2),有的时候是交错执行的(+1),具体串行的有多少,交错的有多少,咱们不知道,都是随机执行的。
在极端情况下:
如果所有的操作都是串行执行的,此时的结果就是10000(可能出现,但是是下概率事件)
如果所有的操作都是交错执行的,此时的结果就是5000(可能出现,但是也是小概率事件)
加锁!!!
但是这样变成串行的,那么就和单向成没撒区别了。
加了锁之后,并发的程度就降低了,此时数据就更可靠了,但是速度就慢了。此时肯定有些童鞋会想有没有一种方案,即可以计算正确,执行线程代码的速度又不会变慢的方法呢?
很可惜没有,要线程安全总是要付出一点代价的嘛。
还有童鞋想,既然加锁之后,线程就变成串行执行了,那么在以后的日常开发中,多线程都是串行执行了吗,串行执行和单线程执行没有区别呀?
其实我们在日常开发的时候,一个线程中的任务有很多。
例如:有四个步骤,步骤一… 步骤二… 步骤三… 步骤四… 其实很可能只有在步骤四中会涉及到线程安全问题。只针对步骤4加锁即可,此时上面的步骤1,2,3都可以并发执行
加锁之后的执行代码:
总结:
线程是抢占式执行的,线程间的调度充满了随机性(线程不安全的万恶之源) 虽然是根本原因但是我们还是无可奈何
在多个线程中,对一个变量进行修改(如果多个线程对一个变量读没事,如果多个线程对不同的变量修改也没事) 解决办法:可以通过调整代码的结构,使不同的线程操作不同的变量。
针对变量的操作不是原子的
一个操作就相当于一个原子,如果在一个程序中有多个指令操作,将几个操作打包成一个整体,要么成一个整体统一执行,要么操作都不执行。
我们学了加锁操作,就可以利用加锁操作,让原本针对变量操作不是原子性的,加上synchronized之后,把针对变量操作的指令打吧成一个整体,统一执行打包后的指令。
在上述两个线程针对同一个变量自增时,针对count++中的3个指令操作,进行打包,把它整成原子性的,这样就线程安全了。
**我们现在就可以解锁synchronized的一个功能:**synchronized关键字可以让原本针对变量的操作不是原子的变成原子的。
内存可见性问题
内存可见性问题,也会影响到线程安全。
针对同一个变量,一个线程进行读操作(循环进行很多次),一个线程进行修改操作(何时的时候执行一次)
如图:
在该图中,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编译器进行代码优化产生的效果,编译器就会对程序员写出的代码做出一些调整,保证原有逻辑不变的前提下,程序的执行效率能够大大提升。但是在多线程中是可能翻车的,多线程代码在执行的时候的一个不确定性,编译器编译阶段,很难预知执行行为。进行优化很可能会产生误判。
那么解决内存可见性问题的方法是什么呢?
我们可以使用synchronized关键字
synchronized不更能保证指令的原子性,同时也能保证内存可见性,被synchronized修饰过得代码,编译器就不会轻易的做出上述假设,相当于手动禁用了编译器的优化。
使用volatile关键字
volatile和原子性无关,但是能够保证内存可见性,禁用编译器做出的优化,编译器每次判定相等都会重新从内存中读取isQuit的值。
使用volatile 关键字
PS:内存可见性是属于编译器优化范围内的一个典型案例。编译器优化本身就是一个玄学问题,对于普通的程序员来说,啥时候优化,啥时候不优化,很难说。
5.指令重排序,也会影响到线程安全问题
指令重排序也是编译器优化的一种操作。
这样的重排序,在多线程中也屡见不鲜,如果代码时单线程的,编译器一般都很准。但是如果是多线程程序的话,编译器就可能会产生误判。
那么如何解决指令重排序问题呢?
我们还要使用synchronized,现在我们已经解锁了synchronized使用的三个场景,它不光能保证原子性,还可以保证能存可见性,同时还能禁止指令重排序。