目录
一、死锁是什么?
死锁是多线程环境中遇到的最严重的问题之一。
一个线程如果重复获取锁,获取了两次以上,如果锁是可重入锁,可以利用多次解锁方式释放锁资源,反之就会出现多个线程同时被阻塞,它们中的一个或多个线程都在等待锁资源的释放。由于线程被无限期阻塞,因此这种现象被称为“死锁”。
举个栗子:
鸡哥出门买鸡蛋,但是他把钥匙忘记带在身上,当他买完鸡蛋想要回家时,发现备用钥匙也被老妈拿走了,两个钥匙都获取不到,进不了门,就造成了死锁现象。
代码示例:
import java.util.concurrent.TimeUnit;
/**
* 模拟死锁
*/
public class Exe_02 {
public static void main(String[] args) {
//创建两个锁
Object locker1=new Object();
Object locker2=new Object();
//创建两个线程
Thread thread1=new Thread(() ->{
System.out.println("t1申请locker1");
synchronized(locker1){
System.out.println(Thread.currentThread().getName()+"获取到了locker1");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(locker2){
System.out.println(Thread.currentThread().getName()+"获取到了locker2");
}
}
},"t1");
Thread thread2=new Thread(() ->{
System.out.println("t2申请locker1");
synchronized(locker2){
System.out.println(Thread.currentThread().getName()+"获取到了locker2");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(locker1){
System.out.println(Thread.currentThread().getName()+"获取到了locker1");
}
}
},"t2");
//启动线程
thread1.start();
thread2.start();
}
}
进一步理解死锁,那么谈论最多就是”哲学家就餐问题“。
- 有个桌子, 围着一圈哲学家, 桌子中间放着一盘意大利面. 每个哲学家两两之间, 放着一根筷子.
- 每个哲学家 只做两件事: 思考人生 或者 吃面条. 思考人生的时候就会放下筷子. 吃面条就会拿起左右两边的筷子(先拿起左边, 再拿起右边).
- 如果哲家发现筷子拿不起来了(被别人占用了), 就会阻塞等待.
- [关键点在这]假设同一时刻,五个哲家 同时拿起左手边的筷子, 然后再尝试拿右手的筷子, 就会
发现右手的筷子都被占用了. 由于哲家 们互不相让, 这个时候就形成了死锁.
死锁是一种严重的BUG!!,导致一个程序的线程”卡死“,无法正常工作。
二、如何避免死锁
2.1、死锁产生的原因
- 互斥访问:线程1拿到了锁A,其它线程就不能得到锁A。
- 不可抢占:获取到锁的线程,除非自己释放锁,别的线程不能把锁从别的线程手中抢过来占有。
- 保持和请求:线程1已经获取了锁A,还要在这个基础上再去获取锁B。
- 循环等待:线程1等待线程2释放锁,线程2等待线程3释放锁,线程3等待线程1释放锁,形成了一个等待回路。
当上述四个条件都成立(必要条件)的时候,便会形成死锁。当然,上述四个条件打破一个使其不成立,那么死锁就会消失。
2.2、如何解决死锁问题
- 互斥访问:锁的基本特性,不能打破。
- 不可抢占:锁的基本特性,不能打破。
- 保持和请求:从代码实现或是设计的角度来讲,可以改变保持和请求的顺序,也就是获取锁的顺序。
- 循环等待:最有可能也是最常见的解决死锁的策略——打破循环等待。
2.2.1、破除循环等待
策略 :
- 每一个筷子都编一个号。
- 让每个哲学家都先拿编号最小的那根筷子,然后再去拿编号最大的那根筷子。
- 吃一口然后放下筷子,让别的哲学家再去吃获取筷子。
过程:
- 从1号到四号哲学家都拿到了编号最小的那根筷子。
- 5号哲学家再拿一号筷子的时候发现已经被1号哲学家拿走了,那么他就要原地等待
- 随着4号哲学家吃完后把两双筷子都放下之后,随即3~1号哲学家都拿到筷子吃到了‘坤’
- 1号哲学家在吃完面之后,把两只筷子都放下之后,5号哲学家就可以拿到1号筷子,再拿到5号筷子,最终吃到‘坤’。
代码示例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 哲学家就餐问题
*/
public class Exe_02 {
static final Lock[] locks = new Lock[5];
static {
for (int i = 0; i < locks.length; i++) {
locks[i] = new ReentrantLock();
}
}
public static void main(String[] args) {
//创建五个示例对象
Task task1 = new Task("小黑子", 1000, 1);
Task task2 = new Task("咯咯", 2000, 2);
Task task3 = new Task("真爱粉", 3000, 3);
Task task4 = new Task("你真菜", 4000, 4);
Task task5 = new Task("菜徐鸡", 5000, 5);
//启动线程
task1.start();
task2.start();
task3.start();
task4.start();
task5.start();
//死循环防止线程退出,导致进程关闭
}
//创建任务的类模拟实现吃鸡和思考操作,继承线程类
static class Task extends Thread {
private String name;
private long time;
private int num;
public Task(String name, long time, int num) {
this.name = name;
this.time = time;
this.num = num;
}
@Override
public void run() {
//模拟吃鸡过程和思考过程
while (true) {
System.out.println(num + "号哲学家" + name + "正在思考中...");
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(num + "号哲学家" + name + "饿了,来一口坤肉");
if (locks[num].tryLock()) {
try {
System.out.println(num + "号哲学家" + name + "拿到了左边的筷子");
if (locks[(num + 1) % 5].tryLock()) {
try {
System.out.println(num + "号哲学家" + name + "拿到了右边的筷子");
System.out.println(num + "号哲学家" + name + "开始吃坤肉...");
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(num + "号哲学家" + name + "放下了右边的筷子");
locks[(num + 1) % 5].unlock();
}
}else{
System.out.println(num + "号哲学家" + name + "没拿到右边的筷子,思考中...");
}
}finally{
System.out.println(num + "号哲学家" + name + "放下了右边的筷子");
locks[num].unlock();
}
}else{
System.out.println(num + "号哲学家" + name + "没拿到左边的筷子,思考中...");
}
System.out.println(num + "号哲学家" + name +"思考ing...");
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
三、ThreadLocal
synchronized是能够通过加锁,可以保证多线程环境下的共享变量的可见性(让每一个线程访问的变量都是一致的),原子性。
ThreadLocal正好相反,对于同一个变量让每一个线程都保持自己线程处理任务过程中不同的值。
场景:
- 统计每个班的学生人数。
- 根据每个班的学生人数去定制校服。
实现方式:
每个班创建一个线程,在线程中去统计每个班的人数,再调用定做校服的方法。
对于count来说,每一个线程都维护着这一个变量,对于线程来说是局部变量。
这时就需要使用ThreadLocal来区分每一个线程的count值(可以理解为一个Map集类)
ThreadLocal里面维护了一个ConcurrentHashMap,线程安全的,set()方法传进value值,线程对象作为Key值。
/**
* ThreadLocal里面维护了一个ConcurrentHashMap,线程安全的,set()方法传进value值,线程对象作为key值
*/
public class Exe_03 {
//初始化ThreadLocal对象
private static ThreadLocal<Integer> threadLocal=new ThreadLocal<>();
public static void main(String[] args) {
//多个线程分别去统计人数
Thread t1=new Thread(() ->{
//统计人数
int count=35;
threadLocal.set(count);
Integer value=threadLocal.get();
System.out.println(Thread.currentThread().getName()+"人数:"+value);
//定制校服
print();
},"一班");
Thread t2=new Thread(() ->{
//统计人数
int count=56;
threadLocal.set(count);
Integer value=threadLocal.get();
System.out.println(Thread.currentThread().getName()+"人数:"+value);
//定制校服
print();
},"二班");
Thread t3=new Thread(() ->{
//统计人数
int count=48;
threadLocal.set(count);
Integer value=threadLocal.get();
System.out.println(Thread.currentThread().getName()+"人数:"+value);
//定制校服
print();
},"三班");
//启动线程
t1.start();
t2.start();
t3.start();
}
private static void print() {
Integer value=threadLocal.get();
System.out.println(Thread.currentThread().getName()+": 需要定制 "+value+"校服");
}
}