Java后端高频知识点学习笔记4---Java的锁

Java后端高频知识点学习笔记4---Java的锁

参考地址:牛_客_网
https://www.nowcoder.com/discuss/819304

1、乐观锁和悲观锁

含义
悲观锁 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会被阻塞,直到拿到锁。Java中悲观锁是通过synchronized关键字或Lock接口来实现的
乐观锁 顾名思义,每次读取数据的时候,乐观地认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据;乐观锁适用于多读场景,可以提高吞吐量;在JDK1.5中新增java.util.concurrent (J.U.C)就是建立在CAS之上的。相对于对于synchronized这种阻塞算法,CAS是非阻塞算法的一种常见实现;所以J.U.C在性能上有了很大的提升
总结 乐观锁适用于多读场景;悲观锁适用于多写场景

2、公平锁和非公平锁

根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁

公平锁:线程获取锁的顺序是按照线程请求锁的先后顺序来决定的,即最早请求锁的线程将最早获取到锁

非公平锁:运行时闯入,先来不一定先得,非公平锁减少了线程挂起的几率,后来的线程有一定几率逃离被挂起的开销

ReentrantLock提供了公平和非公平锁的实现
公平锁:ReentrantLock fairLock = new ReentrantLock(true);
非公平锁:ReentrantLock nofairLock = new ReentrantLock(false);

  • 在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销

公平和非公平是怎么实现?
公平:使用队列
非公平:线程之间使用抢夺策略

3、独占锁和共享锁

根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁和共享锁

独占锁保证任何时候都只有一个线程能得到锁;ReentrantLock就是以独占的方式实现的

共享锁则可以同时由多个线程持有;例如ReadWriteLock读写锁,它允许一个资源可以同时被多个线程进行读操作

独占锁是一种悲观锁;共享锁是一种乐观锁

4、可重入锁

当一个线程再次获取它自己已经获取到的锁时不会被阻塞,就说该锁是可重入的,也就是只要该线程获取了该锁,那么可以无限次数(严格讲是有限次的)地进入该锁锁住的代码

5、自旋锁

当前线程在尝试获取锁时,如果发现锁已经被其他线程占有,它不会马上阻塞自己,在不放弃CPU使用权的情况下,多次尝试获取(默认是10次,可修改),很有可能在后面几次尝试中其他线程已经释放了锁;如果尝试指定的次数后仍然没有获取到锁则当前线程才会被阻塞挂起

应用场景:
TicketLock:为了解决上面的公平性问题,类似于现实中银行柜台的排队叫号:锁拥有一个服务号,表示正在服务的线程,还有一个排队号;每个线程尝试获取锁之前先拿一个排队号,然后不断轮询锁的当前服务号是否是自己的排队号,如果是,则表示自己拥有了锁,不是则继续轮询

6、读写锁

读写锁的规则是可以共享读,但只能一个写,总结起来为:读读不互斥、读写互斥、写写互斥

一般的独占锁是读读互斥、读写互斥、写写互斥,而场景中往往读远远大于写,读写锁就是为了这种优化而创建出来的一种机制

适用场景:多读少写

注意:一般情况下独占锁的效率低下,因为高并发下对临界区的激烈竞争导致线程上下文切换;但当并发不是很高的情况下,读写锁由于需要额外维护读锁的状态,可能还不如独占锁的效率高,因此需要根据实际情况选择使用

7、Java中的锁升级

① 无锁

无锁是指没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功;无锁的特点是修改操作会在循环内进行,线程会不断的尝试修改共享资源;如果没有冲突就修改成功并退出,否则就会继续循环尝试;如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功

② 偏向锁

偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。

③ 轻量级锁

轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能

④ 重量级锁

如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒

8、sychronized 和 ReenteredLock 区别

① 底层实现

synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),对象只有在 同步代码块 或 同步方法 中才能调用 wait()/notify() 方法

ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁

synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、轻量级锁、重量级锁;ReentrantLock 实现则是通过利用 CAS(Compare-And-Swap)自旋机制保证线程操作的原子性 和 volatile 保证数据可见性以实现锁的功能

② 是否可手动释放

synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象,一般通过 lock() 和 unlock() 方法配合 try/finally语句块 来完成,使释放更加灵活

③ 是否可中断

synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成; ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt()方法进行中断

④ 是否公平锁

synchronized 为非公平锁;ReentrantLock 可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁

9、CAS(Compare And Swap,比较并替换)

CAS算法的过程是:它包含3个参数 CAS(V,E,N),其中 V表示要更新的变量, E表示预期值, N表示新值

仅当V的值等于E时,才会将V的值设置为N,如果V值不等于E,说明已经有其他线程做了更新,则当前线程什么都不做;最后CAS返回当前V的真实值

在多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并允许再次尝试,当然也允许失败的线程放弃操作

CAS怎么保证修改的值可见?volatile关键字

ABA问题:在CAS操作中有个经典的ABA问题?解决方式?(版本号、时间戳)

假如线程①使用CAS修改初始值为A的变量X,那么线程①会首先去获取当前变量X的值(为A),然后使用CAS操作尝试修改X的值为B,如果使用CAS操作成功了,程序运行也不一定是正确的

在线程①获取变量X的值A后,在执行CAS前,线程②使用CAS修改了X的值为B,然后又使用CAS修改了变量X的值为A。

所以,线程①执行CAS时X的值是A,但是这个A已经不是线程①获取时的A了,这就是ABA问题。

ABA问题的产生是因为变量的状态值产生了环形转换

避免ABA问题:使用版本号或时间戳。给每个变量的状态值配备一个时间戳或者版本号

10、AQS:抽象同步队列

一个锁对应一个AQS阻塞队列,对应多个条件变量,每个条件变量有自己的条件队列

Node节点:AQS是一个先进先出的双向队列,队列中元素的类型是Node类型,其中Node中的thread变量用来存放进入AQS队列中的线程

ConditionObject:AQS中还有个内部类ConditionObject,用来结合锁实现线程同步;ConditionObject可以直接访问AQS对象内部的变量,比如state状态值和AQS队列;ConditionObject变量是条件变量,每个条件变量都维护了一个条件队列(单向链表队列),其用来存放调用条件变量的await()方法后被阻塞的线程

① 当线程调用条件变量的await()方法时,必须先调用锁的lock()方法获取锁。调用await()方法后,在内部会将当前线程构造一个node节点,插入到条件队列的尾部,之后当前线程会释放获取的锁(也就是操作锁对应的state变量的值),并被阻塞挂起

② 当另外一个线程调用条件变量的signal()方法时,必须先调用锁的lock()方法获取锁,在内部会把条件队列里面对头的一个线程节点从条件队列中移除并放入AQS的阻塞队列中,等待获取锁。

state变量;被volatile修饰的state变量

线程同步的关键是对状态值state进行操作,根据state是否属于一个线程,操作state的方式分为独占方式和共享方式

① 独占方式

使用独占方式时:如果一个线程获取到了资源,就会标记是这个线程获取到了,其他线程再尝试操作state获取资源时会发现当前该资源不是自己持有的,就会在获取失败后被阻塞

  • 比如:ReentrantLock,当一个线程获取了ReentrantLock的锁后,在AQS内部会使用CAS操作把state的值从0变为1,然后设置当前锁的持有者设为当前线程,当该线程再次获取锁时发现它就是锁的持有者,则会把state值从1变为2,也就是设置可重入次数,而当另一个线程获取锁时发现自己不是该锁的持有者就会被放入AQS阻塞队列后挂起

② 共享方式

使用共享方式时:当多个线程去请求资源时通过CAS方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再次去获取时如果当前资源还能满足它的需要,则当前线程只需要使用自旋CAS方式进行获取,否则就把当前线程放入阻塞队列

  • 比如:Semaphore信号量,当一个线程通过acquire()方法获取信号量时,会首先看当前信号量的个数是否满足需要,不满足则把当前线程放入阻塞队列,如果满足则通过自旋CAS获取信号量

11、synchronized关键字的底层原理

① synchronized 同步语句块的情况

synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中 monitorenter指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
当执⾏monitorenter 指令时,线程试图获取锁也就是获取 monitor的持有权(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种⽅式获取锁的,也是为什么Java中任意对象可以作为锁的原因);当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1;相应的在执⾏monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外⼀个线程释放为⽌。

monitorenter:每个对象都是一个监视器锁(monitor),当monitor被占用时就会处于锁定状态,线程执行 monitorenter指令时尝试获取monitor的所有权,过程如下: 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor 的所有者。如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权

monitorexit:执行monitorexit的线程必须是objectref所对应的monitor持有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者;其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权;monitorexit指令出现了两次,第1次为同步正常退出释放锁,第2次为发生异常退出释放锁

② synchronized 修饰⽅法的的情况

synchronized 修饰的⽅法并没有 monitorenter 指令和 monitorexit 指令,取得代之的是 ACC_SYNCHRONIZED 标识,该标识指明了该⽅法是⼀个同步⽅法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别⼀个⽅法是否声明为同步⽅法,从⽽执⾏相应的同步调⽤

12、synchronized修饰普通方法和静态方法的区别

synchronized关键字作用于:

实例方法,被锁的对象为类的实例对象

静态方法,被锁的对象为类对象

13、ReentrantLock底层实现

ReentrantLock是 可重入的独占锁,同时只能有一个线程可以获取该锁,其他获取该锁的线程会被阻塞而被放入该锁的AQS阻塞队列里面

ReentrantLock是基于AQS来实现的,并且根据参数来决定其内部是一个公平锁还是非公平锁,默认是非公平锁(false)

在ReentrantLock中的AQS中的state状态值表示线程获取该锁的可重入次数,在默认的情况下,state的值为0表示当前锁没有被任何线程持有

当一个线程第一次获取该锁时会尝试使用CAS设置state的值为1,如果CAS成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程

在该线程没有释放锁的情况下第二次获取该锁后,状态值被设置为2,这就是可重入次数

在该线程释放该锁时,会尝试使用CAS让状态值减1,如果减1后状态值为0,则当前线程释放该锁

14、volatile关键字

volatile 关键字的主要作⽤就是 保证变量的可⻅性;还有⼀个作⽤是防⽌指令重排序

  • 当修改volatile变量时,JMM(Java内存模型)会把线程对应的工作内存中的共享变量值刷新到主内存中
  • 当读取volatile变量时,JMM会把该线程对应的工作内存置为无效,线程从主内存中读取共享变量值

volatile可以保证可见性且提供了一定的有序性,但是无法保证原子性;在JVM底层volatile是采用“内存屏障"(memory barrier)来实现的;观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障,内存屏障会提供3个功能:
1、确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成
2、强制将对缓存的修改立即写入主存,让其他CPU中对应的缓存行无效

volatile的实际应用场景
(1)双重校验单例模式:防止指令重排
(2)CAS中,使变量的变化对其它线程可见
(3)AQS中,volatile修饰state变量

15、ThreadLocal

ThreadLocal:线程本地变量;如果创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题;创建一个ThreadLocal变量后,每个线程都会复制一个变量到自己的本地内存

ThreadLocal使用场景:
① ThreadLocal经典的使用场景是为每个线程分配一个JDBC连接 Connection,这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A线程关了B线程正在使用的Connection

② ThreadLocal还经常用于管理Session会话,将Session保存在ThreadLocal中,使线程处理多次处理会话时始终是同一个Session( ThreadLocal中可以存储当前的User对象[替代session],在登录拦截器中判断用户是否为null来判断用户是否登录)

ThreadLocal实现原理

Thread类中有一个threadLocals,它是ThreadLocalMap类型的变量;每个线程的本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量里面,也就是说ThreadLocal类型的本地变量存放在具体的线程内存空间里

ThreadLocal就是一个工具壳,通过set方法把value值放入调用线程的threadLocals里面并存放起来。通过get方法从当前线程的threadLocals变量里面将其拿出来使用。通过threadLocal的remove方法,从当前线程的threadLocals里面删除该本地变量。

threadLocals被设计成map结构,每个线程可以关联多个ThreadLocal变量

ThreadLocal是怎么处理hash冲突的?

ThreadLocalMap的结构只用一个数组存储,并没有链表结构,当出现Hash冲突时采用线性查找的方式就是根据初始key的hashcode值确定元素在数组中的位置,如果发现这个位置上已经有其他key值的元素占用,则利用固定的算法寻找一定步长的下个位置,依此判断,直到找到能够存放的位置。如果产生多次hash冲突,处理起来就没有HashMap的效率高,为了避免哈希冲突,使用尽量少的threadLocal变量

InheritableThreadLocal

ThreadLocal不支持继承性,InheritableThreadLocal继承自ThreadLocal,其提供了一个特性,就是让子线程可以访问在父线程中设置的本地变量

InheritableThreadLocal通过set方法将value值放入调用线程的inheritableThreadLocals变量的实例;当父线程创建子线程时,构造函数会把父线程中inheritableThreadLocals变量里面的本地变量复制一份到子线程的inheritableThreadLocals变量里面

16、ThreadLocal内存泄漏问题?

内存泄露:程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光

ThreadLocal实现原理:每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本

由于ThreadLocalMap的生命周期和Thread一样长,如果没有手动删除对应的key,就会导致内存泄漏

但是JVM团队已经考虑到这样的情况,并做了一些措施来保证ThreadLocal尽量不会内存泄漏:在ThreadLocal的get()、set()、remove()方法调用的时候会清除掉线程ThreadLocalMap中所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存

由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露

解决方法:每次使用完ThreadLocal都调用remove()方法清除数据
在ThreadLocal每次get和set的时候都会清理掉key为null的value

posted @ 2021-12-20 17:33  紫薇哥哥  阅读(104)  评论(0编辑  收藏  举报