前言
在多线程的讲解当中,我们可以知道synchronized是加锁操作,让两个线程发生互斥效果,在代码中使用synchronized关键字来实现锁的获取和释放。如果是刚刚接触多线程的人,我希望你可以从第一章多线程开始学习:http://t.csdn.cn/0vEhY
一.synchronized
1.1概念
Synchronized是Java中内置的锁机制,用于实现线程同步。它可以通过在代码中使用synchronized关键字来实现锁的获取和释放。Synchronized关键字可以用在方法上或者代码块中。当一个线程执行到synchronized修饰的代码块时,它会尝试获取锁,如果锁没有被其他线程占用,则获取成功,执行代码块中的内容。如果锁已经被其他线程占用,则该线程会进入等待状态,直到获取到锁才能继续执行。
1.2Synchronized是什么锁?
- 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
- 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
- 实现轻量级锁的时候大概率用到的自旋锁策略
- 是一种不公平锁
- 是一种可重入锁
- 不是读写锁
注:需要使用公平锁,建议使用ReentrantLock来实现。ReentrantLock提供了公平锁和非公平锁两种模式,通过构造函数的参数来指定锁的模式。
1.3Synchronized加锁工作过程
对于锁资源只有一个或者两个线程交替竞争的,仍然需要使用系统调用,无疑对CPU资源是极大的消耗。因此,在jdk1.6针对Synchronized加锁进行了优化。按对锁的竞争程度划分成:无锁,偏向锁,轻量级锁,重量级锁。简单而言就是从无锁-->重量级锁。
无锁
当你添加了锁时,如果编译器认为不需要加锁,会自动删除,因此便是无锁
偏向锁
偏向锁不是真的 "加锁", 只是给对象头中做一个 "偏向锁的标记", 记录这个锁属于哪个线程.
如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)
如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态.
注:相当于做个标记,相当于 "延迟加锁" . 能不加锁就不加锁, 尽量避免不必要的加锁开销.
轻量级锁
随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁).
此处的轻量级锁就是通过 CAS 来实现
- 通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
- 如果更新成功, 则认为加锁成功如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).
注:此处的自旋锁不会一种持续进行,而是达到一定的时间/重试次数, 就不再自旋了.
重量级锁
如果锁竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁
此处的重量级锁就是指用到内核提供的 mutex .
具体流程:
- 执行加锁操作, 先进入内核态.
- 在内核态判定当前锁是否已经被占用
- 如果该锁没有占用, 则加锁成功, 并切换回用户态.
- 如果该锁被占用, 则加锁失败. 此
- 经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒
- 这个线程, 尝试重新获取锁
1.4其他优化操作
我们额外补充2个编译器对锁的优化操作。锁消除和锁粗化
锁消除
代码中, 用到了 synchronized, 但其实没有在多线程环境下. (例如 StringBuffer)
StringBuffer tq02 = new StringBuffer(); tq02.append("a"); tq02.append("b"); tq02.append("c"); tq02.append("d");
每个 append 的调用都会涉及加锁和解锁. 但如果只是在单线程中执行这个代码, 那么这些加
锁解锁操作是没有必要的, 白白浪费了一些资源开销.因此将锁给优化了。
锁粗化
锁的粗化每个 append 的调用都会涉及加锁和解锁. 但如果只是在单线程中执行这个代码, 那么这些加是根据锁的粒度:粗和细
实际开发过程中, 使用细粒度锁, 是期望释放锁的时候其他线程能使用锁.但可能并没有其他线程来抢占这个锁. 这种情况 JVM 就会自动把锁粗化, 避免频繁申请释放锁.
二.死锁
2.1什么是死锁
死锁是指在多进程系统中,每个进程都在等待某个资源,而该资源又被其他进程占用,导致所有进程都无法继续执行的状态。
例如:A、B、C、D和E去上厕所,A进入厕所并且锁门,B.C.D等待,可是A刚刚进入厕所,因为特殊的原因,凭空转移到了外面,A就得重新排队,可是门还是锁着的啊,因此导致了死锁。
2.2死锁的几个经典场景
经典场景有:
- 一个线程,一把锁
- 两个线程,两把锁
- 多个线程,多把锁
1.一个线程,一把锁
一个线程连续被同一个加锁两次,如果是不可重入锁,那么会是死锁。
解析:我去上厕所,我把厕所门锁住,再通过厕所的窗户出去,然后再来上厕所,发现厕所锁住了,就耐心等待,却没想过这是自己锁的。
代码实现:
public class Counter { void increase() { synchronize(this){ increase() //可以理解为翻窗逃走,第二次加锁时,是锁了的 } } public static void main(String[] args) throws InterruptedException { final Counter counter = new Counter(); Thread t1 = new Thread(() -> { counter.increase(); }); t1.start(); } }
2.两个线程,两把锁
线程1先获取锁A,再尝试获取锁B,同时,线程2先获取锁B,再尝试获取锁A,此时两个线程就会互相僵住,谁都获取不到对方持有的锁。
解析:我在汽车里,车钥匙在我妻子手上,我出不来,我妻子在房间里,房间钥匙在我手上,我妻子也出不来,导致双方被锁,导致死锁。
代码示例:
public class Test { public static void main(String[] args) { //2个锁对象 Object lockerA = new Object(); Object lockerB = new Object(); Thread t1 = new Thread(() -> { System.out.println("t1尝试获取锁A"); synchronized (lockerA){ System.out.println("t1获取到锁A"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1尝试获取锁B"); synchronized (lockerB){ System.out.println("t1获取到锁B"); } } }); Thread t2 = new Thread(() -> { System.out.println("t2尝试获取锁B"); synchronized (lockerB){ System.out.println("t2获取到锁B"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t2尝试获取锁A"); synchronized (lockerA){ System.out.println("t2获取到锁A"); } } }); t1.start(); t2.start(); } }
3.多个线程,多把锁
很明显啊,两把锁,两个线程也有问题,更何况是多把锁啊,在这方面最经典的是"哲学家就餐问题"。
如图:火柴人是哲学家、红线是筷子,每一个哲学家的左右都有一根筷子。规定,当有一根哲学家饿了,会先拿起左边的筷子,然后再拿右边的筷子,吃完了就放下筷子。
造成死锁问题:每一个哲学家都饿了,然后都拿起了左边的筷子,可是当拿右边的筷子时,发现有其他人在使用,所以导致阻塞,然后一直等待别人吃饱放下筷子,可是每个人都在等待。
2.3死锁产生的条件
死锁产生需要四个条件:
- 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
- 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
- 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
- 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。
2.4如何解决死锁
想没有死锁,那么我们可以从死锁产生的条件入手,只有破坏其他一条就可以了。 互斥使用和不可抢占是锁的基本特性,因此无法干预,但是请求和保持,也不可能改变,因为这是代码执行逻辑。因此只有循环等待,我们可以打破
为了解决死锁问题,可以采取预防、避免、检测和解除四种方法。
预防:通过设置某些限制条件,以防止死锁的发生。
避免:系统在分配资源时根据资源的使用情况提前作出预测,从而避免死锁的发生。
检测:允许系统在运行过程中产生锁,但系统中有相应的管理模块可以及时检测出已经产生的死锁,并精确地确定与死锁有关的进程和资源,然后采取适当措施清除系统中已经产生的死锁。
解除:当发现有进程死锁后,立即解脱它从死锁状态中出来。常用的方法包括剥夺资源和撤销进程。剥夺资源是从其他进程中剥夺足够数量的资源给死锁进程,以解除死锁状态。撤销进程可以直接撤销死锁进程或撤销代价最小的进程,直至有足够的资源可用,从而消除死锁状态