自学内容网 自学内容网

java加锁问题详解

目录

调用系统api进行加锁

锁的主页特征

产生互斥条件

java代码中加锁

本质

加锁过程

创建锁对象

synchronized(锁对象)+代码块

 加锁的生命周期

死锁典型应用场景

死锁

一、锁是不可重入锁,一个线程针对一个锁对象加锁两次

二、两个线程两把锁

三、N个线程,M把锁

死锁的必要条件


调用系统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)!