Java中锁的总结
Java中的类大体可以分为2类,一种是隐式锁像Synchronized,是JVM级别的锁,一种是显示锁像Lock接口下的一些实现,是API级别的锁。
Synchronized
synchronized使用
- 如果修饰的是
具体对象
:锁的是对象
; - 如果修饰的是
成员方法
:那锁的就是this
; - 如果修饰的是
静态方法
:锁的就是这个对象.class
synchronized底层原理
修饰方法(隐式同步)
我们写一个
public synchronized static void syntask()
这个方法的字节码文件
ACC_PUBLIC
代表public修饰ACC_STATIC
表示是静态方法ACC_SYNCHRONIZED
指明该方法为同步方法
所以当这个方法被调用时,jvm会检查ACC_SYNCHRONIZED是否被设置了,如果设置了,该线程就要求先成功持有管程(monitor),然后才能执行方法,最后等方法执行完成(正常完成或者非正常完成)才会释放管程
在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。
如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法边界之外时自动释放
修饰代码块
public void syncTask(){ synchronized (this){ i++; } }
Java虚拟机的指令集中有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义。(monitorenter 和 monitorexit 两条指令是 C 语言的实现)正确实现 synchronized 关键字需要 Javac 编译器与 Java 虚拟机两者共同协作支持。Monitor的实现基本都是 C++ 代码,通过JNI(java native interface)的操作,直接和cpu的交互编程
改进后的各种锁
我们知道在老版本的JDK中,这个锁的效率是很低下的,但是Oracle也对锁进行了一系列的优化。
偏向锁->自旋锁->轻量级锁->重量级锁。按照这个顺序,锁的重量依次增加。
- 偏向锁。他的意思是这个锁会偏向于第一个获得它的线程,当这个线程再次请求锁的时候不需要进行任何同步操作,从而提高性能。那么处于偏向锁模式的时候,对象头的Mark Word 的结构会变为偏向锁结构。
研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁的代价而引入偏向锁。那么显然,一旦另一个线程尝试获得这个锁,那么偏向模式就会结束。另一方面,如果程序的大多数锁都是多个线程访问,那么偏向锁就是多余的。
- 轻量级锁。当偏向锁的条件不满足,亦即的确有多线程并发争抢同一锁对象时,但并发数不大时,优先使用轻量级锁。一般只有两个线程争抢锁标记时,优先使用轻量级锁。 此时,对象头的Mark Word 的结构会变为轻量级锁结构。
轻量级锁是和传统的重量级锁相比较的,传统的锁使用的是操作系统的互斥量,而轻量级锁是虚拟机基于 CAS 操作进行更新,尝试比较并交换,根据情况决定要不要改为重量级锁。(这个动态过程也就是自旋锁的过程了)
-
重量级锁。重量级锁即为我们在上面探讨的具有完整Monitor功能的锁。
-
自旋锁。自旋锁是一个过渡锁,是从轻量级锁到重量级锁的过渡。也就是CAS。
在生成锁的时候,JVM会从最轻量级的锁开始创建,若不满足条件就会将锁升级
其他锁的分类
按照锁的特性分类:
- 悲观锁:独占锁,会导致其他所有需要所的线程都挂起,等待持有所的线程释放锁,就是说它的看法比较悲观,认为悲观锁认为对于同一个数据的并发操作,一定是会发生修改的。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。比如前面讲过的,最传统的 synchronized 修饰的底层实现,或者重量级锁。(但是现在synchronized升级之后,已经不是单纯的悲观锁了)
- 乐观锁:每次不是加锁,而是假设没有冲突而去试探性的完成操作,如果因为冲突失败了就重试,直到成功。比如 CAS 自旋锁的操作,实际上并没有加锁。
按照锁的顺序分类:
- 公平锁。公平锁是指多个线程按照申请锁的顺序来获取锁。java 里面可以通过 ReentrantLock 这个锁对象,然后指定是否公平
- 非公平锁。非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。使用 synchronized 是无法指定公平与否的,他是不公平的。
独占锁(也叫排他锁)/共享锁:
- 独占锁也叫排他锁,是指该锁一次只能被一个线程所持有。对 ReentrantLock 和 Sychronized 而言都是独占锁。
- 共享锁:是指该锁可被多个线程所持有。对 ReentrantReadWriteLock 而言,其读锁是共享锁,其写锁是独占锁。读锁的共享性可保证并发读是非常高效的,读写、写读、写写的过程都是互斥的
Java中的Lock接口
Lock是java JUC包下的显示锁,是一种API级别的锁,因此在效率层面依赖于程序员如何对锁进行使用,而synchronized是JVM级别的锁,JVM会自动进行优化。
Lock是一个接口,常用的实现类有:
- 重入锁(
ReentrantLock
) - 读锁(
ReadLock
) - 写锁(
WriteLock
)
这些类的底层都是使用了AQS的同步机制,关于AQS请到我的另一篇文章
隐式锁和显示锁的对比
- 隐式锁基本没有灵活性可言,因为 synchronized 控制的代码块无法跨方法,修饰的范围很窄;而显示锁则本身就是一个对象,可以充分发挥面向对象的灵活性,完全可以在一个方法里获得锁,另一个方法里释放。
- 隐式锁简单易用且不会导致内存泄漏;而显式锁的过程完全要程序员控制,容易导致锁泄露;
- 隐式锁只是非公平锁;显示锁支持公平/非公平锁;
- 隐式锁无法限制等待时间、无法对锁的信息进行监控;显示锁提供了足够多的方法来完成灵活的功能;
- 一般来说,我们默认情况下使用隐式锁,只在需要显示锁的特性的时候才选用显式锁
Volatile
是java虚拟机提供的轻量级的同步机制:1.保证可见性,2不保证原子性,3禁止指令重排
Java内存模型:
由于jvm运行程序的实体是线程,而每个线程创建时Jvm都会为其创建一个工作内存,私有数据区域。java内存模型中规定所有变量存储在主内存,主内存时共享区域,所有线程都可以访问。 但线程对变量的操作必须在自己的工作内存执行,首先将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再写入主内存
CAS
Compare and swap,是一条cpu并发原语,使用了大量的sun.misc.Unsafe类,原语的执行必须是连续的,在执行过程中不允许被打断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题
CAS缺点:
1.如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功,可能会给cpu带来很大的开销。
2.只能保证一个共享变量的原子操作
3.ABA问题
什么是ABA问题?
两个线程由于存在时间差,线程一将A->B->A, 然后线程2查看的时候,以为数据没有进行变化,直接就进行了CAS,然而数据已经从A变成到B再变成A。
如果解决ABA问题?
使用带时间戳的原子引用