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

相关文章
|
14天前
|
Java
Java中ReentrantLock释放锁代码解析
Java中ReentrantLock释放锁代码解析
25 8
|
15天前
|
IDE Oracle Java
java基础教程(1)-Java概述和相关名词解释
【4月更文挑战第1天】Java是1995年Sun Microsystems发布的高级编程语言,以其跨平台特性著名。它介于编译型和解释型语言之间,通过JVM实现“一次编写,到处运行”。Java有SE、EE和ME三个版本,分别针对标准、企业及嵌入式应用。JVM是Java虚拟机,确保代码在不同平台无需重编译。JRE是运行环境,而JDK包含开发工具。要安装Java开发环境,可从Oracle官网下载JDK,设置JAVA_HOME环境变量并添加到PATH。
|
1月前
|
Java
Java并发编程中的锁机制
【2月更文挑战第22天】 在Java并发编程中,锁机制是一种重要的同步手段,用于保证多个线程在访问共享资源时的安全性。本文将介绍Java锁机制的基本概念、种类以及使用方法,帮助读者深入理解并发编程中的锁机制。
|
1月前
|
存储 Java 程序员
记一次synchronized锁字符串引发的坑兼再谈Java字符串
记一次synchronized锁字符串引发的坑兼再谈Java字符串
21 2
|
14天前
|
Java 调度
Java中常见锁的分类及概念分析
Java中常见锁的分类及概念分析
15 0
|
7天前
|
安全 Java 调度
Java并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。
|
8天前
|
Java
浅谈Java的synchronized 锁以及synchronized 的锁升级
浅谈Java的synchronized 锁以及synchronized 的锁升级
8 0
|
10天前
|
存储 缓存 Java
线程同步的艺术:探索 JAVA 主流锁的奥秘
本文介绍了 Java 中的锁机制,包括悲观锁与乐观锁的并发策略。悲观锁假设多线程环境下数据冲突频繁,访问前先加锁,如 `synchronized` 和 `ReentrantLock`。乐观锁则在访问资源前不加锁,通过版本号或 CAS 机制保证数据一致性,适用于冲突少的场景。锁的获取失败时,线程可以选择阻塞(如自旋锁、适应性自旋锁)或不阻塞(如无锁、偏向锁、轻量级锁、重量级锁)。此外,还讨论了公平锁与非公平锁,以及可重入锁与非可重入锁的特性。最后,提到了共享锁(读锁)和排他锁(写锁)的概念,适用于不同类型的并发访问需求。
40 2
|
11天前
|
Java 程序员 编译器
Java中的线程同步与锁优化策略
【4月更文挑战第14天】在多线程编程中,线程同步是确保数据一致性和程序正确性的关键。Java提供了多种机制来实现线程同步,其中最常用的是synchronized关键字和Lock接口。本文将深入探讨Java中的线程同步问题,并分析如何通过锁优化策略提高程序性能。我们将首先介绍线程同步的基本概念,然后详细讨论synchronized和Lock的使用及优缺点,最后探讨一些锁优化技巧,如锁粗化、锁消除和读写锁等。
|
12天前
|
Java 编译器
Java并发编程中的锁优化策略
【4月更文挑战第13天】 在Java并发编程中,锁是一种常见的同步机制,用于保证多个线程之间的数据一致性。然而,不当的锁使用可能导致性能下降,甚至死锁。本文将探讨Java并发编程中的锁优化策略,包括锁粗化、锁消除、锁降级等方法,以提高程序的执行效率。
13 4