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


是可重入锁。


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



相关文章
|
19小时前
|
安全 Java
java线程之List集合并发安全问题及解决方案
java线程之List集合并发安全问题及解决方案
6 1
|
19小时前
|
Java
java线程之异步回调
java线程之异步回调
4 0
|
19小时前
|
Java
java线程之读写锁
java线程之读写锁
5 0
|
19小时前
|
Java
java线程之信号同步
java线程之信号同步
7 0
|
4天前
|
存储 Linux C语言
c++进阶篇——初窥多线程(二) 基于C语言实现的多线程编写
本文介绍了C++中使用C语言的pthread库实现多线程编程。`pthread_create`用于创建新线程,`pthread_self`返回当前线程ID。示例展示了如何创建线程并打印线程ID,强调了线程同步的重要性,如使用`sleep`防止主线程提前结束导致子线程未执行完。`pthread_exit`用于线程退出,`pthread_join`用来等待并回收子线程,`pthread_detach`则分离线程。文中还提到了线程取消功能,通过`pthread_cancel`实现。这些基本操作是理解和使用C/C++多线程的关键。
|
6天前
|
安全 Java
【极客档案】Java 线程:解锁生命周期的秘密,成为多线程世界的主宰者!
【6月更文挑战第19天】Java多线程编程中,掌握线程生命周期是关键。创建线程可通过继承`Thread`或实现`Runnable`,调用`start()`使线程进入就绪状态。利用`synchronized`保证线程安全,处理阻塞状态,注意资源管理,如使用线程池优化。通过实践与总结,成为多线程编程的专家。
|
6天前
|
Java 开发者
告别单线程时代!Java 多线程入门:选继承 Thread 还是 Runnable?
【6月更文挑战第19天】在Java中,面对多任务需求时,开发者可以选择继承`Thread`或实现`Runnable`接口来创建线程。`Thread`继承直接但限制了单继承,而`Runnable`接口提供多实现的灵活性和资源共享。多线程能提升CPU利用率,适用于并发处理和提高响应速度,如在网络服务器中并发处理请求,增强程序性能。不论是选择哪种方式,都是迈向高效编程的重要一步。
|
6天前
|
Java 开发者
震惊!Java多线程的惊天秘密:你真的会创建线程吗?
【6月更文挑战第19天】Java多线程创建有两种主要方式:继承Thread类和实现Runnable接口。继承Thread限制了多重继承,适合简单场景;实现Runnable接口更灵活,可与其它继承结合,是更常见选择。了解其差异对于高效、健壮的多线程编程至关重要。
|
8天前
|
Java 程序员
Java多线程编程是指在一个进程中创建并运行多个线程,每个线程执行不同的任务,并行地工作,以达到提高效率的目的
【6月更文挑战第18天】Java多线程提升效率,通过synchronized关键字、Lock接口和原子变量实现同步互斥。synchronized控制共享资源访问,基于对象内置锁。Lock接口提供更灵活的锁管理,需手动解锁。原子变量类(如AtomicInteger)支持无锁的原子操作,减少性能影响。
18 3
|
6天前
|
Java
JAVA多线程深度解析:线程的创建之路,你准备好了吗?
【6月更文挑战第19天】Java多线程编程提升效率,通过继承Thread或实现Runnable接口创建线程。Thread类直接继承启动简单,但限制多继承;Runnable接口实现更灵活,允许类继承其他类。示例代码展示了两种创建线程的方法。面对挑战,掌握多线程,让程序高效运行。