【JUC基础】05. Synchronized和ReentrantLock

这篇具有很好参考价值的文章主要介绍了【JUC基础】05. Synchronized和ReentrantLock。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1、前言

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

【JUC基础】05. 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释放外层锁");
        }
    }
}

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

【JUC基础】05. Synchronized和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();
            }
        }
    }
}

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

【JUC基础】05. Synchronized和ReentrantLock

3.4、实现原理不同

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

【JUC基础】05. Synchronized和ReentrantLock

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

【JUC基础】05. Synchronized和ReentrantLock

3.5、使用场景

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

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

4、小结

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

到了这里,关于【JUC基础】05. Synchronized和ReentrantLock的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【六大锁策略-各种锁的对比-Java中的Synchronized锁和ReentrantLock锁的特点分析-以及加锁的合适时机】

    阅读该文章之前要了解,锁策略是为了解决什么问题 多线程带来的的风险-线程安全的问题的简单实例-线程不安全的原因 提示:以下是本篇文章正文内容,下面案例可供参考 锁冲突是指两个线程对一个对象加锁,产生了阻塞等待。 乐观锁 假设数据一般情况下不会产生并发冲

    2024年02月15日
    浏览(61)
  • JUC并发编程-线程和进程、Synchronized 和 Lock、生产者和消费者问题

    源码 + 官方文档 面试高频问! java.util 工具包、包、分类 业务:普通的线程代码 Thread Runnable Runnable 没有返回值、效率相比入 Callable 相对较低! 线程、进程,如果不能使用一句话说出来的技术,不扎实! 进程:一个程序,QQ.exe Music.exe 程序的集合; 一个进程往往可以包含多

    2024年01月20日
    浏览(50)
  • 《JUC并发编程 - 高级篇》05 -共享模型之无锁 (CAS | 原子整数 | 原子引用 | 原子数组 | 字段更新器 | 原子累加器 | Unsafe类 )

    有如下需求,保证 account.withdraw 取款方法的线程安全 原有实现并不是线程安全的 测试代码 执行测试代码,某次执行结果 5.1.1 为么不安全 withdraw 方法是临界区,会存在线程安全问题 查看下字节码 多线程在执行过程中可能会出现指令的交错,从而结果错误! 5.1.2 解决思路1

    2023年04月12日
    浏览(44)
  • 【JUC基础】14. ThreadLocal

    目录 1、前言 2、什么是ThreadLocal 3、ThreadLocal作用 4、ThradLocal基本使用 4.1、创建和初始化 4.2、存储和获取线程变量 4.3、清理和释放线程变量 4.4、小结 4.5、示例代码 5、ThreadLocal原理 5.1、set() 5.2、get() 5.3、变量清理 5.4、ThreadLocalMap 6、InheritableThreadLocal 一般提到多线程并发总是

    2024年02月07日
    浏览(38)
  • 【JUC基础】09. LockSupport

      LockSupport是一个线程阻塞工具,可以在线程任意位置让线程阻塞。线程操作阻塞的方式其实还有Thread.suspend()和Object.wait()。而LockSupport与suspend()相比,弥补了由于resume()方法而导致线程被挂起(类似死锁)的问题,也弥补了wait()需要先获得某个对象锁的问题,也不会抛出Inte

    2024年02月06日
    浏览(56)
  • 【JUC基础】15. Future模式

    目录 1、前言 2、什么是Future 2.1、传统程序调用 3、JDK中的Future 3.1、Future相关API 3.2、FutureTask 3.2.1、FutureTask实现 3.2.2、FutureTask相关API 3.3、CompletableFuture 3.3.1、thenApply 3.3.2、异步任务编排之thenCompose() 3.3.3、异步任务编排之thenCombine() 3.3.4、异步任务编排之allOf() 3.3.5、异常处理

    2024年02月08日
    浏览(73)
  • JUC 高并发编程基础篇

    • 1、什么是 JUC • 2、Lock 接口 • 3、线程间通信 • 4、集合的线程安全 • 5、多线程锁 • 6、Callable 接口 • 7、JUC 三大辅助类: CountDownLatch CyclicBarrier Semaphore • 8、读写锁: ReentrantReadWriteLock • 9、阻塞队列 • 10、ThreadPool 线程池 • 11、Fork/Join 框架 • 12、CompletableFuture 1 什么

    2024年02月07日
    浏览(50)
  • 【JUC基础】08. 三大工具类

    JUC包中包含了三个非常实用的工具类:CountDownLatch(倒计数器),CyclicBarrier(循环栅栏),Semaphore(信号量)。 英文中Count Down意为倒计数,Latch意为门闩,所以简单称之为倒计数器。门闩的含义就是把门锁起来,不让里面的线程跑出来。因此这个工具通常用来控制线程等待。

    2024年02月07日
    浏览(26)
  • 【JUC基础】04. Lock锁

    java.util.concurrent.locks为锁定和等待条件提供一个框架的接口和类,说白了就是锁所在的包。 Lock是一种锁机制,比同步块(synchronized block)更加灵活,同时也更加复杂的线程同步机制。在JDK1.5就已存在Lock接口了。 其中有三个实现类: ReentrantLock:可重入锁 ReentrantReadWriteLock.R

    2024年02月04日
    浏览(33)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包