开发者社区> 牧小农> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

【多线程与高并发】这可能是最全的多线程面试题了(3)

简介: 【多线程与高并发】这可能是最全的多线程面试题了
+关注继续查看

20.如何在方法栈中进行数据传递?


通过方法参数传递;通过共享变量;如果在用一个线程中,还可以使用ThreadLocal进行传递.


21.描述一下ThreadLocal的底层实现形式及实现的数据结构?


Thread类中有两个变量threadLocals和inheritableThreadLocals,二者都是ThreadLocal内部类ThreadLocalMap类型的变量,我们通过查看内部内ThreadLocalMap可以发现实际上它类似于一个HashMap。在默认情况下,每个线程中的这两个变量都为null:


ThreadLocal.ThreadLocalMap threadLocals = null; 
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;


只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建他们。


public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked") 
            T result = (T)e.value; 
            return result; 
        } 
    }
    return setInitialValue(); 
}
ThreadLocalMap getMap(Thread t) { 
    return t.threadLocals; 
}

除此之外,和我所想的不同的是,每个线程的本地变量不是存放在ThreadLocal实例中,而是放在调用线程的ThreadLocals变量里面。也就是说,ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的工具壳,通过set方法将value添加到调用线程的threadLocals中,当调用线程调用get方法时候能够从它的threadLocals中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的threadLocals中,所以不使用本地变量的时候需要调用remove方法将threadLocals中删除不用的本地变量,防止出现内存泄漏。

public void set(T value) { 
    Thread t = Thread.currentThread(); 
    ThreadLocalMap map = getMap(t); 
    if (map != null) 
        map.set(this, value); 
    else
        createMap(t, value);
}
public void remove() { 
    ThreadLocalMap m = getMap(Thread.currentThread()); 
    if (m != null) 
        m.remove(this); 
}

22.Sychornized是否是公平锁?

不是公平锁

23.描述一下锁的四种状态及升级过程?

以下是32位的对象头描述

屏幕快照 2022-05-11 上午10.21.51.png

synchronized锁的膨胀过程:

当线程访问同步代码块。首先查看当前锁状态是否是偏向锁(可偏向状态)

1、如果是偏向锁:

1.1、检查当前mark word中记录是否是当前线程id,如果是当前线程id,则获得偏向锁执行同步代码 块。

1.2、如果不是当前线程id,cas操作替换线程id,替换成功获得偏向锁(线程复用),替换失败锁撤销升 级轻量锁(同一类对象多次撤销升级达到阈值20,则批量重偏向,这个点可以稍微提一下,详见下面的注意)

2、升级轻量锁

升级轻量锁对于当前线程,分配栈帧锁记录lock_record(包含mark word和object-指向锁记录首地 址),对象头mark word复制到线程栈帧的锁记录 mark word存储的是无锁的hashcode(里面有重入次数 问题)。

3、重量级锁(纯理论可结合源码)

CAS自旋达到一定次数升级为重量级锁(多个线程同时竞争锁时)

存储在ObjectMonitor对象,里面有很多属性ContentionList、EntryList 、WaitSet、 owner。当一个线程尝试获取锁时,如果该锁已经被占用,则该线程封装成ObjectWaiter对象插到 ContentionList队列的对首,然后调用park挂起。该线程锁时方式会从ContentionList或EntryList挑 一个唤醒。线程获得锁后调用Object的wait方法,则会加入到WaitSet集合中(当前锁或膨胀为重量级锁)

注意: 1.偏向锁在JDK1.6以上默认开启,开启后程序启动几秒后才会被激活

2.偏向锁撤销是需要在safe_point,也就是安全点的时候进行,这个时候是stop the word的,所以说偏向 锁的撤销是开销很大的,如果明确了项目里的竞争情况比较多,那么关闭偏向锁可以减少一些偏向锁撤销的开销

3.以class为单位,为每个class维护一个偏向锁撤销计数器。每一次该class的对象发生偏向撤销操作时 (这个时候进入轻量级锁),该计数器+1,当这个值达到重偏向阈值(默认20,也就是说前19次进行加锁的时候, 都是假的轻量级锁,当第20次加锁的时候,就会走批量冲偏向的逻辑)时,JVM就认为该class的偏向锁有问 题,因此会进行批量重偏向。每个class对象也会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时,class中的epoch值。每次发生批量重偏向时,就将该值+1, 同时遍历JVM中所有线程的站,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次 获取锁时,发现当前对象的epoch值和class不相等,那就算当前已经偏向了其他线程,也不会执行撤销操 作,而是直接通过CAS操作将其mark word的Thread Id改为当前线程ID

4.需要看源码的同学:https://github.com/farmerjohngit/myblog/issues/12

23. CAS的ABA问题怎么解决的?


通过加版本号控制,只要有变更,就更新版本号


24. 描述一下AQS?


状态变量state:

AQS中定义了一个状态变量state,它有以下两种使用方法:

(1)互斥锁

当AQS只实现为互斥锁的时候,每次只要原子更新state的值从0变为1成功了就获取了锁,可重入是通过不断把state原子更新加1实现的。

(2)互斥锁 + 共享锁

当AQS需要同时实现为互斥锁+共享锁的时候,低16位存储互斥锁的状态,高16位存储共享锁的状态,主要用于实现读写锁。

互斥锁是一种独占锁,每次只允许一个线程独占,且当一个线程独占时,其它线程将无法再获取互斥锁及共享锁,但是它自己可以获取共享锁。

共享锁同时允许多个线程占有,只要有一个线程占有了共享锁,所有线程(包括自己)都将无法再获取互斥锁,但是可以获取共享锁。


AQS队列

AQS中维护了一个队列,获取锁失败(非tryLock())的线程都将进入这个队列中排队,等待锁释放后唤醒下一个排队的线程(互斥锁模式下)。


condition队列

AQS中还有另一个非常重要的内部类ConditionObject,它实现了Condition接口,主要用于实现条件锁。

ConditionObject中也维护了一个队列,这个队列主要用于等待条件的成立,当条件成立时,其它线程将signal这个队列中的元素,将其移动到AQS的队列中,等待占有锁的线程释放锁后被唤醒。

Condition典型的运用场景是在BlockingQueue中的实现,当队列为空时,获取元素的线程阻塞在notEmpty条件上,一旦队列中添加了一个元素,将通知notEmpty条件,将其队列中的元素移动到AQS队列中等待被唤醒。


模板方法

AQS这个抽象类把模板方法设计模式运用地炉火纯青,它里面定义了一系列的模板方法,比如下面这些:


// 获取互斥锁
public final void acquire(int arg) {
    // tryAcquire(arg)需要子类实现
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}
// 获取互斥锁可中断
public final void acquireInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted()) throw new InterruptedException();
    // tryAcquire(arg)需要子类实现
        if (!tryAcquire(arg)) doAcquireInterruptibly(arg);
}
// 获取共享锁
public final void acquireShared(int arg) {
    // tryAcquireShared(arg)需要子类实现
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
}
// 获取共享锁可中断
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted()) throw new InterruptedException();
    // tryAcquireShared(arg)需要子类实现
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
    }


// 释放互斥锁
public final boolean release(int arg) {
    // tryRelease(arg)需要子类实现
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;

}

// 释放共享锁 
public final boolean releaseShared(int arg) { 
    // tryReleaseShared(arg)需要子类实现 
if (tryReleaseShared(arg)) { 
    doReleaseShared(); 
    return true; 
}
return false; 

}

获取锁、释放锁的这些方法基本上都穿插在ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch的源码解析中

需要子类实现的方法

上面一起学习了AQS中几个重要的模板方法,下面我们再一起学习下几个需要子类实现的方法:

// 互斥模式下使用:尝试获取锁
/protected boolean tryAcquire(int arg) { 
    throw new UnsupportedOperationException();
}
// 互斥模式下使用:尝试释放锁 
protected boolean tryRelease(int arg) { 
    throw new UnsupportedOperationException(); 
}
// 共享模式下使用:尝试获取锁 
protected int tryAcquireShared(int arg) { 
    throw new UnsupportedOperationException(); 
}
// 共享模式下使用:尝试释放锁 
protected boolean tryReleaseShared(int arg) { 
    throw new UnsupportedOperationException(); 
}
// 如果当前线程独占着锁,返回true 
protected boolean isHeldExclusively() { 
    throw new UnsupportedOperationException(); 
}

这几个方法为什么不直接定义成抽象方法呢?

因为子类只要实现这几个方法中的一部分就可以实现一个同步器了,所以不需要定义成抽象方法。


25.介绍一下volatile的功能?


保证线程可见性

防止指令重排序


26.volatile的可见性和禁止指令重排序怎么实现的?


可见性:

volatile的功能就是被修饰的变量在被修改后可以立即同步到主内存,被修饰的变量在每次是用之前都从主内存刷新。本质也是通过内存屏障来实现可见性

写内存屏障(Store Memory Barrier)可以促使处理器将当前store buffer(存储缓存)的值写回主存。

读内存屏障(Load Memory Barrier)可以促使处理器处理invalidate queue(失效队列)。进而避免由于Store Buffer和Invalidate Queue的非实时性带来的问题。


禁止指令重排序:

volatile是通过内存屏障来禁止指令重排序

JMM内存屏障的策略

在每个 volatile 写操作的前面插入一个 StoreStore 屏障。

在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。

在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。

在每个 volatile 读操作的后面插入一个 LoadStore 屏障。

27.简要描述一下ConcurrentHashMap底层原理?

image.png


JDK1.7中的ConcurrentHashMap

内部主要是一个Segment数组,而数组的每一项又是一个HashEntry数组,元素都存在HashEntry数组里。因为每次锁定的是Segment对象,也就是整个HashEntry数组,所以又叫分段锁。

JDK1.8中的ConcurrentHashMap

舍弃了分段锁的实现方式,元素都存在Node数组中,每次锁住的是一个Node对象,而不是某一段数组,所以支持的写的并发度更高。

再者它引入了红黑树,在hash冲突严重时,读操作的效率更高。


注意: 该文献参考博主学习老师课程上的笔记,并且得到过允许


我是牧小农,怕什么真理无穷,进一步有进一步的欢喜,大家加油~


版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
多线程面试题
本文章是自己总结的笔记,如有不对,请多指正,3Q
38 0
ACM模板——无向图连通判断法(或求连通分支个数) - 并查集
ACM模板——无向图连通判断法(或求连通分支个数) - 并查集
33 0
【高并发】线程与多线程
今天,跟大家分享下有关线程与多线程的知识,好了,不多说了,直接进入今天的正题。
78 0
高并发之——线程与多线程
在操作系统中,线程是比进程更小的能够独立运行的基本单位。同时,它也是CPU调度的基本单位。线程本身基本上不拥有系统资源,只是拥有一些在运行时需要用到的系统资源,例如程序计数器,寄存器和栈等。一个进程中的所有线程可以共享进程中的所有资源。
514 0
分布式一致性算法:由数学证明推导出Paxos
# 序 Basic-Paxos是分布式一致性算法的元祖算法之一,现代的分布式一致性算法基本都是Basic-Paxos算法的优化、改进或简化。 由于过于晦涩难懂,Basic-Paxos也是最难理解的算法之一,然而有理由相信,直接从数学证明推导出Basic-Paxos算法是理解它最便捷的方法。 #
5693 0
jQuery 省市区多级(三级/四级/五级。。。)联动 BY 凨来了
/** * 省市县区三级多级联动 * author:凨来了 */ jQuery(function($) { var city=[],cityName=[]; $.fn.city = function (opt) { var $id = $(this), options = $.extend({ u
1666 0
使用 Sticky-Kit 实现基于 jQuery 的元素固定效果
  元素固定效果在网页中应用得很多,比较常见的使用场景有改进导航,显示广告。Sticky-Kit 是一个非常方便的 jQuery 插件,简化了创建/管理粘元素,有复杂的使用功能。这些功能包括:处理多个固定元素,启用/禁用的行为取决于对元素在视口中的位置和回调。
810 0
+关注
牧小农
业精于勤荒于嬉,行成于思毁于随。
134
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
低代码开发师(初级)实战教程
立即下载
阿里巴巴DevOps 最佳实践手册
立即下载
冬季实战营第三期:MySQL数据库进阶实战
立即下载