Java多线程基础-14:并发编程中常见的锁策略(二)

简介: 这段内容介绍了互斥锁和读写锁的概念以及它们在多线程环境中的应用。互斥锁仅允许进入和退出代码块时加锁和解锁,而读写锁则区分读和写操作,允许多个线程同时读但写时互斥。

Java多线程基础-14:并发编程中常见的锁策略(一)+ https://developer.aliyun.com/article/1520608?spm=a2c6h.13148508.setting.14.75194f0edPHRir



4、互斥锁&读写锁


互斥锁如synchronized只有两个操作:


进入代码块,加锁。

出了代码块,解锁。

加锁就只是单纯的加锁,没有更细化的区分了。


而除了这之外,还有一种读写锁,它能够把读和写两种加锁区分开。读写锁有三种操作:


给读加锁。

给写加锁。

解锁。


多线程之间,如果多个线程同时读同一个变量并不会涉及到线程安全问题。但多个线程同时写一个数据,或一个线程读另外一个线程写,是有可能产生线程安全问题的。因此就要求读和读之间不互斥,而写要求与任何人互斥。


注意:只要是涉及到 “互斥”(加互斥锁),就会产生线程的挂起等待。一旦线程挂起,再次被唤醒就不知道隔了多久了。因此尽可能减少 “互斥” 的机会,就是提高效率的重要途径。


读写锁就是把读操作和写操作区分对待了。如果这两种场景下都用同一个锁,就会产生极大的性能损耗。读写锁通过这样的设计,能把锁控制的更加精细。Java标准库中提供了专门的读锁类和写锁类:


Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写锁。

ReentrantReadWriteLock.ReadLock 类表示一个读锁,这个对象提供了 lock / unlock 方法进行加锁解锁;

ReentrantReadWriteLock.WriteLock 类表示一个写锁,这个对象也提供了 lock / unlock 方法进行加锁解锁。

为了提高效率,原则上非必要不加互斥锁。 既然大家在都读取的情况下并没有线程安全问题,就不加锁了。读写锁适用于一写多读的情况,既能保证线程安全,又能保证数据的准确。



synchronized不是读写锁。


5、可重入锁&不可重入锁


可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。


简单来说,当一个线程针对同一把锁连续加锁两次时,如果出现了死锁,那该锁就是不可重入锁(Non-reentrant Lock),如果没有出现死锁,那么该锁就是可重入锁(Reentrant Lock)。


(1)不可重入锁


不可重入锁是一种不允许同一线程多次获取该锁的锁。当线程第一次获取不可重入锁后,再次尝试获取该锁时会被自己所持有的锁所阻塞。如果同一线程在获取不可重入锁后再次尝试获取该锁,会因“把自己锁死”而导致死锁。


如何理解“把自己锁死”?


比如滑稽老哥去上厕所,进入厕所之后将门锁上,后来经历了一些神奇的事情他突然被传送到了厕所外。这时他又要进入厕所上厕所,然而再要进入厕所时他必须先解锁,但由于上一把锁也是他自己加的,没人给他解锁,他也无法解锁。




在代码中即一个线程没有释放锁,却又尝试再次加锁。


如下面这个代码,就是加锁两次的情况,第二次加锁必须等到第一次的锁释放,而第一次锁释放需要执行完外层同步代码块中的代码,这又不得不等待第二次加锁成功。二者相互矛盾,从而导致了死锁:




在日常开发中,同一个线程对同一个对象加两次锁的代码其实是很常见的:


class BlockingQueue {
    synchronized void put(int elem){
        this.size();
        ...
    }
    
    synchronized int size() {
        ...
    }
}


在上述代码中,size()方法与put()方法都对同一个锁对象this加锁。且在put()中调用了size()方法,这就相当于进入put()方法时候该线程对this对象加了锁,而在put()中调用size()时又对this对象加了锁。但该代码在Java中实际并不会造成死锁,因为synchronized是可重入锁。而前面提到的,操作系统原生提供的 mutex 互斥锁是不可重入锁。,除此之外C++标准库的锁,Python标准库的锁也是不可重入锁。


(2)可重入锁

可重入锁是一种支持同一线程多次获取该锁的锁。当线程第一次获取可重入锁后,可以多次重复获取该锁,而不会被自己所持有的锁所阻塞。


比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因的可重入锁也叫做递归锁)。


Java里,只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。可重入锁在加锁时会判定看当前申请锁的线程是否已经是锁的拥有者,如果是,则直接放行。


6、公平锁&非公平锁


遵守先来后到的,就是公平锁;不遵循先来后到的,就是非公平锁。此处定义“先来后到”就是公平。


举个例子:


假设三个线程 A,B,C。 A 先尝试获取锁,获取成功;然后 B 再尝试获取锁,获取失败,阻塞等待;然后 C 也尝试获取锁, C 也获取失败, 也阻塞等待。


当线程 A 释放锁的时候,会发生什么呢?


公平锁策略: 遵守  “ 先来后到”。 B 比 C 先来,所以 当 A 释放锁的之后, B 就能先于 C 获取到锁。

非公平锁策略: 不遵守  “ 先来后到”。  B 和 C 都有可能获取到锁。

操作系统内部的线程调度可以视为是随机的, 如果不做任何额外的限制, 锁就是非公平锁。 要

想实现公平锁,就需要依赖额外的数据结构(队列)来记录加锁线程们的先后顺序。


公平锁和非公平锁没有好坏之分,关键还是看适用场景。


synchronized 是非公平锁。


🚩7、问答题-锁策略


(1)怎么理解乐观锁和悲观锁的,具体怎么实现呢?


悲观锁认为多个线程访问同一个共享变量冲突的概率较大,会在每次访问共享变量之前都真正加锁乐观锁认为多个线程访问同一个共享变量冲突的概率不大,因此它并不会真的加锁,而是直接尝试访问数据。在访问的同时识别当前的数据是否出现访问冲突。


实现:悲观锁的实现是先加锁(如借助操作系统提供的 mutex),获取到锁再操作数据,获取不到锁就等待。乐观锁的实现可以引入一个版本号,借助版本号识别出当前的数据访问是否冲突。


(2) 介绍下读写锁?


读写锁就是把读操作和写操作分别进行加锁。读锁和读锁之间不互斥,写锁和写锁之间互斥,写锁和读锁之间互斥。读写锁最主要用在 “频繁读,不频繁写” 的场景中。


(3) 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?


如果获取锁失败后又立即再尝试获取锁,如此无限循环直到获取到锁为止。若第一次获取锁失败,第二次的尝试会在极短的时间内到来。一旦锁被其他线程释放,就能第一时间获取到锁。


相比于挂起等待锁:


优点: 没有放弃 CPU 资源,一旦锁被释放就能第一时间获取到锁,更高效。在锁持有时间比较短的场景下非常有用。


缺点:如果锁的持有时间较长,就会浪费 CPU 资源。


(4) synchronized 是可重入锁么?


是可重入锁。


可重入锁指的就是连续两次加锁不会导致死锁。实现的方式是在锁中记录该锁持有的线程身份,以及一个计数器(记录加锁次数)。 如果发现当前加锁的线程就是持有锁的线程,则直接计数自增。



相关文章
|
15天前
|
监控 算法 Java
Java虚拟机(JVM)垃圾回收机制深度剖析与优化策略####
本文作为一篇技术性文章,深入探讨了Java虚拟机(JVM)中垃圾回收的工作原理,详细分析了标记-清除、复制算法、标记-压缩及分代收集等主流垃圾回收算法的特点和适用场景。通过实际案例,展示了不同GC(Garbage Collector)算法在应用中的表现差异,并针对大型应用提出了一系列优化策略,包括选择合适的GC算法、调整堆内存大小、并行与并发GC调优等,旨在帮助开发者更好地理解和优化Java应用的性能。 ####
23 0
|
11天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
11天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
32 3
|
17天前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
56 6
|
14天前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
17天前
|
运维 Java 编译器
Java 异常处理:机制、策略与最佳实践
Java异常处理是确保程序稳定运行的关键。本文介绍Java异常处理的机制,包括异常类层次结构、try-catch-finally语句的使用,并探讨常见策略及最佳实践,帮助开发者有效管理错误和异常情况。
60 4
|
17天前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
52 1
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
55 1
C++ 多线程之初识多线程
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
26 3
|
2月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
22 2