13-从Java字节码的角度看线程安全性问题

本讲就来了解线程安全性问题,线程安全性问题是一个非常复杂,就是说,在没有充足的同步的情况下,多个线程中的操作的执行顺序是不可预测的,那么,可能就会,我们说在单线程中正常执行的问题,那么在多线程中可能就会出现非常奇怪的问题,这就是所谓的线程安全性问题,当然了,对线程安全性问题没有一个统一的结论,各有各的说法,其中有一条就是说,能够符合预期的执行结果,那么这个多线程这个线程就没有安全性问题,不符合预期的执行结果那么就会有线程安全性问题,那么,我们不去扣它的概念,那么,我们就来举一个例子来看一下线程安全性问题,我们来写一个数值序列生成器,

13-从Java字节码的角度看线程安全性问题

我们在这个类里面实现一个非常简单的方法,就是获取下一个值的方法,那么,获取下一个值,首先我们需要有一个变量来保存当前的值,然后提供一个方法获取下一个值,

13-从Java字节码的角度看线程安全性问题

这样我们就知道,数值的生成器我们就写好了,那么,当我们程序在调用的过程中,我们就可以

13-从Java字节码的角度看线程安全性问题

13-从Java字节码的角度看线程安全性问题

不管怎么执行,它肯定是自增的,而且是符合我们的预期,在不停的往下执行的。这是我们之前所了解的,这是完全没有问题的,那么,在我们多线程环境下,它就有可能会出现不可预期的问题,它执行的顺序是完全不可预期的,我们就用一个多线程来调用,多个线程一块调用,我们写一个Runnable接口,然后多个任务一块去执行,我们可以这样去写

13-从Java字节码的角度看线程安全性问题

在这里面我们就来调用它的序列的生成器,我们让它不停的去掉用序列生成器,并且我们要知道是哪一个线程在调用的,

13-从Java字节码的角度看线程安全性问题

这样就不停的在调用了,为了方便我们查看,我们让它休息一会,调用一次休息100毫秒,

13-从Java字节码的角度看线程安全性问题

然后,我们把它创建多个,

13-从Java字节码的角度看线程安全性问题

现在是三个线程一块执行,

13-从Java字节码的角度看线程安全性问题

发现出问题了。我们任务数值序列生成器肯定应该是产生一个唯一的,现在你里面产生重复的数值了,这样显然是不行的,那么,我们发现这就是所谓的线程安全性问题,那么,这个问题是怎么出现的呢?线程是多个顺序执行流。

在这里面有多个线程

13-从Java字节码的角度看线程安全性问题

那么,每一个线程是独立的,它都会执行这么一段代码,

13-从Java字节码的角度看线程安全性问题

其实就这一行,都执行value++这个操作,并且返回value++这个操作,value++是个什么意思呢?value = value + 1,也就是说,value的值先加1然后在赋给value,所以,value++其实并不是一步,它其实相当于是两步操作。

我们来直接分析这段代码生成的字节码文件,

13-从Java字节码的角度看线程安全性问题

这里面有内部类,我们就不用管内部类了,我们关注的是Sequence.class,那么,我们如何来解析这个Sequence.class呢?我安装了一个查看二进制文件的软件

13-从Java字节码的角度看线程安全性问题

Sequence.class其实就是一个二进制文件,我们说,字节码文件我们应该能够读懂,肯定是能够读懂字节码文件的,因为字节码文件的格式就是我们人为来规定的,那么,我们按照它规定的格式再反过来去读,显然是能够读懂的。我们能够读懂字节码文件,同样的工具也能够读懂字节码文件,JDK给我们带了一个工具非常好用,这个工具叫做javap,就可以来分析我们的字节码文件,

13-从Java字节码的角度看线程安全性问题

这个命令后面跟上我们要查看的字节码文件

13-从Java字节码的角度看线程安全性问题

 

13-从Java字节码的角度看线程安全性问题

这就是我们的字节码,我们这里只需要看getNext()方法

13-从Java字节码的角度看线程安全性问题

也就是说,我们的Java代码被翻译成了字节码指令,

13-从Java字节码的角度看线程安全性问题

所以,这就是一堆的执行过程,其实它执行的并不是Java代码,而是字节码,所以,我们发现,value++这个操作并不是一步完成的,而是经过了多步完成的,我们只是选取几小段字节码代码进行演示

13-从Java字节码的角度看线程安全性问题

多个线程在一块执行,每一个线程都有一个程序计数器,那么,就按着程序计数器往下去执行,iadd就是执行两个数相加的,加完之后,putfield把它给设置成值,只要知道iadd和putfield这两个就可以了,iadd是执行两个数相加,putfield是把相加的结果赋给后面的结果。最后ireturn是return。我们主要是iadd和putfield这两步。比如说第一个线程执行到第17行了,执行完了,我们定义的这个变量是value,本来这个value是0,我们知道,类的实例化的对象是放到堆内存中的,堆是属于线程共享的区域,程序计数器是线程独享的区域,也就是说,我们的value这个变量是处于多个线程共享的一块区域,它属于一个公共区域里面,最初始的时候它的值为0,当第一个线程执行完iadd之后,value的值就变成1了吗?没有啊,它只是在我们的操作数栈中变成1了,但是它还没有去设置value的值,因此,此时value的值还是0,不要认为执行完iadd之后value的值就变成1了,此时,如果第二个线程抢到了CPU的执行时间片,于是,它也在执行iadd,

13-从Java字节码的角度看线程安全性问题

那么,它在执行iadd的时候,其实上面有一个

13-从Java字节码的角度看线程安全性问题

它其实是把getfield获取这个值,它首先有一步是获取到这个值,在获取的时候,此时它获取到的value的值是0,因为1还没有写进去呢?所以,这个线程获取到的是0,然后,这个线程在iadd之后,相加后的结果是1,如果iadd之后,CUP的时间片又被第一个线程抢去了,于是第一个线程执行了ptufield,于是,处于多个线程共享区域的value这个变量的值变成了1。其实就是栈帧中的局部变量表中的value的值变成了1,然后第二个线程抢到了CUP时间片,开始执行putfield,执行往栈帧中的局部变量表中的value进行赋值的操作,也给value赋值为1,于是就出问题了,value的值本来应该为2才对,结果还是为1,这就是所谓的线程安全性问题,通过这个例子我们应该是能够理解了。问题遇到了,我们就应该解决,那么,如何解决线程安全性问题呢?其实非常简单,我们只需要在这里加一行

13-从Java字节码的角度看线程安全性问题

这样就不再会出现线程安全性问题了,我们再来执行,

13-从Java字节码的角度看线程安全性问题

不会再找到一个重复的或者漏掉的。这就是一个线程安全性的解决方法,解决方法就是加synchronized,让方法变成一个同步方法,那么,加上synchronized这个关键字是什么意思呢?这个我们后面会详细的去讲,包括它底层的原理,这个非常好理解,这里就是一个门加了一把锁,当一个线程进来了之后,它就会获取这把锁,然后把门锁上,其他的线程再来了就在外面等着,等这个线程执行完毕之后,这个线程把锁给释放了,那么,其他的线程才能够进来,所以,这样就导致在执行value++这段代码的过程中,同一时刻只有一个线程在执行,那么,就不会出现线程安全性问题了,当然了,解决线程安全性问题有非常多非常多的方法,也是我们后面要重点讲解的,其中,synchronized就是第一种,也是最原始的一种解决方案,现在只要知道synchronized可以解决线程安全性问题就可以了,后面我们会慢慢的展开来进行讲解。

关于线程安全性问题的演示,以及解决我们已经说完了,下面我们来总结一下,到底什么情况下会出现线程安全性问题呢?比如说这里,我不干value++这件事了,我直接返回value这个值

13-从Java字节码的角度看线程安全性问题

这样就不会出现线程安全性问题了,也就是说,对于变量的读操作,即使是多个线程也不会出现线程安全性问题,只要没有读写操作。++值即读了又写了。我们这里总结几点,具备以下这三个条件才会产生线程安全性问题。第一个是多线程环境下;第二个是多个线程共享一个资源;第三个是对共享资源进行非原子性操作。什么叫做非原子性操作呢?你这里即使在进行读写,如果++这个操作不可分割,也就是说,在

13-从Java字节码的角度看线程安全性问题

字节码里面,它就一条字节码,那么,就不会再出现线程安全性问题了,每一次一个字节就执行完了,这样就不会出现线程安全性问题了,只有进行了非原子性操作,才会出现线程安全性问题,也就是说,出现线程安全性问题必须具备以下三个条件,缺一不可。

13-从Java字节码的角度看线程安全性问题

那么,

13-从Java字节码的角度看线程安全性问题

如果没有这个共享变量value,那么,每一个方法持有它自己的变量的话,那么,它是不会具有线程安全性问题的,这里提到了共享变量的问题,共享资源的问题。

可能大家会有疑问,我们这里不是已经解决了线程安全性问题了嘛,为什么后面还要花大量的时间来解决线程安全性问题呢?我们刚才也提到这个问题了,当有线程进来的时候,别的线程必须在外面等待,那岂不是就成了串行的了,那么,多线程还有什么意义呢?所以我们在解决线程安全性问题的前提下,我们要尽可能的提高程序运行的性能,所以,我们才有了后面还要花大量的时间来解决线程安全性问题,如果我们不讲求性能的话,那么,加synchronized也就结束了。本讲就说到这里。