本篇文章参考以下博文
作为一名前端开发,今天打算不务正业一下,研究研究 JAVA 中的锁,具体细节可能不是很准确,这里主要总结一下各种锁的分类,方便今后复习总结。
言归正传,了解锁必须先了解一下线程安全,即当多个线程访问某个类时,不管运行环境采用何种调度方式,或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都可以表现出正确的行为,那么这个类就是线程安全的。
线程的安全级别也是分高低的,一下是不同级别的分类(从弱到强)
线程对立:不管采用什么同步措施,都会线程不安全,无法在并发中使用。
eg: Thread.suspend (线程挂起)和 Thread.resume (线程复活)
线程兼容:也称非线程安全,对象本身不提供线程安全,但是通过外部调用,可以在并发环境下使用。
eg: ArrayList HashMap
相对线程安全:通常意义上的安全,保证对象单个的操作是安全的,调用的时候不需要进行同步操作,但是连续调用时需要在调用端进行同步手段保持安全性。
eg: hashTable,Vector
绝对线程安全:任何情况下调用者都不需要考虑额外的同步措施来保证线程的安全性。
eg: concurrentHashMap,atomic,random
不可变:绝对安全,不需要线程同步, String,Long 等数据类型。
锁在 JAVA中是非常重要的概念,尤其在高并发情况下,有着至关重要的作用。在计算机科学中,锁( LOCK )与互斥 ( mutex )是一种同步机制,用于在有许多执行线程的环境中约束对资源的访问限制,锁旨在强制实施互斥排他,并发控制。同步机制旨在对公共资源访问所加的限制。
按照锁的作用范围进行以下三种分类:【由小及大】
线程锁 :同操作系统内,多个线程访问同一资源时,保证同一时间内只有一个线程在执行,其余线程必须等待当前线程执行完毕后才能操作该资源。
进程锁 :同操作系统内,多个进程访问同一资源,保证同一时间内只有一个进程操作资源。
分布式锁 :多个进程不在同一个系统中,使用分布式锁控制多个进程对资源访问。
指多个进程(线程)在执行过程中因竞争资源而造成的一种僵局(互相等待),若无外力作用,他们将无法推进下去。
自锁 :执行线程获取到当前枷锁资源后,再次获取改价所资源,造成线程挂起等待资源释放,但是释放机制却在操作完第一次加锁资源之后。
多线程竞争循环等待 :A线程加锁持有A资源,申请使用B资源,B资源正在被B加锁处理,这时A线成挂起等待,如果这时B线程去申请A资源,AB线程互相僵持,形成死锁。
进程顺序推进不当引起死锁 :A进程获取B数据生产A数据,B进程获取A数据生产B数据,AB执行时,先执行数据生产,再执行数据接收,那么没有问题,如果AB先接收数据,再执行生产,就会形成死锁。
互斥条件 :进程申请的资源在一段时间内只能被一个进程使用(线程)
请求条件与等待条件 :已拥有一个资源,但是又申请一个新的资源,拥有的资源保持不变。
不可剥离条件 :在一个进程没有用完,主动释放资源的时候,不能被抢占。
循环等待条件 :多个进程之间存在资源循环链。
4.1.1 synchronized 关键字
锁方法: public synchronized void A () {}
锁对象: synchronized(object) {}
锁类: synchronized(Class) {}
4.1.2 Doug Lea 实现的 Lock 类(典型: reentrantLock reenrantReadWriteLock )
常用方法:
lock() :执行加锁操作, lock 是有次数记录的,执行了几次就需要有对应的 unlock() 。
trylock() :尝试加锁,返回布尔类型。
trylock(long timeout, TimeUnit unit) :一段时间内尝试获取锁
lockIntreeuptibly() :当执行线程被 interrupt 时,该方法会抛异常释放锁,是有效的方死锁方法。
unlock() :解锁方法
Q : T1 线程和 T2 线程同时执行 doSomething() 方法,如何保证 T1 T2 有次序执行?
方法一:信号量
方法二: sync 关键字
方法三: reentrantLock
;4.3.1 实现自己的锁类
信号量
4.3.2 reentrantLock 实现的基础
信号量(内置值)+ CAS 原子操作 (compareAndSwap() )
原子性( Atomic ):程序中所有操作要么全部完成,要么全部不完成,不可以停止在中间某个环节。
Java 中的原子性操作:程序执行的最小单元,例如赋值操作 i = 1,一旦执行既成事实。
CAS 操作: CompareAndSwap(expect, acquires) 译为:比较交换
底层由 C++ 编写一个原子操作,在 java 中作为 native 方法被 unsafe 类引用;
except 表示期望值, acquires 表示新值, state 表示内置值,默认情况下 expect = state
效果:当 state = expect 时,将内置值改为新值,返回 true
当 state != expect 时,不作任何修改,返回 false
CAS 操作旨在线程安全的修改信号量的值。
4.3.3 分析 MyLock.class 并对 MyLock 进行优化
yield() + MyLock ;
sleep() + MyLock ;
park() + MyLock ;
自旋锁 :
当检测到资源被另一个线程占用时,开始进行循环等待,知道可以允许访问资源,线程不会进入阻塞状态,自旋次数固定。
适应性自旋锁 :
基于自旋锁,但是自旋次数不固定,系统依据锁的状态和上一个线程状态来延迟和缩短自旋次数。
非自旋锁 :
当检测到资源被另一个线程占用时,当前线程直接休眠等待再次被唤醒。
yield() :
译为线程让步,线程执行后会立刻放弃 CPU 执行, CPU 自行选择接下来运行什么线程。
sleep() :
当前调用线程进入指定时间睡眠状态,然后进行执行。
park()/unpark() :
当前线程立刻进入水面线程直到被唤醒,由 unsafe 类提供,挂名在 concurrent 包下。
4.3.4 分析 MyLock 是否公平?
公平锁 :
多个线程按照申请锁的顺序来获取锁,线程直接进入到队列中排队,排队的第一个线程才能获得锁。
公平锁特点 :
排队线程不会饿死,但是整体吞吐效率较低,除第一线程,其余线程都会阻塞, CPU 唤醒线程消耗较大。
非公平锁 :
多个线程加锁时直接尝试获取锁,获取不到才会排队。
非公平锁特点 :
可以有效减少唤醒开销,线程有几率直接获取锁, CPU 不用唤醒所有的线程,但是存在部分线程饿死的可能,或者等待时间很长才能获得锁。
悲观锁 :
同一数据的并发操作,自己使用的资源一定有别的线程在使用,所以在获取资源的时候会先加锁,确保数据不会被别的线程修改。[ sync 关键字]
乐观锁 :乐观锁觉得自己在使用数据的时候,不会有别的线程来修改数据,如果这个数据没有被更新,那么当前线程将数据写入,如果数据已经被其他线程改变,则根据不同的方式,执行不同的操作。[ java 无锁编程]
备注: 公平与非公平是相对于获取锁来说的争夺策略,悲观与乐观是相对于读写资源来说的主观判断 。
4.3.5 MyLock 公平锁改造
myQueue 队列 + 排队设定
4.3.6 MyLock 可重入性改造
可重入性( reeenteantcy ):
1. 可以在执行过程中被打断。
2. 被打断之后,在函数依次调用完成之前,可以再次被调用。
3. 再次调用执行完之后,被打断的上次调用可以继续恢复执行,并正确执行。
多线程共享进程内部资源,如果两个线程 A 和 B 分别调用一个 不可重入函数 F , A 线程进入 F 后,线程调度, 切换到 B , B 也执行了 F 那么再次调度到 A 时,调用 F 的结果是未可知的。
4.3.7 reentrantLock 源码对比解读
1. reentrantLock 的核心类为 reentrantLock.class + AQS.class
reentrantLock.class 提供加解锁操作
AQS : AbstractQueueSynchronizer 提供同步队列操作以及 CAS 操作入口。
2. 实例化 reentrantLock 可以通过传入布尔值决定实例化公平锁与非公平锁
公平锁与非公平锁集中表现在 hasQueuePredecessors()
3. 是否可以重入表现在 setExclusiveOwnerThread()
4. 加解锁次数一一对应体现在 CAS 操作以及 nextc 计算。
5. trylock() 本很是非阻塞性的,一次性的,非公平的;
trylock(long, TimeUtls) 是自旋的非阻塞的,占用 CPU 资源 doAcquireNanos()
本节主要整理了一下锁的前世今生,以及线程锁 reentrantLock 的一些常用类,感谢后端同事提供的学习资料,下一节我们继续学习一下锁的相关知识,欢迎各位同学关注我的后续博客。