目录
一、什么是线程不安全?
二、如何引起的线程安全?怎么解决?
1)CPU调度执行是随机的,抢占式执行(根本原因,硬件层面咱们无法干预)
2)多个线程,对同一变量进行修改
3)修改操作中, 不是“原子”的(重点)
死锁是啥,怎么引起的? (重点)
4)内存可见性
5)指令重排序
总结-保证线程安全的思路
一、什么是线程不安全?
线程不安全:
是多个线程并发执行,而产生的结果和我们预期的不相同,这种bug是由多线程引起的,所以我们叫线程安全问题,也就是线程不安全。
比如看下列代码:
public class demo8 {private static int count=0;public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{for(int i=0;i<50000;i++){count++;}});Thread t2=new Thread(()->{for(int i=0;i<50000;i++){count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println("count="+count);}
}
我们预期是t1线程50000次,t2线程50000次,加起来一共100000次。这个是我们预期的结果。但是真实的情况如下:
是79506次,而且每一次的结果都不一样,这样和预期不符合,就是线程安全问题。
为什么会出现这样的情况呢?如果是单线程会如此吗?请看下文:
二、如何引起的线程安全?怎么解决?
上面那个count的例子来解释:
我们知道CPU把线程调度上去执行,最小单位是指令,我们count++其实是3个指令:
1)把值从内存中读取到CPU的寄存器上(load)
2)在寄存器上把count+1(add)
3)把寄存器加完之后的值传到内存中(save)
当然上面那样,是我们最理想,也是我们想要的状态,但是在多线程的加持下,因为CPU的调度是抢占式的,是随机的,我们并不能操控,可能刚执行了一半就给调走了。我们基本上很难遇到上面的情况,可能会出现以下的情况(简单列举了几种):
最小单位是指令,CPU不会执行半个指令,会给你执行完一个,但是count++有3个指令,我也不知道CPU什么时候调度走,如何给别人执行,这样就会造成bug,也就是我们说的线程安全问题!
既然我们明白了什么是线程安全,下面是五种会出现线程安全问题的原因,我们可以思考下面五种原因,从而去解决线程安全问题,或者去避免写出错误的代码:
1)CPU调度执行是随机的,抢占式执行(根本原因,硬件层面咱们无法干预)
CPU的调度执行具体的可以参考我往期的博客,这里就不赘述了。
2)多个线程,对同一变量进行修改
这个行不行,具体要看业务需求,但是对于Java来说,这并不是一个很好的解决方法,业务还有更好的方法。
如果是单个线程,对一变量修改,那就没有关系,不会出线程安全的BUG。
3)修改操作中, 不是“原子”的(重点)
这个来说,难道就是把类似像count++这类不是原子的,变为原子的吗?答:是的。
那咱们要怎么变啊?一条代码写多条?
答:给它加锁! (相当于一夫一妻制,锁上其他线程就进不来了,只能等他释放锁,其他线程才能进去)
如何使用锁?语法是啥?
关键字:synchronized
Object locker=new Object();
synchronized(locker){....}
如图
也就是: 既然大家都是串行化了,那效率会不会大大降低呢?
答:不会,因为这只是小部分需要加锁,其他代码都是不需要的。也就少了那么一点点,对于计算机来说,这些都是小case
注意事项:
1. 这个得都是同一个锁对象(才会发生锁竞争,就是堵塞),也就是传参(我这写为locker),如果一个是locker1一个是locker2,相当于2个厕所,互不影响,都能上厕所。
2.传参(locker)只要不是int,char这种简单类型,其他object子类都行,甚至写class对象都行
3.想要相互不影响的线程必须都得加锁,就是t1和t2的count都得加锁,相当于限制t1和t2只能上这个厕所,有一个人就得排队。如果只是一个加锁,t1打包好的3条指令很大程度会插入t2那3条指令中间。(如上图)
4.如果是多个线程一起加锁,t1执行完了,t2和t3谁先执行呢?是随机执行,抢占式调度。
5.synchronized还可以修饰方法
6.还可以修饰静态的方法,但是要用类对象做锁对象
7.用锁要考虑清楚,不然到时候某个线程堵塞了,什么时候恢复就不知道了
8.可重入锁,可连续加锁两次/多次,遇到加锁的会放行,不加第二次。(请看下面关于死锁的内容)
死锁是啥,怎么引起的? (重点)
Ⅰ:针对一把锁连续加锁两次
如上图,如果连续加两次锁,那么就会构成一个死循环,也就成了死锁。
但是要注意的是,在Java中synchronized可以自动判断,如果有加锁过了,是同一个锁对象的话,则在下次加锁的时候会放行,这也叫可重入锁。不会真正的报错,抛异常。
Ⅱ:两个线程两把锁
1)线程1在对A加锁,线程2对B加锁,
2)线程1在不释放A的情况下,要对B加锁;同时线程2在不释放B的情况下,对A加锁。
依照上面的情况,势必会僵持住,造成死循环,这也是死锁。
比如:
我和朋友在吃饺子,有醋有辣椒可以蘸饺子,我想拿醋放了点,朋友拿了辣椒放了点。这时候我又想要辣椒了,他又想用醋。这时他说你把醋给我,我再把辣椒给你。我说你先给我辣椒,我在给你醋。这时候我们就会僵持不下(当然这件事情可以商量,但是在程序中,代码的执行是死板的,就会变成死锁)
上述代码就是个例子。t1不释放locker1又要locker2,t2不释放locker2又要locker1,两个线程谁也不服谁。
Ⅲ:M个线程M把锁
和2个线程2把锁类似。哲学家都在等右手边的筷子,才能拿起筷子吃面条,但是没人放下右手边的筷子(因为他们也在等他们右手边的筷子才能吃完,吃完才能放下来)都僵持住了。
死锁的四个必要条件:
(1)锁是互斥的(锁的基本特性)
(2)锁是不可被抢占的(锁的基本特性)
比如线程1加锁了,没释放的前提下,线程2不能去抢
(3)请求与保持(代码结构)
比如线程1加锁A了,没释放A的前提下(在保持着),去加锁B(请求)
(4)循环等待/环路等待/循环依赖
多个线程获取锁,就可能会陷入死循环
由于1和2是锁的基本特性,所以我们无法避免。但是对于3来说,有些时候,我们是需要写成3那样的结构的。所以就要看4,只要4不成立,就不会产生死锁。
如何用4解决呢?
答:约定加锁的顺序,编号小的先加锁,如何编号大的再加锁
比如上面的代码,我们先让t1加锁locker1和locker2,再让t2加锁locker1和locker2。这样按顺序加锁就不会产生死锁了。
4)内存可见性
请看下图:
写了2个线程,想要t2输入值修改了n之后,t1的while中结束,因为n不为0了。但是我们输入后会发现并不会结束。为什么?这就是因为内存可见性
内存可见性同时也是bug,也是线程安全问题。但这并不是我们代码书写,而是JVM自己的问题,是JVM自己优化,优化出了问题。
这里的快慢并不是绝对速度的快慢,而是相对速度的快慢:
内存不可见性原因及原理:
JVM发现执行这个代码的时候,发现每次循环的过程中,执行1)操作,开销非常大,而且每次执行1)的结果都是一样的。
并且JVM根本没有意识到,用户未来会修改这个n,于是JVM就做了一个大胆的操作,直接把1)这个操作优化了。
也就是每次循环,不会重新读取内存中的数据,而是直接读取寄存器/cache中的数据。当做了这个操作后,循环的开销也就大大降低了,
但是t2对于在内存中的修改,t1是感知不到的,所以也就是t1的在“内存不可见性”。
为啥JVM要优化代码呢?
答:因为程序员的编程水平是参差不齐的,为了减少程序员的差距,降低程序员的门槛,JVM就会优化代码,让厉害的程序员和一般的程序员不会差距太明显。
如何解决内存可见性问题?
加入关键字:volatile,也就是在n那里加入volatile
在n上面加个volatile就能解决这个问题,volatile(易变的),也就是告诉JVM这个n是易变的,让JVM不要优化这个代码,让它在内存的修改能被读取到。 t1也就不会直接在寄存器上读取了。
5)指令重排序
指令重排序也是JVM自己优化出来的一个bug
比如你妈妈让你去菜单给你个清单,如下
如果你老老实实按清单的买,那就太慢了,而且要走很久,如果你调整一下顺序,则就能快很多,那么JVM也是这么想的,把指令重新调整一下,提高效率。这就叫做指令重排序。
这个是一个单例模式中懒汉模式的代码,一个程序只需要一个对象,所以如果没有创建,我可以创建出来一个,如果有了,我返回这个对象就行了。两层if是不一样的功能,第一层主要是为了不频繁加锁消耗资源(因为如果instance没被new出来,则需要加锁给new出instance,不然可能会被其他线程打断)。第二层是实现唯一的实例,new过就直接return就好了。
为什么这个代码也算线程不安全呢?
我们的JVM在执行new操作的时候,会把2和3调整一下
既然这个问题这么可怕,我们怎么解决这个问题呢?
答:用关键字volatile。
只要我们加上volatile,就告诉JVM这个instance是易变的,让JVM不要去优化它,这样就可以解决指令重排序的问题了。
总结-保证线程安全的思路
1. 使⽤没有共享资源的模型
2. 适⽤共享资源只读,不写的模型
a. 不需要写共享资源的模型
b. 使⽤不可变对象
3. 直⾯线程安全(重点)
a. 保证原⼦性
b. 保证顺序性
c. 保证可⻅性