synchronzied的作用
- 原子性:所谓原子性就是一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么都不执行。被synchronzied修饰的类或对象的所有操作都是原子的,因为在执行之前必须先获得类或对象的锁、直到执行完才能释放。
- 可见性:可见性是指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的。synchronized具有可见性,其中synchronized对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到共享内存当中,保证资源变量的可见性。
- 有序性:有序性指程序执行的顺序按照代码先后执行。synchronized具有有序性,Java允许编译器和处理器对指令进程重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。
synchronzied的主要用法
- 修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁(对于普通同步方法,锁的是当前实例对象,通常指this)
synchronized void method(){
}
- 修饰静态方法(对于静态同步方法,锁的是当前类的Class对象)
synchronzied void static method(){
}
- 修饰代码块(指定加锁对象,对给定对象/类加锁。synchronized(this|object)表示进入同步代码库前要获得给定对象的锁。synchronized(类.class)表示进入同步代码前要获得当前class的锁)
synchronized(this) {
//业务代码
}
对于普通同步方法,锁是当前实例对象;对于静态同步方法,锁是当前类的Class对象;对于同步方法块,锁是synchronized括号里配置的对象。
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。那么锁到底在哪里呢?锁里面会存储什么信息呢?
从JVM规范中可以看到synchronized在JVM里的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的。
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。
synchronized用的锁是存在Java对象头里的。
Java对象头里的Mark Word里默认存储对象的HashCode、分段年龄和锁标记位
synchronized为可重入锁的原理
每个锁对象拥有一个锁计数器和一个指针,该指针是用来指向获得锁的线程的
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1.
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放锁。
当执monitorexit时,Java虚拟机则需要将锁对象的计数器减1。计数器为零代表锁已被释放。
内核态和用户态
CPU有两种工作状态:内核态和用户态。系统中既有操作系统的程序,也有普通用户的程序。为了安全和稳定性操作系统的程序不能随便访问,这就是内核态。内核态可以使用所有的硬件资源。用户态不能直接使用系统资源,也不能改变CPU的工作状态,并且只能访问这个用户程序自己的存储空间。
synchronized的背景
Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock(系统互斥量)来实现的,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个Java线程需要操作系统切换CPU状态来来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长,时间成本相对较高,这也是为什么早期的synchronized效率低的原因,Java6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁。
synchronized锁升级
Java SE1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级
无锁
初始状态,一个对象被实例化后,如果还没有被任何线程竞争锁,那么它就是无锁状态。
偏向锁
(单线程竞争)
当线程A第一次竞争到锁时,通过操作修改Mark Word中的偏向线程ID为偏向模式
Hotspot的作者经过研究发现,大多数情况下:多线程的情况下,锁不仅不存在多线程竞争,还存在锁由同一个线程多次获得的情况,偏向锁就是在这种情况下出现的,它的出现是为了解决只有一个线程执行同步时提高性能。
在实际应用运行过程中发现,“锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用它的线程拥有,这个线程就是锁的偏向线程。
那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁(后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接会去检查锁的MarkWord里面是不是放的自己的线程ID)。
如果存放的是自己的线程ID,表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了。以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外的开销,性能极高。
如果存放的不是自己的线程ID,就表示发生了竞争,锁已经不是总偏向于同一个线程了,这个时候会尝试使用CAS来替换MarkWord里面的线程ID为新线程的ID,如果竞争成功,表示之前的线程不存在了,MarkWord里面的线程ID为新线程的ID,锁不会升级,仍然为偏向锁,如果竞争失败,这时候可能需要升级为轻量级锁,才能保证线程间公平竞争锁。
轻量级锁
轻量级锁的加锁
JVM会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,官方称为Displaced Mark Word。若一个线程获得锁时发现是轻量级锁,会把锁的MarkWord复制到自己的Displaced Mark Word里面。然后线程尝试用CAS将锁的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在于其他线程竞争锁,当前线程就尝试使用自旋来获取锁。
轻量级锁的释放
在释放锁时,当前线程会使用CAS操作将Displaced Mark Word的内容复制回锁的Mark Word里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻塞的线程。