4.ThreadLocal有哪些应用场景?他的底层是如何实现的(https://blog.csdn.net/weixin_44184990/article/details/122279854)
ThreadLocal意为线程本地变量,用于解决多线程并发时访问共享变量的问题。每个线程都会有属于自己的本地内存,在堆中的变量在被线程使用的时候会被复制一个副本线程的本地内存中,当线程修改了共享变量之后就会通过JMM管理控制写会到主内存中。
很明显,在多线程的场景下,当有多个线程对共享变量进行修改的时候,就会出现线程安全问题,即数据不一致问题。常用的解决方法是对访问共享变量的代码加锁(synchronized或者Lock)。但是这种方式对性能的耗费比较大。在JDK1.2中引入了ThreadLocal类,来修饰共享变量,使每个线程都单独拥有一份共享变量,这样就可以做到线程之间对于共享变量的隔离问题。
那么如何究竟是如何实现在每个线程里面保存一份单独的本地变量呢?首先,在Java中的线程是什么呢?是的,就是一个Thread类的实例对象!而一个实例对象中实例成员字段的内容肯定是这个对象独有的,所以我们也可以将保存ThreadLocal线程本地变量作为一个Thread类的成员字段,这个成员字段就是:是一个在ThreadLocal中定义的Map对象,保存了该线程中的所有本地变量。 ThreadLocalMap和Entry都在ThreadLocal中定义。
在JDK早期的设计中,每个ThreadLocal都有一个map对象,将线程作为map对象的key,要存储的变量作为map的value,但是现在已经不是这样了。
JDK8之后,每个Thread维护一个ThreadLocalMap对象,这个Map的key是ThreadLocal实例本身,value是存储的值要隔离的变量,是泛型,其具体过程如下:每个Thread线程内部都有一个Map(ThreadLocalMap::threadlocals);Map里面存储ThreadLocal对象(key)和线程的变量副本(value);Thread内部的Map由ThreadLocal维护,由ThreadLocal负责向map获取和设置变量值;对于不同的线程,每次获取副本值时,别的线程不能获取当前线程的副本值,就形成了数据之间的隔离。
JDK8之后设计的好处在于:每个Map存储的Entry的数量变少,在实际开发过程中,ThreadLocal的数量往往要少于Thread的数量,Entry的数量减少就可以减少哈希冲突。当Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存使用,早期的ThreadLocal并不会自动销毁。
5.ReentrantLock分为公平锁和非公平锁,那底层分别是如何实现的?(https://blog.csdn.net/BASK2311/article/details/127915718)
在面试或者日常开发当中,经常会遇到公平锁和非公平锁的概念。(想要完全理解需要有AQS(抽象的队列同步器)基础)
公平锁:N个线程去申请锁时,会按照先后顺序进入一个队列当中去排队,依次按照先后顺序获取锁。就像下图描述的上厕所的场景一样,先来的先占用厕所,后来的只能老老实实排队。非公平锁:N个线程去申请锁,会直接去竞争锁,若能获取锁就直接占有,获取不到锁,再进入队列排队顺序等待获取锁。同样以排队上厕所打比分,这时候,后来的线程会先尝试插队看看能否抢占到厕所,若能插队抢占成功,就能使用厕所,若失败就得老老实实去队伍后面排队。
针对这两个概念,我们通过ReentrantLock底层源码来分析下:公平锁和非公平锁在ReentrantLock类当中锁怎样实现的。ReentrantLock内部实现的公平锁类是FairSync,非公平锁类是NonfairSync。当ReentrantLock以无参构造器创建对象时,默认生成的是非公平锁对象NonfairSync,只有带参且参数为true的情况下FairSync,才会生成公平锁,若传参为false时,生成的依然是非公平锁。
6.Sychronized的锁升级过程是怎样的?(https://blog.csdn.net/lt_xiaodou/article/details/126986388)
偏向锁:在synchronized进行升级的过程中,第一步会升级为偏向锁。所谓偏向锁,它的本质就是让锁来记住请求的线程。在大多数场景下,其实都是单线程访问锁的情况偏多,JDK的作者在重构synchronized的时候,给对象头设计了一个bit位,专门用于记录锁的信息。
JVM因为在启动预热的阶段中,会有很多步骤使用到synchronized,所以在刚启动的前4秒中,不会直接将synchronized锁的标记升级为biasable状态。这是为了减少一些不必要的性能损耗。biasable是JVM帮我们设置的状态,在这种状态下,一旦有线程访问锁,就会直接CAS(CAS,compare and swap的缩写,中文翻译成比较并交换,CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。)修改对象头中的线程id。如果成功,则直接升级为偏向锁。否则就会进入到锁的下一个状态--轻量级锁。
轻量级锁:当锁被一个线程访问的时候,它会变成偏向锁的状态,那么当新的线程再次访问该锁的时候,锁会有什么变化吗?这里我整理了一张锁的变化流程图,如下所示:
当第一个请求获取到锁的时候,锁的状态会变成偏向锁状态,也就是biased。如果在处于偏向锁状态的时候,还有新的线程参与锁的抢夺,那么就会发生锁的升级,进入到轻量级锁状态阶段。可以看到,当一个锁已经经历过偏向锁状态之后,后去如果再有其他线程访问它,它就会升级为轻量级锁的状态。
当一把锁已经处于轻量级锁的状态时,如果此时又有多个线程来尝试获取锁,那么锁就会被多个线程以自旋的方式来请求访问,当访问的次数达到一定上限之后,synchronized就会自动升级为重量级锁的状态了。在重量级锁的情况下,加解锁的过程涉及到操作系统的Mutex Lock进行互斥操作,线程间的调度和线程的状态变更过程需要在用户态和核心态之间进行切换,会导致消耗大量的cpu资源,导致性能降低。
网友评论