<JavaEE> 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 main2(String[] args) {
        //创建两个任意类型实例作为锁对象;
        Object locker1 = new Object();
        Object locker2 = new Object();

        Thread t1 = new Thread(()->{
            //指定locker1作为锁对象;
            synchronized (locker1) {
                //指定locker2作为锁对象;
                synchronized (locker2) {
                    //需要加锁的代码;
                }
            }
        });
        Thread t2 = new Thread(()->{
            //指定locker2作为锁对象;
            synchronized (locker2) {
                //指定locker1作为锁对象;
                synchronized (locker1) {
                    //需要加锁的代码;
                }
            }
        });
    }

5.1.3 “复杂锁”

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

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

图示演示死锁:

<JavaEE> synchronized关键字和锁机制 -- 锁的特点、锁的使用、锁竞争和死锁、死锁的解决方法_第1张图片

5.2 死锁产生的必要条件

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

5.3 解决死锁的方案

解决死锁的方案有以下几种方法:
<1> 超时放弃。线程进入阻塞等待,当等待时间超过预设时间,则获取锁失败,将持有的锁释放。
<2> 依序加锁。指定加锁的顺序规则,所有线程都需要按照规则规定的加锁顺序进行加锁。

图示演示依序加锁:

<JavaEE> synchronized关键字和锁机制 -- 锁的特点、锁的使用、锁竞争和死锁、死锁的解决方法_第2张图片


你可能感兴趣的:(java-ee,多线程)