【JUC基础】05. Synchronized和ReentrantLock

news2024/9/23 9:31:26

1、前言

前面两篇中分别讲了Synchronized和ReentrantLock。两种方式都能实现同步锁,且也都能解决多线程的并发问题。那么这两个有什么区别呢? 这个也是一个高频的面经题。

2、相同点

2.1、都是可重入锁

什么是可重入锁?

可重入锁,也称为递归锁,是指同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,也就是说线程可以进入任何一个它已经拥有的锁所同步着的代码块。可重入锁是为了避免死锁而出现的一种锁机制,因为当一个线程在持有锁的同时,再次请求获取锁时,如果不是可重入锁,就会发生死锁的情况。

举个例子,当线程 A 获取了锁之后,在锁还没有释放的情况下,再次尝试获取锁时不会阻塞,而是会自动获取锁成功,直到锁的计数器归零后再释放锁。

而ReentrantLock 和 synchronized 都是可重入锁。

简单可重入锁示例:

public class ReentrantLockDemo2 {
    private static Object lock = new Object();
    private static ReentrantLock reentrantLock = new ReentrantLock();

    public static void main(String[] args) {
        // 使用Synchronized实现可重入锁
        synchronizedMethod();

        // 使用ReentrantLock实现可重入锁
        reentrantLockMethod();
    }

    public static void synchronizedMethod() {
        synchronized (lock) {
            System.out.println("synchronized外层加锁");
            synchronized (lock) {
                System.out.println("synchronized内层加锁");
            }
            System.out.println("synchronized释放内层锁");
        }
        System.out.println("synchronized释放外层锁");
    }

    public static void reentrantLockMethod() {
        reentrantLock.lock();
        try {
            System.out.println("reentrantLock外层加锁");
            reentrantLock.lock();
            try {
                System.out.println("reentrantLock内层加锁");
            } finally {
                reentrantLock.unlock();
                System.out.println("reentrantLock释放内层锁");
            }
        } finally {
            reentrantLock.unlock();
            System.out.println("reentrantLock释放外层锁");
        }
    }
}

可以看到返回结果,说明两种锁都是可重入锁。

2.2、都是独占锁

即同一时间只能有一个线程获得锁,其他线程需要等待锁释放后才能获得锁。

2.3、都是阻塞式锁

当一个线程持有锁时,其他线程会被阻塞,直到锁被释放。

3、不同点

3.1、使用方式不同

3.1.1、获取锁的方式不同

使用synchronized获取锁时,只需要在方法或代码块前面加上synchronized关键字即可,Java虚拟机会自动获取锁。例如:

public synchronized void method() {
    // 代码块
}

而使用ReentrantLock获取锁时,需要手动获取锁和释放锁。例如:

// 这里可以指定获取公平锁或非公平锁,默认非公平锁。
// 获取公平锁:ReentrantLock lock = new ReentrantLock();
private ReentrantLock lock = new ReentrantLock();

public void method() {
    lock.lock(); // 获取锁
    try {
        // 代码块
    } finally {
        lock.unlock(); // 释放锁
    }
}

3.1.2、锁释放的方式不同

synchronized锁的释放完全交由虚拟机管理。程序中是无法显式的对锁进行释放。

  • 当线程同步方法或代码块执行结束后释放。
  • 或遇到return语句,或异常时也会自动释放锁。

而ReentrantLock交由程序手动释放。解锁方法:lock.unlock();

3.1.3、可中断性不同

使用synchronized获取锁时,如果一个线程正在等待锁,那么只有等到锁被释放,才能继续执行。

而使用ReentrantLock获取锁时,可以通过lockInterruptibly()方法让等待锁的线程响应中断信号,从而中断等待。例如:

public void method() throws InterruptedException {
    lock.lockInterruptibly(); // 可中断地获取锁
    try {
        // 代码块
    } finally {
        lock.unlock(); // 释放锁
    }
}

3.1.4、条件变量的支持不同

ReentrantLock可以支持多个条件变量,每个条件变量可以管理一个等待队列。使用Condition对象来实现等待/通知机制,例如:

private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();

public void method() throws InterruptedException {
    lock.lock(); // 获取锁
    try {
        while (condition不满足) {
            condition.await(); // 等待
        }
        // 代码块
    } finally {
        lock.unlock(); // 释放锁
    }
}

public void signal() {
    lock.lock(); // 获取锁
    try {
        condition.signal(); // 通知
    } finally {
        lock.unlock(); // 释放锁
    }
}

而synchronized只能使用Object类的wait()和notify()方法进行等待和通知,例如:

public synchronized void method() throws InterruptedException {
    while (条件不满足) {
        wait(); // 等待
    }
    // 代码块
}

public synchronized void signal() {
    notify(); // 通知
}

3.1.5、修饰作用域不同

Synchronized可以修饰实例方法,静态方法,代码块。

ReentrantLock一般需要try catch finally语句,在try中获取锁,在finally释放锁。

3.2、可重入性不同

虽然前面讲到了两个都是可重入锁。但ReentrantLock是同一个线程可以多次获取同一个锁,而synchronized也是可重入锁,但是需要注意的是,它只能在同一个线程内部进行重入,而不是在不同线程之间。

在Java中,可重入性是指线程获取了某个锁之后,仍然能够再次获取该锁,而不会被自己所持有的锁所阻塞。在重入时,每次都会增加一次锁的计数器,而每次解锁时,计数器也会减1,当计数器为0时,锁会被释放。

3.2.1、Synchronized实现可重入锁的机制

每个对象都有一个监视器锁(monitor),当线程第一次访问该对象时,会获取该对象的监视器锁,并将锁的计数器加1,然后再次进入synchronized块时,会再次获取该对象的监视器锁,此时锁的计数器再次加1,线程在退出synchronized块时,会将锁的计数器减1,当计数器为0时,锁被释放。

public class SynchronizedDemo {
    public synchronized void methodA() {
        System.out.println("进入 methodA");
        methodB();
    }

    public synchronized void methodB() {
        System.out.println("进入 methodB");
    }

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

在上面的代码中,methodA和methodB都是synchronized方法,当线程第一次调用methodA时,会获取对象的监视器锁,并将锁的计数器加1,然后再次进入methodB时,会再次获取该对象的监视器锁,此时锁的计数器再次加1,当线程退出methodB时,会将锁的计数器减1,当计数器为0时,锁被释放。由于methodA和methodB都是synchronized方法,它们都使用的是同一个对象的监视器锁,因此线程可以重入这两个方法,即可重入锁。

3.2.2、ReentrantLock实现可重入锁的机制

每个ReentrantLock对象都有一个锁计数器和一个线程持有者,当线程第一次获取锁时,锁计数器加1,并且线程持有者是当前线程,当该线程再次获取锁时,锁计数器再次加1,当线程释放锁时,锁计数器减1,当锁计数器为0时,锁被释放。

以下是一个使用ReentrantLock实现可重入锁的简单例子:

public class ReentrantLockDemo {
    private ReentrantLock lock = new ReentrantLock();

    public void methodA() {
        lock.lock();
        try {
            System.out.println("进入 methodA");
            methodB();
        } finally {
            lock.unlock();
        }
    }

    public void methodB() {
        lock.lock();
        try {
            System.out.println("进入 methodB");
        } finally {
            lock.unlock();
        }
    }

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

在上面的代码中,methodA和methodB都是使用ReentrantLock实现的可重入锁。当线程第一次调用methodA时,会获取lock对象的锁,并将锁计数器加1,然后再次进入methodB时,会再次获取该锁,此时锁计数器再次加1,当线程退出methodB时,会将锁计数器减1,当计数器为0时,锁被释放。

总的来说,Synchronized和ReentrantLock都是可重入锁,但是它们实现可重入锁的机制不同,Synchronized是基于对象监视器锁实现的,而ReentrantLock是基于锁计数器和线程持有者实现的。

3.3、性能不同

Synchronized是Java语言内置的关键字,通过JVM实现,因此使用起来比较简单方便。Synchronized的实现采用的是悲观锁机制,即线程在访问共享资源时,必须先获得锁,如果获取不到,就进入阻塞状态。Synchronized在获取和释放锁时,需要执行系统调用,这个过程的开销相对较大,因此在高并发的场景下,Synchronized的性能会受到影响。

ReentrantLock的实现是基于CAS(Compare And Swap)操作和自旋锁的机制,CAS是一种无锁算法,它是通过比较当前内存值与预期内存值的方式来实现锁的获取和释放。自旋锁是一种忙等待的锁机制,当线程获取锁失败时,它不会进入阻塞状态,而是一直循环执行CAS操作,直到获取到锁。因此,ReentrantLock在高并发的场景下,相对于Synchronized有更好的性能表现。

不过随着JDK版本的升级,Synchronized已经被优化了。优化之前synchronized的性能确实要比ReentrantLock差20%-30%,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized。

写个简单例子验证一波:

public class ReentrantLockDemo {

    private static final int LOOP_COUNT = 10000000;

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        incrementWithSynchronized();
        long end = System.currentTimeMillis();
        System.out.println("Using Synchronized, time cost: " + (end - start) + "ms, count: " + count);

        count = 0; // 重置count值

        start = System.currentTimeMillis();
        incrementWithReentrantLock();
        end = System.currentTimeMillis();
        System.out.println("Using ReentrantLock, time cost: " + (end - start) + "ms, count: " + count);
    }

    private static synchronized void incrementWithSynchronized() {
        for (int i = 0; i < LOOP_COUNT; i++) {
            count++;
        }
    }

    static ReentrantLock lock = new ReentrantLock();
    private static void incrementWithReentrantLock() throws InterruptedException {
        for (int i = 0; i < LOOP_COUNT; i++) {
            lock.lock();
            try {
                count++;
            } finally {
                lock.unlock();
            }
        }
    }
}

可以看到很有意思的差距:

 

3.4、实现原理不同

Synchronized是Java中的一种内置锁,是Java的保留字。它是基于Java对象头中的Mark Word来实现的。每个Java对象都有一个Mark Word,其中包含了一些标识位,如锁标识位。当一个线程要访问一个被Synchronized修饰的方法或代码块时,它会尝试获取锁标识位。如果锁标识位已经被其他线程占用了,那么该线程就会进入阻塞状态,直到锁标识位被释放。

ReentrantLock是Java中的一种可重入锁,它的实现是基于AQS(AbstractQueuedSynchronizer)的。AQS是Java并发包中的一个基础框架,提供了一组底层的同步工具,如CountDownLatch、Semaphore、ReentrantLock等。ReentrantLock的实现依赖于AQS提供的功能,它可以在同一线程中重复获取锁,并支持公平锁和非公平锁两种模式。

3.5、使用场景

  • Synchronized适用于大多数的同步场景,如单线程访问、多线程串行化等。它是一种轻量级的锁,不需要用户去手动管理锁的获取和释放,具有自动释放的功能,因此使用起来比较简单,但在某些高并发的情况下,性能可能会受到影响。
  • ReentrantLock适用于一些特殊的场景,如需要中断等待、尝试获取锁而不是一直等待、可定时的等待等。

在实际应用中,我们可以根据具体的场景来选择合适的锁机制。如果程序的并发性比较低,或者是在单线程中进行访问,那么使用Synchronized可能是更好的选择。如果程序的并发性比较高,或者需要一些高级的功能,比如可中断、可定时等,那么可以选择使用ReentrantLock。同时,在使用ReentrantLock时,需要注意手动管理锁的获取和释放,否则可能会导致死锁等问题。

4、小结

本片文章大幅介绍了Synchronized和ReentrantLock的区别。因为这是高频的面试题,希望通过这篇文章能够进一步熟悉对于JUC中锁的理解,同时也明白Synchronized和ReentrantLock的一些区别,在项目中不同的场景可以更好的选择适当的锁机制,提升系统的可维护性和健壮性。一起加油学习~

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/510801.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mysql查询字符串中某个字符串出现的次数

目录 1.查单个字符出现的次数2.查多个字符出现的次数3.函数讲解 1.查单个字符出现的次数 比如我想查how do you do 字符串当中出现d的次数&#xff1a; 第一眼看上去有点懵&#xff0c;首先mysql并没有直接计算出现字符次数的函数&#xff0c;所以才使用了下面这种方式&#x…

【排错记录】国产航顺HK32F030M驱动TM1624四位数码管显示

问题描述&#xff1a; 航顺单片机 HK32F030MF4P6用数码管显示驱动TM1624问题描述。 航顺单片HK32F030MF4P6的PC3/PC4/PC5引脚分别连接数码管驱动TM1624的DIN/CLK/STB;当单独使用HK32F030MF4P6单片机最小系统和TM1624数码管模块的时候部分最小系统板能驱动数码管正常显示&…

Centos 7 安装系列(11):Kibana

一、系统环境 操作系统&#xff1a;Centos 7 已安装环境&#xff1a;ElasticSearch 8.6.2 二、安装 需要注意的是&#xff1a;Kibana的版本需要和Elasticsearch保持一致。 2.1 下载并解压安装包 cd /opt yum install -y wget wget https://artifacts.elastic.co/downloads…

马哈鱼SQLFLow对SQL Server OUTPUT Clause 的数据血缘分析

SQL Server OUTPUT Clause 会对 SQL 语句的血缘分析产生影响&#xff0c;如果忽略对 OUTPUT Clause 的分析&#xff0c;那么将漏掉一些关键的数据血缘关系&#xff0c;从而影响数据血缘分析的准确性&#xff0c;进而影响组织的数据治理质量。 Gudu SQLFlow 可以对 SQL Server …

Linux下安装MySQL 5.7

安装MySQL 5.7 1、通过命令下载 wget http://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.36-linux-glibc2.12-x86_64.tar.gz 2、解压 tar -zxvf mysql-5.7.36-linux-glibc2.12-x86_64.tar.gz -C /usr/local/mysql/ 3、简化 cd /usr/local mv mysql-5.7.36-linux-…

Hibernate 基本操作、懒加载以及缓存

前言 上一篇咱们介绍了 Hibernate 以及写了一个 Hibernate 的工具类&#xff0c;快速入门体验了一波 Hibernate 的使用&#xff0c;我们只需通过 Session 对象就能实现数据库的操作了。 现在&#xff0c;这篇介绍使用 Hibernate 进行基本的 CRUD、懒加载以及缓存的知识。 提示…

干货|做实验到底应该选取多少被试?

Hello&#xff0c;大家好&#xff01; 这里是壹脑云科研圈&#xff0c;我是喵君姐姐~ 我们都知道心理学实验一般是通过分析被试的一系列数据从而得到相应的结论的。那么&#xff0c;在进行心理学实验的时候需要多少被试&#xff1f;怎么去看实验的被试量够不够呢&#xff1f;…

【Spring Cloud】Spring Cloud Alibaba 实战 Seata (分布式事务)

文章目录 一、Seata 简介简要发展史Seata 设计初衷 二、使用 Docker 快速搭建 Seata 1.4三、在 Spring 项目中使用 Seata 客户端 一、Seata 简介 Seata&#xff08;Simple Extensible Autonomous Transaction Architecture&#xff09; 是一款开源的分布式事务解决方案&#xf…

Datawhale-chatGPT用于句词分类

NLU基础 句子级别的分类 Token级别的分类 相关API chatGPT Style prompt建议 NLU应用 文档问答 分类/实体微调 智能对话

php xdebug配置

1.sublime 火绒 火绒安装插件Xdebug Helper for Firefox 管理插件 -…-选项 填入ide key sublime 第一个插件package control ctrlshifitp 输入install 点击安装第一个包管理 package control 第二个插件 xdebug ctrlshifitp 输入xdebug clinet安装 php配置 这里用的时phps…

网安笔记03 DES概述

DES 概述 分组加密算法 &#xff1a; 明文、密文64位分组长度对称算法 &#xff1a; 加密和解密密钥编排不同&#xff0c;但使用同一算法密钥长度&#xff1a;56位 —— 每个第8位为奇偶校验位密钥为任意的56位数&#xff0c;存在 弱密钥&#xff0c; 容易避开混乱与扩散的组合…

初学者自学Web安全的三个必经阶段(含系统路线脑图+工具笔记)

一、为什么选择网络安全&#xff1f; 这几年随着我国《国家网络空间安全战略》《网络安全法》《网络安全等级保护2.0》等一系列政策/法规/标准的持续落地&#xff0c;网络安全行业地位、薪资随之水涨船高。 未来3-5年&#xff0c;是安全行业的黄金发展期&#xff0c;提前踏入…

React学习1

JSX使得创建虚拟DOM更便捷&#xff0c;纯JS创建虚拟DOM太过繁琐 JSX语法规范&#xff1a; JSX的{}&#xff0c;读变量的时候只能存放表达式&#xff0c;不能写语句&#xff08;代码&#xff09; react可以遍历数组&#xff0c;但是无法遍历对象 react是面向组件编程 函数式…

5月10号软件资讯更新合集....

elementary OS 图像注释工具 Annotator 1.2 发布 Annotator 是一个免费开源图像注释工具&#xff0c;最初是为 elementary OS 设计&#xff0c;但也可用于其他 Linux。近日该工具发布了 1.2 版本&#xff0c;新版本增加了对全屏、当前窗口和选择区域进行截图等功能。 新版本还…

Dcoker高级篇

一、复杂安装详细说明 &#xff08;一&#xff09;安装mysql主从复制 主从复制原理 原理&#xff1a; &#xff08;1&#xff09;master服务器将数据的改变记录二进制binlog日志&#xff0c;当master上的数据发生改变时&#xff0c;则将其改变写入二进制日志中&#xff1b;&…

quill编辑器自定义音频、视频、行内style样式(字符边框、首行缩进)

文章目录 一、音频1、自定义内容2、引入使用 二、视频1、自定义内容2、引入使用 三、文本添加行内style样式&#xff08;文本边框&#xff09;1、不带有下拉框&#xff08;1&#xff09;自定义内容&#xff08;2&#xff09;引入使用 2、带有下拉框&#xff08;1&#xff09;自…

【SpringCloud】Nacos安装与入门

目录 一、认识与安装 1、访问 2、下载 3、解压 4、启动 5、访问 二、基本使用 1、搭建服务 2、服务注册 3、服务发现 一、认识与安装 他是阿里巴巴的产品&#xff0c;也是SpringCloud中的一个注册中心组件&#xff0c;其功能相比Eureka更丰富&#xff0c;在国内更受欢…

设计师常用的7款界面设计工具!

不同的界面设计工具都有其独特的优点和不足之处。本文为大家介绍设计师常用的7款界面设计工具&#xff1a;即时设计是一款在线UI界面设计工具&#xff0c;拥有中文界面和丰富的社区资源&#xff0c;适合初学者和专业设计师使用。Sketch具有直观的矢量编辑工具和可重复使用的符号…

Prometheus原理与二次开发

Prometheus的功能、架构、组件、配置 1 介绍 1.1 简介 Promethues是一套开源系统监控及报警框架&#xff0c;启发于Google的borgmon, 由SoundCoud员工于2012年创建&#xff0c;并于2015年正式发布,2016年正式加入Cloud Native Computing Fundation 1.2 监控目的 趋势分析&…

用递归算法删除不带头结点的单链表中值为x的值

#include<stdio.h> #include<stdlib.h> typedef struct Lnode{int data;//数据域 struct Lnode *next;//指针域 }Lnode,*LinkList; int a[5]{1,2,3,3,4}; int n5; //这里使用尾插法&#xff0c;把数据放在L中 void InitList(Lnode *L){int i;Lnode *s,*rL;//r为尾…