程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • JUC并发编程

    • 第一章 线程的基础概念
    • 第二章 并发编程的三大特性
    • 第三章 synchronized的总结
      • 1. 锁的分类
        • 1.1 可重入锁、不可重入锁
        • synchronized实现可重入锁
        • ReentrantLock实现可重入锁
        • 1.2 乐观锁、悲观锁
        • 1.3 公平锁、非公平锁
        • 1.4 互斥锁、共享锁
      • 2. 深入synchronized
        • 2.1 类锁、对象锁
        • 2.1.1 修饰普通方法
        • 2.1.2 修饰静态方法
        • 2.1.3 修饰代码块
        • 1. 成员锁
        • 2. this
        • 3. .class
        • 2.2 synchronized的优化
        • 2.3 synchronized实现原理
        • 2.4 synchronized的锁升级
        • 2.5 重量锁底层ObjectMonitor
    • 第四章 深入ReentrantLock
    • 第五章 深入ReentrantReadWriteLock
    • 第六章 阻塞队列
    • 第七章 线程池
    • 第八章 并发集合
    • 第九章 并发工具
    • 第十章 异步编程
  • JavaEE
  • JUC并发编程
scholar
2024-03-14
目录

第三章 synchronized的总结

# 1. 锁的分类

# 1.1 可重入锁、不可重入锁

Java中提供的synchronized,ReentrantLock,ReentrantReadWriteLock都是可重入锁。

重入:当前线程获取到A锁,在获取之后尝试再次获取A锁是可以直接拿到的。

不可重入:当前线程获取到A锁,在获取之后尝试再次获取A锁,无法获取到的,因为A锁被当前线程占用着,需要等待自己释放锁再获取锁。

可重入锁和不可重入锁是多线程编程中两种重要的锁机制,它们控制着线程对共享资源的访问。

总结

  • 可重入锁允许线程再次获取它已经持有的锁。这意味着,如果一个线程已经获得了某个锁,它可以再次请求并获得这个锁,而无需等待这个锁被释放。这种机制对于递归调用、循环调用或者调用其他需要同一锁的同步方法时非常有用,因为它避免了线程自己将自己阻塞。
  • 这种可重入性是针对同一个线程的。如果一个线程已经持有了某个锁,只有这个线程可以再次获取这个锁,其他的线程如果尝试获取这个锁,就会被阻塞,直到锁被持有的线程释放锁。

# synchronized实现可重入锁

public class SynchronizedExample {
    public synchronized void outerMethod() {
        System.out.println("在外层方法中");
        innerMethod();
    }

    public synchronized void innerMethod() {
        System.out.println("在内层方法中");
        // 这里可以再次获得这个对象的锁
    }

    public static void main(String[] args) {
        SynchronizedExample example = new SynchronizedExample();
        example.outerMethod();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# ReentrantLock实现可重入锁

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void outerMethod() {
        lock.lock();
        try {
            System.out.println("在外层方法中");
            innerMethod();
        } finally {
            lock.unlock();
        }
    }

    public void innerMethod() {
        lock.lock();
        try {
            System.out.println("在内层方法中");
            // 这里可以再次获得这个锁
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockExample example = new ReentrantLockExample();
        example.outerMethod();
    }
}
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

# 1.2 乐观锁、悲观锁

Java中提供的synchronized,ReentrantLock,ReentrantReadWriteLock都是悲观锁。

Java中提供的CAS操作,就是乐观锁的一种实现。

悲观锁:获取不到锁资源时,会将当前线程挂起(进入BLOCKED、WAITING),线程挂起会涉及到用户态和内核态的切换,而这种切换是比较消耗资源的。

  • 用户态:JVM可以自行执行的指令,不需要借助操作系统执行。
  • 内核态:JVM不可以自行执行,需要操作系统才可以执行。

乐观锁:获取不到锁资源,可以再次让CPU调度,重新尝试获取锁资源。

Atomic原子性类中,就是基于CAS乐观锁实现的。

# 1.3 公平锁、非公平锁

Java中提供的synchronized只能是非公平锁。

Java中提供的ReentrantLock,ReentrantReadWriteLock可以实现公平锁和非公平锁

公平锁:线程A获取到了锁资源,线程B没有拿到,线程B去排队,线程C来了,锁被A持有,同时线程B在排队。直接排到B的后面,等待B拿到锁资源或者是B取消后,才可以尝试去竞争锁资源。

非公平锁:线程A获取到了锁资源,线程B没有拿到,线程B去排队,线程C来了,先尝试竞争一波

  • 拿到锁资源:开心,插队成功。
  • 没有拿到锁资源:依然要排到B的后面,等待B拿到锁资源或者是B取消后,才可以尝试去竞争锁资源。

# 1.4 互斥锁、共享锁

Java中提供的synchronized、ReentrantLock是互斥锁。

Java中提供的ReentrantReadWriteLock,有互斥锁也有共享锁。

互斥锁:同一时间点,只会有一个线程持有者当前互斥锁。

共享锁:同一时间点,当前共享锁可以被多个线程同时持有。

# 2. 深入synchronized

synchronized根据加锁目标的不同,分为类锁(加在静态方法或.class对象上)和对象锁(加在实例方法或实例对象上),以控制同步代码的访问,哪个线程先执行这样的方法或代码块,哪个线程就获得了锁。

# 2.1 类锁、对象锁

类锁的工作原理:当一个线程进入一个类的任一静态同步方法时,它会获取那个类的Class对象的锁。在该线程持有锁期间,其他任何线程都无法进入同一个类的任何静态同步方法。这是因为所有的静态同步方法共享同一个类锁。

对象锁的工作原理

  1. 锁定实例方法:当一个线程想要执行某个对象的同步实例方法时,它必须首先获得该对象的锁。如果锁可用(即当前没有其他线程持有该锁),则该线程获得锁,并进入方法。如果锁不可用(已被其他线程持有),该线程将阻塞,直到锁被释放。
  2. 锁定代码块:对于同步代码块(synchronized(this)或synchronized(对象引用)),工作机制类似。执行同步代码块的线程必须获得与this或指定对象引用相关联的锁。这种方式允许更细粒度的锁控制,可以仅对需要同步的最小代码量加锁。

synchronized 的 三种用法:

  • 修饰方法
  • 修饰静态方法
  • 修饰代码块

# 2.1.1 修饰普通方法

当synchronized修饰非静态方法时,它是使用当前实例对象作为锁,也就是所谓的“对象锁”或“实例锁”。这意味着,对于该方法的同步是基于调用这个方法的对象实例。在任何时刻,只有一个线程能够访问该对象的任一synchronized非静态方法。

public synchronized void increase() {
    i++;
}
1
2
3
  • 每个实例对象都有自己的锁(this锁)。
  • 如果多个线程试图同时执行同一个对象的synchronized非静态方法,这些线程会因为试图获得同一个锁而相互阻塞,直到锁被释放。
  • 如果每个线程操作的是不同的实例,则每个线程都能够进入各自操作的对象的同步方法,因为它们访问的是不同对象的锁,彼此之间不会产生互斥。

假设我们有一个简单的计数器类,它有一个非静态的synchronized方法increase用来增加计数器的值:多个线程使用同一个实例

public class Counter {
    private static int count = 0; // 将count声明为静态变量

    // 使用对象锁同步静态变量的增加操作
    public synchronized void increase() {
        count++;
    }

    public static int getCount() {
        return count;
    }

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

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increase(); // t1使用counter实例增加count
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increase(); // t2使用counter实例增加count
            }
        });

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

        t1.join();
        t2.join();

        System.out.println("Final count is: " + Counter.getCount()); 
    }
}
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

image-20240314135623475

在这个例子中,t1和t2两个线程共享同一个Counter实例。当它们尝试调用increase方法时,由于该方法是synchronized的,且锁定的是当前实例对象(this锁),所以在任何时刻只能有一个线程能够执行该方法。这就实现了互斥,确保了count的正确性。

多个线程使用不同的实例,如果每个实例调用的方法加的是对象锁的情况下可能会出现线程安全问题。

public class Counter {
    private static int count = 0; // 将count声明为静态变量

    // 使用对象锁同步静态变量的增加操作
    public synchronized void increase() {
        count++;
    }

    public static int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter1 = new Counter();
        Counter counter2 = new Counter();  

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter1.increase(); // t1使用counter1实例增加count
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter2.increase(); // t2使用counter2实例增加count
            }
        });

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

        t1.join();
        t2.join();

        System.out.println("Final count is: " + Counter.getCount()); // 输出期望是2000,但方法不是线程最安全的做法
    }
}
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

image-20240314135402886

在这个例子中,t1和t2操作不同的Counter实例。因为每个实例都有自己的锁,所以t1和t2可以同时执行它们各自实例的increase方法,没有互斥发生,最终的结果不一致导致线程安全问题。

这两个示例清晰地说明了当多个线程使用同一个实例调用synchronized非静态方法时会产生互斥,而使用不同实例则不会。

# 2.1.2 修饰静态方法

当synchronized修饰静态方法时,它锁定的是整个类的Class对象,这种锁通常被称为“类锁”。静态方法属于类级别而不是实例级别,因此它锁定的不是某个对象实例,而是与类相关联的Class对象本身。这意味着,无论有多少个实例对象被创建,静态同步方法的锁都是唯一的,并且是共享的。

public static synchronized void increase() {
    i++;
}
1
2
3

假设我们有一个简单的计数器类,它包含一个静态变量count和一个静态同步方法increase用于递增count的值。此外,我们创建多个线程来调用increase方法,以演示类锁的效果。

public class StaticCounter {
    private static int count = 0; // 定义一个静态变量count,用于计数

    // 定义一个静态同步方法increase,每次调用时count增加1
    public static synchronized void increase() {
        count++;
    }

    // 获取当前的count值
    public static int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[100]; // 创建一个线程数组,总共100个线程
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    StaticCounter.increase(); // 每个线程调用increase方法1000次
                }
            });
            threads[i].start(); // 启动线程
        }

        for (Thread thread : threads) {
            thread.join(); // 等待所有线程执行完毕
        }

        // 所有线程执行完毕后,打印最终的count值
        System.out.println("Final count is: " + StaticCounter.getCount()); // 应该输出100000
    }
}
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

image-20240314120705169

  • 在main方法中,我们创建了100个线程,每个线程都会尝试调用increase方法1000次。这意味着,理论上increase方法会被调用总共100 * 1000 = 100000次。
  • 由于increase方法是同步的,所有这些线程会争夺同一个锁(即StaticCounter.class对象的锁),因此在任何时刻只能有一个线程能够执行increase方法,保证了count的正确增加和线程安全。

需要注意的是

  • ​ 如果一个线程 A 调用一个实例对象的非 static静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的 static静态 synchronized 方法,是允许的,不会发生互斥现象,

  • ​ 因为访问静态 synchronized 方法锁住的是当前类的 class 对象,而访问非静态 synchronized 方法锁住的是当前实例对象,二者的锁并不一样,所以不冲突。

# 2.1.3 修饰代码块

在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方法对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了。我们可以使用如下几种对象来作为锁的对象:

# 1. 成员锁

静态成员变量作为锁

  • 作用范围:类级别,影响类的所有实例。
  • 锁对象:由于是静态的,这个对象是属于类的,而不是属于任何单独的实例。因此,不论是哪个实例的线程,或者是直接通过类名访问的线程,都共享这一个锁。
  • 适用场景:适用于需要控制静态数据访问或者跨所有实例的共享资源访问的场景。

非静态变量作为锁

  • 作用范围:实例级别,只影响锁定对象的实例。
  • 锁对象:每个实例都有自己的lock对象,因此,每个实例的同步代码块是独立的。不同实例的线程不会因为争夺这个锁而相互影响。
  • 适用场景:适用于控制实例内部状态的并发访问,当不同实例之间的状态是独立的时候。

这里的锁是由传入的参数a1指向的对象提供的。这种锁通常被称为对象锁,但更具体地,因为它是基于传入的对象参数的,我们可以称之为成员锁或自定义对象锁。这种锁的特点和作用域完全依赖于传入的对象a1。

public Object synMethod(Object a1) {
    // 使用a1对象作为锁
    synchronized(a1) {
        // 只有持有a1对象锁的线程才能执行这里的代码
        // 这里进行需要同步的操作
    }
    // 当退出synchronized块时,a1对象的锁被释放,其他线程可以尝试获取这个锁来执行同步块
}
1
2
3
4
5
6
7
8

假设我们有一个类MessagePrinter,该类的作用是打印一条消息。为了保证在打印消息时不会被其他线程中断,我们决定只同步打印消息的那部分代码,而不是整个打印方法。

public class MessagePrinter {
    // 定义一个静态成员变量作为锁对象
    private static final Object lock = new Object();

    // 模拟的消息内容
    private String message;

    public MessagePrinter(String message) {
        this.message = message;
    }

    // 打印消息的方法,只有打印部分需要同步
    public void printMessage() {
        synchronized(lock) { // 使用成员变量lock作为锁
            // 下面这行代码是需要同步的操作,以保证消息完整打印
            try {
                System.out.println(Thread.currentThread().getName() + "进入了");
                Thread.sleep(1);
                System.out.println(Thread.currentThread().getName() +"打印了:"+message);
                System.out.println(Thread.currentThread().getName() + "退出了");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        }
        // 可以添加更多非同步的操作
    }

    public static void main(String[] args) {
        MessagePrinter printer1 = new MessagePrinter("Hello, World!");
        MessagePrinter printer2 = new MessagePrinter("Hello, World!");
        MessagePrinter printer3 = new MessagePrinter("Hello, World!");

        // 创建多个线程来调用printMessage方法
        Thread t1 = new Thread(printer1::printMessage);
        Thread t2 = new Thread(printer2::printMessage);
        Thread t3 = new Thread(printer3::printMessage);

        // 启动线程
        t1.start();
        t2.start();
        t3.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
38
39
40
41
42
43
44

image-20240314130414603

  • 由于synchronized(lock)使用的是一个静态成员变量lock作为锁对象,这实际上创建了一个类锁,意味着所有的MessagePrinter实例都共享这一个锁。
  • 因此,即使是不同的MessagePrinter实例,多个线程在执行printMessage方法时也会因为争夺同一个锁而相互阻塞。

此时如果我把静态成员变量lock的static关键字去掉变成非静态变量,这实际上创建了一个对象锁,测试打印结果如下:

image-20240314133222658

每个线程开启都会创建一个新的对象,所以它们共用的不是同一个对象锁,所以不会产生锁竞争。

# 2. this

这里的this代表当前实例对象本身,因此锁定的是当前对象的实例,这种锁确实被称为对象锁。

synchronized(this) {
    for (int j = 0; j < 100; j++) {
		i++;
    }
}
1
2
3
4
5

假设我们有一个BankAccount类,该类包含存款和取款的方法。为了确保账户余额的一致性,我们需要在修改余额时进行同步控制。

public class BankAccount {
    private int balance = 0; // 账户余额

    // 存款方法
    public void deposit(int amount) {
        synchronized(this) { // 使用当前实例对象作为锁
            balance += amount; // 增加余额
            System.out.println("存款:" + amount + ",当前余额:" + balance);
        }
    }

    // 取款方法
    public void withdraw(int amount) {
        synchronized(this) { // 使用当前实例对象作为锁
            if (balance >= amount) {
                balance -= amount; // 减少余额
                System.out.println("取款:" + amount + ",当前余额:" + balance);
            } else {
                System.out.println("余额不足,取款失败");
            }
        }
    }

    public static void main(String[] args) {
        BankAccount account = new BankAccount();

        // 创建一个存款线程
        Thread depositThread = new Thread(() -> {
            account.deposit(100);
        });

        // 创建一个取款线程
        Thread withdrawThread = new Thread(() -> {
            account.withdraw(50);
        });

        depositThread.start(); // 启动存款线程
        withdrawThread.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
38
39
40

image-20240314125810312

  • 因为两个线程(存款线程和取款线程)都是针对同一个BankAccount实例进行操作的,所以它们在执行deposit和withdraw方法内部的synchronized(this)同步代码块时,都需要获取这个特定BankAccount实例的锁。
  • 如果一个线程已经持有了这个锁(比如正在执行存款操作),那么另一个线程(比如试图执行取款操作的线程)就必须等待,直到第一个线程完成同步代码块的执行并释放了锁。在这个锁被释放之前,任何尝试进入任何synchronized(this)块的其他线程都将被阻塞。

假设有两个BankAccount对象实例,分别由线程A和线程B操作:就会出现线程安全问题。

BankAccount accountA = new BankAccount();
BankAccount accountB = new BankAccount();

Thread threadA = new Thread(() -> {
    accountA.deposit(100); // 线程A操作accountA实例
});

Thread threadB = new Thread(() -> {
    accountB.withdraw(50); // 线程B操作accountB实例
});

threadA.start();
threadB.start();
1
2
3
4
5
6
7
8
9
10
11
12
13

image-20240314130631761

  • 在这种情况下,即使deposit和withdraw方法使用synchronized(this)同步,线程A和线程B也能同时执行各自的操作,因为它们分别锁定了不同的对象实例(accountA和accountB),线程B在没有存款之前就进行了取款操作,导致操作失败。
# 3. .class

当使用.class与synchronized关键字结合来同步代码块时,锁定的是当前类的Class对象,也就是通常所说的类锁。

synchronized(AccountingSync.class) {
    for (int j = 0; j < 100; j++) {
        i++;
    }
}
1
2
3
4
5

提示

不论是通过synchronized关键字修饰的静态方法加锁,还是在代码块中使用.class来加锁,它们都是锁定的同一个对象——该类的Class对象本身。因此,无论哪种方式加锁,都会阻止其他线程同时访问同一个类中任何其他使用了类锁(.class对象锁)的同步代码块或静态同步方法。这保证了在任何时刻,只有一个线程能执行该类中的任何一个静态同步代码块或方法,从而实现线程间的同步和数据的一致性。

总结:选择合适的同步锁对于保证数据的一致性和避免线程安全问题非常重要。在多个线程需要操作同一个共享资源的情况下,通常需要选择一个所有线程都能访问到,且在整个应用中都是唯一的对象作为锁。

# 2.2 synchronized的优化

在JDK1.5的时候,Doug Lee推出了ReentrantLock,lock的性能远高于synchronized,所以JDK团队就在JDK1.6中,对synchronized做了大量的优化。

锁消除:在synchronized修饰的代码中,如果不存在操作临界资源(共享资源)的情况,会触发锁消除,你即便写了synchronized,它也不会触发。

public synchronized void method(){
    // 没有操作临界资源(共享资源)
    // 此时这个方法的synchronized你可以认为木有~~
}
1
2
3
4

锁膨胀:如果在一个循环中,频繁的获取和释放做资源,这样带来的消耗很大,锁膨胀就是将锁的范围扩大,避免频繁的竞争和获取锁资源带来不必要的消耗。

public void method(){
    for(int i = 0;i < 999999;i++){
        synchronized(对象){

        }
    }
    // 上面的代码会触发锁膨胀变成下面的代码
    synchronized(对象){
        for(int i = 0;i < 999999;i++){

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

锁升级:ReentrantLock的实现,是先基于乐观锁的CAS尝试获取锁资源,如果拿不到锁资源,才会挂起线程。synchronized在JDK1.6之前,完全就是获取不到锁,立即挂起当前线程,所以synchronized性能比较差。

synchronized就在JDK1.6做了锁升级的优化

  • 无锁、匿名偏向:对象在没有被任何线程锁定时处于无锁状态。启用偏向锁后但还未有线程尝试获取它的状态被称为匿名偏向状态
  • 偏向锁:如果当前锁资源,只有一个线程在频繁的获取和释放,那么这个线程过来,只需要判断,当前指向的线程是否是为自己。
    • 如果是,直接拿着锁资源走。
    • 如果当前线程不是自己,那么偏向锁会尝试使用CAS将偏向锁指向自己。如果获取不到,触发锁升级,升级为轻量级锁。(偏向锁状态出现了锁竞争的情况就会升级为轻量级锁)
  • 轻量级锁:会采用自旋锁的方式去频繁的以CAS的形式获取锁资源(采用的是自适应自旋锁)
    • 如果成功获取到,拿着锁资源走。
    • 如果自旋了一定次数,没拿到锁资源,会升级为重量级锁。
  • 重量级锁:就是最传统的synchronized锁,拿不到锁资源,就挂起当前线程,直到锁被释放。(使用重量级锁时,存在用户态和内核态之间的切换开销,因此相对于前两种锁的性能开销较大)

偏向锁的工作流程:

  1. 首次加锁:当线程A第一次对对象加锁时,偏向锁机制会在对象头的Mark Word中记录线程A的ID,并偏向于线程A。这个过程需要一定的同步开销(比如CAS操作),但只在第一次加锁时发生。
  2. 后续加锁:之后,如果线程A再次尝试对这个对象加锁,它会检查对象头中的Mark Word,发现锁已经偏向于自己(线程ID匹配),那么线程A可以直接进入同步块,而无需任何额外的同步开销。这意味着,对于这个线程来说,后续的锁操作几乎是零开销的。
  3. 其他线程加锁:如果另一个线程B尝试对这个已经偏向于线程A的对象加锁,偏向锁机制会检测到线程ID不匹配。这时,偏向锁可能会被撤销,并根据具体情况升级为轻量级锁或重量级锁,以处理更复杂的线程竞争情况。

synchronized 是固定自旋次数吗?

  • synchronized是自适应自旋锁。自适应自旋锁是指,线程自旋的次数不再是固定的值,而是一个动态改变的值。这个值会根据前一次自旋获取锁的状态来决定此次自旋的次数。
  • 例如,上一次通过自旋成功获取到了锁,那么这次通过自旋也有可能会获取到锁,所以这次自旋的次数就会增多一些,而如果上一次通过自旋没有成功获取到锁,那么这次自旋可能也获取不到锁,所以为了避免资源的浪费,就会少循环或者不循环,以提高程序的执行效率。简单来说,如果线程自旋成功了,则下次自旋的次数会增多,如果失败,下次自旋的次数会减少。

# 2.3 synchronized实现原理

synchronized是基于对象实现的。

先要对Java中对象在堆内存的存储有一个了解。

image.png

展开MarkWord

MarkWord中标记着四种锁的信息:无锁、偏向锁、轻量级锁、

# 2.4 synchronized的锁升级

为了可以在Java中看到对象头的MarkWord信息,需要导入依赖

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>
1
2
3
4
5

锁默认情况下,开启了偏向锁延迟。

  • 偏向锁在升级为轻量级锁时,会涉及到偏向锁撤销,需要等到一个安全点(STW),才可以做偏向锁撤销,在明知道有并发情况,就可以选择不开启偏向锁,或者是设置偏向锁延迟开启

  • 因为JVM在启动时,需要加载大量的.class文件到内存中,这个操作会涉及到synchronized的使用,为了避免出现偏向锁撤销操作,JVM启动初期,有一个延迟4s开启偏向锁的操作

  • 如果正常开启偏向锁了,那么不会出现无锁状态,对象会直接变为匿名偏向

public static void main(String[] args) throws InterruptedException {
    // JVM启动后等待5秒,确保偏向锁已经被启用
    Thread.sleep(5000);
    Object o = new Object();
    // 打印对象o的布局信息,此时偏向锁已经启动,对象处于匿名偏向状态
    System.out.println(ClassLayout.parseInstance(o).toPrintable());

    new Thread(() -> {
        // 线程t1对对象o加锁
        synchronized (o){
            // 在t1线程内,打印对象o的布局信息,展示对象当前为偏向锁状态,偏向于线程t1
            System.out.println("t1:" + ClassLayout.parseInstance(o).toPrintable());
        }
    }).start();

    // 主线程对对象o加锁
    synchronized (o){
        // 在主线程内,打印对象o的布局信息
        // 如果这个打印发生在t1线程之后,并且t1线程已经释放了对o的锁,
        // 根据锁的状态,可能展示为轻量级锁或重量级锁(如果有锁竞争)
        System.out.println("main:" + ClassLayout.parseInstance(o).toPrintable());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

整个锁升级状态的转变:

image.png

Lock Record以及ObjectMonitor存储的内容

image.png

# 2.5 重量锁底层ObjectMonitor

需要去找到openjdk,在百度中直接搜索openjdk,第一个链接就是

找到ObjectMonitor的两个文件,hpp,cpp

先查看核心属性:http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/runtime/objectMonitor.hpp (opens new window)

ObjectMonitor() {
    _header       = NULL;   // header存储着MarkWord
    _count        = 0;      // 竞争锁的线程个数
    _waiters      = 0,      // wait的线程个数
    _recursions   = 0;      // 标识当前synchronized锁重入的次数
    _object       = NULL;
    _owner        = NULL;   // 持有锁的线程
    _WaitSet      = NULL;   // 保存wait的线程信息,双向链表
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;  // 获取锁资源失败后,线程要放到当前的单向链表中
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // _cxq以及被唤醒的WaitSet中的线程,在一定机制下,会放到EntryList中
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

适当的查看几个C++中实现的加锁流程

http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/runtime/objectMonitor.cpp (opens new window)

TryLock

int ObjectMonitor::TryLock (Thread * Self) {
   for (;;) {
	  // 拿到持有锁的线程
      void * own = _owner ;
      // 如果有线程持有锁,告辞
      if (own != NULL) return 0 ;
      // 说明没有线程持有锁,own是null,cmpxchg指令就是底层的CAS实现。
      if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) {
		 // 成功获取锁资源
         return 1 ;
      }
      // 这里其实重试操作没什么意义,直接返回-1
      if (true) return -1 ;
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

try_entry

bool ObjectMonitor::try_enter(Thread* THREAD) {
  // 在判断_owner是不是当前线程
  if (THREAD != _owner) {
    // 判断当前持有锁的线程是否是当前线程,说明轻量级锁刚刚升级过来的情况
    if (THREAD->is_lock_owned ((address)_owner)) {
       _owner = THREAD ;
       _recursions = 1 ;
       OwnerIsThread = 1 ;
       return true;
    }
    // CAS操作,尝试获取锁资源
    if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) {
      // 没拿到锁资源,告辞
      return false;
    }
    // 拿到锁资源
    return true;
  } else {
    // 将_recursions + 1,代表锁重入操作。
    _recursions++;
    return true;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

enter(想方设法拿到锁资源,如果没拿到,挂起扔到_cxq单向链表中)

void ATTR ObjectMonitor::enter(TRAPS) {
  // 拿到当前线程
  Thread * const Self = THREAD ;
  void * cur ;
  // CAS走你,
  cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
  if (cur == NULL) {
     // 拿锁成功
     return ;
  }
  // 锁重入操作
  if (cur == Self) {
     // TODO-FIXME: check for integer overflow!  BUGID 6557169.
     _recursions ++ ;
     return ;
  }
  //轻量级锁过来的。
  if (Self->is_lock_owned ((address)cur)) {
    _recursions = 1 ;
    _owner = Self ;
    OwnerIsThread = 1 ;
    return ;
  }


  // 走到这了,没拿到锁资源,count++
  Atomic::inc_ptr(&_count);

  
    for (;;) {
      jt->set_suspend_equivalent();
      // 入队操作,进到cxq中
      EnterI (THREAD) ;
      if (!ExitSuspendEquivalent(jt)) break ;
      _recursions = 0 ;
      _succ = NULL ;
      exit (false, Self) ;
      jt->java_suspend_self();
    }
  }
  // count--
  Atomic::dec_ptr(&_count);
  
}
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
38
39
40
41
42
43
44

EnterI

for (;;) {
    // 入队
    node._next = nxt = _cxq ;
    // CAS的方式入队。
    if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;

    // 重新尝试获取锁资源
    if (TryLock (Self) > 0) {
        assert (_succ != Self         , "invariant") ;
        assert (_owner == Self        , "invariant") ;
        assert (_Responsible != Self  , "invariant") ;
        return ;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

以上便是本文的全部内容,本人才疏学浅,文章有什么错误的地方,欢迎大佬们批评指正!我是scholar,一个在互联网行业的小白,立志成为更好的自己。

如果你想了解更多关于scholar (opens new window) (opens new window),可以关注公众号-书生带你学编程,后面文章会首先同步至公众号。

公众号封面

编辑此页 (opens new window)
上次更新: 2024/12/28, 18:32:08
第二章 并发编程的三大特性
第四章 深入ReentrantLock

← 第二章 并发编程的三大特性 第四章 深入ReentrantLock→

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式