目录
前言
在多线程的讲解当中,我们可以知道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加锁进行了优化。按对锁的竞争程度划分成:无锁,偏向锁,轻量级锁,重量级锁。简单而言就是从无锁-->重量级锁。
1.4其他优化操作
我们额外补充2个编译器对锁的优化操作。锁消除和锁粗化
锁消除
代码中, 用到了 synchronized, 但其实没有在多线程环境下. (例如 StringBuffer)
StringBuffer tq02 = new StringBuffer();
tq02.append("a");
tq02.append("b");
tq02.append("c");
tq02.append("d");
每个 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如何解决死锁
想没有死锁,那么我们可以从死锁产生的条件入手,只有破坏其他一条就可以了。 互斥使用和不可抢占是锁的基本特性,因此无法干预,但是请求和保持,也不可能改变,因为这是代码执行逻辑。因此只有循环等待,我们可以打破
为了解决死锁问题,可以采取预防、避免、检测和解除四种方法。
预防:通过设置某些限制条件,以防止死锁的发生。
避免:系统在分配资源时根据资源的使用情况提前作出预测,从而避免死锁的发生。
检测:允许系统在运行过程中产生死锁,但系统中有相应的管理模块可以及时检测出已经产生的死锁,并精确地确定与死锁有关的进程和资源,然后采取适当措施清除系统中已经产生的死锁。
解除:当发现有进程死锁后,立即解脱它从死锁状态中出来。常用的方法包括剥夺资源和撤销进程。剥夺资源是从其他进程中剥夺足够数量的资源给死锁进程,以解除死锁状态。撤销进程可以直接撤销死锁进程或撤销代价最小的进程,直至有足够的资源可用,从而消除死锁状态。
---------------------懒惰的tq02