Java中常用的锁机制
1.锁的定义和锁粒度
1.1什么是锁?
在计算机科学中,锁(lock)或互斥(mutex)是一种同步机制,用于在有许多执行线程的环境中强制对资源的访问限制。锁旨在强制实施互斥排他、并发控制策略。
锁通常需要硬件支持才能有效实施。这种支持通常采取一个或多个原子指令的形式,如"test-and-set", "fetch-and-add" or "compare-and-swap"”。这些指令允许单个进程测试锁是否空闲,如果空闲,则通过单个原子操作获取锁。
1.2.锁的一个重要属性 粒度 Granularity [grænjʊ‘lærɪtɪ]
在引入锁粒度之前,需要了解关于锁的三个概念:
1、锁开销 lock overhead 锁占用内存空间、 cpu初始化和销毁锁、获取和释放锁的时间。程序使用的锁越多,相应的锁开销越大
2、锁竞争 lock contention 一个进程或线程试图获取另一个进程或线程持有的锁,就会发生锁竞争。锁粒度越小,发生锁竞争的可能性就越小
3、死锁 deadlock 至少两个任务中的每一个都等待另一个任务持有的锁的情况锁粒度是衡量锁保护的数据量大小,通常选择粗粒度的锁(锁的数量少,每个锁保护大量的数据),在当单进程访问受保护的数据时锁开销小,但是当多个进程同时访问时性能很差。因为增大了锁的竞争。相反,使用细粒度的锁(锁数量多,每个锁保护少量的数据)增加了锁的开销但是减少了锁竞争。例如数据库中,锁的粒度有表锁、页锁、行锁、字段锁、字段的一部分锁
相关术语 Critical Section(临界区)、 Mutex/mutual exclusion(互斥体)、 Semaphore/binary semaphore(信号量)
2.锁的种类
2.1.独享锁/共享锁
1)独享锁是指该锁一次只能被一个线程所持有。 (ReentrantLock、 Synchronized)
2)共享锁是指该锁可被多个线程所持有。 (ReadWriteLock)
3)互斥锁/读写锁
独享锁/共享锁这是广义上的说法,互斥锁/读写锁就分别对应具体的实现。在Java中如ReentrantLock就是互斥锁(独享锁), ReadWriteLock就是读 写 锁(共享锁)。 独享锁与共享锁也是通过AQS来实现的
锁升级:读锁到写锁 (不支持)
锁降级:写锁到读锁 (支持)
4)读写锁 ReentrantReadWriteLock
高16位代表写锁,低16位代表读锁
2.2.公平锁/非公平锁
1)公平锁: 是指多个线程按照申请锁的顺序来获取锁。
2)非公平锁 : 是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能会造成饥饿现象。
对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的控制线程对锁的获取, 所以并没有任何办法使其变成公平锁。
2.3.可重入锁
可重入锁,又名,递归锁,是指同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取
锁。
ReentrantLock和Synchronized都是可重入锁。可重入锁的一个好处是可一定程度避免死锁
如上面的代码,如果synchronized不是可重入锁的话,testB就不会被当前线程执行,从而形成死锁。
需要注意的是,可重入锁 加锁和解锁的次数要相等。
C==0表明未获得锁,Else表示已经获得锁,这时对state加1,相应的,每次释放锁都会对state减1
2.4.乐观锁/悲观锁
乐观锁/悲观锁不是指具体类型的锁,而是看待并发的角度。
悲观锁认为存在很多并发更新操作,采取加锁操作,如果不加锁一定会有问题
乐观锁认为不存在很多的并发更新操作,不需要加锁。数据库中乐观锁的实现一般采用版本号,Java中可使用CAS实现乐观锁。
2.5.分段锁
分段锁是一种锁的设计,并不是一种具体的锁。对于ConcuttentHashMap就是通过分段锁实现高效的并发操作。
2.6.自旋锁
自旋锁是指尝试获取锁的线程不会阻塞,而是采用循环的方式尝试获取锁。好处是减少上下文切换,缺点是一直占用CPU资源。
2.7.偏向锁/轻量级锁/重量级锁
这是jdk1.6中对Synchronized锁做的优化,首先了解下对象头(Mark Word):
运行时JVM内存布局
Mark Word在不同锁状态下的标志位存储
从jdk1.6开始为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。
锁共有四种状态,级别从低到高分别是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。随着竞争情况锁状态逐渐升级、锁可以升级但不能降级。
偏向锁的获取和撤销:
HotSpot作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入偏向锁。
线程1检查对象头中的Mark Word中是否存储了线程1,如果没有则CAS操作将Mark Word中的线程ID替换为线程1。此时,锁偏向线程1,后面该线程进入同步块时不需要进行CAS操作,只需要简单的测试一下Mark Word中是否存储指向当前线程的偏向锁,如果成功表明该线程已经获得锁。如果失败,则再需要测试一下Mark Word中偏向锁标识是否设置为1(是否是偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将偏向锁指向当前线程
偏向锁的竞争结果:
根据持有偏向锁的线程是否存活
1.如果不活动,偏向锁撤销到无锁状态,再偏向到其他线程
2.如果线程仍然活着,则升级到轻量级锁
偏向锁在Java6和Java7中默认是开启的,但是在应用程序启动几秒后才激活,如果有必要可以关闭延迟:
-XX:BiasedLockingStartupDelay=0
如果确定应用程序中所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:
-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁。
-XX:BiasedLockingStartupDelay=0 -XX:+TraceBiasedLocking
轻量级锁膨胀:
1.线程在执行同步块之前,JVM会在当前栈桢中创建用于存储锁记录的空间(Lock record),并将对象头中的Mark Word复制到锁记录中(Displaced Mark Word)。
2.然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针
3.如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程尝试使用自旋来获取锁
偏向锁、轻量级锁、重量级锁的优缺点
1.偏向锁是为了避免某个线程反复获得/释放同一把锁时的性能消耗,如果仍然是同个线程去获得这个锁,尝试偏向锁时会直接进入同步块,不需要再次获得锁。
2.而轻量级锁和自旋锁都是为了避免直接调用操作系统层面的互斥操作,因为挂起线程是一个很耗资源的操作。
为了尽量避免使用重量级锁(操作系统层面的互斥),首先会尝试轻量级锁,轻量级锁会尝试使用CAS操作来获得锁,如果轻量级锁获得失败,说明存在竞争。但是也许很快就能获得锁,就会尝试自旋锁,将线程做几个空循环,每次循环时都不断尝试获得锁。如果自旋锁也失败,那么只能升级成重量级锁。
3.可见偏向锁,轻量级锁,自旋锁都是乐观锁。
逃逸分析:
逃逸分析:通俗一点讲,当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了逃逸,必须在JIT里完成
锁粗化:
如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展到整个操作序列的外部,这样就只需要加锁一次就够了
锁消除:
如果你定义的类的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。
栈上分配:
分析找到未逃逸的变量,将变量类的实例化内存直接在栈里分配(无需进入堆),分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。
从jdk1.6开始默认开启:
开启: -XX:+DoEscapeAnalysis
关闭: -XX:-DoEscapeAnalysis
3.1.Synchronized与ReentrantLock的区别
从字节码角度看实例synchronized方法、静态synchronized方法、synchronized代码块实现的不同
ReentrantLock = 一个AQS同步器(维护同步状态) + 一个AQS同步队列 + 多个Condition等待队列
3.2 ReentrantLock继承体系类图
ReentrantLock#lock()方法时序图
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。