目录
一.加锁(synchronized)
加锁是给对象加锁,我们要明确的是代码是针对哪个对象加锁的。
1.修饰方法
如果synchronized修饰普通方法(静态方法),则加在方法的返回类型前,如下三例:
synchronized public void function(){
return ;
}
public synchronized void function(){
return ;
}
public static synchronized void function(){
return ;
}
但如果加在返回类型与方法名之间则会报错。以上代码的前两例,加锁的对象是this,而后一例则是对类方法加锁
2.修饰代码块
修饰代码块,需要制定一个加锁的对象。进入代码块加锁,退出代码块解锁。
public void function(){
synchronized (this){
return;
}
}
3.锁竞争
锁竞争是一个线程对某个对象加锁了,另一个线程需要对某个对象加锁,这个加锁操作会被阻塞,只有当对象解锁了,其他线程的加锁操作才能执行。synchronized具有加锁的能力。
1️⃣.如果两个线程对同一个对象进行加锁,就会出现锁竞争,按照先到先得的原则,一个线程会先获取到锁,另一个线程则会进入阻塞等待,直到第一个线程解锁,才能获取到锁。
2️⃣.如果两个线程对两个不同的对象加锁,则不会出现锁竞争,都可以成功获取到各自的锁。
3️⃣.如果两个线程,一个加锁,一个不加锁,则不会出现锁竞争。
二.synchronized的特性
1.互斥
互斥导致了锁竞争,当某个线程执行了某个对象的synchronized修饰的部分,即对对象加锁了,那么其他线程要执行同一个对象的synchronized部分,则会阻塞等待。
2.可重入
当遇到以下代码的时候,
public synchronized void function(){
synchronized (this){
return;
}
}
线程调用这个方法,已经对this对象加锁了,而方法内部的代码还要对this对象加锁,此时,就涉及到了锁是否可重入的问题了。
对于this对象,它已经被一个线程加锁了,那么第二个线程对它加锁时,它通过判断这两个线程是不是同一个线程,如果是同一个线程第二次加锁则不需要阻塞等待,可以直接获取到锁,那么这种锁就是可重入的,反之则是不可重入。
那么如果是不可重入的,执行上面的代码,会出现第二次加锁要等第一个锁释放,但第一个锁释放的前提是代码块内执行完毕,那么出现了循环等待,则导致死锁。
三.死锁
1.导致死锁的情况
1️⃣.当一个线程对一个对象连续加锁两次,且锁是不可重入的,就会发生死锁。
在java中,synchronized和ReentrantLock都是可重入的,但是在C++,Python,操作系统原生的锁都是不可重入锁。
2️⃣.两个线程对两个不同对象加锁后,在尝试获取对方的锁。
如:
public static void main(String[] args) throws InterruptedException {
Object o1 = new Object();
Object o2 = new Object();
Thread t1 = new Thread(){
public void run(){
synchronized (o1){
try {
sleep(1000);//确保加上锁
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (o2){//o2已经加锁,加锁会阻塞等待t2线程
System.out.println("t1");
}
}
}
};
Thread t2 = new Thread(){
public void run(){
synchronized (o2){
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (o1){//对o1加锁会阻塞等待t1线程
System.out.println("t2");
}
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
}
由于两个线程的加锁操作都要等待对方的加锁代码块执行完毕,这就导致了一个循环等待的情况,造成了死锁。
3️⃣.多个线程对多个对象加锁,再各自尝试获取已加锁对象的锁。
哲学家就餐问题。
2.死锁的条件
1️⃣.互斥使用,线程1拿到锁后,线程2要获取锁就必须等待。
2️⃣.不可抢占,线程1拿到锁后,必须是线程1主动释放,线程2无法强行获取。
3️⃣.请求和保持,线程1拿到锁A后,获取锁B,此时锁A仍然保持。
4️⃣.循环等待,线程1拿到锁A,线程2拿到锁B之后,在两把锁都没解锁的情况下,线程1获取锁B,线程2获取锁A。
3.如何破除死锁
在构成死锁的4个基本条件中,前三种是锁的基本属性,第4个条件的发生与否是我们取决于我们的代码,所以如何破除死锁,我们就必须消除循环等待,在实际应用中,我们需要根据实际情况,给锁约定顺序,避免出现循环等待。