synchronized关键字和锁机制 -- 锁的特点、锁的使用、锁竞争和死锁、死锁的解决方法

目录

一、synchronized 关键字简介

二、synchronized 的特点 -- 互斥

三、synchronized 的特点 -- 可重入

四、synchronized 的使用示例

4.1 修饰代码块 - 锁任意实例

4.2 修饰代码块 - 锁当前实例

4.3 修饰普通方法 - 锁方法所在实例

4.4 修饰代码块 - 锁指定类对象

4.5 修饰静态方法 - 锁方法所在类对象

五、锁竞争和死锁

5.1 出现死锁的三种典型场景

5.1.1 “重复锁”

5.1.2 “互相锁”

5.1.3 “复杂锁”

5.2 死锁产生的必要条件

5.3 解决死锁的方案


一、synchronized 关键字简介

概述:Java中加锁的方式有很多种,其中使用 synchronized 关键字进行加锁是最常用的。synchronized 是一种监视器锁(monitor lock)。
加锁的目的:是为了将多个操作“打包”为一个有“原子性”的操作。
加锁的核心规则:进行加锁的时候必须先准备好“锁对象”,锁对象可以是任何类型的实例。
synchronized 的底层实现:synchronized 的底层是使用操作系统的 mutex lock 实现的,本质上依然是调用系统的 API ,依靠 CPU 的特定指令完成加锁功能的。

二、synchronized 的特点 -- 互斥

1)什么是互斥?
某个对象使用了 synchronized 进行修饰,当一个线程访问这个对象时,就会加锁,其他线程想要访问这个对象,就会先阻塞等待,直到这个对象解锁。这就是使用 synchronized 关键字时产生的互斥效果。
2)什么是加锁、解锁?

当程序进入由 synchronized 修饰的代码块、对象或方法时,即相当于加锁。

当程序退出由 synchronized 修饰的代码块、对象或方法时,即相当于加锁。

3)由互斥到冲突,什么是锁冲突/锁竞争?

由于 synchronized 具有互斥的特点,因此当多个线程同时竞争同一个锁时,线程间的冲突就不可避免。当有一个线程获得了锁,那么此时其他还想获得该锁的线程就只能阻塞等待,直到锁被释放后,才能再次竞争这个锁。这就是锁冲突或者说锁竞争。


三、synchronized 的特点 -- 可重入

1)什么是不可重入锁?

同一个线程在还没释放锁的情况下,访问同一个锁。

从 synchronized 的互斥特点可以了解到,当锁未被释放,访问该锁的线程会阻塞等待。

由于锁还没有释放,第二次加锁时,线程进入阻塞等待。

线程进入阻塞等待,则第一次的锁无法释放。

这样程序就进入了僵持状态。

这种状态被称为“死锁”。

而这样的锁,被称为“不可重入锁”。

2)什么是可重入锁?

可重入锁与不可重入锁不同,不会出现自己把自己锁死的情况。synchronized 就是可重入锁。

3)可重入锁是怎么实现可重入的?

可重入锁,锁内部会有两个属性,分别是“线程持有者”和“计数器”。

线程持有者

记录了当前锁是被哪一个线程持有的。

当发生重复加锁时,会判断是否是同一线程加锁。

如果是则跳过加锁步骤,只是在另一个属性“计数器”上自增1。

如果不是,则阻塞等待。

计数器

用于记录当前锁的加锁次数。

每次加锁,“计数器”计数会自增1(比如重复加锁10次,那么计数器的值就会等于10)。

每次解锁,“计数器”计数会自减1,当计数器的值归零时,才是真正的释放锁,此时该锁才能被其他线程获取。


四、synchronized 的使用示例

4.1 修饰代码块 - 锁任意实例

public class Test{
    //创建任意类型实例作为锁对象;
    Object locker = new Object();

    public void lockTest(){
        //使用synchronized,指定locker作为锁对象,在需要加锁的代码块上加锁;
        synchronized (locker) {
            //需要加锁的代码;
        }
    }
}

4.2 修饰代码块 - 锁当前实例

public class Test{
    public void lockTest(){
        //使用synchronized,指定this(当前实例)作为锁对象,在需要加锁的代码块上加锁;
        synchronized (this) {
            //需要加锁的代码;
        }
    }
}

4.3 修饰普通方法 - 锁方法所在实例

public class Test{
    //在普通方法上,使用synchronized,指定当前实例作为锁对象,将方法加锁;
    public synchronized void lockTest(){
        //需要加锁的代码;
    }
}

4.4 修饰代码块 - 锁指定类对象

//任意类;
public class Locker{

}

public class Test{
    public void lockTest(){
        //使用synchronized,指定class(类对象)作为锁对象,在需要加锁的代码块上加锁;
        synchronized (Locker.class) {
            //需要加锁的代码;
        }
    }
}

4.5 修饰静态方法 - 锁方法所在类对象

public class Test{
    //在静态方法上,使用synchronized,指定当前类对象作为锁对象,将方法加锁;
    public synchronized static void lockTest(){
        //需要加锁的代码;
    }
}

五、锁竞争和死锁

1)由锁竞争到死锁,什么是死锁?

上文在“synchronized 的特点 -- 互斥”中,介绍了什么是锁竞争。

人可以卷,但卷到一定程度就可以卷死自己或卷死别人。

那么,锁,也是可以卷的,比如锁竞争

加锁可以解决线程安全问题,但是如果加锁方式不当,就可能产生死锁。

2)死锁对程序来说意味着什么?

死锁是程序中最严重的一类BUG。

程序可能因此停摆、崩溃。

当然,人也可能因此“停摆、崩溃”。

5.1 出现死锁的三种典型场景

死锁有以下三种典型场景。
<1>

“重复锁”:如,一个线程,一把锁,自己把自己拷上了。

<2>“互相锁”:如,两个线程,两把锁,互相把对方拷上了。
<3>“复杂锁”:如,上述两种锁重复或复合发生的情况,多个线程多把锁,超级加倍。
以上三个锁的名字,是笔者归纳总结后,为方便记忆而概括出的锁名,不是公认的专业名词。

5.1.1 “重复锁”

“重复锁”是指什么情况?

锁在被释放前,同一个线程再次要求获得同一个锁。

锁没被释放,线程无法获得锁,进入阻塞。

但线程阻塞,代码就不会继续执行,锁也就一直得不到释放。

由此实现了自己卡死自己的“壮举”。

代码演示死锁:

    public static void main(String[] args) {
        //创建任意类型实例作为锁对象;
        Object locker = new Object();

        Thread t = new Thread(()->{
            //指定locker作为锁对象;
            synchronized (locker) {
                //再次指定locker作为锁对象;
                synchronized (locker){
                    //需要加锁的代码;
                }
            }
        });
    }
synchronized  是“可重入锁”。

“可重入锁”和“不可重入锁”的定义和区别,在上文“synchronized 的特点 -- 可重入”中说明了。

Java 提供的 synchronized 关键字,实现的是一个“可重入锁”。所以不用担心会发生这种死锁。

5.1.2 “互相锁”

1)“互相锁”是指什么情况?

两个线程,都获取了一个不同的锁。

但是在各自的锁释放前,又分别去获取了对方的锁。

但此时两把锁都还没有被释放,那么两个线程都进入阻塞等待的状态,都在等对方把锁释放。

代码演示死锁:

    public static void main(String[] args) {
        //创建两个任意类型实例作为锁对象;
        Object locker1 = new Object();
        Object locker2 = new Object();

        Thread t1 = new Thread(()->{
            //指定locker1作为锁对象;
            synchronized (locker1) {
                System.out.println("t1获取locker1");
                //休眠1秒,保证线程t2可以获取到锁locker2。
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //指定locker2作为锁对象;
                synchronized (locker2) {
                    //需要加锁的代码;
                    System.out.println("t1获取locker2");
                }
            }
        });
        Thread t2 = new Thread(()->{
            //指定locker2作为锁对象;
            synchronized (locker2) {
                System.out.println("t2获取locker2");
                //休眠1秒,保证线程t1可以获取到锁locker1。
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //指定locker1作为锁对象;
                synchronized (locker1) {
                    //需要加锁的代码;
                    System.out.println("t2获取locker1");
                }
            }
        });

        t1.start();
        t2.start();
    }

//运行结果:
t2获取locker2
t1获取locker1
...

程序没有正常执行完毕,因为出现了死锁。

5.1.3 “复杂锁”

“复杂锁”是指什么情况?

“复杂锁”指前两种情况重复发生,或复合发生时,锁与锁之间相互叠加、“犬牙交错”的局面。

图示演示死锁:

5.2 死锁产生的必要条件

产生死锁有以下四个必要条件:
<1>互斥使用。获取锁的过程需要是互斥的,当锁被一个线程获取,另一个线程想获取这把锁就必须阻塞等待。这是锁的基本特性之一。
<2>不可抢占。锁被一个线程获取后,另一个线程不能强行把锁抢走,除非锁被持有线程释放。这也是锁的基本特性之一。
<3>请求保持。当一个线程申请锁而进入阻塞等待时,对自己已经持有的锁保持持有状态。这个条件与代码结构相关。
<4>循环等待/环路等待。线程申请锁,而被线程申请的这个锁又在等待作为申请者的线程释放,形成环路。这个条件与代码结构相关。

5.3 解决死锁的方案

解决死锁的方案有以下几种方法:
<1>超时放弃。线程进入阻塞等待,当等待时间超过预设时间,则获取锁失败,将持有的锁释放。
<2>依序加锁。指定加锁的顺序规则,所有线程都需要按照规则规定的加锁顺序进行加锁。
<...>还有许多解决死锁的方式,如增加锁,减少线程,计数器,银行家算法等。

上述死锁的四个必要条件中,最容易破环的是“循环等待”。依序加锁是对锁进行排序,指定线程的加锁顺序。

图示演示依序加锁:


阅读指针 -> 《锁进阶 -- 锁策略》

<JavaEE> 锁进阶 -- 锁策略(乐观锁和悲观锁、重量级锁和轻量级锁、自旋锁和挂起等待锁、可重入锁和不可重入锁、公平锁和非公平锁、读写锁)-CSDN博客文章浏览阅读3次。介绍了以下锁策略:乐观锁和悲观锁、重量级锁和轻量级锁、自旋锁和挂起等待锁、可重入锁和不可重入锁、公平锁和非公平锁、读写锁;https://blog.csdn.net/zzy734437202/article/details/134916407

  • 21
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值