java加锁问题详解
目录
锁
调用系统api进行加锁
锁本质上也是操作系统提供的功能,应用程序通过调用操作系统提供关于锁的API达到加锁操作。关于锁最主要的操作在于加锁和解锁两个操作。
锁的主页特征
互斥是锁的主要特征,一个线程枷锁后,另一个线程也尝试加同一个锁,就会进入阻塞状态。
产生互斥条件
一个进程中锁的数量可以是多个,多个线程竞争同一把锁时才会产生互斥。
java代码中加锁
本质
java(JVAM)对上述加锁的API进行了封装,通过使用synchronized关键字来完成加锁操作。
加锁过程
创建锁对象
首先创建一个对象,使用这个对象作为锁,,这里我使用了Object对象作为锁对象。
Object object = new Object();
在Java中,任何一个类都是直接或者间接继承自Object类的,随便拿出一个对象都是可以 作为加锁的对象(其他语言不适用)。
锁对象的用途只有一个,就是用来区分线程是否针对同一个对象进行加锁,如果是同一个对象,就会出现锁竞争/锁冲突/互斥,如果不是针对同一个对象进行加锁,就不会出现锁竞争,就不存在阻塞等待的问题。
synchronized(锁对象)+代码块
使用synchronized关键字,用花括号将需要加锁的对象括起来,完成加锁操作。
这是加锁之前的代码:
public class SynchronizedTest {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
count++;
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
运行结果为:
进行加锁操作后:
此时可以看到两个线程全部完成执行,打印结果也没有问题。此时的加锁操作是针对count++操作进行加锁,也就是说,当线程t1进入循环进行count++操作时,t2也进入循环,但却进入阻塞状态,当线程t1执行一次count++操作完毕后,t1线程释放锁,t2线程拿到锁,执行count++操作,此时的t1线程和t2线程针对cout++操作是串行执行的,但两个线程总体还是并行执行的。
切换加锁方式:
synchronized (object) {
for (int i = 0; i < 50000; i++) {
count++;
}
}
将for循环和count++一起包含在代码块内部,此时针对它们进行加锁操作,观察结果:
结果看起来和上面的一样,但是执行过程确实不相同的:
t1线程拿到锁后,执行完整个for循环后释放锁,然后t2线程才能拿到锁进入循环,执行完代码块中 的内容,释放锁。
加锁的生命周期
加锁的生命周期和方法的生命周期是一样的,此时就可以把synchronized写到方法上去。写一个加法方法,放入多线程中执行,把锁加在方法上:
public synchronized static void add() {
count++;
}
放在多线程中执行,查看结果:
此时可以看到结果正确,把锁加在方法上面,相当于对this加锁,此处的this就是锁对象,不过省略了,上述对方法加锁就等价与下面代码:
public void add() {
synchronized (this) {
count++;
}
}
注意:当synchronized修饰普通方法时,相当于对this加锁,当synchronized修饰static方法时,相当于针对该类的类对象进行加锁。
死锁典型应用场景
死锁
当一个线程进行加锁操作后,尝试第二次加锁,此时由于第一次加锁后未解锁,这时线程就会进入阻塞等待状态,这种等待永无休止,这种情况称为死锁。
synchronized (link) {
synchronized (link) {
for (int i = 0; i < 10000; i++) {
count++;
}
}
}
两次针对link对象进行加锁,此时程序应该进入阻塞等待状态,但在Java中使用synchorized进行加锁,则不会出现这种情况。
一、锁是不可重入锁,一个线程针对一个锁对象加锁两次
创建一个线程,执行这个线程,内部包含对同一对象两次加锁操作,查看上面代码的运行结果:
可以看到结果正常输出,没有进入阻塞状态,因为JVM在加锁中进行了特殊处理,这种特殊处理类似于:
每个锁对象会记录当前哪个线程持有了这个锁,当针对这个对象加锁操作时,就会先判定当前加锁线程是否已经持有锁。
也就是说,java中使用synchorized加锁,实际上是可重入锁,当这个对象进行相同的二次加锁时,不会出现阻塞状态,而不可重入锁则会进入阻塞等待状态。
二、两个线程两把锁
现在,有两个线程t1和t2,锁A和锁B,让两个线程分别拿到一把锁,不解锁的情况下尝试获取对方的锁。
public class TwoThread {
private static Object A = new Object();
private static Object B = new Object();
private static int count = 0;
public static void main(String[] args) {
Thread t1 = new Thread(()->{
synchronized (A) {
System.out.println("t1进入锁A,,,尝试获取锁B");
synchronized (B) {
count++;
}
}
});
Thread t2 = new Thread(()->{
synchronized (B) {
System.out.println("t2进入锁B,,,尝试获取锁A");
synchronized (A) {
count++;
}
}
});
t1.start();
t2.start();
}
}
运行查看结果:
可以看到,程序没有终止,两个线程同时等待对方释放锁,这样就会陷入阻塞状态中,此时就产生了死锁,这里使用jdk提供的jconsole工具查看线程状态,先打印一下锁A和锁B。
通过工具追踪线程状态:
两个线程都被互相的锁阻塞。
三、N个线程,M把锁
随着线程和锁的数量增加,情况更加复杂,更容易出现死锁,这里和第二种有相似指出,就不过多介绍了。
死锁的必要条件
1.锁具有互斥性,一个线程拿到锁后,其他线程就得阻塞等待,
2.锁不可抢占(不可被剥夺),一个线程拿到锁后,只能自己释放锁,其他线程不能抢占。
3.请求和保持,一个线程拿到一把锁后,不释放这个锁的前提下,在尝试获取其他锁。
4.循环等待,多个线程获取多个锁的过程中,出现了循环等待。
原文地址:https://blog.csdn.net/qq_74056922/article/details/143650818
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!