前言
上篇博文主要讲解了在多线程环境下,当某个线程执行非原子性操作(例如count++),其他线程的抢占式执行会导致线程安全问题,所以我们利用synchronized将非原子性操作打包成一个原子(synchronized能保证原子性),这样就保证了线程安全,但是synchronized的使用也会导致其他线程安全问题(死锁),本文就围绕synchronized的特性和死锁问题进行展开论述
1.synchronized特性
原子性
确保了代码块的原子性,即被同步的代码块在执行过程中不会被其他线程中断。这意味着在一个线程执行完整个同步块之前,其他线程无法进入同一个同步块,从而保证了操作的完整性
互斥性
确保在同一时间只有一个线程可以进入被同步的代码块或方法,这意味着当一个线程进入同步块或方法时,其他试图进入同一同步块的线程会被阻塞,直到第一个线程退出同步块
内存可见性
保证了内存的可见性。当一个线程修改了被同步的共享变量后,其他线程在进入同步块或方法时能够看到这些修改。这是因为 synchronized 在释放锁时会将工作内存中的修改刷新到主内存中,而在其他线程获取锁时会从主内存中读取最新的值
可重入性
synchronized关键字是可重入的,这意味着如果一个线程已经持有某个对象的锁,那么它可以再次获取该对象的锁,而不会被阻塞
可重入锁代码示例
可重入锁通常会维护一个计数器,记录当前线程获取锁的次数。每次获取锁时,计数器加一;释放锁时,计数器减一。当计数器为零时,锁才真正被释放
2.死锁代码示例
经过上述操作,确保thread1线程获取到了a锁,thread2获取到了b锁,thread3获取到了c锁
图片中的thread1线程等待thread2线程释放b锁,thread2线程等待thread3线程释放c锁,thread3线程等待thread1线程释放a锁,导致三个进程相互卡住了
3.怎么解决死锁问题
3.1 导致死锁的四个必要条件
- 互斥条件(Mutual Exclusion)
定义:
至少有一个资源必须处于非共享模式,即一次只能被一个进程使用。如果另一个进程请求该资源,那么请求进程必须等待,直到该资源被释放
- 请求与保持条件(Hold and Wait)
定义:
一个进程已经持有至少一个资源,并且正在等待获取其他被其他进程占用的资源
解释:
thread1持有a锁,但是它需要获取到b锁才能继续执行,但是b锁已经被thread2线程持有了,那么thread1线程只能等待thread2线程释放b锁
- 不可剥夺条件(No Preemption)
定义:
资源一旦被分配给某个进程,就不能被强制性地剥夺,只能由占有该资源的进程自行释放
解释:
thread1线程需要获取b锁才能继续执行,但是thread1不能强行把b锁从thread2那里抢过来
环路等待条件(Circular Wait)
定义:
存在一个进程资源的循环等待链,其中每个进程都在等待下一个进程所持有的资源。
解释:
thread1等待thread2,thread2等待thread3,thread3等待thread1,形成闭合回路了
3.2 破解死锁
上文提到了导致死锁的四个必要条件,那么我们只需要破除其中之一就好了
分析:互斥条件和不可剥夺条件是synchronized的固有属性,我们程序员不好做出修改,请求与保持条件和具体业务场景有关,如果thread1线程就是需要等待thread2线程,那我们也不好做出修改。综上所述,最有可能破除的条件就是环路等待
具体做法就是,不要让三个线程相互等待,如果有一个线程先执行完毕,那么剩下两个线程就一定能执行完毕,我们只需要稍作修改就行了
等thread2或者thread3执行完毕后,b锁就被释放了,thread1线程迟早能执行完毕,这样死锁问题就解决啦