Jtoss Jtoss
首页
  • 数据结构与算法

    • 数据结构与算法 - 概述
    • 数据结构与算法 - 复杂度分析
    • 数据结构 - 线性表
    • 算法 - 常见排序算法
  • 代码规范

    • 代码简洁之道
    • 阿里巴巴开发手册
    • 谷歌Java编程风格指南
  • 设计模式

    • 编写高质量代码概述
    • 面向对象
    • 设计原则
    • 设计模式-创建型
    • 设计模式-结构型
    • 设计模式-行为型(上)
    • 设计模式-行为型(下)
    • 浅析框架源码中的设计模式
    • 业务框架实战案例
  • MySQL 基础

    • MySQL - 数据库设计规范
    • MySQL - 必知必会
  • MySQL 进阶

    • MySQL - 基础架构
    • MySQL - InnoDB存储引擎
    • MySQL - InnoDB缓冲池
    • MySQL - 事务与锁
    • MySQL - 索引
    • MySQL - 查询执行计划
    • MySQL - 性能优化
  • Redis 系列

    • Redis入门 - 基础相关
    • Redis进阶 - 数据结构
    • Redis进阶 - 持久化RDB和AOF
    • Redis进阶 - 事件机制
    • Redis进阶 - 事务
    • Redis进阶 - 高可用高可扩展
    • Redis进阶 - 缓存问题
    • Redis进阶 - 性能调优
  • Java 基础

    • Java 基础 - 知识点
    • Java 基础 - 面向对象
    • Java 基础 - Q/A
  • Java 进阶 - 集合框架

    • Java 集合框架详解
  • Java 进阶 - 多线程与并发

    • Java 并发 - 理论基础
    • Java 并发 - 线程基础
    • Java 并发 - 各种锁
    • Java 并发 - 关键字 volatile
    • Java 并发 - 关键字 synchronized
    • JUC - CAS与原子操作
    • JUC - 锁核心类AQS
    • JUC - 锁接口和类简介
    • JUC - 并发容器简介
    • JUC - 通信工具类
    • JUC - Fork-Join框架
    • JUC - 线程池
  • Java 进阶 - JVM

    • JVM - 概述
    • JVM - 类加载机制
    • JVM - 内存结构
    • JVM - 垃圾回收机制
    • JVM - 性能调优
  • Maven系列

    • Maven基础知识
    • Maven项目构建
    • Maven多模块配置
  • Spring 框架

    • Spring 框架 - 框架介绍
    • Spring 框架 - IOC详解
    • Spring 框架 - AOP详解
    • Spring 框架 - SpringMVC详解
  • Spring Boot 系列

    • Spring Boot - 开发入门
    • Spring Boot - 接口相关
  • Spring Cloud 系列
  • Mybatis 系列

    • Mybatis - 总体框架设计
    • Mybatis - 初始化基本过程
    • Mybatis - sqlSession执行过程
    • Mybatis - 插件机制
    • Mybatis - 事务管理机制
    • Mybatis - 缓存机制
  • 业务常见问题

    • Java 业务开发常见错误(一)
    • Java 业务开发常见错误(二)
    • Java 业务开发常见错误(三)
    • Java 业务开发常见错误(四)
    • Java 业务开发常见错误(五)
    • Java 业务开发常见错误(六)
  • IDEA系列

    • IDEA 2021开发环境配置
    • IDEA 快捷键
  • Git系列

    • git status中文乱码
  • 其他

    • Typora+Picgo 自动上传图片
    • hsdis 和 jitwatch
  • 实用技巧
  • 收藏
  • 摄影
  • 学习
  • 标签
  • 归档

Jason Huang

后端程序猿
首页
  • 数据结构与算法

    • 数据结构与算法 - 概述
    • 数据结构与算法 - 复杂度分析
    • 数据结构 - 线性表
    • 算法 - 常见排序算法
  • 代码规范

    • 代码简洁之道
    • 阿里巴巴开发手册
    • 谷歌Java编程风格指南
  • 设计模式

    • 编写高质量代码概述
    • 面向对象
    • 设计原则
    • 设计模式-创建型
    • 设计模式-结构型
    • 设计模式-行为型(上)
    • 设计模式-行为型(下)
    • 浅析框架源码中的设计模式
    • 业务框架实战案例
  • MySQL 基础

    • MySQL - 数据库设计规范
    • MySQL - 必知必会
  • MySQL 进阶

    • MySQL - 基础架构
    • MySQL - InnoDB存储引擎
    • MySQL - InnoDB缓冲池
    • MySQL - 事务与锁
    • MySQL - 索引
    • MySQL - 查询执行计划
    • MySQL - 性能优化
  • Redis 系列

    • Redis入门 - 基础相关
    • Redis进阶 - 数据结构
    • Redis进阶 - 持久化RDB和AOF
    • Redis进阶 - 事件机制
    • Redis进阶 - 事务
    • Redis进阶 - 高可用高可扩展
    • Redis进阶 - 缓存问题
    • Redis进阶 - 性能调优
  • Java 基础

    • Java 基础 - 知识点
    • Java 基础 - 面向对象
    • Java 基础 - Q/A
  • Java 进阶 - 集合框架

    • Java 集合框架详解
  • Java 进阶 - 多线程与并发

    • Java 并发 - 理论基础
    • Java 并发 - 线程基础
    • Java 并发 - 各种锁
    • Java 并发 - 关键字 volatile
    • Java 并发 - 关键字 synchronized
    • JUC - CAS与原子操作
    • JUC - 锁核心类AQS
    • JUC - 锁接口和类简介
    • JUC - 并发容器简介
    • JUC - 通信工具类
    • JUC - Fork-Join框架
    • JUC - 线程池
  • Java 进阶 - JVM

    • JVM - 概述
    • JVM - 类加载机制
    • JVM - 内存结构
    • JVM - 垃圾回收机制
    • JVM - 性能调优
  • Maven系列

    • Maven基础知识
    • Maven项目构建
    • Maven多模块配置
  • Spring 框架

    • Spring 框架 - 框架介绍
    • Spring 框架 - IOC详解
    • Spring 框架 - AOP详解
    • Spring 框架 - SpringMVC详解
  • Spring Boot 系列

    • Spring Boot - 开发入门
    • Spring Boot - 接口相关
  • Spring Cloud 系列
  • Mybatis 系列

    • Mybatis - 总体框架设计
    • Mybatis - 初始化基本过程
    • Mybatis - sqlSession执行过程
    • Mybatis - 插件机制
    • Mybatis - 事务管理机制
    • Mybatis - 缓存机制
  • 业务常见问题

    • Java 业务开发常见错误(一)
    • Java 业务开发常见错误(二)
    • Java 业务开发常见错误(三)
    • Java 业务开发常见错误(四)
    • Java 业务开发常见错误(五)
    • Java 业务开发常见错误(六)
  • IDEA系列

    • IDEA 2021开发环境配置
    • IDEA 快捷键
  • Git系列

    • git status中文乱码
  • 其他

    • Typora+Picgo 自动上传图片
    • hsdis 和 jitwatch
  • 实用技巧
  • 收藏
  • 摄影
  • 学习
  • 标签
  • 归档
  • Java 基础

  • Java 进阶 - 集合框架

  • Java 进阶 - 多线程与并发

    • Java 并发 - 概述
    • Java 并发 - 理论基础
    • Java 并发 - 线程基础
    • Java 并发 - 各种锁
    • Java 并发 - JVM 锁优化
    • Java 并发 - 关键字 volatile
    • Java 并发 - 关键字 synchronized
      • 前言
      • 1. Java 语言提供的锁技术:synchronized
      • 2. synchronized 使用方式
        • 2.1 对象锁
        • 2.2 类锁
      • 3. synchronized 原理
        • 3.1 加锁和释放锁的原理
        • 3.2 可重入原理
        • 3.3 保证可见性原理
      • 4. Synchronized 与 Lock
      • 5. 参考
    • Java 并发 - syschronized 应用及死锁问题
    • Java 并发 - 关键字 final
    • JUC - CAS与原子操作
    • JUC - 锁核心类AQS
    • JUC - 锁接口和类简介
    • JUC - 并发容器简介
    • JUC - 阻塞队列
    • JUC - 通信工具类
    • JUC - Fork/Join框架
    • JUC - Stream并行计算原理
    • JUC - 线程池
  • Java 进阶 - JVM

  • Java 进阶 - 版本特性

  • Java
  • Java 进阶 - 多线程与并发
Jason
目录

Java 并发 - 关键字 synchronized

# Java 并发 - 关键字 synchronized

  • Synchronized 可以作用在哪里? 分别通过对象锁和类锁进行举例。
  • Synchronized 本质上是通过什么保证线程安全的? 分三个方面回答:加锁和释放锁的原理,可重入原理,保证可见性原理。
  • Synchronized 由什么样的缺陷? Java Lock 是怎么弥补这些缺陷的。
  • Synchronized 和 Lock的对比,和选择?
  • Synchronized 在使用时有何注意事项?
  • Synchronized 修饰的方法在抛出异常时,会释放锁吗?
  • 多个线程等待同一个 Synchronized 锁的时候,JVM 如何选择下一个获取锁的线程?
  • Synchronized 使得同时只有一个线程可以执行,性能比较差,有什么提升的方法?
  • 我想更加灵活的控制锁的释放和获取(现在释放锁和获取锁的时机都被规定死了),怎么办?
  • 什么是锁的升级和降级? 什么是 JVM 里的偏斜锁、轻量级锁、重量级锁?
  • 不同的 JDK 中对 Synchronized 有何优化?

# 前言

原子性问题的源头是线程切换,如果我们能够保证对共享变量的修改是互斥的,那么就都能保证原子性了。这里的互斥指的是:同一时刻只有一个线程执行,

当谈到互斥,一般都会联想到”锁“,对于锁有两个非常非常重要的点:我们锁的是什么?我们保护的又是什么?下图为锁模型:

java-concurrent-lock-model

首先,我们要把临界区要保护的资源标注出来,如图中临界区里增加了一个元素:受保护的资源 R;其次,我们要保护资源 R 就得为它创建一把锁 LR;最后,针对这把锁 LR,我们还需在进出临界区时添上加锁操作和解锁操作。另外,在锁 LR 和受保护资源之间特地用一条线做了关联,这个关联关系非常重要。很多并发 Bug 的出现都是因为把它忽略了,然后就出现了类似锁自家门来保护他家资产的事情,这样的 Bug 非常不好诊断,因为潜意识里我们认为已经正确加锁了。

# 1. Java 语言提供的锁技术:synchronized

说到锁,我们通常会谈到 synchronized 这个关键字。它翻译成中文就是“同步”的意思。

首先需要明确的一点是:Java 多线程的锁都是基于对象的,Java 中的每一个对象都可以作为一个锁。

还有一点需要注意的是,我们常听到的类锁其实也是对象锁。

Java 类只有一个 Class 对象(可以有多个实例对象,多个实例共享这个 Class 对象),而 Class 对象也是特殊的Java 对象。所以我们常说的类锁,其实就是 Class 对象的锁。

在使用 sychronized 关键字时需要把握如下注意点:

  • 一把锁只能同时被一个线程获取,没有获得锁的线程只能等待;
  • 每个实例都对应有自己的一把锁(this),不同实例之间互不影响;例外:锁对象是 *.class 以及 synchronized 修饰的是 static 方法的时候,所有对象公用同一把锁
  • synchronized 修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁
  • 锁对象不能为空,因为锁的信息都保存在对象头里。也不能用可变对象做锁。
  • 作用域不宜过大,影响程序执行的速度,控制范围过大,编写代码也容易出错
  • 避免死锁
  • 在能选择的情况下,既不要用 Lock 也不要用 synchronized 关键字,用 java.util.concurrent 包中的各种各样的类,如果不用该包下的类,在满足业务的情况下,可以使用 synchronized 关键,因为代码量少,避免出错
  • synchronized 实际上是非公平的,新来的线程有可能立即获得监视器,而在等待区中等候已久的线程可能再次等待,这样有利于提高性能,但是也可能会导致饥饿现象。

# 2. synchronized 使用方式

class X {
  // 修饰非静态方法
  synchronized void foo() {
    // 临界区
  }
  // 修饰静态方法
  synchronized static void bar() {
    // 临界区
  }
  // 修饰代码块
  Object obj = new Object();
  void baz() {
    synchronized(obj) {
      // 临界区
    }
  }
}  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

可以发现上面代码中没有加锁 lock() 和解锁 unlock(),其实这两个操作都是有的,只是这两个操作是被 Java 默默加上的,Java 编译器会在 synchronized 修饰的方法或代码块前后自动加上加锁 lock() 和解锁 unlock(),这样做的好处就是加锁 lock() 和解锁 unlock() 一定是成对出现的,毕竟忘记解锁 unlock() 可是个致命的 Bug(意味着其他线程只能死等下去了)。

那 synchronized 里的加锁 lock() 和解锁 unlock() 锁定的对象是什么?Java 的一条隐式规则:

  • 当修饰静态方法的时候,锁定的是当前类的 Class 对象,在上面的例子中就是 Class X;
  • 当修饰非静态方法的时候,锁定的是当前实例对象 this。

对于上面的例子,synchronized 修饰静态方法相当于:

class X {
  // 修饰静态方法
  synchronized(X.class) static void bar() {
    // 临界区
  }
}
1
2
3
4
5
6

修饰非静态方法,相当于:

class X {
  // 修饰非静态方法
  synchronized(this) void foo() {
    // 临界区
  }
}
1
2
3
4
5
6

具体例子如下:

# 2.1 对象锁

包括方法锁(默认锁对象为this,当前实例对象)和同步代码块锁(自己指定锁对象)

  1. 代码块形式:手动指定锁定对象,也可是是this,也可以是自定义的锁

    示例1:

    public class SynchronizedObjectLock implements Runnable {
        static SynchronizedObjectLock instance = new SynchronizedObjectLock();
    
        @Override
        public void run() {
            // 同步代码块形式——锁为this,两个线程使用的锁是一样的,线程1必须要等到线程0释放了该锁后,才能执行
            synchronized (this) {
                System.out.println("我是线程" + Thread.currentThread().getName());
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "结束");
            }
        }
    
        public static void main(String[] args) {
            Thread t1 = new Thread(instance);
            Thread t2 = new Thread(instance);
            t1.start();
            t2.start();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    输出结果:

    我是线程Thread-0
    Thread-0结束
    我是线程Thread-1
    Thread-1结束
    
    1
    2
    3
    4

    实例2:

    public class SynchronizedObjectLock implements Runnable {
        static SynchronizedObjectLock instance = new SynchronizedObjectLock();
        // 创建2把锁
        Object block1 = new Object();
        Object block2 = new Object();
    
        @Override
        public void run() {
            // 这个代码块使用的是第一把锁,当他释放后,后面的代码块由于使用的是第二把锁,因此可以马上执行
            synchronized (block1) {
                System.out.println("block1锁,我是线程" + Thread.currentThread().getName());
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("block1锁,"+Thread.currentThread().getName() + "结束");
            }
    
            synchronized (block2) {
                System.out.println("block2锁,我是线程" + Thread.currentThread().getName());
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("block2锁,"+Thread.currentThread().getName() + "结束");
            }
        }
    
        public static void main(String[] args) {
            Thread t1 = new Thread(instance);
            Thread t2 = new Thread(instance);
            t1.start();
            t2.start();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37

    输出结果:

    block1锁,我是线程Thread-0
    block1锁,Thread-0结束
    block2锁,我是线程Thread-0  // 可以看到当第一个线程在执行完第一段同步代码块之后,第二个同步代码块可以马上得到执行,因为他们使用的锁不是同一把
    block1锁,我是线程Thread-1
    block2锁,Thread-0结束
    block1锁,Thread-1结束
    block2锁,我是线程Thread-1
    block2锁,Thread-1结束
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  2. 方法锁形式:synchronized 修饰普通方法,锁对象默认为 this

    public class SynchronizedObjectLock implements Runnable {
        static SynchronizedObjectLock instance = new SynchronizedObjectLock();
    
        @Override
        public void run() {
            method();
        }
    
        public synchronized void method() {
            System.out.println("我是线程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "结束");
        }
    
        public static void main(String[] args) {
            Thread t1 = new Thread(instance);
            Thread t2 = new Thread(instance);
            t1.start();
            t2.start();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    输出结果:

    我是线程Thread-0
    Thread-0结束
    我是线程Thread-1
    Thread-1结束
    
    1
    2
    3
    4

# 2.2 类锁

指 synchronized 修饰静态的方法或指定锁对象为 Class 对象

  1. synchronized 修饰静态方法

    实例1:

    public class SynchronizedObjectLock implements Runnable {
        static SynchronizedObjectLock instance1 = new SynchronizedObjectLock();
        static SynchronizedObjectLock instance2 = new SynchronizedObjectLock();
    
        @Override
        public void run() {
            method();
        }
    
        // synchronized用在普通方法上,默认的锁就是this,当前实例
        public synchronized void method() {
            System.out.println("我是线程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "结束");
        }
    
        public static void main(String[] args) {
            // t1和t2对应的this是两个不同的实例,所以代码不会串行
            Thread t1 = new Thread(instance1);
            Thread t2 = new Thread(instance2);
            t1.start();
            t2.start();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28

    输出结果:

    我是线程Thread-0
    我是线程Thread-1
    Thread-1结束
    Thread-0结束
    
    1
    2
    3
    4

    实例2:

    public class SynchronizedObjectLock implements Runnable {
        static SynchronizedObjectLock instance1 = new SynchronizedObjectLock();
        static SynchronizedObjectLock instance2 = new SynchronizedObjectLock();
    
        @Override
        public void run() {
            method();
        }
    
        // synchronized用在静态方法上,默认的锁就是当前所在的Class类,所以无论是哪个线程访问它,需要的锁都只有一把
        public static synchronized void method() {
            System.out.println("我是线程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "结束");
        }
    
        public static void main(String[] args) {
            Thread t1 = new Thread(instance1);
            Thread t2 = new Thread(instance2);
            t1.start();
            t2.start();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27

    输出结果:

    我是线程Thread-0
    Thread-0结束
    我是线程Thread-1
    Thread-1结束
    
    1
    2
    3
    4
  2. synchronized 指定锁对象为 Class 对象

    public class SynchronizedObjectLock implements Runnable {
        static SynchronizedObjectLock instance1 = new SynchronizedObjectLock();
        static SynchronizedObjectLock instance2 = new SynchronizedObjectLock();
    
        @Override
        public void run() {
            // 所有线程需要的锁都是同一把
            synchronized(SynchronizedObjectLock.class){
                System.out.println("我是线程" + Thread.currentThread().getName());
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "结束");
            }
        }
    
        public static void main(String[] args) {
            Thread t1 = new Thread(instance1);
            Thread t2 = new Thread(instance2);
            t1.start();
            t2.start();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    输出结果:

    我是线程Thread-0
    Thread-0结束
    我是线程Thread-1
    Thread-1结束
    
    1
    2
    3
    4

# 3. synchronized 原理

# 3.1 加锁和释放锁的原理

深入JVM看字节码,创建如下的代码:

public class SynchronizedDemo {

    Object object = new Object();
    public void method1() {
        synchronized (object) {

        }
        method2();
    }

    private static void method2() {

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

使用 javac 命令进行编译生成 .class 文件

javac SynchronizedDemo.java
1

使用 javap 命令反编译查看 .class 文件的信息

javap -verbose SynchronizedDemo.class
1

结果如下:

java-concurrent-synchronized-monitor

其中monitorenter和mnitorexit指令,会让对象在执行,使其锁计数器加 1 或者减 1。每一个对象在同一时间只与一个 monitor(锁)相关联,而一个 monitor 在同一时间只能被一个线程获得,一个对象在尝试获得与这个对象相关联的 monitor 锁的所有权的时候,monitorenter 指令会发生如下 3 种情况之一:

  • monitor 计数器为 0,意味着目前还没有被获得,那这个线程就会立刻获得,然后把锁计数器 +1,一旦 +1,别的线程再想获取,就需要等待
  • 如果这个 monitor 已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成 2,并且随着重入的次数,会一直累加
  • 这把锁已经被别的线程获取了,等待锁释放

monitorexit指令:释放对于 monitor 的所有权,释放过程很简单,就是将 monitor 的计数器减 1,如果减完以后,计数器不是 0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成 0,则代表当前线程不再拥有该 monitor 的所有权,即释放锁。

下图为对象,对象监视器,同步队列以及执行线程状态之间的关系:

java-concurrent-synchronized-monitor2

该图可以看出,任意线程对 Object 的访问,首先要获得 Object 的监视器,如果获取失败,该线程就进入同步状态,线程状态变为 BLOCKED,当 Object 的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。

# 3.2 可重入原理

可重入:(来源于维基百科)若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant 或 re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。

可重入锁:又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者 class),不会因为之前已经获取过还没释放而阻塞。

看如下的例子:

public class SynchronizedDemo {

    public static void main(String[] args) {
        SynchronizedDemo demo =  new SynchronizedDemo();
        demo.method1();
    }

    private synchronized void method1() {
        System.out.println(Thread.currentThread().getId() + ": method1()");
        method2();
    }

    private synchronized void method2() {
        System.out.println(Thread.currentThread().getId()+ ": method2()");
        method3();
    }

    private synchronized void method3() {
        System.out.println(Thread.currentThread().getId()+ ": method3()");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

结合前文中加锁和释放锁的原理,不难理解:

  • 执行 monitorenter 获取锁
    • (monitor 计数器 = 0,可获取锁)
    • 执行 method1() 方法,monitor 计数器 +1 -> 1 (获取到锁)
    • 执行 method2() 方法,monitor 计数器 +1 -> 2
    • 执行 method3() 方法,monitor 计数器 +1 -> 3
  • 执行monitorexit命令
    • method3() 方法执行完,monitor 计数器 -1 -> 2
    • method2() 方法执行完,monitor 计数器 -1 -> 1
    • method2() 方法执行完,monitor 计数器 -1 -> 0 (释放了锁)
    • (monitor计数器=0,锁被释放了)

这就是 synchronized 的重入性,即在同一锁程中,每个对象拥有一个 monitor 计数器,当线程获取该对象锁后,monitor 计数器就会加一,释放锁后就会将 monitor 计数器减一,线程不需要再次获取同一把锁。

# 3.3 保证可见性原理

synchronized 的 happens-before 规则,即监视器锁规则:对同一个监视器的解锁 happens-before 于对该监视器的加锁。看下面代码:

public class MonitorDemo {
    private int a = 0;

    public synchronized void writer() {     // 1
        a++;                                // 2
    }                                       // 3

    public synchronized void reader() {    // 4
        int i = a;                         // 5
    }                                      // 6
}
1
2
3
4
5
6
7
8
9
10
11

该代码的 happens-before 关系如图所示:

java-concurrent-synchronized-happend-before

在图中每一个箭头连接的两个节点就代表之间的 happens-before 关系,黑色的是通过程序顺序规则推导出来,红色的为监视器锁规则推导而出:线程 A 释放锁 happens-before 线程 B 加锁,蓝色的则是通过程序顺序规则和监视器锁规则推测出来 happens-befor 关系,通过传递性规则进一步推导的 happens-before 关系。现在来重点关注 2 happens-before 5,通过这个关系我们可以得出什么?

根据 happens-before 的定义中的一条:如果 A happens-before B,则 A 的执行结果对 B 可见,并且 A 的执行顺序先于 B。线程 A 先对共享变量 a 进行加一,由 2 happens-before 5 关系可知线程 A 的执行结果对线程 B 可见即线程 B 所读取到的 a 的值为 1。

# 4. Synchronized 与 Lock

synchronized 的缺陷

  • 效率低:锁的释放情况少,只有代码执行完毕或者异常结束才会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock 可以中断和设置超时
  • 不够灵活:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活
  • 无法知道是否成功获得锁,相对而言,Lock 可以拿到状态,如果成功获取锁,....,如果获取失败,.....

Lock 解决相应问题

Lock 类这里不做过多解释,主要看里面的 4 个方法:

  • lock(): 加锁
  • unlock(): 解锁
  • tryLock(): 尝试获取锁,返回一个 boolean 值
  • tryLock(long,TimeUtil): 尝试获取锁,可以设置超时

synchronized 加锁只与一个条件(是否获取锁)相关联,不灵活,后来Condition 与 Lock 的结合解决了这个问题。

多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断。高并发的情况下会导致性能下降。ReentrantLock 的 lockInterruptibly() 方法可以优先考虑响应中断。 一个线程等待时间过长,它可以中断自己,然后 ReentrantLock 响应这个中断,不再让这个线程继续等待。有了这个机制,使用 ReentrantLock 时就不会像synchronized 那样产生死锁了。

# 5. 参考

  • http://concurrent.redspider.group/article/02/9.html
  • Java锁优化--JVM锁降级 (opens new window)
  • 死磕Synchronized底层实现 (opens new window)
  • 《Java并发编程的艺术》
  • https://pdai.tech/md/java/thread/java-thread-x-key-synchronized.html
#Java#synchronized
上次更新: 2024-08-19
Java 并发 - 关键字 volatile
Java 并发 - syschronized 应用及死锁问题

← Java 并发 - 关键字 volatile Java 并发 - syschronized 应用及死锁问题→

最近更新
01
开始
01-09
02
AI工具分享
01-09
03
AI 导读
01-07
更多文章>
Theme by Vdoing | Copyright © 2022-2025 Jason Huang | 闽ICP备2025088096号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式