常见面试题之线程中并发锁(二)

1. 什么是AQS

1.1. 概述

全称是AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,它是构建锁或者其他同步组件的基础框架

AQSSynchronized的区别

synchronized AQS
关键字,c++语言实现 java语言实现
悲观锁,自动释放锁 悲观锁,手动开启和关闭
锁竞争激烈都是重量级锁,性能差 锁竞争激烈的情况下,提供了多种解决方案

AQS常见的实现类

  • ReentrantLock 阻塞式锁

  • Semaphore 信号量

  • CountDownLatch 倒计时锁

1.2. 工作机制

  • AQS中维护了一个使用了volatile修饰的state属性来表示资源的状态,0表示无锁,1表示有锁
  • 提供了基于FIFO的等待队列,类似于MonitorEntryList
  • 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于MonitorWaitSet

在这里插入图片描述

  • 线程0来了以后,去尝试修改state属性,如果发现state属性是0,就修改state状态为1,表示线程0抢锁成功
  • 线程1和线程2也会先尝试修改state属性,发现state的值已经是1了,有其他线程持有锁,它们都会到FIFO队列中进行等待,
  • FIFO是一个双向队列,head属性表示头结点,tail表示尾结点

如果多个线程共同去抢这个资源是如何保证原子性的呢?

在这里插入图片描述

在去修改state状态的时候,使用的cas自旋锁来保证原子性,确保只能有一个线程修改成功,修改失败的线程将会进入FIFO队列中等待

AQS是公平锁吗,还是非公平锁?

  • 新的线程与队列中的线程共同来抢资源,是非公平锁

  • 新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁

比较典型的AQS实现类ReentrantLock,它默认就是非公平锁,新的线程与队列中的线程共同来抢资源

2. ReentrantLock的实现原理

2.1. 概述

ReentrantLock翻译过来是可重入锁,相对于synchronized它具备以下特点:

  • 可中断

  • 可以设置超时时间

  • 可以设置公平锁

  • 支持多个条件变量

  • synchronized一样,都支持重入

在这里插入图片描述

2.2. 实现原理

ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似

构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。

查看ReentrantLock源码中的构造方法:

在这里插入图片描述

提供了两个构造方法,不带参数的默认为非公平

如果使用带参数的构造函数,并且传的值为true,则是公平锁

其中NonfairSyncFairSync这两个类父类都是Sync

在这里插入图片描述

Sync的父类是AQS,所以可以得出ReentrantLock底层主要实现就是基于AQS来实现的

在这里插入图片描述

工作流程

在这里插入图片描述

  • 线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功

  • 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部

  • exclusiveOwnerThreadnull的时候,则会唤醒在双向队列中等待的线程

  • 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁

3. synchronizedLock有什么区别 ?

参考回答

  • 语法层面
    • synchronized是关键字,源码在jvm中,用c++语言实现
    • Lock是接口,源码由jdk提供,用java语言实现
    • 使用synchronized时,退出同步代码块锁会自动释放,而使用Lock时,需要手动调用unlock方法释放锁
  • 功能层面
    • 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
    • Lock提供了许多synchronized不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量
    • Lock有适合不同场景的实现,如ReentrantLockReentrantReadWriteLock
  • 性能层面
    • 在没有竞争时,synchronized做了很多优化,如偏向锁、轻量级锁,性能不赖
    • 在竞争激烈时,Lock的实现通常会提供更好的性能

4. 死锁产生的条件是什么?

死锁:一个线程需要同时获取多把锁,这时就容易发生死锁

例如:

t1线程获得A对象锁,接下来想获取B对象的锁

t2 线程获得B对象锁,接下来想获取A对象的锁

代码如下:

package com.dcxuexi.basic;

import static java.lang.Thread.sleep;

public class Deadlock {
    
    

    public static void main(String[] args) {
    
    
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(() -> {
    
    
            synchronized (A) {
    
    
                System.out.println("lock A");
                try {
    
    
                    sleep(1000);
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
                synchronized (B) {
    
    
                    System.out.println("lock B");
                    System.out.println("操作...");
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
    
    
            synchronized (B) {
    
    
                System.out.println("lock B");
                try {
    
    
                    sleep(500);
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
                synchronized (A) {
    
    
                    System.out.println("lock A");
                    System.out.println("操作...");
                }
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}

控制台输出结果

在这里插入图片描述

此时程序并没有结束,这种现象就是死锁现象…线程t1持有A的锁等待获取B锁,线程t2持有B的锁等待获取A的锁。

5. 如何进行死锁诊断?

当程序出现了死锁现象,我们可以使用jdk自带的工具:jpsjstack

步骤如下:

第一:查看运行的线程

在这里插入图片描述

第二:使用jstack查看线程运行的情况,下图是截图的关键信息

运行命令:jstack -l 46032

在这里插入图片描述

其他解决工具,可视化工具

  • jconsole

用于对jvm的内存,线程,类 的监控,是一个基于jmxGUI性能监控工具

打开方式:java安装目录bin目录下 直接启动jconsole.exe就行

  • VisualVM:故障处理工具

能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈

打开方式:java安装目录bin目录下 直接启动jvisualvm.exe就行

6. ConcurrentHashMap

ConcurrentHashMap是一种线程安全的高效Map集合

底层数据结构:

  • JDK1.7底层采用分段的数组+链表实现

  • JDK1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。

6.1. JDK1.7concurrentHashMap

数据结构

在这里插入图片描述

  • 提供了一个segment数组,在初始化ConcurrentHashMap的时候可以指定数组的长度,默认是16,一旦初始化之后中间不可扩容
  • 在每个segment中都可以挂一个HashEntry数组,数组里面可以存储具体的元素,HashEntry数组是可以扩容的
  • HashEntry存储的数组中存储的元素,如果发生冲突,则可以挂单向链表

存储流程

在这里插入图片描述

  • 先去计算keyhash值,然后确定segment数组下标
  • 再通过hash值确定hashEntry数组中的下标存储数据
  • 在进行操作数据的之前,会先判断当前segment对应下标位置是否有线程进行操作,为了线程安全使用的是ReentrantLock进行加锁,如果获取锁是被会使用cas自旋锁进行尝试

6.2. JDK1.8concurrentHashMap

JDK1.8中,放弃了Segment臃肿的设计,数据结构跟HashMap的数据结构是一样的:数组+红黑树+链表

采用CAS + Synchronized来保证并发安全进行实现

  • CAS控制数组节点的添加

  • synchronized只锁定当前链表或红黑二叉树的首节点,只要hash不冲突,就不会产生并发的问题 , 效率得到提升

在这里插入图片描述

7. 导致并发程序出现问题的根本原因是什么?

Java并发编程三大特性

  • 原子性

  • 可见性

  • 有序性

(1)原子性

一个线程在CPU中操作不可暂停,也不可中断,要不执行完成,要不不执行

比如,如下代码能保证原子性吗?

在这里插入图片描述

以上代码会出现超卖或者是一张票卖给同一个人,执行并不是原子性的

解决方案:

1.synchronized:同步加锁

2.JUC里面的lock:加锁

在这里插入图片描述

(2)内存可见性

内存可见性:让一个线程对共享变量的修改对另一个线程可见

比如,以下代码不能保证内存可见性

在这里插入图片描述

解决方案:

  • synchronized

  • volatile(推荐)

  • LOCK

(3)有序性

指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的

还是之前的例子,如下代码:

在这里插入图片描述

解决方案:

  • volatile

猜你喜欢

转载自blog.csdn.net/qq_37726813/article/details/131490429
今日推荐