前言:
加锁是解决多线程安全问题的一个重要方式,通过加锁可以让某些操作变成原子的操作,从而解决多线程安全问题,但是对某些线程进行多次加锁的时候,可能会出现死锁。当一个锁处于被加锁状态,如果有另一个线程也想要获取到这把锁的话就会产生“锁冲突/锁竞争”,后一个线程就会进入阻塞等待,直到前一个线程释放了该锁为止。如果这两个线程互相“不让对方”,那么此时就会出现死锁!!
1.加锁的基本方式
synchronized() {
//你所想要实现的内容
}
ps:括号里需要一个用来加锁的对象,这个对象是啥不重要(任意对象),重要的是通过这个对像来区分两个线程是否在竞争同一把锁。
除了上述synchronized修饰代码块之外,还可以修饰实例方法/静态方法
//修饰实例方法,this可以作为锁对象进行加锁
public void increase1 () {
synchronized(this) {
count++;
}
}
synchronized public void increase2 () {
count++;
}
//修饰静态方法,相当于针对类对象进行加锁
//Counter是自己创建的一个类,Counter.class就表示类对象
public static void increase3 () {
synchronized(Counter.class) {
count++;
}
}
synchronized public static void increase4 () {
count++;
}
java里面所创建的对象,在对应的内存空间当中,除了你自己定义的一些属性,还有一些自带属性。比如说synchronized用的锁的对象就是一个,其中有表示当前对是否已经进行加锁。
2.锁的重要特性
synchronized的相关锁特性是可重入的。所谓可重入就是指一个线程针对一把锁,加锁两次,不会出现死锁,满足这个要求就称可重入,否则不可重入。而不可重入就会造成死锁,所以在实际生存过程中,应该避免这个问题。
3.死锁(锁的不可重入性)
(1)一个线程,多次加锁 ==> 会造成死锁
看下面一段代码:
public class test{
Object locker = new Object();
Thread t = new Thread(() -> {
synchronized (locker) {
try {
Thread.sleep(500);
}catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker) {
try {
Thread.sleep(500);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
}
我们可以从这段代码中看出只有一个锁对象那就是locker,第二次想要加锁成功就需要第一次加锁然后将锁给释放掉,而第一次想要释放锁,就需要执行完整个语句,要执行到语句的最后就需要给第二次加锁成功,此时代码才可以继续执行,这里由于第一次并没有释放锁,导致第二次加锁失败,此时线程进行阻塞。这就形成了死锁!!
(2)两个线程,两把锁
如果是嵌套关系,就可能会造成死锁。
public static void main(String[] args) {
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (locker1) {
try {
Thread.sleep(500);
}catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker2) {
System.out.println("t1正在执行");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (locker2) {
try {
Thread.sleep(500);
}catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker1) {
System.out.println("t2正在执行");
}
}
});
t1.start();
t2.start();
}
运行结果:
运行代码可以发现没有打印任何东西,因为此时就出现了死锁!!
如果是并列关系,就不会出现死锁。
public static void main(String[] args) {
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (locker1) {
try {
Thread.sleep(500);
}catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker2) {
System.out.println("t1正在执行");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (locker1) {
try {
Thread.sleep(500);
}catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker2) {
System.out.println("t2正在执行");
}
}
});
t1.start();
t2.start();
}
运行结果:
所以说如果有多个锁对象,且有多个线程的话,锁对象之间应该要写成并列的关系,避免写嵌套关系。
4.造成死锁的原因
(1)互斥使用(锁的基本特性):当一个线程持有一个线程,另一个线程也想获得这把锁就需要进行等待。
(2)不可抢占(锁的基本特性):当锁已经被线程1拿到之后,线程2只能等待线程1自动释放锁之后线程2才可能获得这把锁1,不能强行抢占过来
(3)请求保持:一个线程尝试获得多把锁(当某个线程拿到锁1之后,还想尝试获取锁2,获取锁2时,锁1不会释放)
(4)循环等待(环路等待):等待的依赖关系,形成环了
要解决死锁,只能解决(3)(4),(1)(2)属于锁自带的特性,破坏不了。所以只能去解决(3)(4)。
对于(3)来说,调整代码结构,避免写锁嵌套的逻辑结构
对于(4)来说,可以约定加锁顺序,就可以避免循环等待
5.关于sleep休眠问题
两个线程,两个锁对象实现嵌套结构可能会造成死锁,可能的其中原因之一就是:sleep是否进行休眠。
如果线程t1和t2没有进行休眠,则此时线程1会飞速进行加锁然后释放锁,此时线程t2也就不存在锁冲突了,可以顺利的进行加锁和释放锁。
运行结果: