1. 观察线程不安全
开局我们先看一段代码:
public class testDemo { static class Counter{ private int count; public void isAdd() { count++; } public int getCount() { return count; } } public static void main(String[] args) throws InterruptedException { Counter counter=new Counter(); Thread t1=new Thread(()->{ for (int i = 0; i < 5000; i++) { counter.isAdd(); } }); Thread t2=new Thread(()->{ for (int i = 0; i < 5000; i++) { counter.isAdd(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.getCount()); } }
该代码是t1对Counter类中的count进程加5000操作,t2对Counter类中的count进程加5000操作,因为两个线程都是向count加5000,在我们的理解中count的值应该是10000,运行代码我们得到:
观察代码可以看出,虽然我们我们理论是将count共相加到10000,但是结果却告诉我们,count并没有相加到10000这个值,这是为啥呢?我们下面开始说。
2.线程安全的概念
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
3.线程不安全原因
回到2.1中,我们现在开始解释为啥没有加到10000?这是因为在isAdd()函数中,count++并不是原子性的,它这个操作本质上是三个cpu指令组成:
load,把内存中的数据读取到工作内存(寄存器或缓存)中
add,将工作内存中的值执行+1操作
save 把工作内存(寄存器或缓存)中的数据读取到内存中
因为在线程运行时,它们之间时并发执行的,我们无法确定它先运行那一步,也就是说,我们无法确定load、add、save这三个步骤谁先执行,因此我们就可能会出现以下情况。
可以看出它并不会仅仅按照我们想要的那要运行,那像情况1、2、3这样运行会由什么影响呢?
我们就拿情况1来举例子:
- 先t1(load):将内存中的值读入到t1的工作内存当中;再t2(load):将内存中的值读入到t2的工作内存当中。
- 然后执行t2(add):把t2的工作内存放入到内存当中;再执行t2(save):将t2工作内存中给的数据放入到内存当中。
- 之后在执行t1(add):将t1工作内存中的数据进行+1操作;在执行t1(save):这里注意了,在此之前t2执行save时,内存中已经放入了1,这里我们仍放入的sh是1,因此,我们这样操作是导致我们count 的值不是10000原因之一。
上方线程不安全原因总结:由于当前这两个线程调度顺序是无序的,我们也不知道这两个线程自增过程中,到底经历了什么?有多少此"顺序执行",又有多少次“交错执行”,因此得到的结果是啥也就是变化的。
线程不安全原因可分为四种:
3.1抢占式执行
这个就是我们上方举得例子,可谓是罪魁祸首,万恶之源了。
3.2修改操作,不是原子性的
原子性的概念:不可分割的最小单位称为原子性
一条java指令也有可能不是原子的,就像我们上方count++操作,它就是由3个cpu指令构成的。
3.3内存可见性,引发的线程不安全
可见性:一个线程对共享变量值的修改,能够及时地被其他线程看到
上方举得例子算是比较直白的了,初始情况下, 两个线程的工作内存内容一致,一旦线程1修改了 a 的值, 此时主内存不一定能及时同步. 对应的线程2 的工作内存的 a 的值也不一定能及时同步,这个时候代码中就容易出现问题。
3.4指令重排序引发的线程不安全
为什么要进行指令重排序呢?这是因为,编译器觉得我们代码按照我们给定的顺序不是最高效了,因此就对我们的代码进行了指令重排序。
上方说的比较机械,举一个例子来帮助了解以下,我把我们 写的指令比喻成要完成的任务,映射到一个事务上,如买菜,我们需要购买1.黄瓜 、2.豆角、3.鸡肉、4.大米,如图:
按照我们代码的顺序应该就是先买黄瓜,之后去买豆角,再买鸡肉,最后买大米:
这样购买确实能满足购买需求,但是走的路程就多了。如果经过指令重排序后,它就是进来先购买大米,之后去买鸡肉,再买黄瓜,最后买豆角。这样就大大提升了效率。
3.5如何结果上方不安全的问题
两个字:加锁 !
在isAdd()方法前面加上synchronized关键字
public class testDemo { static class Counter{ private int count; synchronized public void isAdd() { count++; } public int getCount() { return count; } public void setCount(int count) { this.count = count; } } public static void main(String[] args) throws InterruptedException { Counter counter=new Counter(); int i=1; while (i<10) { Thread t1=new Thread(()->{ for (int j = 0; j < 5000; j++) { counter.isAdd(); } }); Thread t2=new Thread(()->{ for (int j = 0; j < 5000; j++) { counter.isAdd(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.print("第"+i+"次运行:"); System.out.println(counter.getCount()); i++; counter.setCount(0); } } }
我们再次运行这个代码,代码结果就一直是10000了: