浅谈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,一个在互联网行业摸鱼写代码的打工人,卑微求个【关注】和【在看】,你们的支持是我创作的最大动力,我们下期见~

相关文章
|
2月前
|
安全 Java 调度
Java编程时多线程操作单核服务器可以不加锁吗?
Java编程时多线程操作单核服务器可以不加锁吗?
43 2
|
2天前
|
缓存 Java
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
本文介绍了几种常见的锁机制,包括公平锁与非公平锁、可重入锁与不可重入锁、自旋锁以及读写锁和互斥锁。公平锁按申请顺序分配锁,而非公平锁允许插队。可重入锁允许线程多次获取同一锁,避免死锁。自旋锁通过循环尝试获取锁,减少上下文切换开销。读写锁区分读锁和写锁,提高并发性能。文章还提供了相关代码示例,帮助理解这些锁的实现和使用场景。
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
|
24天前
|
Java
Java 中锁的主要类型
【10月更文挑战第10天】
|
2月前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
2月前
|
算法 Java 关系型数据库
Java中到底有哪些锁
【9月更文挑战第24天】在Java中,锁主要分为乐观锁与悲观锁、自旋锁与自适应自旋锁、公平锁与非公平锁、可重入锁以及独享锁与共享锁。乐观锁适用于读多写少场景,通过版本号或CAS算法实现;悲观锁适用于写多读少场景,通过加锁保证数据一致性。自旋锁与自适应自旋锁通过循环等待减少线程挂起和恢复的开销,适用于锁持有时间短的场景。公平锁按请求顺序获取锁,适合等待敏感场景;非公平锁性能更高,适合频繁加解锁场景。可重入锁支持同一线程多次获取,避免死锁;独享锁与共享锁分别用于独占和并发读场景。
|
25天前
|
安全 Java 开发者
java的synchronized有几种加锁方式
Java的 `synchronized`通过上述三种加锁方式,为开发者提供了从粗粒度到细粒度的并发控制能力,满足了不同场景下的线程安全需求。合理选择加锁方式对于提升程序的并发性能和正确性至关重要,开发者应根据实际应用场景的特性和性能要求来决定使用哪种加锁策略。
13 0
|
25天前
|
Java 应用服务中间件 测试技术
Java21虚拟线程:我的锁去哪儿了?
【10月更文挑战第8天】
30 0
|
2月前
|
Java 数据库
JAVA并发编程-一文看懂全部锁机制
曾几何时,面试官问:java都有哪些锁?小白,一脸无辜:用过的有synchronized,其他不清楚。面试官:回去等通知! 今天我们庖丁解牛说说,各种锁有什么区别、什么场景可以用,通俗直白的分析,让小白再也不怕面试官八股文拷打。
|
3月前
|
存储 Java
Java锁是什么?简单了解
在高并发环境下,锁是Java中至关重要的概念。锁或互斥是一种同步机制,用于限制多线程环境下的资源访问,确保排他性和并发控制。例如,超市储物柜仅能存放一个物品,若三人同时使用,则需通过锁机制确保每次只有一个线程访问。Java中可以通过`synchronized`关键字实现加锁,确保关键代码段的原子性,避免数据不一致问题。正确使用锁可有效提升程序的稳定性和安全性。
Java锁是什么?简单了解
|
3月前
|
小程序 Java 开发工具
【Java】@Transactional事务套着ReentrantLock锁,锁竟然失效超卖了
本文通过一个生动的例子,探讨了Java中加锁仍可能出现超卖问题的原因及解决方案。作者“JavaDog程序狗”通过模拟空调租赁场景,详细解析了超卖现象及其背后的多线程并发问题。文章介绍了四种解决超卖的方法:乐观锁、悲观锁、分布式锁以及代码级锁,并重点讨论了ReentrantLock的使用。此外,还分析了事务套锁失效的原因及解决办法,强调了事务边界的重要性。
99 2
【Java】@Transactional事务套着ReentrantLock锁,锁竟然失效超卖了