首页 > 编程语言 >简直了,被“Java并发锁”问题追问到自闭...

简直了,被“Java并发锁”问题追问到自闭...

时间:2024-04-02 22:47:01浏览次数:31  
标签:... Java lock 自闭 线程 公平 共享

故事

地铁上,小帅双目空洞地望着窗外...绝望,发自内心地感到绝望...

距离失业已经过去两个月了,这是小帅接到的第四次面试邀请。“回去等通知吧...”,简简单单的六个字,把小帅的心再次打入了冰窖。

上次“【ThreadLocal问出花】”,小帅其实也有吸取教训得,这次对于多线程的问题还是做了很多准备的...可是没想到这次的结果居然也还是这样。

“Java中的锁了解吧?介绍一下吧”,面试官不紧不慢地问到。

“乐观锁、悲观锁、公平锁、非公平锁,然后平时咱们的synchronized是基于.....”小帅把知道的所有关于锁的基本都回答了一遍。

面试官对他笑了笑,“就这些吗?还有呢?比如自旋锁、可重入锁、独占锁....并且说一下你的理解,或者聊一下使用场景的优劣吧。”

“额.....以前好像看到过...”小帅语无伦次地回答到。

“嗯,行吧,之前的那些答得可以的,不过一会我这边有个会,要不今天咱们就聊到这里?回去等通知吧...”

Java中让人眼花缭乱的锁你是否真的一一清楚了?

试问这样一个大而宽的问题,大家能够总结全吗,如果让各位来回答,能否回答完全呢?

我们在实际的并发编程中,常常遇到多个线程访问一个共享变量的情况,当同时对共享变量进行读写操作的时候,就会产生数据不一致的情况。为了保证资源获取的有序性,我们就常常会用到并发锁。

那么接下来咱们就来聊聊这些Java并发锁的理解吧。我们将从以下这些方面来一起回顾一下Java中的并发锁。

概要

乐观锁和悲观锁:线程是否锁住同步资源

大家其实对乐观锁和悲观锁听说的比较多一些,所以咱们就先来聊聊这两种类型的锁。这两种类型的锁,本质区分是要看线程是否锁住同步资源。

先来看一下悲观锁。悲观锁就是每次去拿数据的时候都会认为别人会修改数据,所以在读取数据的时候都会上锁。这样就会导致线程临时阻塞。

悲观锁

再来看一下乐观锁,乐观锁就是每次在拿数据的时候都假设别人不会修改数据,所以都不会进行上锁;只有在更新数据的时候才去判断之前有没有别的线程更新了这条数据。如果没有更新,那么当前线程会自己修改数据并且写入成功。如果数据已经被其他线程更新了,那么会报错或者自动重试,例如下图。

乐观锁

上述两种锁,并没有优劣之分。只是看相关的场景然后分别去使用。

乐观锁:适用于写少读多的场景。因为不用上锁,释放锁,省去了锁的开销,从而提升了吞吐量。

悲观锁:适用于写多读少的场景。因为线程竞争激烈,如果使用乐观锁会导致线程不断进行重试,反而降低吞吐量。

共享锁和独占锁:多个线程是否共享同一把锁

并发场景下,如果多个线程能够共享一把锁,那么就是所谓的共享锁,如果不能,那么则为独占锁(其他命名:排他锁或者独享锁)。

共享锁指锁可以被多个线程持有。如果一个线程对数据加上共享锁,那么其他线程只能对数据再加共享锁,不能加独占锁。另外的共享锁的线程只能读数据,不能修改数据。如下图。

共享锁

独占锁是指锁一次只能被一个线程持有,如果一个线程对数据加上独占锁,那么其他的线程则不能对该数据再加任何类型的锁。如果一个线程获取独占锁,那么则该线程既可以读数据又可以修改数据。

独占锁

对于独占锁来说,大家比较熟悉的就是synchronized和J.U.C包中的Lock实现类。

大家可能也听说过互斥锁,其实互斥锁就是独占锁的一种常规实现。

读写锁是共享锁的一种具体实现。读写锁管理一组锁,一个是只读的锁,一个是写锁。

读锁可以再没有写锁的时候被多个线程同时持有,而写锁是独占的,于此同时写锁的优先级要高于读锁,一个获得了读锁的线程必须能看到前一个释放的写锁更新的内容。

读写锁和互斥锁对比,其性能更高,每次只有一个写线程,但是有多个线程可以并发读。

读写锁

例如,ReentrantReadWriteLock。具体伪代码如下:

import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
 * 公众号:程序员老猫
 **/
public class ReadWriteLockDemo {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void readData() {
        lock.readLock().lock(); // 获取读锁
        try {
            // 读取共享数据
        } finally {
            lock.readLock().unlock(); // 释放读锁
        }
    }

    public void writeData() {
        lock.writeLock().lock(); // 获取写锁
        try {
           // 修改或写入数据
        } finally {
            lock.writeLock().unlock(); // 释放写锁
        }
    }
}

公平锁和非公平锁:多线程竞争时是否要排队

我们根据多线程在竞争锁的时候是否需要排队从来判断其锁的类型是公平锁还是非公平锁。

公平锁指多个线程按照申请锁的顺序来获取锁。类似食堂排队打饭,先到的可以先打饭。

公平锁

非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序进行的,有可能后申请的比先申请的优先获得锁,高并发场景下,优先级就有可能发生反转。如下图:

非公平锁

咱们在日常开发的过程中经常用到synchronized,其底层其实就是非公平锁。当然如果我们要使用公平锁的情况下,我们也可以使用ReentrantLock。
伪代码如下:

Lock lock = new ReetrantLock(false);

ReentrantLock默认为非公平锁,设置为true的时候表示公平锁。当设置为false的时候表示非公平锁。

可重入锁和不可重入锁:同一个线程中多个流程是否能够获取同一把锁。

如果一个线程中的多个流程能够获取同一把锁,那么我们就叫该所为可重入锁,反之则为不可重入锁。咱们光看文字描述的话可能比较抽象。我们看一下下图。
可重入锁

在Java中可重入锁一般有ReentrantLock,其命名就已经很明确了。另外的synchronized也是可重入锁。
可重入锁的优势是可以一定程度上避免死锁发生。上面的示意图转换为如下demo:

public synchronized void methodA() {
  methodB()
}

public synchronized void methodB() {
  methodC()
}

public synchronized void methodC(){
  doSomeThing()
}

自旋锁或者自适应自旋锁:线程锁定同步资源失败,如该线程没有被阻塞场景下发生

如果一个线程锁住同步资源失败,但是又希望这个线程不被阻塞,那么此时咱们就可以使用自旋锁或者自适应自旋锁。
自旋锁指线程没有获得锁的情况下不被挂起,而是执行一个忙循环。那么这个忙循环的话就成为自旋。如下:

自旋锁

目的:减少线程被挂起的概率,因为线程被挂起和唤醒也是消费资源。

Java中AtomicInteger类就有自旋的操作,如下源代码:

@HotSpotIntrinsicCandidate
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!weakCompareAndSetInt(o, offset, v, v + delta));
        return v;
    }

上述方法中weakCompareAndSetInt(),就可以被称为是CAS操作,如果失败,那么会一直循环获取当前的value值然后进行重试操作。那么这个过程其实就是自旋了。

其他分类的锁。

上述我们聊到的这系列的锁应该是大家听到比较多的。其实还有其他的分类。在此不做一一展开了,有兴趣的小伙伴当然也可以深入去了解一下。
例如根据线程竞争同步资源的时候,细节流程是否发生变化,分为偏向锁、轻量级锁和重量级锁。
在比如,相信大家对HashMap底层原理倒背如流吧,对ConcurrentHashMap应该也有了解,那么ConcurrentHashMap底层其实将锁的粒度进一步细化了,存在了分段锁的概念等等。

总结

这些让人眼花缭乱的锁,如果面试官问到的话,大家是否能够说出一二呢?相信看完上面的解释,大家心里多多少少也有数了吧。当然关于最后一点其他分类的锁,老猫没有展开。有兴趣的小伙伴可以自行查阅一下这些分类。

标签:...,Java,lock,自闭,线程,公平,共享
From: https://www.cnblogs.com/kdaddy/p/18111654

相关文章

  • Java 读取MacOS 本地的 rtf 文件内容:中英文皆可
    原是抄的大神的源码,东拼西凑的找了大半天,已忘记原链接了。特此声明⭐️本人不生产代码,只是代码的搬运工。 /***用换行符讲读取的整个文档内容截取成若干字符串*/publicstaticString[]interfacePath;//读取本地文件内容publicstaticvoidch......
  • 【知识点】Java代理实现方式
    Java代理的几种实现方式静态代理通过创建一个代理类来控制对目标对象的访问(也就是手动编写一个代理类)。静态代理的优点是易于理解和实现,缺点是每个接口都需要定义一个代理类,而且功能较弱。动态代理动态代理分两种,Proxy接口代理和CGLib代理接口代理Proxy代理是JDK内置代理......
  • Java基础
    Java标识符Java所有的组成部分都需要名字。类名、变量名以及方法名都被称为标识符。关于Java标识符,有以下几点需要注意:所有的标识符都应该以字母(A-Z或者a-z),美元符($)、或者下划线(_)开始首字符之后可以是字母(A-Z或者a-z),美元符($)、下划线(_)或数字的任何字符组合关键字不......
  • JavaGUI实现文本转换、复制、顶置窗口等功能
    需求描述:实现对文本的处理,比如输入123输出('123')窗口顶置、取消顶置功能一键复制到剪切板源码实现packageJavaGUI;importjavax.swing.*;importjavax.swing.border.LineBorder;importjava.awt.*;importjava.awt.event.ActionEvent;importjava.awt.event.Actio......
  • java基础
    一、运算符     赋值运算符       =   +=  -=  *=  /=  %=  从右边往左执行     基本运算符       +   -   *   /  %     逻辑运算符       &  &&  ......
  • Java方法06:递归讲解
    递归1.A方法调用B方法,我们很容易理解!2.递归就是:A方法调用A方法!就是自己调用自己3.利用递归可以用简单的程序来解决一些复杂的问题。它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要......
  • Java方法05:可变参数
    可变参数1.JDK1.5开始,Java支持传递同类型的可变参数给一个方法2.在方法声明中,在指定参数类型后加一个省略号(...)3.一个方法中只能指定一个可变参数,它必须是方法的最后一个参数。任何普通的参数必须在它之前声明。 ......
  • Java API操作ES
    1、项目搭建Elasticsearch软件是由Java语言开发的,所以也可以通过JavaAPI的方式对Elasticsearch服务进行访问。先IDEA开发工具中创建简单的javaseMaven项目(模块也可),如下:修改pom文件,增加Maven依赖关系如下:<dependencies><dependency><groupId>......
  • 蓝桥杯javaB组备赛
    15届蓝桥杯备赛java语法基础IO框架importjava.util.*;importjava.IO.IOException;importjava.IO.BufferedReader;publicclassMain{publicstaticvoidmain(String[]args)throwsIOException{BufferedReaderreader=newBufferedReader(newInputStre......
  • JavaScript库,编写$()和getElementsByClassName()方法
    背景:JavaScript库是一组预先编写好的JavaScript代码集合,旨在简化常见的网页开发任务。这些库通常包含了许多函数和方法,可以帮助开发人员处理各种任务,比如DOM操作、事件处理、动画效果、AJAX请求等等。使用JavaScript库可以节省开发时间,并提供了一种标准化的方法来解决常见的......