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 是可重入锁么?


是可重入锁。


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



相关文章
|
3天前
|
存储 缓存 Java
java线程内存模型底层实现原理
java线程内存模型底层实现原理
java线程内存模型底层实现原理
|
2天前
|
算法 Java 关系型数据库
Java中到底有哪些锁
【9月更文挑战第24天】在Java中,锁主要分为乐观锁与悲观锁、自旋锁与自适应自旋锁、公平锁与非公平锁、可重入锁以及独享锁与共享锁。乐观锁适用于读多写少场景,通过版本号或CAS算法实现;悲观锁适用于写多读少场景,通过加锁保证数据一致性。自旋锁与自适应自旋锁通过循环等待减少线程挂起和恢复的开销,适用于锁持有时间短的场景。公平锁按请求顺序获取锁,适合等待敏感场景;非公平锁性能更高,适合频繁加解锁场景。可重入锁支持同一线程多次获取,避免死锁;独享锁与共享锁分别用于独占和并发读场景。
|
1天前
|
Java 调度
Java-Thread多线程的使用
这篇文章介绍了Java中Thread类多线程的创建、使用、生命周期、状态以及线程同步和死锁的概念和处理方法。
Java-Thread多线程的使用
|
1天前
|
Java 数据处理 调度
Java中的多线程编程:从基础到实践
本文深入探讨了Java中多线程编程的基本概念、实现方式及其在实际项目中的应用。首先,我们将了解什么是线程以及为何需要多线程编程。接着,文章将详细介绍如何在Java中创建和管理线程,包括继承Thread类、实现Runnable接口以及使用Executor框架等方法。此外,我们还将讨论线程同步和通信的问题,如互斥锁、信号量、条件变量等。最后,通过具体的示例展示了如何在实际项目中有效地利用多线程提高程序的性能和响应能力。
|
2天前
|
安全 算法 Java
Java中的多线程编程:从基础到高级应用
本文深入探讨了Java中的多线程编程,从最基础的概念入手,逐步引导读者了解并掌握多线程开发的核心技术。无论是初学者还是有一定经验的开发者,都能从中获益。通过实例和代码示例,本文详细讲解了线程的创建与管理、同步与锁机制、线程间通信以及高级并发工具等主题。此外,还讨论了多线程编程中常见的问题及其解决方案,帮助读者编写出高效、安全的多线程应用程序。
|
1月前
|
存储 监控 Java
Java多线程优化:提高线程池性能的技巧与实践
Java多线程优化:提高线程池性能的技巧与实践
58 1
|
7天前
|
Java Spring
spring多线程实现+合理设置最大线程数和核心线程数
本文介绍了手动设置线程池时的最大线程数和核心线程数配置方法,建议根据CPU核数及程序类型(CPU密集型或IO密集型)来合理设定。对于IO密集型,核心线程数设为CPU核数的两倍;CPU密集型则设为CPU核数加一。此外,还讨论了`maxPoolSize`、`keepAliveTime`、`allowCoreThreadTimeout`和`queueCapacity`等参数的设置策略,以确保线程池高效稳定运行。
54 10
spring多线程实现+合理设置最大线程数和核心线程数
|
15天前
|
Java 数据库 Android开发
一个Android App最少有几个线程?实现多线程的方式有哪些?
本文介绍了Android多线程编程的重要性及其实现方法,涵盖了基本概念、常见线程类型(如主线程、工作线程)以及多种多线程实现方式(如`Thread`、`HandlerThread`、`Executors`、Kotlin协程等)。通过合理的多线程管理,可大幅提升应用性能和用户体验。
28 15
一个Android App最少有几个线程?实现多线程的方式有哪些?
|
1天前
|
Python
5-5|python开启多线程入口必须在main,从python线程(而不是main线程)启动pyQt线程有什么坏处?...
5-5|python开启多线程入口必须在main,从python线程(而不是main线程)启动pyQt线程有什么坏处?...
|
17天前
|
Java 数据库 Android开发
一个Android App最少有几个线程?实现多线程的方式有哪些?
本文介绍了Android应用开发中的多线程编程,涵盖基本概念、常见实现方式及最佳实践。主要内容包括主线程与工作线程的作用、多线程的多种实现方法(如 `Thread`、`HandlerThread`、`Executors` 和 Kotlin 协程),以及如何避免内存泄漏和合理使用线程池。通过有效的多线程管理,可以显著提升应用性能和用户体验。
37 10