Java并发编程之锁的艺术:面试与实战指南(一)

Java并发编程之锁的艺术:面试与实战指南(一)

🌈你好呀!我是 山顶风景独好
💝欢迎来到我的博客,很高兴能够在这里和您见面!
💝希望您在这里可以感受到一份轻松愉快的氛围!
💝不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。
🚀 欢迎一起踏上探险之旅,挖掘无限可能,共同成长!

前言

本系列地址:
Java并发编程之锁的艺术:面试与实战指南(一)
Java并发编程之锁的艺术:面试与实战指南(二)
Java并发编程之锁的艺术:面试与实战指南(三)
Java并发编程之锁的艺术:面试与实战指南(四)

一、什么是锁?

锁(Lock)是一种同步机制,用于控制多个线程对共享资源的访问。当一个线程需要访问某个共享资源时,它必须先获取该资源的锁,以确保在访问过程中其他线程不会同时访问该资源,从而避免数据的不一致性和其他并发问题。

二、Java中有哪些类型的锁?

  1. 乐观锁(Optimistic Locking):

    • 乐观锁认为一个线程去拿数据的时候不会有其他线程对数据进行更改,所以不会立即上锁。它会在数据更新时进行检查,如果数据在此期间没有被其他线程修改过,则更新成功;否则,操作失败。
    • 实现方式包括 CAS(Compare and Swap)机制、版本号 机制等。
  2. 悲观锁(Pessimistic Locking):

    • 悲观锁认为一个线程去拿数据时一定会有其他线程对数据进行更改,所以一个线程在拿数据的时候都会顺便加锁,这样别的线程此时想拿这个数据就会阻塞。
    • Java中的 synchronized关键字Lock的实现类 都是悲观锁的例子。
  3. 自旋锁(Spinlock):

    • 当一个线程尝试获取某个锁时,如果该锁已经被其他线程持有,则该线程不会立即阻塞,而是会采用循环的方式去尝试获取锁,直到获取到锁或者超过设定的最大循环次数。
    • 自旋锁适用于锁被持有的时间较短,且线程切换的开销较大的场景。
  4. 适应性自旋锁(Adaptive Spinlock):

    • 适应性自旋锁是自旋锁的一种优化形式。在获取锁的过程中,它会根据前一次获取锁的成功与否以及上一次自旋等待的时间等因素,动态地调整本次自旋等待的时间。
  5. 锁升级(Lock Escalation):

    • Java中的synchronized关键字在JVM层面进行了优化,包括锁升级机制。锁升级是指从一种锁状态逐渐过渡到另一种锁状态的过程,例如从偏向锁轻量级锁逐渐过渡到重量级锁
  6. 公平锁(Fair Lock)与非公平锁(Non-fair Lock):

    • 公平锁表示线程按照申请锁的顺序来获取锁,即等待时间最长的线程将优先获取锁。
    • 非公平锁则不保证等待时间最长的线程会先获得锁,有可能后申请的线程比先申请的线程优先获取锁。Java中的ReentrantLock可以通过构造函数指定是否为公平锁,默认是非公平锁。
  7. 可重入锁(Reentrant Lock):

    • 可重入锁允许同一个线程多次获取同一把锁而不会造成死锁。Java中的ReentrantLock就是可重入锁的一个实现。
  8. 独享锁(Exclusive Lock)与共享锁(Shared Lock):

    • 独享锁又称排它锁,同一时间只允许一个线程获取该锁,其他线程必须等待锁释放后才能获取。
    • 共享锁又称读锁,允许多个线程同时获取该锁进行读操作,但在读锁被释放之前,不允许其他线程进行写操作。
  9. 互斥锁(Mutex)与读写锁(Read-Write Lock):

    • 互斥锁是一种最简单的独享锁,同一时间只允许一个线程访问被保护的资源。
    • 读写锁则是对互斥锁的扩展,允许多个线程同时读取被保护的资源,但在读取过程中不允许其他线程进行写操作;而在写操作时,则不允许其他线程进行读或写操作。
  10. 分段锁(Segment Lock):

    • 分段锁是一种锁的设计思想,并不是具体的一种锁。它将一个大的数据结构(如数组或哈希表)分成多个小的段(Segment),每个段都有自己的锁。这样,多个线程可以并行地访问不同的段,从而提高了并发性能。例如,Java中的ConcurrentHashMap就采用了分段锁的设计。

三、synchronized 和 ReentrantLock的区别是什么?

  1. 获取锁的方式:

    • synchronized:是隐式锁,它在进入同步代码块或方法时自动获取锁,退出时自动释放锁。开发者无需显式地调用任何方法来获取或释放锁。
    • ReentrantLock:是显式锁,需要手动调用lock()方法获取锁,并在finally块中调用unlock()方法释放锁。这种方式提供了更大的灵活性,但也增加了出错的可能性,因为开发者必须确保在finally块中释放锁。
  2. 锁的公平性:

    • synchronized:是非公平锁,它并不保证等待时间最长的线程会先获得锁。
    • ReentrantLock:默认情况下也是非公平锁,但可以通过构造函数设置为公平锁。在公平锁的情况下,等待时间最长的线程会先获得锁。
  3. 功能丰富性:

    • synchronized:是Java内置的关键字,其功能相对较为简单,主要用于实现线程同步。
    • ReentrantLock:提供了比synchronized更丰富的功能。例如,它可以设置获取锁的超时时间,可以判断锁是否被其他线程持有,以及可以使用Condition类实现线程等待/通知机制等。
  4. 可重入性:

    • synchronized:是可重入的,这意味着一个线程可以多次获取同一把锁,而不会造成死锁。
    • ReentrantLock:同样是可重入的,与synchronized在这方面具有相同的行为。
  5. 中断响应:

    • synchronized:在获取锁的过程中,如果线程被中断,它会抛出InterruptedException异常,但不会释放锁。这可能导致死锁,因为其他等待锁的线程将无法获取锁。
    • ReentrantLock:提供了更灵活的中断响应。如果线程在等待锁的过程中被中断,它可以决定是继续等待、放弃等待还是响应中断。
  6. 性能:

    • 在高并发的情况下,ReentrantLock的性能可能会优于synchronized。但需要注意的是,synchronized的优化已经足够好,在许多场景下,其性能与ReentrantLock相当甚至更好。

四、什么是乐观锁和悲观锁?

悲观锁(Pessimistic Locking)

悲观锁认为并发操作之间发生冲突的可能性很高,因此,在数据被处理时,它会锁定资源以确保数据在处理过程中不会被其他事务修改。

  • 特点

    1. 悲观锁在数据被修改前就已经加锁,数据在被处理的过程中不会被其他事务读取或修改。
    2. 悲观锁的实现依赖于数据库提供的锁机制,如行锁、表锁等。
    3. 悲观锁适用于写操作频繁的场景,因为它可以避免脏读、不可重复读和幻读等并发问题。
  • 示例

    • 在SQL中,使用SELECT … FOR UPDATE语句可以对选定的行进行加锁,以确保在事务完成之前这些行不会被其他事务修改。
    • 在Java中,synchronized关键字和ReentrantLock等锁机制可以视为悲观锁的实现,因为它们会阻塞其他尝试访问共享资源的线程。

乐观锁(Optimistic Locking)

乐观锁认为并发操作之间发生冲突的可能性很小,因此它不会立即锁定资源,而是在数据提交更新时,检查数据是否被其他事务修改过。

  • 特点

    1. 乐观锁在数据被读取时不会加锁,而是在数据更新时检查版本号或时间戳等信息,以确保在读取到数据和提交更新之间的时间段内,数据没有被其他事务修改。
    2. 如果数据在读取到和提交更新之间被其他事务修改了,则更新操作会失败,需要采取重试或其他策略。
    3. 乐观锁适用于读操作频繁的场景,因为它可以减少加锁的开销,提高系统的并发性能。
  • 示例

    • 在数据库中,可以使用版本号或时间戳字段来实现乐观锁。在读取数据时,获取版本号或时间戳;在更新数据时,检查版本号或时间戳是否发生了变化,如果没有变化则更新数据并更新版本号或时间戳,否则认为数据已经被其他事务修改过,更新操作失败。
    • 在Java中,虽然没有直接的乐观锁实现类,但可以通过版本号、时间戳等机制在代码中实现乐观锁的逻辑。例如,在更新数据时,先读取数据的版本号,然后更新数据并更新版本号,最后提交更新时检查版本号是否发生了变化。

五、死锁是什么?如何避免?

  1. 避免嵌套锁:当需要加锁多个对象时,应将它们的锁顺序统一,尽量避免嵌套锁。
  2. 使用tryLock()方法:可以使用ReentrantLock类的tryLock()方法,在获取锁时设置超时时间,避免一直等待而产生死锁。
  3. 避免无限期等待:在获取锁时,应设置一个等待的超时时间,即一段时间后如果还没有获取到锁,就放弃任务执行。
  4. 使用不同的锁:如果可以使用不同的锁来代替原有的锁,那么可以尝试使用不同的锁来避免死锁。
  5. 尽量减少锁的持有时间:如果持有锁的时间过长,就会增加死锁的可能性,因此需要尽量减少锁的持有时间。
  6. 避免使用多个锁:在程序中避免使用多个锁,因为使用多个锁会增加死锁的可能性。可以采用一些技巧来避免使用多个锁,如采用粗粒度锁,将多个细粒度锁合并成一个大锁。
  7. 按照规定的顺序申请锁:为了避免死锁,可以规定一个申请锁的顺序,在申请锁的时候按照规定的顺序进行申请。
  8. 统一管理锁资源:将锁资源的管理进行统一管理,可以更好地避免死锁。
  9. 使用死锁检测工具:可以使用一些工具(如Java探针、Eclipse自带的死锁检测等)来检测和解决死锁问题。

六、如何在Java中实现一个自定义的锁?

通常可以通过实现java.util.concurrent.locks.Lock接口来完成。Lock接口定义了一些基本的锁操作方法,如lock(), unlock(), tryLock(), tryLock(long timeout, TimeUnit unit)等

以下是一个简单的自定义锁的实现示例:

import java.util.concurrent.locks.Lock;  
  
public class CustomLock implements Lock {  
  
    private boolean isLocked = false;  
    private Thread lockedBy = null;  
  
    @Override  
    public void lock() {  
        Thread callingThread = Thread.currentThread();  
        while (isLocked && lockedBy != callingThread) {  
            // 等待锁被释放  
            try {  
                Thread.sleep(10); // 可以使用更复杂的等待策略  
            } catch (InterruptedException e) {  
                Thread.currentThread().interrupt(); // 恢复中断状态  
                throw new IllegalStateException("Interrupted while waiting for lock", e);  
            }  
        }  
        isLocked = true;  
        lockedBy = callingThread;  
    }  
  
    @Override  
    public void unlock() {  
        if (Thread.currentThread() != lockedBy) {  
            throw new IllegalMonitorStateException("Thread does not own lock");  
        }  
        isLocked = false;  
        lockedBy = null;  
    }  
  
    @Override  
    public boolean tryLock() {  
        if (!isLocked) {  
            isLocked = true;  
            lockedBy = Thread.currentThread();  
            return true;  
        }  
        return false;  
    }  
  
    @Override  
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {  
        long nanos = unit.toNanos(time);  
        long deadline = System.nanoTime() + nanos;  
        Thread callingThread = Thread.currentThread();  
        while (isLocked && lockedBy != callingThread) {  
            if (nanos <= 0) {  
                return false;  
            }  
            nanos = deadline - System.nanoTime();  
            // 使用nanos进行更精确的等待  
            Thread.sleep(nanos / 1000000, (int) (nanos % 1000000));  
        }  
        isLocked = true;  
        lockedBy = callingThread;  
        return true;  
    }  
  
    @Override  
    public Condition newCondition() {  
        throw new UnsupportedOperationException("Conditions not supported");  
    }  
}

七、什么是可重入锁(ReentrantLock)?

可重入锁(ReentrantLock),也称为递归锁,是一种支持同一个线程多次获取同一个锁的锁机制。在并发编程中,当一个线程获得了锁之后,如果再次尝试获取同一个锁时,可重入锁会允许该线程继续获取锁而不会被阻塞。这种机制允许线程在执行过程中多次获取同一个锁,并且在释放锁之前需要相同次数的解锁操作。

可重入锁的主要目的是解决在递归调用或嵌套代码中的锁定问题。当一个线程已经获得了锁,但在持有锁的代码块中又调用了另一个需要同样锁的方法时,如果使用非可重入锁,线程会因为无法再次获得同一个锁而陷入死锁状态。而可重入锁允许线程多次获得同一个锁,避免了死锁问题。

在Java中,ReentrantLock类是可重入锁的一种实现方式。这个类实现了Lock接口,提供了比内置锁(synchronized关键字)更多的灵活性和功能。ReentrantLock支持公平性设置,使得等待时间最长的线程优先获取锁。此外,ReentrantLock还提供了可中断的获取锁(lockInterruptibly()方法)和尝试获取锁(tryLock()方法)的功能,进一步增加了其灵活性。

使用可重入锁的场景包括递归函数、锁的嵌套、锁的互斥和锁的继承等,即任何需要在同一线程中多次获取同一把锁的场景,以及需要在方法调用链中多次获取同一把锁的场景。

八、什么是公平锁和非公平锁?

公平锁和非公平锁是两种类型的锁机制,它们的主要区别在于线程获取锁的顺序。

  • 公平锁:公平锁是指多个线程按照申请锁的顺序来获取锁。也就是说,如果一个线程比另一个线程早地请求了某个锁,那么在释放锁时,等待时间最长的线程(也就是最早请求锁的线程)会获得该锁。这种策略保证了线程间的公平性,但可能会导致整体的效率降低,因为线程需要等待更长的时间来获取锁。
  • 非公平锁:非公平锁则不保证线程获取锁的顺序。在释放锁时,任何等待的线程都有可能立即获得该锁,而不管它们等待时间的长短。这种策略可能导致某些线程长时间得不到锁,从而产生“饥饿”现象。但是,非公平锁的整体效率通常比公平锁高,因为线程在等待锁时不需要进行额外的排序或调度操作。

在Java中,ReentrantLock类提供了公平锁和非公平锁的实现。通过构造函数的参数,我们可以指定锁是公平的(true)还是非公平的(false)。默认情况下,ReentrantLock是非公平锁。

  • 23
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《Java并发编程实战》是一本经典的Java并发编程指南,由Brian Goetz等人撰写。这本书针对Java多线程并发编程的实践问题给出了详细的解决方案和最佳实践。 该书的主要内容包括:线程安全性、对象的共享与发布、的优化、性能与可伸缩性、构建和组合对象、基础构建模块、任务执行、取消与关闭、线程池的使用、显式、构建自定义的同步工具等。 通过阅读这本书,读者可以了解Java中的各种并发问题,并学习如何设计和实现线程安全的Java应用。该书引入了很多并发编程的常见问题,例如:竞态条件、死、活跃性危险等,并提供了一些模式和技术来解决这些问题。 除了基础知识之外,该书还介绍了一些高级的并发编程概念,例如:并发集合、同步类、线程池等。这些内容将帮助读者更好地理解和利用Java并发编程能力。 总的来说,《Java并发编程实战》是一本权威而实用的Java并发编程指南,适合对多线程编程有一定了解的Java开发人员阅读。通过阅读这本书,读者可以更深入地理解Java并发编程的原理与应用,提高自己的并发编程能力。 ### 回答2: 《Java并发编程实战》是由美国计算机科学家布莱恩·戈策等人合著的一本书,是学习Java并发编程的经典教材。这本书系统地介绍了Java中的多线程、并发和并行编程的基本知识和应用。该书分为三个部分,分别是基础篇、高级主题篇和专题扩展篇。 在基础篇中,书中详细介绍了Java内存模型、线程安全性、对象的共享、发布和逸出等概念。同时,还对Java中的、线程池、阻塞队列等常用并发工具进行了深入讲解,帮助读者理解并发编程的基本原理和机制。 高级主题篇则讨论了一些更加复杂的并发编程问题,如线程间的协作、线程间通信、并发集合类的使用和自定义的同步工具的设计等内容。该篇章通过讲解常见的并发问题和解决方案,提供了应对复杂并发场景的实践经验。 专题扩展篇主要讨论了一些与并发编程相关的主题,如并发性问题的调试与测试、程序性能调优等。这些内容能够帮助读者进一步提升对并发编程的理解和应用水平。 《Java并发编程实战》通过深入浅出的语言和大量实例,帮助读者掌握并发编程的基本概念和技术,为读者提供设计和编写高效多线程程序的实践经验。无论是Java初学者还是有一定经验的开发人员,都可以从中获得丰富的知识和实用的技巧,加速自己在并发编程领域的成长。 ### 回答3: 《Java并发编程实战》是一本经典的Java多线程编程指南,被广泛认可为学习并发编程的必读之作。本书由Brian Goetz等多位并发编程领域的专家合著,内容全面且深入浅出,适合从初学者到高级开发人员阅读。 该书分为四个部分,共16章。第一部分介绍了并发编程的基础知识,包括线程安全性、对象的共享、对象组合等。第二部分讲解了如何构建可复用的并发构件,包括线程安全性、发布与初始化安全性等。第三部分深入讨论了Java并发编程中常见的问题和挑战,例如活跃性、性能与可伸缩性等。第四部分则介绍了一些高级主题,如显式、原子变量和并发集合等。 书中包含了大量的示例代码和实践案例,可以帮助读者更好地理解并发编程的概念和技术。此外,本书还提供了一些最佳实践和经验教训,帮助读者避免常见的并发编程陷阱和错误。 《Java并发编程实战》从理论到实践的结合非常好,书中所介绍的内容都是经过实践验证的,具有很高的可靠性和实用性。无论是初学者还是有一定经验的开发人员,都可以从中获得实际应用的知识和经验。 综上所述,如果你想系统地学习Java并发编程,了解如何编写高效、可靠的多线程代码,那么《Java并发编程实战》是一本值得推荐的书籍。它可以帮助你深入理解并发编程的原理和技术,提高自己在并发编程领域的能力和水平。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值