synchronized使用
- 修饰实例方法:锁对象是当前实例对象
- 修饰静态方法:锁对象是当前类的Class对象
- 修饰代码块:锁对象是自己指定的对象
synchronized实现
- 当其作用于某个实例方法或者代码块时,在编译后的字节码中会加入monitorenter 和 monitorexit 这两个字节码指令
- 当其作用于静态方法时,在编译后的字节码中,该方法的flags属性中会被标记为 ACC_SYNCHRONIZED 标志,当虚拟机访问一个被标记为ACC_SYNCHRONIZED 的方法时,会自动在方法的开始和结束位置添加 monitorenter 和 monitorexit 指令
- 总的来说,当代码执行到monitorenter指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁,当执行到monitorexit指令时,会释放锁
Monitor对象
Monitor 可以理解为一个同步工具,实际上,它是被保存在对象头中的一个对象,Java对象的对象头由 mark word 和 klass pointer 两部分组成,其中mark word存储了同步状态、标识、hashcode、GC状态等等;而Monitor锁则是存在于对象的mark word中。
在Monitor的内部有一些关键属性:
- owner指向持有锁的线程
- entryList存放等待获取锁处于block状态线程的队列
- waitSet存放wait状态线程的队列
- 计数器:记录线程获取锁的次数 等等
在多线程同时访问一段同步代码块,首先都会进入entry队列,当某个线程通过竞争获取锁时,owner会指向它(即此获取锁),同时monitor中的计数器也会加+1,若持有锁的线程执行结束会释放锁,并复位owner值,计数器也会-1,若持有锁的线程调用wait方法会进入waitSet线程队列
Java虚拟机对synchronized的优化
从 Java 6 开始,虚拟机对 synchronized 关键字做了很多优化,主要目的是减少重量级锁的使用次数,最终减少线程上下文切换的频率 。其中主要做了以下几个优化: 自旋锁、轻量级锁、偏向锁
-
自旋锁
由于重量级锁底层通过操作系统的mutex lock实现,线程的阻塞和唤醒涉及到用户态和内核态切换,比较耗资源,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
但是线程自旋其实是不停的循环,是需要消耗CPU的,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间 -
偏向锁:
大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁,减少不必要的CAS操作
偏向锁会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁,改变对象头中的标志位,并将对象头中线程id指向自己。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。 -
轻量级锁
对于一块同步代码,虽然有多个不同线程会去执行,但是这些线程是在不同的时间段交替请求这把锁对象,也就是不存在锁竞争的情况。在这种情况下,锁会保持在轻量级锁的状态,从而避免重量级锁的阻塞和唤醒操作。
锁的状态
一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率
- 锁升级过程:无锁->偏向锁->轻量级锁->重量级锁
- 线程首先检查该对象头的线程ID是否为当前线程,如果对象头中线程id和当前id一致,则直接执行代码(下次线程再进入依然这样),如果不是当前线程id,则使用CAS方式替换对象头中的线程id,如果使用CAS替换不成功,说明有线程竞争,这时候需要撤销偏向锁(撤销偏向锁的时候会导致stop the word操作),升级为轻量锁。
- 偏向锁升级为轻量级锁时会首先暂停拥有偏向锁的线程,比如线程1持有锁正在执行,当前是偏向锁,线程2来竞争锁,发生锁升级时Java 虚拟机会在当前线程的栈帧中开辟一块空间(Lock Record)作为该锁的记录,然后 Java 虚拟机会尝试使用 CAS(Compare And Swap)操作,将锁对象的 Mark Word 拷贝到这块空间中,拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。
- 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,那么它就会自旋等待锁,一定次数后仍未获得锁对象。重量级线程指针指向竞争线程,竞争线程也会阻塞,等待轻量级线程释放锁后唤醒他。锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态.
volatile原理
有volatile变量修饰的变量进行写操作的时候会使用CPU提供的Lock前缀指令,会对CPU总线和高速缓存加锁
- 将当前处理器缓存行的数据写回到系统内存。
- 这个写回内存的操作也会使在其他CPU里缓存了该内存地址的数据无效。
volatile作用:
- 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入
- 原子性:单次读/写的原子性(long和double两种数据类型的操作可分为高32位和低32位两部分,因此普通的long或double类型读/写可能不是原子的,加上volatile即可保证),但无法保证复合操作原子性
- 有序性:对volatile变量的写操作 happen-before 后续的读操作
- 禁止指令重排序
网友评论