给我一首歌的时间,带你了解线程安全和死锁(2)
大家好,我是bug郭,一名双非科班的在校大学生。对C/JAVA、数据结构、Linux及MySql、算法等领域感兴趣,喜欢将所学知识写成博客记录下来。 希望该文章对你有所帮助!如果有错误请大佬们指正!共同学习交流
作者简介:
- CSDN java领域新星创作者blog.csdn.net/bug…
- 掘金LV3用户 juejin.cn/user/bug…
- 阿里云社区专家博主,星级博主,developer.aliyun.com/bug…
- 华为云云享专家 bbs.huaweicloud.com/bug…
线程不安全原因
-
线程是抢占性执行的,线程间的调度充满了随机性(线程不安全的根本原因)
-
多个线程对同一个资源进行了更改操作(如果是不同的资源,或者只是对一个资源进行读操作就不会出现线程不安全问题)
我们可以更改代码结构,让不同的线程对不同的变量进行更改就不会出现问题 -
针对变量的操作不是原子的!
因为对变量的操作的指令并不是一条!而是多条指令!
我们可以通过加锁操作,使多个指令打包成一个原子,避免线程不安全问题! -
内存可见性
什么是内存可见性呢?
就是编译器对cpu
操作的优化!
举个简单的例子:
当一个线程一直循环读一个数据时,我们的cpu
就要一直在内存中读数据,而我们知道内存读取的速度想必于cpu
中的寄存器慢了好几个数量级!那么这时编译器就进行优化,他懒得去内存中读取数据了,直接将数据保存在寄存器进行读取操作,而如果这时有另外一个线程对该数据进行修改,那么就会产生线程不安全问题!!!
我们的编译器都是由大佬编写的,所以在不改变逻辑性和结果的情况下,会对代码进行优化!!!
如何避免该问题呢?
我们可以采用synchronized
关键字对线程加锁或者使用volatile
关键字保证内存可见性! -
指令重排序导致线程不安全问题,这也是由于编译器的优化操作而导致的线程不安全问题!
我们的代码先后执行顺序有时候并不会影响我们的结果,那么这时编译器在不改变代码逻辑的基础上就会改变一下顺序,提高运行效率,而这个操作在多线程往往会出现线程不安全问题!
这里也可以使用synchronized
关键字避免指令重排列!
synchronized
关键字
我们已经了解到了线程不安全问题可以用java
提供的synchronized
关键字来避免!
我们来学习synchronized
如何使用!
- 修饰实例方法
class Count{
int count=0;
//对实例方法进行加锁
synchronized public void increase(){
count++;
}
}
这里的加速操作就是相当于对该实例的对象(this
)进行加锁,而代码底层又是如何完成这个加锁操作的呢?
我们知道一切对象的父类都是Object
类,而我们创建一个类,除了有我们描述的基本属性外,java
还会自动开辟一块空间保存对象头
信息!显然我们没有听说过,这里的属性是给jvm
使用的!我们程序员并没有用!而加锁就是在对象头
设置一个锁的标志位!
如果多个线程对同一个锁进行操作就会有锁竞争
对不同的锁进行操作就不会出现锁竞争!
- 修饰代码块
java可以在任意位置加锁!,但修饰代码块时我们需要指明加锁的对象!
class Count{
int count=0;
public void increase(){
synchronized(this){//指明加锁对象!
count++;
}
}
}
- 修饰静态方法
class Count{
static int count=0;
public static void increase(){
synchronized(Count.class){//修饰静态方法!
count++;
}
}
}
当synchronozed
修饰静态方法时,并不能对this
加锁,因为这是类方法!
我们可以采用反射的反射对类对象进行加锁!!!
死锁
死锁类型
- 一个线程一把锁
我们知道synchronized
关键字可以给对象加锁!那如果我们不小心给同一个对象加锁两次,会出现什么情况呢?
class Count_1{
private int count=0;
synchronized public void increase(){ //外层锁
synchronized (this){ //内层锁
count++;
}
}
}
假设synchronized
是不可重入锁,就是不能进行多次加锁!
我们的外层锁,在进入方法后就会对该对象进行加锁,有效加锁!而里层锁,会一直阻塞等待外层锁释放锁,才会进行加锁,此时代码就阻塞在这里了! 而外层锁要方法执行结束,才能释放锁!
显然现在的情形是谁也不让着谁!这就导致了一个尴尬的局面,就是我们所说的死锁
!
就好比生活中的例子:
你手机没电了,要先老板借个充电宝!老板说,你先付钱我就借你,而你的手机已经关机了,又没带现金,然后你说,你借我我就付钱!然后就两个阿叉棍在哪死锁了!!!
不过我们写jvm
的大佬设计时,将synchronized
设置成了可重入锁!
多次加锁并不会发生死锁!
加锁: 如果我们现在有一个线程
t1
,对象a
加锁后,t1
线程拿到了该锁!synchronized
就会在锁信息中记入该线程信息,还有标志该线程的加锁次数为1,如果t1
线程再次对a
加锁,那么并不会真正的再次加锁,只会把加锁次数加一!
解锁:如果该线程解锁,锁信息就会将锁次数减一,直到锁次数为0,此时该线程就将该锁释放!
显然jvm
这样可重入锁设置,如果我们多次对一个对象加锁,会我们的运行速度降低,但是这样提高了我们人力成本,如果为不可重入锁,但造成死锁,那该程序就会中断,我们需要花大量时间进行调试!!!
-
两个线程两把锁
假设一种情况:
当有两个人手机需要充电,而一个人有充电头,一个人有数据线,然后想要充电的话,需要两者结合,然后两个人都比较倔,谁都不肯退让,这就造成了死锁! -
N个线程M把锁
这里就需要讲到教科书上的经典案例:哲学家就餐问题
假如有一群哲学家围在一个圆桌上干饭,然后他们干饭的时候还会思考人生,思考人生时不那筷子吃饭! 他们吃饭和思考人生是随机的!
假如有5个人,5根筷子! 显然筷子不够!但是就将用!
每个哲学家的两边都分别有一根筷子!
情形如下:
而且哲学家都比较倔,当他们拿到一根筷子时,如果没有筷子了,他们会一直等待,直到又一双筷子,才会干饭!
如果哲学家同时拿一根筷子时,就会造成死锁!
这顿饭估计永远都结束不了!!!
如何解决上面的问题呢?
我们可以先个筷子编个号,然后让哲学家们约定好,先拿编号小的筷子,再拿编号大的筷子,如果编号小的筷子被拿了,那就一直等待!
这样一约定,哲学家就可以将这顿饭干完了!
- 点赞
- 收藏
- 关注作者
评论(0)