文章目录
提到多线程,肯定离不开线程的同步方式,线程的同步方式有三种
一、同步方法块
二、同步方法
三、锁
接下来面试管肯定矛头直指synchronized和Lock的区别?
什么?你说你不知道?那你回家等通知吧
1、乐观锁与悲观锁
我觉得很有必要讲一下什么是乐观锁,什么是悲观锁
- 所谓的悲观锁,就是我认为操作是不安全的,所以每次操作我都要加锁保证安全
- 所谓的乐观锁,就是我认为操作时安全的,只有在真正操作的时候,我才去验证这个操作的安全性
(1)乐观锁
CAS(Compare And Swap),就是一种乐观锁的实现
1、那么CAS时怎么实现线程安全的?
- 拿数据库做例子就是,我第一次从数据库查询
select userid username from user where age=26;
,我查询到了一条数据,当我需要吧这条记录的username
去更改的时候,我需要先去看一下当前我已经拿到的数据是不是和数据库的一致,如果是一致的话,说明当前我获取到的是最新的值,允许更新。否则的话,说明当前的值被更改过了,你必须要拿最新的值,在那个基础之上去修改,当前的操作被拒绝,你要重新去获取最新的值 - (这很好理解,就比如说你和你的同事A工作都要先从远程仓库拉取代码,当你们工作完了再把代码提交远程仓库。有一天你提前把自己的工作做完了,下班前把代码上传了,而同事A在提交的时候,直接强制提交了。第二天你来到公司你就会发现你的代码都不见了,这样明显是不合理的)
2、CAS会出现什么问题?
- 提到CAS就不得不了解ABA的问题,什么是ABA?
小明 | 小华 |
---|---|
从数据读取到数据为A | |
更改同一条记录为B | |
又把记录从B改为A | |
比较发现当前的数据为A,更改 |
以上就是ABA的问题,就是小华这个人把记录从A改为B,又把B改为A。你说说现在记录是被修改过还是没被修改过?
解决方案:
加一个标志位,像数据库那样,每次操作标志位+1,或者拿时间戳作对比
- 循环时间长开销大的问题
如果我CAS一直比较的数据都不是最新的数据,那么我们就要不断去自旋(不断重复的获取最新的值,相当于死循环)
(2)悲观锁
synchronized就是悲观锁的典型代表
1、对象的结构
- 对象由3个部分组成:对象头、实例数据、对齐填充。而对象头里有一个部分称为
Mard World
,里面默认存储的是对象的hashcode
,还有一些分代年龄,锁的标志位信息,而synchronized
的锁实现也是通过更改对象头的信息来实现的。
2、synchronized的锁实现
通过对代码的编译和反编译,查看反编译后的代码,发现synchronized
的实现是通过一个monitorenter
来加锁的,通过monitorexit
来解锁
步骤:
- 每个monitor维护着一个记录着拥有次数的计数器。未被拥有的monitor的该计数器为0,当一个线程获得monitor(执行monitorenter)后,该计数器自增变为 1 。
当同一个线程再次获得该monitor的时候,计数器再次自增;
当不同线程想要获得该monitor的时候,就会被阻塞。
- 当同一个线程释放 monitor(执行monitorexit指令)的时候,计数器再自减。当计数器为0的时候,monitor将被释放,其他线程便可以获得monitor。
3、锁优化
synchronized
在大家的眼里看来,一直都是一种重量级锁的实现,但是在jdk1.6以后,对synchronized
做了很多的优化。具体的优化请参考《深入理解Java虚拟机》
锁优化:
- 自旋锁与自适应自旋
如果物理机器有一个以上的处理器或者处理器核心,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只须让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。 - 锁消除
- 锁粗化
- 锁升级过程
我们在前面讲了,对象头里面有Mark World
这样的一个区域,里面有2比特用于记录锁的状态。分为:未锁定、轻量级锁定、重量级锁定、偏向锁
(1)在代码即将进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间
(2)然后,虚拟机将使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,即代表该线程拥有了这个对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后两个比特)将转变为“00”,表示此对象处于轻量级锁定状态
(3)如果这个更新操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,那直接进入同步块继续执行就可以了,否则就说明这个锁对象已经被其他线程抢占了。如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁,锁标志的状态值变为“10”,此时Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也必须进入阻塞状态。
2、synchronized
(1)一般用法
静态方法
成员方法
代码块
首先我们来看一下一些经常的用法,在什么时候锁住什么对象?
(2)特性保证
- 有序性
as-if-serial
happen-before - 可见性(与volatile对比)
JMM - 原子性
确保同一时间只有一个线程能拿到锁 - 可重入性
3、Lock
4、Synchronized和Lock的区别
synchronized | lock | |
---|---|---|
实现层面 | synchronized是关键字,是JVM层面的底层啥都帮我们做了 | 而Lock是一个接口,是JDK层面的有丰富的API |
锁的释放 | synchronized会自动释放锁 | 而Lock必须手动释放锁 |
锁中断 | synchronized是不可中断的 | Lock可以中断也可以不中断 |
是否拿到锁 | synchronized不能知道有没有获取到锁 | 通过Lock可以知道线程有没有拿到锁 |
作用域 | synchronized能锁住方法和代码块 | 而Lock只能锁住代码块 |
公平锁 | synchronized是非公平锁 | ReentrantLock可以控制是否是公平锁 |
5、synchronized和ReentrantLock可重入实现
(1)synchronized
因为synchronized使用的是锁对象,当某个线程第一次持有锁后,会修改锁对象的mark word锁状态为偏向锁,偏向锁锁会在当前线程的栈帧中建立一个锁记录空间,mark word会将指针指向栈中的锁记录。当线程再次获取锁对象的时候,会检查mark word 中的指针是否指向当前线程的栈帧,如果是就直接获取锁,如果不是就需要竞争
(2)ReentrantLock
由于ReentrantLock是通过AQS来实现的,其使用了AQS的state状态值来表示线程获取该锁的可重入次数,默认情况下state为0表示当前锁没有被任何线程持有,当一个线程获取该锁时会尝试使用CAS设置state值为1,如果CAS设置成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程,在该线程没有释放锁的情况下第二次获取该锁后,状态值被设置2,这就是可以重入次数,在释放锁的时候,需要通过CAS将状态值减1,直到状态值为0,表示当前线程释放该锁
AbstractQueuedSynchronizer的作用
抽象同步队列简称AQS,是实现同步器的基础组件,并发包中的锁都是基于其实现的,关键是先进先出的队列,state状态,并且定义了
ConditionObject ,拥有两种线程模式,独占模式和共享模式
AQS核心思想
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制使用CLH队列实现的,即将暂时获取不到锁的线程加入到队列中
CLH(Craig,Landin,and
Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配,
并保持了上下节点,当前请求资源的线程