JAVA锁相关术语及同步关键字synchronized详解
JAVA中锁的概念
自旋锁:为了不放弃CPU执行事件,循环中使用CAS技术对数据尝试进行更新,直到成功。(理解上类似乐观锁)
悲观锁:假定会发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁。
乐观锁:假定没有冲突,在修改数据时如果发现数据和之前获取的不一致,则读最新数据,修改后重试修改。(有比较过程)
独享锁(写):给资源加上写锁,线程可以修改资源,其他线程不能再加锁。(单写)
共享锁(读):给资源加上锁后只能读不能改,其他线程也只能加读锁,不能加写锁。(多读)(缓存cache中有用到单写多读)
可重入锁、不可重入锁:线程拿到一把锁之后,可以自由进入同一把锁所同步的其他代码。
公平锁、非公平锁:争抢锁的顺序,如果是按先来后到,则为公平。
几种重要的锁的实现方式:synchronized(同步关键字)、ReentrantLock(可重入锁)、ReentrantReadWriteLock(可重入读写锁),这三种都是可重入锁,而自旋锁就属于不可重入锁
同步关键字synchronized
属于最基本的线程通信原则,基于对象监视器(monitor)实现的。
Java中的每个对象都与一个监视器相关联,一个线程可以锁定或解锁。
一次只有一个线程可以锁定监视器。
试图锁定该监视器的任何其他线程都会被阻塞,直到它们可以获得该监视器上的锁定为止。
特性:可重入、独享、悲观锁
锁的范围:类锁、对象锁(可理解为实例)、锁消除、锁粗化
提示:同步关键字,不仅是实现同步,根据JMM规定还能保证可见性(读取最新主内存数据,结束后写入主内存)
demo
1 | package com.study.lock.sync; |
输出结果
1 | 我开始执行 |
若synchronized关键字加在public void test1()上面
1 | public synchronized void test1( |
输出结果
1 | 我开始执行 |
则不能起到锁的作用,原因是方法是类的一个实例,类的实例每次new的时候都会新建一个,所以起不到锁的效果,这种称为对象锁(实例锁),而
1 | public void test1() { |
这种关键字加载类上,类是独一份的,则可以起到锁的效果,称为类锁。
Monitorenter(加锁)/ Monitorexit(退出锁)
看下上面demo的字节码 javap -v ObjectSyncDemo.class,截取某段
锁粗化————范围扩大
1 | // 锁粗化(运行时 jit 编译优化) |
jit会把代码优化成(从jdk1.6开始)
1 | synchronized (this) { |
锁消除
1 | package com.wyj.jvm.sync; |
test1方法是不存在竞争,StringBuffer之所以是线程安全的,是因为他的方法上都加上了synchronized同步关键字,JIT优化会发现这个方法没有竞态条件,是线程安全的,所以对于这种锁就不会再锁,这就是锁消除。
同步关键字加锁原理
对象中Mark Word即为偏向锁->轻量锁->重量级锁过程图(下图)中标橙色的部分,正因为这个关键字有00(无锁),01(轻量级锁),10(重量级锁),11(等待被GC回收)这几种状态,才有了锁的变化。
JVM锁经历:偏向锁->轻量锁->重量级锁,一种三个过程,如下图:
![](JAVA锁相关术语及同步关键字synchronized详解/图片 2.png)
偏向锁
偏向锁到轻量锁的过程
epoch代表偏向锁的线程ID
一个锁默认是开启偏向锁,若发现某对象存在大量争抢,就会在创建的时候撤销偏向锁。
偏向标记第一次有用,出现过争用后就没用了。-XX:-UseBiasedLocking禁用使用偏置锁定,
偏向锁,本质就是无锁,如果没有发生过任何多线程争抢锁的情况,JVM认为就是单线程,无需做同步(jvm为了少干活:同步在JVM底层是有很多操作来实现的,如果是没有争用,就不需要去做同步操作)
为什么会有偏向锁的概念?
当对象刚创建的时候,JVM并不知道对象会被锁多次,据统计,很多代码虽然用到锁,但是并发情况较少,也就是开发者事前会考虑到多线程情况,实际情况下不代表一点会有,所以偏向锁(纳秒微妙级别)出现了,是为了减少锁对象的创建。若JVM已经发现有竞争,不会再偏向。退出。
只有锁升级,没有降级
同步关键字加锁原理——轻量级锁
使用CAS修改mark world完毕,加锁成功。则mark world中的tag进入00(轻量级锁)状态。
解锁的过程,则是一个逆向恢复mark world的过程
重量级锁——监视器(monitor)
修改mark worldrug1失败,会自旋CAS一定次数,该次数可以通过参数配置:
超过次数,仍未抢到锁,则锁升级为重量级锁,进入阻塞。
minitor也叫做管程,计算机操作系统原理中有提及类似概念。一个对象会有一个对应的monitor。
![](JAVA锁相关术语及同步关键字synchronized详解/图片 1.png)
总结
偏向锁(减少在无竞争情况,JVM资源消耗)——— > 出现两个及以上的线程争取升级为轻量级锁(CAS修改状态)
——— > 线程CAS自旋一定次数之后,升级为重量级锁(对象的mark word 内部会保存一个监视器锁的一个地址)
对象mark word里面 包含四种状态tag( 00 01 10 11 )
01 无锁
00 轻量锁
10 重量锁
11 GC废弃
LockSupport
park:挂起,阻塞线程
Unpark:释放,解除阻塞
LockSupport不能被实例化,LockSupport 的核心方法都是使用的 sun.misc.Unsafe 类中的 park 和 unpark 实现的。不同于wait/notify针对object,park/unpark是针对Thread的
扩展
实现一个Lock(流程如上图)
1 | package com.wyj.jvm.lock; |
test
1 | package com.wyj.jvm.lock; |
运行结果
1 | 10678 |
可见没有达到预期的20000,原因就是thread-1执行完毕之后,进入unlock方法后,第27行从等待队列中获取一个新线程thread-2,若拿到的是空的,则unpark不会生效,thread-2就会被永久挂起(park状态),少了一个线程运行,所以会出现运行结果< = 20000的情况。
优化版
1 | public class DemoLock implements Lock { |
运行结果
1 | 20000 |
Condition
用于替代wait/notify
Object中的wait(),notify(),notify()方法适合synchronized关键字配合使用的,可以唤醒一个或者全部(单个等待集);Condition是需要与Lock配合使用的,提供多个等待集合,更精确的控制(底层是park/unpark机制);
如何选择?
当Lock锁使用公平模式的时候,可以使用Condition的signal(),线程会按照FIFO的顺序冲await()中唤醒。当每个锁上有多个等待条件时,可以优先使用Condition,这样可以具体一个Condition控制一个条件等待。
上图场景的代码实现如下
1 | package com.wyj.jvm.condition; |
一张图理解Thread.sleep、Object.wait、LockSupport.park