JavaEE——synchronized关键字的运用与死锁

文章详细介绍了Java中synchronized的用法,包括其互斥性和可重入性,并通过代码示例展示了未加锁与加锁后的线程安全问题。接着,文章阐述了死锁的概念,提供了死锁的四个必要条件,并通过哲学家就餐问题说明如何避免死锁。
摘要由CSDN通过智能技术生成

我们已经知道,在多线程中抢占式执行,随机调度是一个非常麻烦的问题,将代码中的操作原子 (也就是加锁) 化是解决这个问题的一个方法。

一. synchronized 中的使用方法

synchronized 会起到互斥效果,如果两个线程同时尝试加锁,此时一个线程可以获取锁成功,另一个线程只能阻塞等待(BLOCKED),一直阻塞到刚才的线程释放锁,当前线程才能进行加锁。

  • 进入synchronized 修饰的代码块,相当于 加锁。
  • 退出 synchronized 修饰的代码块,相当于 解锁。

下面我通过代码来解释一下加锁是如何作用的。

让一个数字增加 10 万。
代码示例1
这里是没有加锁的代码

class Counter{
    public int count = 0;

//对方法不使用 synchronized 进行加锁
    public void increase(){
        count++;
    }
}

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        //启动两个线程
        t1.start();
        t2.start();

        //让主线程等待线程的执行
        t1.join();
        t2.join();

        System.out.println(counter.count);
    }
}

运行结果

在这里插入图片描述
很显然结果并不符合预期。
示例代码2
这里对内部方法进行加锁

class Counter{
    public int count = 0;

//对内部方法进行 synchronized 加锁
    synchronized public void increase(){
        count++;
    }
}

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        //启动两个线程
        t1.start();
        t2.start();

        //让主线程等待线程的执行
        t1.join();
        t2.join();

        System.out.println(counter.count);
    }
}

在这里插入图片描述
这样代码的结果就符合预期了

为了跟好的理解加锁的操作,下面我再通过画图来给大家详细解释一下。
如图所示:
在这里插入图片描述
需要注意的是,synchronized 修饰代码分为下面几种:

  • 修饰方法(普通方法)
    如图所示:
    在这里插入图片描述
  • 修饰方法(静态方法)
    在这里插入图片描述
  • 修饰代码块
    也就是手动去指定将锁加到那个对象上
    如图所示:
    在这里插入图片描述
  • 锁类对象
    在这里插入图片描述

二. synchronized 的可重入性

对于可重入,简单理解就是:一个线程针对同一个对象,连续加锁两次,如果没有问题,就表明是可重入的。

按照前面对锁的设定,第二次加锁的时候就会阻塞等待。直到第一次锁被释放,才能获取到第二个锁。但是,当线程出现下面的情况,如图:
在这里插入图片描述
这明显是第一次锁没有被释放,后面的线程也就不能进入。
这种情况被称为 死锁
这样的锁,即就是加锁后不进行释放的锁,称为不可重入锁。

代码示例

class Counter{
    public  int count = 0;
    //这里首先对方法进行了加锁
     synchronized public void increase(){
         //这里的 this 可以是任意想指定的对象
         //这里又一次对方法中的元素进行了加锁
         synchronized (this){
             count++;
         }
    }
}
public class ThreadDemo14 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        //启动两个线程
        t1.start();
        t2.start();

        //让主线程等待线程的执行
        t1.join();
        t2.join();

        System.out.println(counter.count);
    }
}

在这里插入图片描述

如上图所示,没有对线程进行锁的释放,代码仍然正常运行,synchronized 关键字不会有上面的问题,因此它是一个可重入锁。

三.什么是(synchronized的互斥)死锁

  1. 一个线程,一把锁,连续加锁两次。当锁不是可重入锁是,就会发生死锁。

  2. 两个线程两把锁,t1 线程 t2 线程相互针对,分别对 A 和 B 进行加锁,之后再尝试获取对方的锁。
    简单的图示如下:
    在这里插入图片描述

这样对方获取的都是被加锁后的操作,自然程序就会僵死在原地。
下面,我通过代码进行举例,通过 JDK 自带的工具包来观察这里的死锁到底是怎么一回事。
代码演示:

    public static void main(String[] args) {
        Object A = new Object();
        Object B = new Object();

        Thread t1 = new Thread(() -> {

            //先让 t1 线程对 A 进行加锁
            synchronized (A){
                //通过等待,先让两个线程拿到各自的元素
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (B){
                    System.out.println("t1 线程将 A 和 B 都获得了");
                }
            }
        });

        Thread t2 = new Thread(()->{
            //先让 t2 线程对 B 进行加锁
            synchronized (B){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (A){
                    System.out.println("t2 线程将 A 和 B 都获得了");
                }
            }
        });
        t1.start();
        t2.start();
    }

运行结果:
在这里插入图片描述
运行后发现,不会出现任何日志,这就表明了没有线程拿到了两个元素。

之后,打开 jconsole 找到我们的进程名,如图:
在这里插入图片描述
双击打开后找到 “线程” 栏并点击。

在这里插入图片描述
如上图所示,我们可以通过 JDK 的工具包发现,代码的死锁就是出现在想要获取对方加锁后的元素。

  1. 多个线程多把锁 (相较于情况 2 更加一般的情况)

书本上有一个非常经典的案例,哲学家就餐问题。
在这里插入图片描述
如上图所示,有 5 位哲学家围坐在桌子周围,每两个人之间有一根筷子,规则如下:

  • 每个哲学家有两种状态。
    1.思考问题(即就是阻塞状态)
    2.拿起筷子吃面(即线程获取到锁进行计算)
  • 想要吃面,就需要拿起左手和右手的两根筷子

我们知道,由于操作系统的随机调度,这 5 位哲学家随时都有可能想吃面条,也随时可能要思考人生。

这里我们假设一种极端情况同一时刻,所有的哲学家同时拿起左手的筷子,
这时就会出现所有的哲学家都拿不起右手的筷子,都需要等待右边的哲学家将筷子放下,这时就产生了死锁。

要解决这里的死锁问题,其实很简单,只需要设定一个规则,将筷子从大到小编号,让每位哲学家先拿编号小的,后拿编号打大的,这样,问题就迎刃而解了。

在这里插入图片描述
到这里,我们最常见的死锁情况基本介绍结束了,但是实际的情况往往更加复杂,因此,了解死锁的形成条件也是我们认识死锁的关键所在。

四.死锁的四个必要条件

  1. 互斥使用
    线程 1 拿到锁,线程 2 就需要等待。
  2. 不可抢占
    线程 1 拿到锁后,必须由线程 1 主动释放。线程 2 不可强行获取。
  3. 请求和保持
    线程 1 拿到锁 A 后,在尝试获取锁 B ,A 的锁仍然保持。
  4. 循环等待
    线程 1 尝试获取到锁 A 和锁 B
    线程 2 尝试获取到锁 B 和锁 A
    线程 1 在获取锁 B 时等待线程 2 释放 B;线程 2 在获取锁 A 时等待线程 1 释放 A。

上述的四个死锁的必要条件缺一不可,在 synchronized 关键字中,前 3 点是锁自身的特性,无法修改,关键的一点就在第 4 点。

对于第 4 点的解决办法,前面的哲学家问题就给出了很好的答案。
对锁进行编号,指定固定顺序进行加锁!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值