浅谈Java中的“八锁”——概述、常见实现方式与使用场景

简介: 浅谈Java中的“八锁”——概述、常见实现方式与使用场景

一、锁


锁其实是操作系统中的一个概念。在多线程编程中,操作系统引入了锁机制。通过锁机制,能够保证在多核多线程环境中,在某一个时间点上,只能有一个线程进入临界区代码,从而保证临界区中操作数据的一致性。


所谓的锁,可以理解为内存中的一个整型数,拥有两种状态:空闲状态和上锁状态。加锁时,判断锁是否空闲,如果空闲,修改为上锁状态,返回成功;如果已经上锁,则返回失败。解锁时,则把锁状态修改为空闲状态。


二、Java中的八锁


1、乐观锁与悲观锁


(1)乐观锁和悲观锁其实是在数据库中引入的名词,但在Java并发编程中也体现了这样的思想。


悲观锁指对数据被外界修改持保守态度,认为数据很容易就会被其他线程修改,所以在数据被处理前先对数据进行加排它锁,并在整个数据处理过程中,使数据处于锁定状态。

乐观锁相对悲观锁来说的,认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排它锁,而在进行数据提交更新时,才会正式对数据冲突与否进行检测 。

(2)实现方式


在数据库中,悲观锁的实现往往靠数据库提供的锁机制,在对数据记录操作前给记录加排它锁;乐观锁的实现则不会使用数据库的锁机制,一般在表中添加version字段来做类似CAS的自旋操作。

在Java中,synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现;java.util.concurrent.atomic 等原子变量类就是基于CAS机制乐观锁思想的实现。

(3)使用场景


乐观锁适用于多读少写的场景,即线程间的冲突发生较少的时候。若使用synchronized同步锁进行线程阻塞、唤醒切换以及用户态内核态间的切换操作会额外浪费消耗CPU资源; 而CAS机制其实是基于硬件实现的,不需要进入内核与切换线程,操作自旋几率较少,因此可以获得更高的性能。这样可以省去了锁的开销,加大了系统的整个吞吐量。


悲观锁适用于少读多写的场景,即线程间的冲突发生较多的时候。若使用CAS机制的乐观锁实现,这就会导致上层应用会不断的进行重试(比较并交换),这样反倒是降低了系统的性能,所以一般多写的场景下使用悲观锁就比较合适。


2、公平锁与非公平锁


(1)根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁。


公平锁表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,有个先来后到的原则,最早请求锁的线程会先获得锁。

非公平锁表示线程获取锁的顺序与线程请求锁的时间早晚无关,先来不一定先获得锁。

(2)实现方式


ReentrantLock 提供了公平锁和非公平锁的实现


非公平锁: ReentrantLock nonfairLock = new ReentrantLock(); ReentrantLock 的无参构造方法是非公平锁,这与有参构造方法传入false的效果一样。

公平锁:ReentrantLock fairLock = new ReentrantLock(true); 有参构造方法传入true。

(3)使用场景


在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销。

如果业务中线程处理时间要远长于线程等待,那用非公平锁其实效率并不明显,但是用公平锁会给业务增强很多的可控制性。


3、独占锁与共享锁


(1)根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁和共享锁。


独占锁保证任何时候都只有一个线程能得到锁。

共享锁则可以同时由多个线程持有。

(2)实现方式


ReentrantLock是以独占方式实现的。

ReadWriteLock读写锁,它允许一个资源可以被多个线程同时进行读操作。


(3)使用场景


独占锁适用于多写的场景。因为读操作并不会影响数据的一致性 ,而独占锁是一种悲观锁,由于每次访问资源都先加上互斥锁,这限制了并发性,而独占锁只允许在同一时间由一个线程读取数据,其他线程必须等待当前线程释放锁才能进行读取。

共享锁适用于多读的场景。共享锁是一种乐观锁,它放宽了加锁的条件,允许多个线程同时进行读操作。


4、可重入锁


(1)当一个线程要获取一个被其他线程持有的独占锁时,该线程会被阻塞;但当一个线程再次获取它自己已经获取的锁时,如果不被阻塞,那么这个锁就是可重入锁。


看看下面的例子:

public class ReentrantLockTest {
    public synchronized void helloA(){
        System.out.println("Hello A!");
    }
    public synchronized void helloB(){
      System.out.println("Hello B!");
        helloA();
    }
    public static void main(String[] args) {
        ReentrantLockTest test = new ReentrantLockTest();
        test.helloB();
    }
}

执行结果为:


Hello B!
Hello A!


上述代码,调用helloB()方法前,线程或先获取内置锁,然后打印输入Hello B!;之后调用helloA()方法,在调用前会去获取内置锁,若内置锁是不可重入的,那么调用线程会一直被阻塞。但结果表明,synchronized内置锁是可重入的。


(2)实现方式


synchronized内置锁是可重入锁。可重入锁的原理是在锁内部维护一个线程标识,用来标志该锁目前被哪个线程占用,然后关联一个计数器。一开始计数器值为0,说明该锁未被任何线程占用。当一个线程获取了该锁时,计数器值+1,这时其他线程再来获取该锁时会发现锁的所有者不是自己而被挂起;但当获取了该锁的线程再次获取锁时发现锁拥有者是自己,就会把计数器值+1,释放锁把计数器值-1;当计数器值为0时,锁里面的线程标识被置为null,这时被阻塞的其他线程就会被唤醒来竞争该锁。


5、自旋锁


由于 Java 中的线程是与操作系统中的线程一一对应的,所以当一个线程在获取锁(比如独占锁)失败后,会被切换到内核状态而被挂起,当该线程获取到锁时又需要将其切换到内核状态而唤醒该线程。而从用户状态切换到内核状态的开销是比较大的,在一定程度上会影响并发性能。


(1)自旋锁则是,当前线程在获取锁时,如果发现锁已经被其他线程占有,它不马上阻塞自己,在不放弃CPU使用权的情况下,多次尝试获取(默认次数是10 ,可以使用-XX:PreBlockSpinsh参数设置该值),很有可能在后面几次尝试中其他线程己经释放了锁,如果尝试指定的次数后仍没有获取到锁则当前线程才会被阻塞挂起。由此看来自旋锁是使用CPU时间换取线程阻塞与调度的开销,但是很有可能这些CPU时间白白浪费。


(2)实现方式


CAS机制是自旋锁的主要实现方式。


三、总结


本文主要介绍了Java中基本的八种锁机制,能够为学习并发编程奠定基础,另外这也是大厂面试中经常考察的问题,想进大厂的同学要好好掌握。另外,有一些博客有一些很不错的锁机制的使用案例,可以学习一下,增强对本文的理解:Java多线程——线程八锁案例分析


好了,本期的学习就到这里,不知不觉,又摸了半天鱼。


我是Zhongger,一个在互联网行业摸鱼写代码的打工人,卑微求个【关注】和【在看】,你们的支持是我创作的最大动力,我们下期见~

相关文章
|
5天前
|
Java 程序员 开发者
深入理解Java并发编程:线程同步与锁机制
【4月更文挑战第30天】 在多线程的世界中,确保数据的一致性和线程间的有效通信是至关重要的。本文将深入探讨Java并发编程中的核心概念——线程同步与锁机制。我们将从基本的synchronized关键字开始,逐步过渡到更复杂的ReentrantLock类,并探讨它们如何帮助我们在多线程环境中保持数据完整性和避免常见的并发问题。文章还将通过示例代码,展示这些同步工具在实际开发中的应用,帮助读者构建对Java并发编程深层次的理解。
|
5天前
|
安全 Java 开发者
Java中的读写锁ReentrantReadWriteLock详解,存在一个小缺陷
【5月更文挑战第8天】Java中的读写锁ReentrantReadWriteLock详解,存在一个小缺陷
14 2
|
5天前
|
网络协议 算法 Java
【Java网络编程】网络编程概述、UDP通信(DatagramPacket 与 DatagramSocket)
【Java网络编程】网络编程概述、UDP通信(DatagramPacket 与 DatagramSocket)
19 3
|
3天前
|
Java 编译器
Java并发编程中的锁优化策略
【5月更文挑战第18天】在Java并发编程中,锁是一种常用的同步机制,用于保护共享资源的访问。然而,不当的锁使用可能导致性能问题和死锁风险。本文将探讨Java中锁的优化策略,包括锁粗化、锁消除、锁分离和读写锁等技术,以提高并发程序的性能和可靠性。
|
3天前
|
Java 编译器
Java 并发编程中的锁优化策略
【5月更文挑战第17天】在 Java 并发编程中,锁是一种常见的同步机制,用于保护共享资源的访问。然而,不当使用锁可能导致性能问题和死锁风险。本文将探讨 Java 中的锁优化策略,包括锁粗化、锁消除、锁降级以及读写锁等技术,以提高并发程序的性能和可靠性。
|
4天前
|
Java 编译器
Java并发编程中的锁优化策略
【5月更文挑战第17天】在Java并发编程中,锁是一种常见的同步机制,用于保护共享资源。然而,使用不当的锁可能导致性能下降和死锁等问题。本文将探讨Java中锁的优化策略,包括锁粗化、锁消除、锁排序等方法,以提高程序的性能和可靠性。
|
5天前
|
Java 编译器 开发者
Java并发编程中的锁优化策略
【5月更文挑战第15天】 在Java的多线程编程中,锁机制是实现线程同步的关键。然而,不当的锁使用往往导致性能瓶颈甚至死锁。本文深入探讨了Java并发编程中针对锁的优化策略,包括锁粗化、锁消除、锁分离以及读写锁的应用。通过具体实例和性能分析,我们将展示如何有效避免竞争条件,减少锁开销,并提升应用程序的整体性能。
|
5天前
|
消息中间件 安全 前端开发
字节面试:说说Java中的锁机制?
Java 中的锁(Locking)机制主要是为了解决多线程环境下,对共享资源并发访问时的同步和互斥控制,以确保共享资源的安全访问。 锁的作用主要体现在以下几个方面: 1. **互斥访问**:确保在任何时刻,只有一个线程能够访问特定的资源或执行特定的代码段。这防止了多个线程同时修改同一资源导致的数据不一致问题。 2. **内存可见性**:通过锁的获取和释放,可以确保在锁保护的代码块中对共享变量的修改对其他线程可见。这是因为 Java 内存模型(JMM)规定,对锁的释放会把修改过的共享变量从线程的工作内存刷新到主内存中,而获取锁时会从主内存中读取最新的共享变量值。 3. **保证原子性**:锁
20 1
|
5天前
|
Java 编译器 开发者
Java并发编程中的锁优化策略
【5月更文挑战第13天】在Java并发编程中,锁是一种重要的同步机制,用于保证多线程环境下数据的一致性。然而,不当的使用锁可能会导致性能下降,甚至产生死锁等问题。本文将介绍Java中锁的优化策略,包括锁粗化、锁消除、锁降级等,帮助开发者提高程序的性能。
|
5天前
|
安全 Java 数据安全/隐私保护
【JAVA进阶篇教学】第十一篇:Java中ReentrantLock锁讲解
【JAVA进阶篇教学】第十一篇:Java中ReentrantLock锁讲解