synchronized原理:
-
并发中的三个问题:
-
可见性
- 是指一个线程对共享变量进行修改,另一个线程立即得到修改后的值
-
原子性
- 是指在一次或多次操作中,要么所有的操作都执行,要么都不执行
-
有序性
- 是指代码的执行顺序
- java会在编译和运行时对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写的代码顺序
-
-
JMM
-
JMM是标准化的,屏蔽了底层不同计算机的区别
-
JMM是一套规范,描述了java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节
-
JMM分两部分:
- 主内存:
- 所有线程共享,都能访问,共享变量都存储在主内存
- 工作内存:
- 每一个线程都有自己的工作内存,工作内存只存储共享变量的副本。线程对共享变量的所有操作都在工作内存中完成,不能直接对主内存中的变量进行操作,不同线程的工作内存也是隔离
- 主内存:
-
JMM中的主内存和工作内存都可能对应着CPU寄存器、CPU缓存、内存中的任意一个
-
JMM中的8个原子操作:
lock -> read -> load -> use-> assign -> store -> write -> unlock
- 注意:
- 对一个变量执行lock时,将会清空工作内存中此变量的值
- 对一个变量执行unlock时,必须先将此变量同步到主内存中
- 注意:
-
JMM是一套在多线程读写共享变量时,对共享变量的可见性、有序性、和原子性的规则和保障
-
-
synchtonized保证三大特性:
-
原子性:synchtonized上锁保证锁住区域的原子性
-
可见性:synchtonized会执行lock原子操作会刷新工作内存中的值
-
有序性:synchtonized保证只有一个线程中执行同步代码块
-
-
synchtonized的特性
- 可重入性:
- 是指一个线程可以多次执行synchronized,重复获取同一把锁
- 原理:synchronized的锁对象中有一个**计数器(recursions变量)**会记录线程获得几次锁
- 好处:
- 可以避免死锁
- 可以让我们更好的来封装代码
- 小结:synchronized是可重入锁,内部锁对象会有一个计数器记录线程获取几次锁了,在执行完同步代码块时,计数器的技术会 -1 ,当计数为0,就释放这个锁
- 不可中断性:
- 一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直处于阻塞或等待状态,不可被中断
- synchronized属于不可被中断的
- Lock的lock方法是不可被中断的
- Lock的tryLock方法是可以中断的
- 可重入性:
-
synchtonized原理
- 通过javap反汇编字节码文件,我们可以看到synchronized内部使用monitorenter和moniyorexit两个指令,每个锁对象会关联一个monitor(监视器,它才是真正的锁对象),它内部有两个重要的成员变量,owner变量会保存获得锁的线程,recursions变量会保存同一线程获得锁的次数,当执行到monitorexit时,recursions会减一,减为零的时候会释放锁
- monitorexit插入在方法结束处和异常处,JVM保证了每个monitorenter必须有对应的monitorexit
- monitor是重量锁:
- 在JVM里,ObjectMonitor的函数调用会涉及到Atomic::cmpxchg_pr,Atomic::inc_ptr等内核函数,执行同步代码块,没有竞争到锁的对象会park()挂起,竞争到锁的线程会unpack()唤醒。这些都会进行系统调用,进行用户态和内核态的转换,消耗大量系统资源,降低性能,所以monitor是重量级(Heavyweight)的操作
- ObjectMonitor对象里有waitSet(放处于wait状态的线程)、cxq(多线程竞争锁时的单向列表篇)和EntryList(处于等待锁block状态的线程,放入此列表)
- 执行wait(),线程会被放入waitSet
- 第一次竞争失败会被放入cxq
- 第二次还失败就放入EntryList
- 执行monitorenter时,会调用InterpreterRuntime.cpp
- 看是否设置偏向锁(默认是有的),如果没设置,就是重量级锁慢启动,调用slow_enter()
- 最终调用enter(),开始尝试设置Owner和recursions
- 如果竞争锁失败了,就调用EnterI()
- 当前线程被封装成ObjectWaiter对象node,通过CAS把node节点push到cxq列表中
- 进入cxq后,通过自旋尝试获得锁,如果还不行就通过pack将当前线程挂起,等待被唤醒
- 当该线程被唤醒后,会从挂起的点继续执行,通过TryLock尝试获取锁
- 等到释放锁后
- 根据不同策略(由QMode指定),从cxq或EntryList获取头节点,调用ExitEpilog()唤醒该节点封装的线程,唤醒操作最终由unpack()完成
-
JDK1.6 synchtonized优化
-
CAS:
- CAS(Compare And Swap , 比较再交换)是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令
- CAS可以将比较和交换转换为原子操作,这个原子操作直接由CPU保证。CAS可以保证共享变量赋值时的原子操作。CAS操作三个值:内存中的值V,旧的预估值X,要修改的新值B,如果 X 等于 V,就将B保存到内存中
- 乐观锁:,就算改了也没关系,再重试即可。所以不会上锁,但是在更新的时候会去判断此期间别人是否修改数据,如果没人修改就更新,如果有人修改就重试
- CAS这种机制称为乐观锁,综合性能较好
- CAS获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。
- 结合CAS和volatile可以实现无锁并发,适用于竞争不激烈,多核CPU的场景下
- 悲观锁:总是假设最差的情况,每次拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞。
- synchronized也称为悲观锁。JDK中的ReentrantLock也是一种悲观锁
- 悲观锁性能较差
-
为了线程间更高效地共享数据,以及解决竞争问题,从而提高程序地执行效率,JDK1.6进行synchronized锁升级:
-
java对象的布局:
- 对象头(Mark word和Klass word)和实例数据和对齐数据
- 64位系统中,Mark word和Klass word分别占8个字节
-
偏向锁原理:
- 当线程第一次访问同步块并获取锁时,偏向锁处理流程:
- 虚拟机会将对象头中的锁标志位设为01,偏行锁位设置为1,即偏向模式
- 同时使用CAS操作把获取这个锁的线程ID记录在Mark Word中,等到下次再来这个同步块的时候,JVM不进行任何同步操作,偏向锁的效率高
- 偏向锁的撤销:
- 偏向锁的撤销动作必须等待全局安全点(所有线程都停下来)
- 暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
- 撤销偏向锁,恢复到无锁或轻量级锁的状态
- 偏向锁的好处:
- 偏向锁是在一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一把锁的情况。偏向锁可以提高有同步但无竞争的程序性能
- 它是一个带有效益权衡性质的优化,并不总是对程序运行有利的,如果程序中大多数的锁总是被多个不同的线程访问比如线程池,那偏向模式是多余的
- 偏向锁是默认开启的,但在程序启动后几秒才激活,使用-xx:BiasedLockingStartupDelay = 0参数 关闭延迟
- 当线程第一次访问同步块并获取锁时,偏向锁处理流程:
-
轻量级锁原理:
- 当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,其步骤如下:
- 判断当前对象是否为无锁状态(hashcode、0、01),如果是无锁状态,JVM就会在当前线程的栈帧中创建一个空间(Lock Record,锁记录),用于存储锁对象目前的Mark Word的拷贝(Displaced Mark Word),将Lock Record中的owner指向当前对象
- JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针
- 如果成功竞争到锁的话,就将锁标志位变成00,执行同步操作
- 如果失败则判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,就去执行同步代码块,如果不是的话,就说明当前锁对象已经被其他线程抢占了,这时轻量级锁就升级为重量级锁,锁标志位变成10,后面等待的线程进入阻塞状态、
- 轻量级锁的释放:
- 轻量级锁的释放是通过CAS操作来进行的,主要步骤如下:
- 还原:将之前放在Lock Word里的拷贝(Displaced Mark Word)取出来,放在当前对象的Mark Word,此操作成功就说明释放锁成功
- 此操作失败就说明,有其他线程尝试获取该锁,则需要将轻量级锁升级为重量级锁
- 轻量级锁的释放是通过CAS操作来进行的,主要步骤如下:
- 轻量级锁的好处:
- 多线程交替执行同步代码块的时候,可以避免重量级锁带来的性能消耗
- 当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,其步骤如下:
-
自旋锁原理:
- Monitor会阻塞和唤醒线程,这需要CPU从用户态到核心态的切换,会耗费性能,且一般来说同步代码块的运行耗时很短,所以这个切换成本就没必要了,在多核CPU上可以让后面等待的线程执行忙循环 (自旋锁)
- 当然自旋不能替代阻塞,自旋虽然避免了线程切换的开销,但是占用了CPU的时间。
- 锁被占用的时间很短的话,自旋锁的效果就很好,但如果占用时间较长,就是对CPU的白白消耗,所以自旋有一个限度,默认10次,超过限度就去挂起线程
- 自适应:自旋次数不固定,根据上一次在同一个锁上的自旋时间和锁拥有者的状态来决定
-
锁消除:
- 锁消除是根据即时编译器(JIT)在运行时,对数据进行逃逸分析,逃逸不出的话就把锁消除了
-
锁粗化:
- JVM会检测出一连串细小的操作都使用同一个对象加锁,将同步代码块的范围放大,放到这串操作的外面,这样就只需要加一次锁
-
面试题:
- synchronized与Lock的区别:
- synchronized是关键字,而Lock是一个接口
- synchronized会自动释放锁,而Lock必须手动释放锁
- synchronized是不可中断的,Lock可以中断也可以不中断
- 通过Lock可以知道线程有没有拿到锁,而synchronized不可以
- synchronized 能锁住方法和代码块,而Lock只能锁住代码块
- Lock可以使用读锁提高多线程读效率
- synchronized是非公平锁(随机选拔),ReentrantLock可以控制是否为公平锁(先来先到)
编写代码的优化:
- 推荐将同步代码块的作用范围限制的尽量小,这样就算存在锁竞争,也能尽快拿到锁
- 降低synchronized锁的粒度
- 读写分离:读取时不加锁,写入和删除时加锁