Java中的加锁和死锁

整体架构流程

1.了解典型的线程安全问题(通过线程安全问题中的原子性提出加锁)

2.简单了解加锁(通过举例提出加锁的关键字synchronized)

3.了解synchronized的用法(通过synchronized提出死锁)

4.了解死锁

5.死锁的四个必要条件

6.如何避免死锁

技术名词解释

一.线程安全问题(5个典型原因,并不包含所有导致线程不安全的原因)

那么造成线程安全问题有些什么原因呢?可分为5点:

1.抢占式执行,随意调度(最根本原因,但我们无能为力)

2.多个线程同时修改统一变量(由代码结构决定,可以通过调整代码结构来规避,但普适性低)

3.原子性(如果修改操作是原子的那就罢了,但要是修改的操作不是原子的,那么出现问题概率就大大提高了)

4.内存可见性问题(一个线程读一个线程改,将导致读的结果不符合预期)

5.指令重排序(编译器自动优化代码出现bug,其实就是编译器觉得你写的代码太水看不下去了)

Tips:线程是否安全需要具体问题具体分析,并不是出现导致线程安全的原因线程就会不安全 ,也并不是没有出现导致线程不安全的原因线程就会安全

(原则就是只要多线程编写的代码不出现bug就是安全的)

二.简单了解加锁

其实加锁的由来就是针对线程安全问题中的原子性问题,将非原子性的操作,通过特殊手段(即枷锁)变成原子性操作

  左边的代码我们预期结果是输出count = 100000,可是多次运行发现count的最终结果总会小于预期值.这是因为线程安全问题1中抢占式执行随意调度的原因.存在无限种可能,下面随意列举一种吧.count最初的默认值为0,存在内存当中,t1调用add方法后,count的值被读取到了cpu1的寄存器中(此时在cpu1的寄存器中count的值为0),然后进行++的操作,此时cpu1寄存器中的count值变为1,这时t2调用add方法,也将内存里count的值(此时内存中count的值任为0)读取到了cpu2的寄存器中,然后也进行++操作后,此时cpu2寄存器中的count值也变为1,然后cpu1将自增后的count的值保存到内存中(此时内存中count的值为1),cpu2也再将自增后的count的值保存到内存中(此时会覆盖内存中原本的值,将cpu2中count = 1的值覆盖在了内存上),此刻经过两个线程的add后原本预期内存中count的值为2,但此时count为1比预期小了.                                                                                                                            由于cpu是随意调度的,所以造成上述结果的多种情况大概率会发生多次,这也就导致最终count的值总小于预期(当然要是运气特别好,cpu的每一次随意调度都正常执行那就会的到预期的值).对于这种情况我们就可以采取加锁的方式处理.代码如下:                               在方法这里加入synchronized就意味着进入这个方法就会加锁,出了这个方法就会自动解锁.如果两个线程同时加锁,就会一个加锁成功后,另一个进入阻塞状态(BLOCKED),直到枷锁成功后的线程释放锁后才可以从解除阻塞状态然后加锁.(其实加锁说是保证原子性,只是让其他也想进行操作的线程进入阻塞状态等待了,本质是将并发变成了串行),通俗点讲就是一个女孩子谈了一个男朋友的时候,男朋友就对这个女孩子进行了加锁操作,如果其他男生也想追求这个女孩子,就要等到这个女孩子和现任男朋友分手才可以(即解锁).当然加锁虽然会提高计算准确性,但由于加锁和释放锁都需要时间,将会降低cpu的计算速度(还是会比单线程运算快的).下面为加锁后的运算结果,count = 100000,达到了预期值.

注意counter对象只是被加锁了会产生线程阻塞,但是不影响这个对象的使用.

三.synchronized(监视器锁)的用法

1.修饰方法(synchronized public..或是public synchronized..都可以)

进入方法就加锁,出方法就解锁.加锁是要明确对哪个对象进行加锁的,如果两个线程对同一个对象加锁就会出现阻塞等待(锁竞争/锁冲突),如果两个线程对不同对象加锁就不会出现阻塞等待.修饰静态方法锁对象就是类对象(Counter.class), 修饰普通方法锁对象就是this,修饰代码块锁对象可以手动指定.

1)修饰静态方法

2)修饰普通方法

2.修饰代码块进入代码块就加锁,出代码块就解锁,()内不一定就要是this,可以为你指定的其他对象

3.synchronized的可重入性

一个线程针对同个对象连续加锁两次两次,如果没有问题就是可重入的,如果有问题就是不可重入. 

站在this的视角来看,自己已经被另外的线程占用了,但第二个线程和第一个线程是同一个线程,如果允许这个情况发生就是可重入的,如果不允许就是不可重入的.(不可重入将会导致线程僵住出现死锁的情况).放心,java里的synchronized是可重入的,不会因为这种情况出现死锁.

四.死锁

死锁是一个非常严重的问题,一旦出现死锁程序就会崩溃出现bug(无法执行后续操作)你的年终奖可能就要被这个死锁给带走咯,而且死锁十分隐蔽不容易发现(比如系统开发的时候并没有出现死锁,但等你系统发布上线后死锁就可能会出现),针对死锁问题可以借助像jconsole这样的工具来定位死锁位置,看线程的状态和调用栈.下面就来简单介绍下死锁的典型出现情况吧.

1.一个线程一把锁,连续加锁两次时,如果锁是不可重入锁就会发生死锁. 当然Java里的syncronized和ReentrantLook都是可重入锁,是不会发生这种情况的,但到了c++或是python或是操作系统原生的加锁API中就要考虑这种情况的发生了.

2. 两个线程两把锁,即t1和t2各自先对锁A和锁B进行加锁后,再尝试获取对方的锁.通俗讲就是小明要进公司上班,但是门禁卡放在公司里了,保安说要拿出门禁卡才可以进去,但小寰说要进去才可以拿门禁卡,这时就会发生死锁.左图就是这种死锁情况,运行代码后发现结果啥也没打印,说明代码根本没有运行到两个System.out.println语句中.这时由于t1获取到locker1时t2也获取到了locker2(保安获取到了进公司大门的资格,小寰获取到了在公司里拿门禁卡的资格),但t1要再获取locker2和t2要再获取locker1时就会获取不到进入阻塞等待(保安要再获取门禁卡,小寰要再获取进公司大门的资格,这时两人就会在门口).                         

3.多个线程多把锁(2的一般情况),案例参考哲学家的就餐问题.

五.死锁形成的四个必要条件

1.互斥使用:线程1拿到了锁,线程2要想再拿到这把锁就必须进行等待线程1 释放锁.(女神成为小寰女朋友后,就是对女神进行加锁了,要是其他人也想成为女神男朋友就要等小寰和女神分手)

2.不可抢占:线程1拿到锁后,必须是线程1主动释放锁才行,线程2是不能强行抢占的.(小寰追到女神后,其他的男生要等小寰分手后才可以追求女神,不能强行占取)

3.请求和保持:线程1拿到锁A后,如果还想获取锁B,锁A还是被线程1持有.(小寰追到女神后,不会因为还想追另一个美女就把女神抛弃了)

4.循环等待:线程1尝试获取锁A和锁B,线程2尝试获取锁B和锁A.线程1在获取锁A后等待线程2释放锁B,线程2获取锁B后等待线程1释放锁A.

注:对于synchronized来说,前三点是无法改变的,所以我们只能通过改变代码块来控制第4点.

六.避免死锁

上述说到Java里的synchronized是很难改变前三点的,所以我们的突破口就是第四点(循环等待).就好比咱们带妹上分是吧,双方大概率都是高分加低分的配置,我们的目标如果是和对面高分硬拼,将会付出大收获小,这时我们就应该多找对面的低分提款,刷起经济再去乱杀.

打破循坏等待:给锁进行编号(从小到大或是从大到小都可以)来进行加锁,任意线程加多把锁的时候都遵循这个原则,自然可以破除循坏等待.参考哲学家就餐问题的解法.                

 

从代码来看,修改了加锁的顺序后,打印出了我们预期的结果,说明我们拿低分提款的策略还是很有效果的

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值