目录
JAVA标准库中的线程安全
java标准库中有很多都是线程不安全的,这些类可能会涉及到多线程修改共享数据,又没有任何的加锁措施。如果多线程操作同一个集合类,就需要考虑到线程安全的问题。
ArrayList |
LinkedList |
HashMap |
TreeMap |
HashSet |
TreeSet |
Stringbuilder |
既然不安全为什么不全都加锁?
加锁这个操作是有负面影响的,如果对于没有线程安全的情况下,可以放心使用;如果有线程安全的问题时可以手动的进行加锁,这样给了我们更多的选择空间,如果都加上锁之后,就会产生额外的时间开销。
Vector | 不推荐使用 |
HashTable | 不推荐使用 |
ConcurrentHashMap | |
StringBuffer |
String |
※※死锁
死锁问题是一个十分影响我们幸福感的事情,一旦我们的程序出现了死锁,就会导致线程崩掉,无法继续执行后续的工作,程序就有很严重的bug,而且死锁是非常隐蔽的,在开发阶段不经意间就会写出死锁代码,不容易被测试出来。
几种关于死锁的情况
1.一个线程一把锁
连续加锁两次,如果锁是不可重入锁,就会死锁。
2.两个线程两把锁
t1和t2各自先针对A锁和B锁进行加锁,再尝试获取对方的锁,就会产生死锁现象。
【故事:假设S和Z两个人去吃饺子,S拿到了醋,Z拿到了酱油;这时S想拿Z的酱油,Z想拿S的醋,在这种情况下两个人都想等对方释放锁,互不相让就会产生死锁。】
代码实现:
public class ThreadD14 {
public static void main(String[] args) {
Object jiangyou =new Object();
Object cu = new Object();
//建立两个线程分别表示S和Z两个人
Thread S = new Thread(() -> {
synchronized (cu){
//这里让程序休眠确保S可以顺利拿到cu
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (jiangyou){
//这里打印一下看程序是否可以顺利的执行到此处
System.out.println("S顺利的拿到了酱油&醋");
}
}
});
Thread Z = new Thread(() -> {
synchronized (jiangyou){
//这里同理让程序休眠,使Z顺利拿到酱油
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (cu){
System.out.println("Z顺利的拿到了酱油&醋");
}
}
});
//让线程运行
S.start();
Z.start();
}
}
我们开始运行后发现:
可以看出程序进入了死锁状态。
现在有一个人,把房间的钥匙锁在了车里,而车钥匙锁在了房间里,这种例子也就是死锁状态。
针对这样的死锁问题,就需要我们借助像jconsole这样的工具进行定位,看线程和状态的调用栈,就可以分析出来代码是在哪里产生的死锁问题。
3.多个线程多把锁
多个线程多把锁相当于两个线程两把锁的一般情况。
哲学家就餐--死锁问题
教科书上的经典案例:哲学家就餐问题。
有五个哲学家和五根筷子,每个哲学家存在两种状态
状态一:思考人生(相当于线程阻塞)
状态二:拿起手套吃西瓜(这里假设必须两只手套都拿着才能吃西瓜,硬性假设)
由于操作系统的随即调度,这五个哲学家随时都有可能想吃西瓜,也随时有可能要思考人生。
假设出现了极端的情况,就会产生死锁。在同一时刻,所有的哲学家都同时拿起了左手的手套,都在等待着右边的哲学家把手套放下。
锁更多,线程就更多,情况就更复杂。
下面我们来分析一下死锁是怎么形成的。
死锁形成的四个必要的条件
同时具备,才能出现死锁!!
(1)互斥作用
线程1拿到了锁,线程2就需要等待
(2)不可抢占
线程1拿到锁后,必须是线程1主动的释放,不能说是线程2强行的获取到。
(3)请求和保持
线程1拿到锁A后,再尝试想获取锁B,A的这把锁还是保持的(不会因为获取锁B就把A给释放了)
(4)循环等待
线程1尝试获取锁A和锁B,线程2尝试获取锁B和锁A,线程1在获取B的时候等待线程2释放B。同时线程2在获取A的时候等待线程1释放A。
说是四个条件,实际上就是一个条件,前三个条件都是锁的基本特性,对于synchronized来说我们也不能修改。
如何避免死锁呢?
打破必要条件即可,突破口就是循环等待。
办法:给锁进行编号,然后指定一个固定的顺序(比如从小到大)来加锁。
任意线程加多把锁的时候,都让线程遵守上述的顺序,此时的循环等待就自然破除。
回到我们的上一个故事,关于获取酱油和获取醋的例子,我们只需要将代码进行更改,让两个人获取锁的执行顺序是一样的即不会产生死锁的情况。
代码执行的结果:
以上的内容就是关于死锁的内容。