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


是可重入锁。


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



相关文章
|
6天前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
5天前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
|
5天前
|
Java 开发者
Java多线程编程的艺术与实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的技术文档,本文以实战为导向,通过生动的实例和详尽的代码解析,引领读者领略多线程编程的魅力,掌握其在提升应用性能、优化资源利用方面的关键作用。无论你是Java初学者还是有一定经验的开发者,本文都将为你打开多线程编程的新视角。 ####
|
4天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
10天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
34 9
|
7天前
|
安全 Java 开发者
Java多线程编程中的常见问题与解决方案
本文深入探讨了Java多线程编程中常见的问题,包括线程安全问题、死锁、竞态条件等,并提供了相应的解决策略。文章首先介绍了多线程的基础知识,随后详细分析了每个问题的产生原因和典型场景,最后提出了实用的解决方案,旨在帮助开发者提高多线程程序的稳定性和性能。
|
13天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
10天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
12天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
13天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
23 1