目录
1.synchronized 特性
1.1互斥性
1.2内存刷新
1.3可重入
2.Java 标准库中的线程安全类
3.死锁问题
3.1 一个线程,一把锁
3.2 两个线程,两把锁
3.3 多个线程,多把锁
4.死锁的条件
1.synchronized 特性
1.1互斥性
synchronized 关键字会起到互斥效果,当某个线程执行到某个对象的synchronized中时,如果其他线程也执行到了同一个对象的synchronized了,就会阻塞等待
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁
有的编程语言加锁和解锁往往是两个分开的操作,例如,加锁lock(),解锁unlock(),这样的方法缺点就是,容易忘记 写unlock(),那后果就比较严重了,别的线程就一直阻塞等待这个对象了.或者没有忘记写unlock(),但是加锁的代码中有条件语句,return等,直接就出方法了,无法执行到unlock().synchronized修饰代码块的方式就很好地解决了这个问题
如何理解阻塞等待呢?
针对每一把锁,操作系统内部都维护了一个等待队列.当这个锁被某个线程占有的时候,其他线程尝试进行加锁,就不会成功,陷入等待这个锁被释放后继续加锁的状态,这个状态就是阻塞等待状态,一直到之前的线程解锁了之后,操作系统唤醒另一个线程来获取到这个锁
1.2内存刷新
synchronized 的工作过程:
1. 获得互斥锁
2. 从主内存拷贝变量的最新副本到工作的内存
3. 执行代码
4. 将更改后的共享变量的值刷新到主内存
5. 释放互斥锁
1.3可重入
一个线程对同一个对象可以连续加锁两次,是否出现问题,如果可以加,就是可重入的,否则是不可重入的
锁的对象是this,线程调用add,进入方法时就会加锁,能够加上,然后又到了代码块,开始尝试加锁
问题来了,锁对象已经被加锁了,被一个线程占用了,第二次加锁是否要阻塞等待呢,并且这两个线程还是同一个线程
如果上述场景允许加第二把锁,就是可重入的,反之是不可重入的,不可重入那么就会陷入死锁状态,因为线程一直阻塞等待获取锁
java中的synchronized是可重入的
2.Java 标准库中的线程安全类
Java 标准库中很多都是线程不安全的,这些类可能会涉及到多线程修改共享数据,有没有加锁措施
有一些是线程安全的. 使用了一些锁机制来控制
还有像String类型的,不涉及到修改,就是线程安全的
既然加了锁就会安全,为什么不都加上锁,让线程安全呢,是因为加锁操作也是有额外的时间开销的,有的地方用不到锁,反而浪费时间
3.死锁问题
死锁问题一旦出现,线程就会陷入僵持等待,程序就无法执行,并且死锁非常隐蔽,难以测试
死锁问题有很多种情况,上文中死锁只是其中一种
3.1 一个线程,一把锁
就是上文的情况,一个线程,一把锁,连续加锁两次,如果是不可重入的,就会死锁
3.2 两个线程,两把锁
场景:
t1和t2两个线程先各自针对锁A和锁B加锁,再尝试获取对方的锁
这个场景就像有个人的车钥匙锁在房子里了,房子钥匙锁在车里了
来看这种情况的代码
public class ThreadDemo15 {
public static void main(String[] args) {
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(()->{
synchronized (locker1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker2){
System.out.println("t1获取到了两把锁");
}
}
});
Thread t2 = new Thread(()->{
synchronized (locker2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker1){
System.out.println("t2获取到了两把锁");
}
}
});
t1.start();
t2.start();
}
}
结果什么都没有打印,就代表着,两个线程都没有执行到获取两把锁这里的代码,也就是一直在僵持不下,互相等待对方释放自己需要的锁,出现了相互阻塞的现象!!!
使用jconsole查看一下线程的情况
两个线程都出现了BLOCKED状态, t1是阻塞到17行代码,t2阻塞到30行这里,因为相互都在等待释放锁,两个线程会一直处于这个阻塞状态
因此我们也可以使用jconsole工具来定位死锁,查看线程的状态和调用栈,就可以分析出哪里死锁了 !!
解决方法:下文理解锁的条件后就能解决这种死锁情况
3.3 多个线程,多把锁
经典案例就是"哲学家就餐问题"
哲学家就餐问题可以这样表述,假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。哲学家就餐问题有时也用米饭和筷子而不是意大利面和餐叉来描述,因为很明显,吃米饭必须用两根筷子
哲学家从来不交谈,这就很危险,可能产生死锁,每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反)。即使没有死锁,也有可能发生资源耗尽,例如,假设规定当哲学家等待另一只餐叉超过五分钟后就放下自己手里的那一只餐叉,并且再等五分钟后进行下一次尝试.这个策略消除了死锁(系统总会进入到下一个状态),但仍然有可能发生“活锁”。如果五位哲学家在完全相同的时刻进入餐厅,并同时拿起左边的餐叉,那么这些哲学家就会等待五分钟,同时放下手中的餐叉,再等五分钟,又同时拿起这些餐叉
如果出现了极端的情况,就陷入死锁
同一时刻,所有的哲学家都拿起来左手的筷子,此时所有的哲学家都拿不起右手的筷子,都要等待右边的哲学家放下筷子
4.死锁的条件
1.互斥使用:线程1拿到了锁,线程2如果想要获取锁就必须阻塞等待
2.不可抢占:线程1获取到锁之后,其他线程不能强行获取到
3.请求和保持:线程1拿到锁A之后,再尝试获取锁B时,A这把锁还是保持的,不会因为这个释放了A
4.循环等待:线程1尝试获取锁A和B,线程2在尝试获取锁B和锁A.
线程1尝试获取锁B时等待线程2释放B,线程2在尝试获取锁B时等待线程1释放锁A
四个条件同时具备才会死锁!!前三个都是锁的基本特性,循环等待是最关键的!
循环等待是四个条件中唯一一个和代码结构相关的,也是可以被程序员控制的,为了避免循环等待,突破口就是循环等待!!可以给锁编号,然后指定一个固定的顺序来加锁,任意线程加多把锁的时候,都让线程遵守顺序,循环等待自然破除!
给筷子编号,规定每次拿较小号的筷子,假设4个哲学家都拿起筷子,那么和5相邻的两个哲学家肯定要阻塞一个,必然还剩一只5号筷子,拿到较小号筷子的哲学家就可以拿5号筷子就餐,另一位哲学家阻塞等待!破除了循环等待避免死锁
再来看上文提到的,如何让t1t2都获取到A锁和B锁?也是对锁进行编号,让他们都先获取A锁,再获取B锁!
Thread t1 = new Thread(()->{
synchronized (locker1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker2){
System.out.println("t1获取到了两把锁");
}
}
});
Thread t2 = new Thread(()->{
synchronized (locker1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker2){
System.out.println("t2获取到了两把锁");
}
}
});
改动t2线程获取锁的顺序,也先获取A锁,后获取B锁
此时两个线程都获取到了两把锁,解决了死锁问题!